思考
- 是否有官方推荐(自己先思考如何实现,然后再参考其他人的实践,总结优缺点)
- 通过哪些方式可以实现锁
- 锁是否具有原子性
- 锁请求失败了如何处理
- 如果避免发生死锁
- 如果避免发生资源抢占
- 如果避免锁的误删
官方实现策略
- 安全性能:互斥。在任何给定的时刻,只有一个客户可以持有锁
- 活性属性A:无死锁。最终,即使锁定资源的客户端崩溃或被分区,也始终有可能获取锁
- 活性性质B:容错性。只要大多数Redis节点都启动了,客户端就可以获取和释放锁
储备知识
原子操作
解释一
原子操作是指不会被线程调度机制打断操作,这种操作一旦开始,就一直到结束,
中间不会有任何context switch(切换到另外一个线程)
解释二
原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。
可用加锁命令
INCR SETNS SET
加锁命令优劣分析
INCR
实现思路
key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中
实现方式
- 客户端A请求服务器获取key的值为1表示获取了锁
- 客户端B也去请求服务器获取key的值为2表示获取锁失败
- 客户端A执行代码完成,删除锁
- 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
- 客户端B执行代码完成,删除锁
remark
加入过期时间是为了防止意外退出,锁没有删除,锁一直存在,以至于无法再次获取锁资源
demo
- r e d i s − > i n c r ( redis->incr( redis−>incr(key);
- r e d i s − > e x p i r e ( redis->expire( redis−>expire(key, $ttl); //设置生成时间为1秒
缺点
借住Expire设置,不再是原子操作。
SETNX
实现思路
如果key不存在,则将key设置为value,如果key已存在,则SETNX不做任何操作
实现方式
- 客户端A请求服务端设置key的值,如果设置成功,表示加锁成功
- 客户端B请求服务器设置key的值,如果返回失败,表示加锁失败
- 客户端A执行代码完成,删除锁
- 客户端B在等待一段时间后再去请求设置key值,设置成功
- 客户端B执行代码完成,删除锁
demo
- r e d i s − > s e t N X ( redis->setNX( redis−>setNX(Kkey, $value);
- r e d i s − > e x p i r e ( redis->expire( redis−>expire(key, $ttl);
缺点
不是原子操作
非原子性潜在问题:无法实现互斥,出现单点故障时,比如说如果Redis主机坏了怎么办?好吧,
让我们增加一个假设!如果主机不可用,我们增加一个备机,请使用它。不幸的是,这是不可行的。
这样做我们就无法实现互斥的安全属性,因为Redis复制是异步的。
场景描述
- 客户机A获取主机中的锁。
- 在向从机发送对密钥的写入之前,主机崩溃。
- 备机被提升为主机。
- 客户端B获取已为其持有锁的同一资源A的锁。违反安全规定!(在A拥有锁的同时,B也用用了锁)
- 有时,在特殊情况下,比如在故障期间,多个客户机可以同时持有锁,这是非常好的。
如果是这种情况,则可以使用基于复制的解决方案。否则,我们建议实现本文档中描述的解决方案。
SET
实现思路
设置key的同时,这只过期时间
实现方式
- 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
- 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
- 客户端A执行代码完成,删除锁
- 客户端B在等待一段时间后在去请求设置key的值,设置成功
- 客户端B执行代码完成,删除锁
demo
r e d i s − > s e t ( redis->set( redis−>set(key, $value, array('nx', 'ex' => $ttl)); //ex表示秒
存在问题
以上几种方式仍存在的问题
- redis发现锁失败了要怎么办?中断请求还是循环请求?
- 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
- 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁,这时候客户端A执行完了,会不会在删锁的时候把B的锁给删(非原子操作的影响)
解决办法
针对问题1:使用循环请求,循环请求去获取锁
针对问题2:针对第二个问题,在循环请求获取锁的时候,加入睡眠功能,等待几毫秒在执行循环
针对问题3:在加锁的时候存入的key是随机的。这样的话,每次在删除key的时候判断下存入的key里的value和自己存的是否一样
do { //针对问题1,使用循环
$timeout = 10;
$roomid = 10001;
$key = 'room_lock';
$value = 'room_'.$roomid; //分配一个随机的值针对问题3
$isLock = Redis::set($key, $value, 'ex', $timeout, 'nx');//ex 秒
if ($isLock) {
if (Redis::get($key) == $value) { //防止提前过期,误删其它请求创建的锁
//执行内部代码
Redis::del($key);
continue;//执行成功删除key并跳出循环
}
} else {
usleep(5000); //睡眠,降低抢锁频率,缓解redis压力,针对问题2
}
} while(!$isLock);