Redis 的分布式锁使用注意

Redis 的分布式锁使用注意

Jan 3, 2020
数据库, 系统设计

为什么需要分布式锁 #

锁机制用于管理对共享资源的并发访问控制,当同一服务的多台机器间需要控制并发的时候,需要保证服务间在同一时间只能被一个线程访问。由于 Redis 的特点是单进程,并且具有高性能的特点,所以一般用 Redis 来做分布式锁。另外分布式锁还有以下几个特点:

  1. 互斥性:同一时刻只能有一个实例持有锁。
  2. 高可用:部分节点宕机不会影响其他节点对锁的使用。
  3. 防止死锁:如果客户端没有主动释放锁,锁有过期时间也会自动释放。可以防止服务宕机或网络因素。
  4. 安全性:锁除了过期解锁外,只能被同一客户端加锁并解锁。

Redis 分布式锁的实现 #

一般使用 Redis 的 setnx 命令。加锁的步骤如下:

  1. setnx 给 key 赋值
> SETNX key 1
  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 同时进行了并发。

解决方案:

  1. 延长过期时间,确保自动释放锁之前能够执行完成。
  2. 为将要过期的锁添加守护进程,在将要过期时延长过期时间。

问题 3:锁误解除 #

继续上面的问题,如果 A 和 B 并发执行过程中,A 完成了执行,并释放了 B 的锁,这时 B 还没有执行完成,显然还是有问题。

解决方案:

某个线程加锁时为加锁的 value 值生成一个特殊签名或 UUID,在解锁时进行校验。所以这里也是两步操作:

  1. 获取 key 值 value,并进行对比。
  2. 如果是自己加的锁,则进行释放。

会有原子性问题,安全性要求高的话同样需要借助 Lua 脚本实现原子操作。

问题 4:时钟跳跃 #

对于 Redis 服务器如果其时间发生了时钟跳跃,那么肯定会影响我们锁的过期时间,那么我们的锁过期时间就不是我们预期的了,也会出现 A 和 B 获取到同一把锁的问题。

解决方案:

zookeeper 不依赖时间,不存在这个问题。

Zookeeper不需要配置锁超时,由于我们设置节点是临时节点,我们的每个机器维护着一个ZK的session,通过这个session,ZK可以判断机器是否宕机。如果我们的机器挂掉的话,那么这个临时节点对应的就会被删除,所以我们不需要关心锁超时。

其他方案 #

  • zookeeper
  • etcd
  • MySQL

虽然 Redis 极端情况下还是会有上诉一些问题不能彻底解决,但是 Redis 可能是这里面性能最好的方案了,如果对安全性要求极高可以继续尝试另外几个组件,另外最好也在业务层做好幂等校验,可以最大程度降低并发安全问题。

参考资料 #

搞懂“分布式锁”,看这篇文章就对了

本文共 1214 字,上次修改于 Sep 27, 2023,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» Redis 实现布隆过滤器

» Redis 基础知识

» 说说实际工作中 GraphQL 的使用体验

» UTF、Unicode 和 ASCII 编码的关系

» Django 中 N+1 查询问题优化