Redis 分布式锁

分布式应用进行逻辑处理需要考虑并发问题。

一个常见的场景如修改用户状态,修改前需要先读取用户当前状态,修改后进行保存。由于整个过程不是原子操作,一旦多个操作同时进行,就可能导致状态异常。

引入分布式锁可以解决上述问题,而 Redis 分布式锁是使用非常广泛的一种实现方式。

分布式锁的特性

分布式锁需要具备获取锁、释放锁的功能,并且需要能够处理异常情况。

获取锁

Redis 中的 setnx 指令可以用于实现获取锁的操作, set if not exists ,在锁没有被其他客户端占有的情况下获取锁,先来先占。

释放锁

Redis 中的 del 指令可以用于实现释放锁的操作,逻辑处理完成之后通过 del 删除指定的 key 来释放锁。

异常处理

如果在逻辑处理执行过程中发生异常,可能会导致 del 指令不被调用,锁会一直得不到释放。

解决方法是在获取锁的同时给锁加一个过期时间,确保即使出现异常也会在过期后自动释放。

在 Redis 2.8 版本之前,没有 setnx 和 expire 合并到一起的原子指令,如果分开执行可能在 setnx 成功后 expire 指令执行失败,依然造成死锁。而 Redis 又不支持事务中 if - else 分支逻辑,所以 Redis 开源社区涌现了很多 Library 来解决这个问题。

Redis 2.8 版本中加入了 set 指令的扩展参数,使 setnx 和 expire 指令可以作为原子操作来执行,格式为

1
2
# set <key> <value> ex <seconds> nx
> set lock:update true ex 5 nx

超时问题

Redis 的分布式锁不能解决超时问题,所以 Redis 分布式锁不适用于耗时较长的任务。

如果获取锁后的操作耗时太长,超过锁的过期时间,其他线程就可以提前获取到锁,导致临界区代码不能得到严格的串行执行。

可重入性

可重入性,指线程在持有锁的情况下,能够再次请求获取到锁,比如 Java 中的 ReentrantLock 就是可重入锁。

Redis 分布式锁如果需要支持可重入,可以对客户端 set 方法进行包装,通过 ThreadLocal 存储当前持有锁的计数。

可重入锁还需要考虑内存锁计数的过期时间等问题,代码复杂度会持续升高,最好通过合理的处理逻辑来避免使用可重入锁。

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 线程一获取锁
# 如果 lock:test 不存在,则设置 lock:test 键的值为 true ,10秒后过期
> set lock:test true ex 10 nx
OK

# 线程二同时请求获取锁,获取失败
> set lock:test true ex 10 nx
(nil)

# 线程一业务逻辑处理 ...

# 线程一释放锁
# 删除 lock:test
> del lock:test
(integer) 1
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • © 2016-2020 姜越

谢谢老板

支付宝
微信