Redis 的分布式锁使用注意
1月 3, 2020
为什么需要分布式锁 #
锁机制用于管理对共享资源的并发访问控制,当同一服务的多台机器间需要控制并发的时候,需要保证服务间在同一时间只能被一个线程访问。由于 Redis 的特点是单进程,并且具有高性能的特点,所以一般用 Redis 来做分布式锁。另外分布式锁还有以下几个特点:
- 互斥性:同一时刻只能有一个实例持有锁。
- 高可用:部分节点宕机不会影响其他节点对锁的使用。
- 防止死锁:如果客户端没有主动释放锁,锁有过期时间也会自动释放。可以防止服务宕机或网络因素。
- 安全性:锁除了过期解锁外,只能被同一客户端加锁并解锁。
Redis 分布式锁的实现 #
一般使用 Redis 的 setnx
命令。加锁的步骤如下:
setnx
给 key 赋值
> SETNX key 1
setex
给 key 设置过期时间,举例过期时间为 1s:
> EXPIRE Key 1
最后在使用完锁的时候,记得释放锁:
> DEL key
如果这么简单就可以实现分布式锁,而且满足上面的分布式锁特点就不用写下来记录了,但是显然不满足,使用 Redis 实现分布式锁目前还会有一些问题。
问题 1:非原子性 #
上面这样分成两步会有一个问题:如果在第一步成功之后,服务由于一些原因没有成功执行第二步,已经存在 Redis 里的 key 就会变成死锁。要解决这个问题,Redis 2.6.12 之前一般是通过 Lua 脚本将两个步骤封装成一步原子操作,2.6.12 之后Redis 可以通过set
实现原子操作:
> SET key value ex 1 nx
是不是这样就没问题了?也不是。
问题 2:超时解锁导致并发 #
假设有线程 A 和 B,A 先加锁,并设置了过期时间10秒,10 秒后 A 没有执行完,锁自动释放,这时 B获取锁进行执行。结果就是超时解锁导致了 A 和 B 同时进行了并发。
解决方案:
- 延长过期时间,确保自动释放锁之前能够执行完成。
- 为将要过期的锁添加守护进程,在将要过期时延长过期时间。
问题 3:锁误解除 #
继续上面的问题,如果 A 和 B 并发执行过程中,A 完成了执行,并释放了 B 的锁,这时 B 还没有执行完成,显然还是有问题。
解决方案:
某个线程加锁时为加锁的 value 值生成一个特殊签名或 UUID,在解锁时进行校验。所以这里也是两步操作:
- 获取 key 值 value,并进行对比。
- 如果是自己加的锁,则进行释放。
会有原子性问题,安全性要求高的话同样需要借助 Lua 脚本实现原子操作。
问题 4:时钟跳跃 #
对于 Redis 服务器如果其时间发生了时钟跳跃,那么肯定会影响我们锁的过期时间,那么我们的锁过期时间就不是我们预期的了,也会出现 A 和 B 获取到同一把锁的问题。
解决方案:
zookeeper 不依赖时间,不存在这个问题。
Zookeeper不需要配置锁超时,由于我们设置节点是临时节点,我们的每个机器维护着一个ZK的session,通过这个session,ZK可以判断机器是否宕机。如果我们的机器挂掉的话,那么这个临时节点对应的就会被删除,所以我们不需要关心锁超时。
其他方案 #
- zookeeper
- etcd
- MySQL
虽然 Redis 极端情况下还是会有上诉一些问题不能彻底解决,但是 Redis 可能是这里面性能最好的方案了,如果对安全性要求极高可以继续尝试另外几个组件,另外最好也在业务层做好幂等校验,可以最大程度降低并发安全问题。