public static boolean doSecKill(String uid, String proid)throws IOException {
// 1.uid和proid判断
if(uid == null||proid == null){
return false;
}
// 2.连接jedis
Jedis jedis = new Jedis("192.168.50.105",6379);
// 3. 拼接key
String invenKey = "sk:"+proid+"qt";
String userKey = "sk:"+proid+"qt";
// 4. 判断是否开始
String inven = jedis.get(invenKey);
if(inven == null){
System.out.println("秒杀尚未开始");
jedis.close();
return false;
}
// 5.判断是否抢过
if(jedis.sismember(userKey,uid)){
System.out.println("不可重复参与!");
jedis.close();
return false;
}
// 6.判断库存是否充足
if(Integer.parseInt(jedis.get(invenKey))<=0){
System.out.println("秒杀已结束,失败");
jedis.close();
return false;
}
// 进行库存扣除,添加成功人员名单
jedis.decr(invenKey);
jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
# 更新brew
$ brew update
# 安装 APR
$ brew install apr
# 安装 APR-UTIL (费时有些长)
$ brew install apr-util
# 安装 PCRE
$ brew install pcre
# 安装ab工具
$ brew install httpd-tools
# ab常用命令
$ ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded url
# -n:请求数
# -c:并发数
# -k:开启 HTTP Keep-Alive,客户端在请求服务端的资源时,不会关闭与服务端的连接,
# -p:请求参数存放的文件
# -T:设置类型,就固定写: application/x-www-form-urlencoded
# url:替代为请求连接的url
解决超时问题:
连接池
节省每次连接redis服务带来的消耗,把连接好的实例反复利用。
通过参数管理连接的行为
链接池参数
MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
estOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "192.168.50.105", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.close();
jedis.close();
}
}
}
解决超卖问题:
public class SecKill {
public static void main(String[] args) {
Jedis jedis = new Jedis();
}
public static boolean doSecKill(String uid, String proid) throws IOException {
// 1.uid和proid判断
if (uid == null || proid == null) {
return false;
}
// 2.连接jedis,使用连接池,避免超时
JedisPool jedisPool = new JedisPool();
Jedis jedis = jedisPool.getResource();
// 3. 拼接key
String invenKey = "sk:" + proid + "qt";
String userKey = "sk:" + proid + "qt";
// 监视库存
jedis.watch(invenKey);
// 4. 判断是否开始
String inven = jedis.get(invenKey);
if (inven == null) {
System.out.println("秒杀尚未开始");
jedis.close();
return false;
}
// 5.判断是否抢过
if (jedis.sismember(userKey, uid)) {
System.out.println("不可重复参与!");
jedis.close();
return false;
}
// 6.判断库存是否充足
if (Integer.parseInt(jedis.get(invenKey)) <= 0) {
System.out.println("秒杀已结束,失败");
jedis.close();
return false;
}
// multi事务操作
Transaction multi = jedis.multi();
multi.decr(invenKey);
multi.sadd(userKey, uid);
List<Object> result = multi.exec();
if (result == null || result.size() == 0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
redis默认不能直接使用悲观锁,使用LUA脚本语言操作。
public class SecKill_redisByScript {
private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
public static void main(String[] args) {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
System.out.println(jedis.ping());
Set<HostAndPort> set=new HashSet<HostAndPort>();
// doSecKill("201","sk:0101");
}
static String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
static String secKillScript2 =
"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
" return 1";
public static boolean doSecKill(String uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
//String sha1= .secKillScript;
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
}
Redis 提供了2个不同形式的持久化方式。
RDB(Redis DataBase)
AOF(Append Of File)
dbfilename dump.rdb
dir ./
save 3600 1
:每隔3600秒有一个修改就会持久化一下save 20 3
然后启动redis之后40s左右时进行了3次修改,dump文件会立刻出现,因为40s时间早已达到20s的范围,然后立刻进行了三次修改,也满足了条件,所以立刻进行了持久化。而后我又紧跟着3次修改,之后立刻复制dump.rdb文件,关停redis服务,之后删除原dump文件,将我复制的dump备份文件更名为dump.rdb,之后发现其只保存了3次修改,紧随其后的3次修改需要再等20s才能进行持久化。redis-check-aof --fix appendonly.aof
命令可以进行文件修复概念:
作用:
操作:
info replication
打印自己信息slaveof
成为主机的从机复制原理:
一主二从:
薪火相传:
反客为主:
slaveof no one
将从机变成主机哨兵模式(反客为主的自动版)
解释:后台自动监控主机是否故障,然后后台进行投票将从机晋升为主机。
操作:
myredis目录下创建sentinel.conf配置文件
启动哨兵:redis-sentinel /myredis/sentinel.conf
新主登基:
群臣俯首:
旧主俯首:
复制延时:
Java实现:
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
解释:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
现象:
原因现象:
解决方案:
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
**预先设置热门数据:**在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
**实时调整:**现场监控哪些数据热门,实时调整key的过期时长
使用锁:
描述:
现象:
原因:
解决方案:
描述:
主流解决方案:
对比各自优缺点:
基于redis实现分布式锁
setnx key value
只有在key为空时才可以赋值,所以设置值的同时上了锁del key
set key value nx ex time
设置值的同时设置了超时时间,ex后面的值就是超时时间Java代码:
@GetMapping("testLock")
public void testLock() {
//1获取锁,setne , 同时设置3秒过期,以避免中间出现异常,导致锁一直无法释放
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if (lock) {
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if (StringUtils.isEmpty(value)) {
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value + "");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
} else {
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
对于if段执行如果出现异常导致锁没有被释放的问题:设置超时自动释放
对于释放错锁,和释放别人锁的问题:
描述:a上锁之后遇到卡顿,导致锁超时自动释放;之后b抢到锁进行操作;再之后a的服务器恢复,a操作完成之后释放了b的锁,导致接下来全部错位释放锁。c会抢到b的锁。
解决:
UUID防止误删除:可以防止误删除链,但是依然存在超时误删的问题
第一步:set lock uuid nx ex 10
,然后用uuid进行判断。
第二步:释放锁时候,判断当前uuid和锁里面uuid是否一致,一致则释放,不一致则不管
代码改造:
String uuid = UUID.randomUUID().toString();
//1获取锁,setne ,同时设置3秒过期,以避免中间出现异常,导致锁一直无法释放
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
...
//2.4释放锁,del
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUuid)){
redisTemplate.delete("lock");
}
遗留问题:当比较uuid结果相等的时候,锁过期了,又会导致误删锁
LUA保证删除原子性操作:
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分布式锁四个条件:
简介
Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。
在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制 :
之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。
Redis6新功能还有:
1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信
2、Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。
3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。
4、Modules API
Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。