一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
Redis分布式锁
锁的种类
- 单机版同一个JVM虚拟机内,synchronized或者Lokc接口
- 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。 一个靠谱分布式锁需要具备的条件和刚需。
- 独占锁->OnlyOne,任何时刻只能有且仅有一个线程持有
- 高可用->若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 防死锁->杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
- 不乱抢->防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
- 重入性->同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
分布式锁
setnx key value ,这个命令set值+expire不安全,两条命令非原子性。
换它重载的命令 set key value [EX seconds] [PX milliseconds] [NX|XX] 这样就是一条命令。
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods()
{
String key = "zzyyRedisLock";
RLock redissonLock = redisson.getLock(key);
redissonLock.lock();
try
{
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if(goodsNumber > 0)
{
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口:"+serverPort;
}else{
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口:"+serverPort;
}finally {
if(redissonLock.isLocked () && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
}
}
}
面试经常考点:①加锁关键逻辑、②解锁关键逻辑
tryLock、releaseLock
基于setnx的分布式锁有什么缺点?
线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点;
在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;
redis 触发故障转移,其中一个 slave 升级为新的 master; 此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁;
此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的
多机案例
Redlock算法
单机和集群都会有额外几率的问题出现。多主模式就能解决。
@GetMapping(value = "/redlock")
public void getlock() {
//CACHE_KEY_REDLOCK为redis 分布式锁的key
RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
//waitTime 锁的等待时间处理,正常情况下 等5s
//leaseTime就是redis key的过期时间,正常情况下等5分钟。
isLock = redLock.tryLock(5, 300, TimeUnit.SECONDS);
log.info("线程{},是否拿到锁:{} ",Thread.currentThread().getName(),isLock);
if (isLock) {
//TODO if get lock success, do something;
//暂停20秒钟线程
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
} catch (Exception e) {
log.error("redlock exception ",e);
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
System.out.println(Thread.currentThread().getName()+"\t"+"redLock.unlock()");
}
}
Redisson底层有个看门狗,守护线程"续命",官网也写了 看门狗的作用是Redission实例被关闭前,不断的延长锁的有效期,默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来自行指定时间。
通过redisson新建出来的锁key,默认是30秒
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
- 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
- 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
- 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了lockzzyy这个锁key的剩余生存时间),加锁失败
解锁解释