使用Redis实现分布式锁

本文介绍基于单个Redis节点实现分布式锁与基于多个Redis节点实现分布式锁的方案。

1. 基于单个Redis节点

对于基于单个Redis节点的分布式锁来说,加锁时带上uniqueIDtimeout,释放时对比uniqueID就是目前的最佳做法,至少官方就是这么推荐的。但为了有更深刻的理解,我们还是需要知道为什么需要uniqueID和timeout。

方案1: 使用SETNX + DEL(没有uniqueIDtimeout

// 加锁
SETNX lock_key 1
// 业务逻辑
DO THINGS
// 释放锁
DEL lock_key

问题:拿到锁的客户端可能当机,永远无法释放锁,因此加锁的时候要带上个timeout。

改进1:使用SET $key 1 NX PX $milliseconds(有timeout,没有uniqueID)

// 加锁, PX 10000表示加锁10秒
SET lock_key 1 EX PX 10000
// 释放锁
DEL lock_key

问题: 其它客户端可能误执行DEL。考虑如下的场景

  1. 客户端 1 加锁成功,开始操作共享资源
  2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
  3. 客户端 2 加锁成功,开始操作共享资源
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

解决方案(有uniqueIDtimeout:为每个客户端分配一个唯一ID,客户端请求释放锁时,带上该ID。释放锁的逻辑由lua脚本来做,先判断请求者的ID是否等于当前持有锁的ID,再决定是否释放。

// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000

//释放锁, 使用lua脚本, 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

其它问题: 锁过期时间不好评估。
一种方案:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

此外,单个Redis节点的分布式锁在主从集群+哨兵的模式下存在问题,因为该模式下发生主从切换可能导致数据丢失,那么发生切换时可能主库上有锁的记录,而从库上没有锁的记录,切换完成后,主库上锁的记录丢失了。

2. Redlock: 基于多个独立的Redis节点

单机的系统永远需要面对单点故障的问题,而主从Redis又有数据丢失的问题,因此官方提出了基于多个独立Redis节点的分布式锁方案Redlock

方案前提

  1. 不再需要部署从库和哨兵实例,只部署主库
  2. 但主库要部署多个

Redlock算法

  1. 客户端获取当前时间
  2. 客户端按顺序依次向 N 个 Redis 实例执行加锁操作,加锁时要带上锁的有效时间
  3. 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时

客户端只有在满足下面的这两个条件时,才能认为是加锁成功

  1. 客户端从超过半数(大于 N/2)的 Redis 实例上成功获取到了锁
  2. 客户端获取锁的总耗时没有超过锁的有效时间

满足上面两个条件后,客户端需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
如果不满足上面的条件,客户端向所有的Redis实例执行释放锁的操作。


参考:
官方文档 https://redis.io/topics/distlock
深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

posted @ 2022-02-10 18:07  elimsc  阅读(174)  评论(0编辑  收藏  举报