分布式锁背景分析
在学习一个新技术时,建议大家最好从以下两个方面出发,渐进式学习,可以更好地帮助我们滤清思路。拿学习分布式锁举例,请大家先记住这两个问题:
- 为什么要使用分布式锁?
- 分布式锁解决了什么问题?
在一些业务中,分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。
这个时候就要使用到分布式锁来限制程序的并发执行。理解了这个场景,也就理解了上面两个问题了。
(那么原子是什么意思呢?
在 Wiki 中是这样解释的:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)
分布式锁
分布式锁本质上要实现的目标就是在 Redis 里面占一个“位置”,当别的进程也要来占时,发现已经有人在这个位置了,
举个例子: 某天你的饭卡丢失了,这时候你要去学校里唯一的一台自助补卡机去补卡,你到了之后会有两种情况:
- 如果没有人在使用,那么你就可以直接使用,并且占了这个位置,其他人只能等你使用完。
- 如果有人正在使用,那么你就只好放弃或者等等再试了。
实现思路一
最简易的,我们可以使用 setnx(set if not exists) 指令,在开始逻辑时设置一个特殊 key 执行完毕后删除释放 key ,执行任务时只需要判断这个特殊 key 是否已经被创造了,如果被创造了就代表有人正在执行逻辑,你就需要等一等。
//尝试创建特殊 key ,根据返回值判断是否创造失败,失败则已存在
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, 10, TimeUnit.SECONDS);
if (!flag) {
//失败
return "加锁失败";
} else {
//成功,执行相关逻辑
//并删除 key
}
但是这种思路有个很明显的缺点,如果逻辑执行到中间出现异常了,可能会导致删除 key 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
实现思路二
为了改进思路一的缺点,我们通常会选择 expire 指令给这个特殊 key 加上一个过期时间,如果逻辑出现异常无法调用删除指令,也能在超时后自动释放锁。
这里要注意,不要把 setnx 和 expire 分成两条指令写,因为这样如果系统在刚执行完 setnx 指令后挂掉,expire 得不到执行还是会出现无法释放问题。(在 Redis 2.8 版本之前为了解决此问题需要耗费很多精力)在 Redis 2.8 版本之后官方为了解决这个问题,允许 setnx 和 expire 合并为一条指令执行。
但是这种思路又面临新的问题,不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。
tag = random.nextint() # 随机数
if redis.set(key, tag, nx=True, ex=5):
do_something()
redis.delifequals(key, tag) # 假想的 delifequals 指令
有一个稍微安全一点的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其它线程释放,除非这个锁是过期了被服务器自动释放的。 但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于delifequals这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。
# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
实现思路三
上面说了这么多,大家看了肯定觉得:“这也太繁琐了吧?”,没错,上面只是简单的分布式锁实现,甚至还没涉及到可重入性设计,从头设计一个分布式锁相当麻烦的,所以我推荐大家用现成的轮子来实现分布式锁,使用 Redisson 来实现分布式锁是目前最好的选择。
Redisson 是什么?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
springboot 集成 Redisson
集成很简单,只需要两步
- 引入 pom 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
- application.yml增加redis配置
spring:
application:
name: test
redis:
host: 127.0.0.1
port: 6379
使用起来也很简单,只需要注入RedissonClient即可
public class RedissonTest {
@Resource
RedissonClient redissonClient;
public void test() {
RLock rLock = redissonClient.getLock("anyKey");
rLock.lock();
try {
// do something
} catch (Exception e) {
log.error("业务异常", e);
} finally {
rLock.unlock();
}
}
}
可能不了解redisson的小伙伴会不禁发出疑问。
what?加锁时不需要加过期时间吗?这样会不会导致死锁啊。解锁不需要判断是不是自己持有吗?别担心,这些已经都被 Redisson 巧妙地设计给解决了,我们只需要使用就可以了。想深入了解 Redisson的话我之后会在出一篇文章来把源码哦!