背景
在分布式系统中,分布式锁是确保资源互斥访问的重要机制。我们在系统中修改已有数据时,需要先读取,然后进行修改保存,此时很容易遇到并发问题。由于修改和保存不是原子操作,在并发场景下,部分对数据的操作可能会丢失。在单服务器系统我们常用本地锁来避免并发带来的问题,然而,当服务采用集群方式部署时,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。
实现方式
通过 Redis 的 SET
和 DEL
命令实现锁的设置和释放,并使用 Lua 脚本确保操作的原子性。
加锁命令:
SETNX key value
,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。解锁命令:
DEL key
,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。锁超时:
EXPIRE key timeout
, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
以下是详细的代码分析:
获取锁
getLock
方法尝试获取锁。它使用 Redis 的 SET
命令,并通过 Lua 脚本确保操作的原子性。关键点如下:
- 锁超时:设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
- 唯一值生成:使用
md5(uniqid('', true))
生成唯一的锁值,在后面释放锁时需要用到。 - 原子性:
SET
命令结合NX
和EX
选项确保锁的唯一性和过期时间。Lua 脚本避免了多命令操作的并发问题。 - 重试机制:如果锁获取失败,方法会自旋重试,直到达到最大重试次数。每次重试之间,线程会等待 200 毫秒。
1 | /** |
释放锁
releaseLock
方法用于释放锁。它通过 Lua 脚本实现,确保只有持有锁的客户端才能释放锁。关键点如下:
- 锁误解除:如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。所以我们要获取当前锁的值并检查是否与传入的
lockValue
匹配。如果匹配,则执行DEL
命令释放锁。这样可以防止误删其他客户端持有的锁。
1 | public function releaseLock(string $lockKey, string $lockValue): bool |
使用
获取了锁之后一定要释放锁,所以用try finally的错误捕获方法保证不管在获取锁之后是否发生错误,最后都会释放锁,这是安全使用锁的一种姿势。
1 | $lockKey = 'xxx';//业务key |
Enjoy it