已经有了实现redis分布式锁的第三方包,比如redisson就有各种锁,这篇主要研究下实现redis分布式锁的思路。
当我们的系统是单机时,可以使用synchronized/CAS来控制数据共享问题,但是当系统做大做强了,单机顶不住了,则需要加机器,这时候还用synchronized/CAS来处理数据共享问题就不行了,这时候就可以借助redis来实现。
我们知道redis有个setnx命令:set一个值,如果存在则返回false,并且不set进去,如果不存在则set进去,并且返回true;根据这特性我们可以用该命令来设置lock。
- 样例1:
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/redisTest")
public String redisTest(){
// 模拟 统计接口访问次数
String lock = "clickTimeLock";
// 尝试去获取锁
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, "true");
if (!isLock){
return "正在被其他系统使用中,稍后再试";
}
// 获取到锁了,执行业务逻辑
try{
//访问次数
String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
int times = Integer.parseInt(clickTimes) + 1;
stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
System.out.println(times);
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁
stringRedisTemplate.delete(lock);
}
return "统计成功";
}
}
从这段代码里会发现一个问题:
系统a获取了锁,突然间挂掉宕机了,则锁一直保留在redis里没有被释放掉(走不到finally块里)。
那我们就可以根据业务情况设置一个超时时间。
- 样例2
// 模拟 统计接口访问次数
String lock = "clickTimeLock";
// 尝试去获取锁, 设置一个锁过时的超时时间,超时了就释放掉
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, "true", 5, TimeUnit.SECONDS);
if (!isLock){
return "正在被其他系统使用中,稍后再试";
}
又有一个问题:超时时间设置多少比较好?
- 场景一:业务执行需要5s,我们设置6s的超时时间,当系统线程卡顿时超过了6s,则会超时锁释放,被其他线程加锁或删锁,但是实际上原来的线程还没有结束业务操作,这会造成数据安全问题。
- 场景二:业务执行需要5s,我们设置60s的超时时间,当系统宕机时,其他的系统线程要想操作数据,得等待50多秒的时间,对一个高并发的系统来说是不可接受的。
这样一看,我们只能接受场景一的情况,那就会出现下面的问题:
要解决这样的问题,就需要不能让其他线程删了自己的线程,给自己的锁加个uuid。
- 样例3
@RequestMapping("/redisTest")
public String redisTest(){
// 模拟 统计接口访问次数
String lock = "clickTimeLock";
// 给自己的锁加个uuid的值,这样就防止被修改了
String uuid = UUID.randomUUID().toString();
// 尝试去获取锁, 设置一个锁过时的超时时间,超时了就释放掉
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 6, TimeUnit.SECONDS);
if (!isLock){
return "正在被其他系统使用中,稍后再试";
}
// 执行业务逻辑
try{
//访问次数
String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
int times = Integer.parseInt(clickTimes) + 1;
stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
System.out.println(times);
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁,判断是不是自己的uuid, 但是这块代码不是原子性的
if (uuid.equals(stringRedisTemplate.opsForValue().get(lock))){
stringRedisTemplate.delete(lock);
}
}
return "统计成功";
}
这样的情况下,我们还是需要处理超时导致的锁失效问题
- 样例4
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lock, uuid, 6, TimeUnit.SECONDS);
if (!isLock){
return "正在被其他系统使用中,稍后再试";
}
// 加锁成功了,起个子线程,定时查看主线程状态,还在使用中则延迟过时时间
// 代码省略,可直接查看redisson的源码
// 执行业务逻辑
到这里,应该就能满足并发不是很高的需求应用了。
redisson里的实现
来看下redisson里是如何实现分布式锁的,代码改造如下:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.1</version>
</dependency>
@Autowired
private Redisson redisson;
@RequestMapping("/redisTest")
public String redisTest(){
// 模拟 统计接口访问次数
String lockName = "clickTimeLock";
RLock lock = redisson.getLock(lockName);
lock.lock();
// 执行业务逻辑
try{
//访问次数
String clickTimes = stringRedisTemplate.opsForValue().get("clickTimes");
int times = Integer.parseInt(clickTimes) + 1;
stringRedisTemplate.opsForValue().set("clickTimes", String.valueOf(times));
System.out.println(times);
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁
lock.unlock();
}
return "统计成功";
}
先看一下lock获取锁的逻辑代码:
到此可以发现redisson加锁是通过lua脚步实现的原子性,里面也有超时的设置。
当你查看unlock逻辑时,也是使用的lua脚本实现的原子性,而且会发现通过线程id来解锁的,而锁值是通过uuid来实现的。
上面有个加锁成功后开启子线程去续命锁,省略了代码,来看看redisson的代码:
加锁成功后异步开个schedule
可以看到里边弄了个定时任务来处理超时时间。
里面还有一些其他线程请求获取锁,通过信号量来等待唤醒等操作,有兴趣可以自行查看下。
redisson里还有很多锁的实现:
比如搭建高可用redis集群时,可用红锁来处理,记录就到此了,菜鸟一个,继续努力。