网站建设简单合同模板,删除wordpress googleapis在线字体,婚庆公司名字大全,凡客集团最终代码(通用类)
1 面试中、实际工作中#xff0c;经常涉及到 redis 分布式锁#xff0c;正确写法如下。先奉上代码#xff0c;再讲解。
?php
namespace app\common\library;
/*** 通用分布式锁(原子操作)*/
class Lock
{/*** 获取redis实例* return \Redis* throws…最终代码(通用类)
1 面试中、实际工作中经常涉及到 redis 分布式锁正确写法如下。先奉上代码再讲解。
?php
namespace app\common\library;
/*** 通用分布式锁(原子操作)*/
class Lock
{/*** 获取redis实例* return \Redis* throws \RedisException*/public static function redis(){$redis new \Redis();$redis-connect(192.168.4.147,6179);return $redis;}/*** 加锁(原子操作)* param string $key 要加锁的key* param string $value 必须是唯一值* param int $expires 锁的过期时间(秒)* return bool* throws \RedisException*/public static function lock(string $key,string $value,int $expires): bool{# redis实例$redis self::redis();# 原子操作 -- 设置锁且设置过期时间return $redis-set($key,$value,[nx,ex$expires]);}/*** 解锁(原子操作)* param string $key 要解锁的key* param string $value 加锁时的值* return mixed|\Redis* throws \RedisException*/public static function unlock(string $key,string $value){# redis实例$redis self::redis();# lua 脚本$lua if redis.call(GET,KEYS[1]) ARGV[1] thenreturn redis.call(DEL,KEYS[1])elsereturn 0end;return $redis-eval($lua,[$key,$value],1);}/*** 生成唯一值* return string*/public static function generateValue(): string{return microtime(true).mt_rand(10000,99999);}
}2 使用方式
use app\common\library\Lock;
# 抢到锁
if(Lock::lock($key,$value,300)){try{# 业务逻辑}catch (\Exception $e){}finally {# 释放锁Lock::unlock($key,$value);}
}讲解
使用场景
抢红包、秒杀下单、扣库存、…
目的
在并发情况下避免业务逻辑的重复执行导致性能低下或数据不一致。重复执行没意义的工作浪费性能比如数据清理、数据归档、检测日志等 。重复执行有意义的工作导致数据出错比如重复多扣了库存。
一些常见的写法
1 setnx expire 说到 redis 的分布式锁很多同学马上会想到 setnx expire先用 setnx 抢锁如果抢到之后再用expire 给锁设置一个过期时间防止忘记释放。
setnx 是 set if not exists 的缩写表示如果 key 不存在则去设置成功返回1否则返回0。
//抢锁
if($redis-setnx($key,$value)){//设置过期时间$redis-expire($key,300);try{//业务逻辑}catch (\Exception $e){}finally {//释放锁$redis-del($key);}
}注这个方案的问题在于setnx 和 expire 两个命令分开了不是原子操作。如果执行完加锁操作(setnx)后正要执行 expire 设置过期时间进程崩了或者要重启维护那么这个锁就长生不老了别的线程永远也获取不到这个锁了。
2 使用Lua脚本(包含setnx expire两条指令) 抢锁的改进如下解决了上面写法1抢锁时的非原子操作。利用 lua 脚本把多个指令一起执行达到原子操作的目的。 # lua脚本$lua if redis.call(setnx,KEYS[1],ARGV[1]) 1 thenredis.call(expire,KEYS[1],ARGV[2])elsereturn 0end;# 执行lua脚本并传入参数return $redis-eval($lua,[$key,$value,$expires],1);3 SET的扩展命令(SET EX PX NX) 效果等同于写法2也是原子性的。
//抢锁
if($redis-set($key,$value,[NX,EX300])){try{//业务逻辑}catch (\Exception $e){}finally {//释放锁$redis-del($key);}
}SET key value[EX seconds][PX milliseconds][NX|XX]
NX :表示key不存在的时候才能set成功也即保证只有第一个客户端请求才能获得锁而其他客户端请求只能等其释放锁才能获取。 EX seconds :设定key的过期时间时间单位是秒。 PX milliseconds: 设定key的过期时间单位为毫秒 XX: 仅当key存在时设置值
注但是这个方案还有个问题「锁被别的线程误删」 比如线程A执行完后去释放锁但它不知道当前的锁可能是线程B持有。线程A就把线程B的锁释放了但线程B临界区业务代码可能还没执行完成。
释放锁的代码改进如下判断所有者是否是自己然后再释放。也用原子操作
# redis实例
$redis $this-redis();
# lua 脚本
$lua
if redis.call(GET,KEYS[1]) ARGV[1] thenreturn redis.call(DEL,KEYS[1])
elsereturn 0
end
;
return $redis-eval($lua,[$key,$value],1);4 SET EX PX NX 校验所有者再删除 改进后最终的代码见文章开头。
后记
注意请根据实际业务情况合理设置锁的过期时间 expires 。
一、 无论如何依然存在一个问题「锁过期释放了业务还没执行完」 假设线程A获取锁成功一直在执行业务代码300秒过后它还没执行完。这时候锁就过期了别的线程又请求进来获取到锁了也开始执行业务代码。问题就来了业务代码并不是严格串行执行。
一般情况中小项目中做好日志、容错判断等即可。如果你项目到了一定规模如果你追求锁的决定安全性解决方案是自动续期。
这个话题值得再另写一篇文章讲解。自行上网搜索学习此处省略。
JAVA 提供了很好的一个分布式锁框架 Redisson 它很好的解决了此问题。PHP 暂时我还没找到好的类库。
还有就是自己实现自动续期。
二、 多个 redis 实例、集群模式时解决方案请看官方提供的 RedLock。这又值得另写一篇文章讲解。自行上网搜索学习此处省略。