1、分布式锁的做用:php
redis写入时不带锁定功能,为防止多个进程同时进行一个操做,出现意想不到的结果,so...对缓存进行插入更新操做时自定义加锁功能。laravel
2、Redis的NX后缀命令面试
Redis有一系列的命令,其特色是以NX结尾,NX的意思能够理解为 NOT EXISTS(不存在),SETNX命令 (SET IF NOT EXISTS) 能够理解为若是不存在则插入,Redis分布式锁的实现主要就是使用SETNX命令。redis
3、实现原理sql
在进程请求执行操做前进行判断,加锁是否成功,加锁成功容许执行下步操做;shell
若是不成功,则判断锁的值(时间戳)是否大于当前时间,若是大于当前时间,则获取锁失败不容许执行下步操做;缓存
若是锁的值(时间戳)小于当前时间,而且GETSET命令获取到的锁的旧值依然小于当前时间,则获取锁成功容许执行下步操做;服务器
若是锁的值(时间戳)小于当前时间,而且GETSET命令获取到的锁的旧值大于当前时间,则获取锁失败不容许执行下步操做;架构
4、$redis->setnx() 设置锁并发
$expire = 10;//有效期10秒 $key = 'lock';//key $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期 $lock = $redis->setnx($key, $value); //判断是否上锁成功,成功则执行下步操做 if(!empty($lock)) { //下步操做... }
若是返回 1 ,则表示当前进程得到锁,并得到了当前插入/更新缓存的操做权限。
若是返回 0,表示锁已被其余进程获取,这是进程能够返回结果或者等待当前锁失效再请求。
5、解决死锁
若是只用SETNX命令设置锁的话,若是当持有锁的进程崩溃或删除锁失败时,其余进程将没法获取到锁,问题就大了。
解决方法是在获取锁失败的同时获取锁的值,并将值与当前时间进行对比,若是值小于当前时间说明锁以过时失效,进程可运用Redis的DEL命令删除该锁。
$expire = 10;//有效期10秒 $key = 'lock';//key $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期 $status = true; while($status) { $lock = $redis->setnx($key, $value); if(empty($lock)) { $value = $redis->get($key); if($value < time()) { $redis->del($key); } }else{ $status = false; //下步操做.... } }
可是,简单粗暴的用DEL命令删除锁再SETNX命令上锁也会出现问题。好比,进程1得到锁后崩溃或删除锁失败,这时进程2检测到锁存在当已过时,用DEL命令删除锁并用SETNX命令设置锁,进程3也检测到锁过时,也用DEL命令删除锁也用SETNX命令设置了锁,这时进程2和进程3同时得到了锁。问题大了!
为了解决这个问题,这里用到了Redis的GETSET命令,GETSET命令在给锁设置新值的同时返回锁的旧值,这里利用了GETSET命令同时获取和赋值的特性,在此期间其余进程没法修改锁的值。
例如:
进程1得到锁后操做超时/崩溃/删除锁失败,
进程2检测到锁已存在,但获取锁的值对比当前时间发现锁已过时,
进程2经过GETSET命令从新给锁赋予新的值,并获取到的锁的旧值,再次对比锁的旧值与当前时间,若是锁的旧值依然小于当前时间的话,这时进程2就能够忽略进程1余留下的废锁进行下步操做了。
进程2完成下步操做后返回前应该删除锁,但在删除锁时能够先检测锁是否还未过时,未过时才作删除操做,已过时的就不必在去删除锁了,由于颇有可能其余进程检测到锁过时时已经去获取锁了。
这里要说明的是,若是有其余进程在进程2以前获取到锁,那么进程2将获取锁失败,可是进程2在用GETSET获取锁的旧值时也赋予了锁新的值,改写了其余进程赋予锁的超时值。看到这你们可能会有疑问了,进程2没获取到锁怎么能改变锁的值呢?是的,进程2改变了锁的原有值,但这一点小小的时间偏差带来的影响是能够忽略。
如下是Redis实现分布式锁的完整PHP代码:
<?php /** * 实现Redis分布锁 */ $key = 'test'; //要更新信息的缓存KEY $lockKey = 'lock:'.$key; //设置锁KEY $lockExpire = 10; //设置锁的有效期为10秒 //获取缓存信息 $result = $redis->get($key); //判断缓存中是否有数据 if(empty($result)) { $status = TRUE; while ($status) { //设置锁值为当前时间戳 + 有效期 $lockValue = time() + $lockExpire; /** * 建立锁 * 试图以$lockKey为key建立一个缓存,value值为当前时间戳 * 因为setnx()函数只有在不存在当前key的缓存时才会建立成功 * 因此,用此函数就能够判断当前执行的操做是否已经有其余进程在执行了 * @var [type] */ $lock = $redis->setnx($lockKey, $lockValue); /** * 知足两个条件中的一个便可进行操做 * 一、上面一步建立锁成功; * 二、 1)判断锁的值(时间戳)是否小于当前时间 $redis->get() * 2)同时给锁设置新值成功 $redis->getset() */ if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() )) { //给锁设置生存时间 $redis->expire($lockKey, $lockExpire); //****************************** //此处执行插入、更新缓存操做... //****************************** //以上程序走完删除锁 //检测锁是否过时,过时锁不必删除 if($redis->ttl($lockKey)) $redis->del($lockKey); $status = FALSE; }else{ /** * 若是存在有效锁这里作相应处理 * 等待当前操做完成再执行这次请求 * 直接返回 */ sleep(2);//等待2秒后再尝试执行操做 } } }
实现分布式锁用到的Redis命令介绍:
setnx(key, value)
将key的值设为value,当且仅当key不存在。
若给定的key已经存在,则SETNX不作任何动做。
SETNX是”SET if Not eXists”(若是不存在,则SET)的简写。
返回值:
设置成功,返回1。
设置失败,返回0。
get(key)
返回key所关联的字符串值。
若是key不存在则返回特殊值nil。
假如key储存的值不是字符串类型,返回一个错误,由于GET只能用于处理字符串值。
返回值:
key的值。
若是key不存在,返回nil。
getset(key, value)
将给定key的值设为value,并返回key的旧值。
当key存在但不是字符串类型时,返回一个错误。
返回值:
返回给定key的旧值(old value)。
当key没有旧值时,返回nil。
expire(key, seconds)
为给定key设置生存时间。
当key过时时,它会被自动删除。
在Redis中,带有生存时间的key被称做“易失的”(volatile)。
在低于2.1.3版本的Redis中,已存在的生存时间不可覆盖。
从2.1.3版本开始,key的生存时间能够被更新,也能够被PERSIST命令移除。(详情参见 http://redis.io/topics/expire)。
返回值:
设置成功返回1。
当key不存在或者不能为key设置生存时间时(好比在低于2.1.3中你尝试更新key的生存时间),返回0。
ttl(key)
返回给定key的剩余生存时间(time to live)(以秒为单位)。
返回值:
key的剩余生存时间(以秒为单位)。
当key不存在或没有设置生存时间时,返回-1 。
del(key)
移除给定的一个或多个key。
返回值:
被移除key的数量。
以上内容但愿帮助到你们, 不少PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提高,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货须要的能够免费分享给你们 ,须要戳这里 PHP进阶架构师>>>实战视频、大厂面试文档免费获取