dino-ma/distributed-lock

一个基于PHP实现的分布式锁

V1.0.0 2019-09-10 10:40 UTC

This package is auto-updated.

Last update: 2024-04-09 15:21:19 UTC


README

安全和可靠性保证

在描述我们的设计之前,先提出三个属性,这三个属性是实现高效分布式锁的基础。

1、安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
2、效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
3、效率属性B:容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回nil。

SETEX命令
语法:
SETEX key seconds value
功能:
无论key存在与否,根据key 做value的替换 如成功则返回1,否则返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词redislock加锁,客户端可以尝试

SETNX redislock timestamp

如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过 DEL redislock 命令来释放锁。

如果返回0,说明 redislock 已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。

如果是堵塞调用调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。理想是美好的,现实是残酷的。

仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

时间戳的问题

我们看到redislock的value值为微秒时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。

时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。

锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为毫秒,从加锁到执行操作再到解锁,一般操作肯定都能在500毫秒内完成。

这样的话,我们上面的例子,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

死锁问题

    在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。
    
    所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,

    如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过
    
    SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

分布式锁的问题

1、必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。

2、分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在锁串的情况。
要适度的机制,可以承受小概率的事件产生。

3、只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,
直接进行操作,然后释放,尽量减少持有锁的时间。

4、在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,
但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,
玩客没有选择做锁的检查。

5、sleep学问,为了减少对Redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。
需要根据自己的Redis的QPS,加上持锁处理时间等进行合理计算。