Redis实现分布式锁?
本篇文章将简单的通过Spring Boot 项目展示三种常见的redis分布式锁的实现
一. SETNX
语法:SETNX key value
将 key
的值设为 value
,当且仅当 key
不存在。
若给定的 key
已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
-
可用版本:
= 1.0.0
- 时间复杂度:
O(1)
- 返回值:
设置成功,返回
1
。设置失败,返回0
。
在Spring Boot项目中实现
- 加入引用
org.springframework.boot
spring-boot-starter-data-redis
- 实现具体的逻辑
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void demo() {
String lockKey = "lock";
String clientId = UUID.randomUUID().toString();
try{
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,30,TimeUnit.SECOND);
//如果设置失败,则说明锁已经被占用,直接返回‘失败’
if(!result){
return "error_code";
}
//获取锁成功的情况下,正常的执行逻辑
}catch(Exception e){
}finally{
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
}
存在的问题:
- A进程设置的锁,因为超时,被自动释放。B进程设置新的锁,然后A结束后释放了B进程的锁。形成连锁反应
- A进程设置的锁,因为超时,被自动释放。B进程设置新的锁,提前开始执行。致使锁的同步功能失效。
解决方法:
- 使用UUID,每一次生成只有自己知道的特殊值,释放时判断(上例代码已实现)
- 开启一个守护线程,每隔更短的时间,监测锁存不存在,若存在则加时。简称锁续命
二. Redisson
网站: redisson.org
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
在Spring Boot项目中实现
- 加入引用
org.redisson
redisson
3.6.5
- 配置SpringBean
@Bean
public Redisson redisson(){
//此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
- 实现具体的逻辑
@Autowired
private Redisson redisson;
public String RedissonDemo () {
String lockKey = "product_001";
String clientId = UUID.randomUUID().toString();
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock();
//业务逻辑
} finally {
redissonLock.unlock();
}
}
使用Redisson的逻辑图如下:
存在的问题:
- 性能问题:多个请求,只有第一个会成功,其他的自循等待
- 主从架构问题:主节点还没有将锁给出去的信息同步给从节点就挂了,其他的从节点不知道该锁已经给出去了,当另一个客户端申请时,新的主节点重复的给出了锁的权限。
三. Redlock
官方给出了Redlock算法,大致意思如下:在分布式版本的算法里我们假设我们有N个Redis Master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们把N设成5,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
1、获取当前时间(单位是毫秒)。
2、轮流用相同的key和随机值(客户端的唯一标识)在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
虽然说RedLock算法可以解决单点Redis分布式锁的高可用问题,但如果集群中有节点发生崩溃重启,还是会出现锁的安全性问题。具体出现问题的场景如下:
假设一共有A, B, C, D, E,5个Redis节点,设想发生了如下的事件序列:
1、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)
2、节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了
3、节点C重启后,客户端2锁住了C, D, E,获取锁成功
这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了。
通过redisson来使用Redlock算法实例
public String redLock() {
String lockKey = "product_001";
//这里实际上是需要自己实例子化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化
RLock lock1 =redisson.getLock(lockKey);
RLock lock2 =redisson.getLock(lockKey);
RLock lock3 =redisson.getLock(lockKey);
/**
* 根据多个RLock对象构建RedissonRedLock(最核心)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1,lock2,lock3);
try{
/**
* waitTimeout (第一个)尝试获取锁的最大等待时间,超过这个时间,则认为获取锁失败
* leaseTime (第二个)锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务可以完成
*/
boolean res = redLock.tryLock(10,30, TimeUnit.SECONDS);
if(res){
//成功获取锁,此处处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
redLock.unlock();
}
return "end" ;
}
四. 总结
以上是使用Redis实现分布式锁的几种常见的方式,它实际上是基于缓存的锁。其特点总结如下:
- 优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。
- 缺点:通过锁超时机制不是非常可靠,当线程获得锁后,处理时间过长导致锁超时,就失去了锁的作用。