本文转载自:RedLock & Redisson分布式锁
前言
Redis为什么可以做分布式锁
大家都知道有个setNx指令,set if not exist 。但是分布式锁从设计角度来讲,我🉐️有三个前提:
1、必须有个标记,一般通过String字符串标识是否拿到了锁,然后我才能去开展我的业务
2、去拿锁的时候必须保证只有一个人拿到,也就是说第二个线程进来的时候它会阻塞
3、这把锁对所有线程都是可见的,当我拿到这把锁的时候,后面的人都知道这把锁被我拿了
那么Redisson客户端的出现解决了什么问题?
1、解决了setNx会导致死锁的问题
2、解决了锁可重入的问题(对比setNx,会存储加锁的线程信息,加锁的次数信息 - 加了几次就要释放几次,通过hash数据结构进行存储)
3、解决了业务还没执行完,锁被释放的问题(时间轮 + 看门狗)
应用场景
1、秒杀
2、抢优惠券
3、接口幂等性校验
一、单机锁
1.1、基于opsForValue().setIfAbsent()实现
格式:setnx key value
将key的值设为value,当且仅当key不存在
若给定的key已存在,则setnx不做任何操作
setnx是【set if not exists】(若果不存在,则set)的简写
可用版本 >= 1.0.0
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
if(!result){
return "something exception";
}
// 大量业务代码
...
redisTemplate. delete(lockKey);
return "program End ...";
Tip:存在的问题,如果业务代码抛异常了,他还能delete吗?
删除不了,就死锁了
1.2、改进:加入try … finally. 无论如何,它最后都会delete
try{
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
redisTemplate. delete(lockKey);
}
return "program End ...";
Tip:继续看看现在存在的问题 ?
释放锁的问题解决了,但是执行到业务代码宕机了怎么办?finally就没用了,又会出现死锁问题。因为redis里的key始终存在。
1.3、改进,设置key的过期时间
try{
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// 加超时时间
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
redisTemplate. delete(lockKey);
}
return "program End ...";
但是我们这么写,就咩问题了吗?
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// 会出现的问题
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
Tip:如果这个key刚刚执行完setIfAbsent(), 结果程序挂了,还没到expire(), 那怎么办??这两行代码有原子性的问题
1.4、改进,解决原子性的问题
还别说,redis原生就有这两个合二为一的,在底层,redis会帮我们保证原子性
// Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
redisTemplate. opsForValue().setIfAbsent(lockKey, "hello",10,TimeUnit.SECONDS);
这么写的问题是什么呢??
如果放到超高并发场景那就很可怕了,就不小问题了。
假设过来个请求执行了15秒,结果到10秒这个锁就被清理调了,外面又不断有新的请求进来了… …
如此恶性循环下去会有怎样的后果??高并发不断有请求进来… … 这样会导致什么问题呢?可能这把锁会永久失效!
1.5、针对高并发的问题,该怎么解决??
本质在于我自己的加的锁,结果会被别的线程给删掉,对不对。
接下来我们能想到的办法就是给这个锁加一个唯一标志。把id加到锁里边去。
String uniqueId = UUID.randomUUID.toString() + threadId;
当我们要解锁的时候判断一下,加锁的id是否是我本人加的
String uniqueId = UUID.randomUUID.toString() + threadId;
try{
String lockKey = "lockKey";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
if(uniqueId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate. delete(lockKey);
}
}
return "program End ...";
当执行到这儿,还是有点问题哈,如果程序执行15秒才结束,但是10秒锁才释放。还是会有并发的问题。
Question:时间不是解决问题的方案,还有没有别的思路?
在redis的处理方式上确实有实现方法:在此现况的情景下,通过定时器去扫,分线程每过10秒扫一次,判断一下主线程是否持有锁,key是否还存在,如果存在,把超时时间再设置长一点,跟token续期类似。
二、Redisson
现在市面上都有很多开源框架,来帮我们实现这些问题。而且人家都已经封装好了。
pom坐标
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
java代码
// 初始化客户端
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// 注入到 Spring IOC 中
@Bean
public Redisson redissonClient(){
// 此为单机模式
Config config = new Config();
// 当然,这儿有很多模式可选择,主从、集群、复制、哨兵 等等 ... ...
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
两个常用参数:
waitTime:等待时间 - 当锁拿不到的时候最多等你多久,通过自旋等待锁
leaseTime:锁释放时间,如果不传,默认是30秒,这个参数可配置
// 业务端
@Autowire
private Redisson redisson;
String lockKey = String.format(CommonConstant.RedisKey.EXAMPLE_KEY);
// 执行加锁的操作
// 可以看成 setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
boolean lockIs = redissonClient.tryLock(lockKey, 0L, 10L, TimeUnit.SECONDS);
if (lockIs) {
log.info("================== 当前已获取到锁,进行业务处理 ==============================");
try{
// 业务代码
... ...
}
catch (Exception e) {
log.error("================== 异常 ====================", e);
}
}
Question:Redisson就不会有问题了吗??
如果redis是做的主从、哨兵,那么依然有问题
主节点写入了key,他是异步的再把数据同步到slave,只要主节点的key写成功了,马上返回给客户端,告诉客户端你这把锁加成功了,key设置成功,客户端线程1开始做业务逻辑了,这个时候,redis主节点才会往从节点去同步。
也就是说:同步在后
- 在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 导致锁丢失。
假设,他刚准备同步给从节点,然后主节点挂了,怎么办?假设要同步的这个从节点被选举为master,高并发场景下新的线程(线程3)又进来,他请求的时候发现新的master他没有key,对于这种问题怎么解决呢?
三、Redis主从架构锁失效问题?
其实用redis去解决相对麻烦,当然也是有方法的。但是redis我们都已经做到极致了此时可以换种思路,比如用zk,他也是key-value结构,不过是树形的,CAP中满足CP,他不会马上告诉你成功(C:表示一致性),牺牲时间做同步。所以不存在这种问题。
怎么选择redis或者zookeeper,对并发要求比较高还是要选择redis(单机可支持10W的QPS),锁失效也只有手动对数据进行补偿,出错的概率是比较小。
四、RedLock实现
redis官方推荐使用
底层实现手段跟zk很类似
4.1、RedLock 简介
在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:
- 安全属性:互斥,不管什么时候,只有一个客户端持有锁
- 效率属性A:不会死锁
- 效率属性B:容错,只要大多数redis节点能够正常工作,客户端端都能获取和释放锁。
Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。关于这个算法的讨论可以看下官方文档。
超过半数redis节点(没有任何的依赖关系,就是单独节点)加锁成功才算加锁成功
4.2、RedLock的弊端?
- 性能,原来只要主节点写成功了就行了,现在要很多台机器都加锁成功。
- 都不能100%解决锁失效问题
详细实现可参考 方志朋
大佬好文:RedLock的实现