前言

本文围绕 redis 的 SETNX 命令展开对“锁”的研究与实现。多个进程同时对 redis 执行 SETNX string_key timestamp_expired 命令,只有一个进程会成功,其余都会失败。上述命令中的 string_key 代表被当作锁的 redis 键名,其类型为 String,timestamp_expired 代表该键的过期时间戳,下同。


软件版本

  • windows 10
  • php 7.4.14 nts
  • thinkphp 6.1.0
  • redis 6.2.7
  • predis/predis 2.0.3(php第三方扩展包)

锁算法

算法中使用 redis 的 EVAL 命令保证执行语句的原子性。

申请加锁前需约定锁的有效期、加锁失败后的重试次数、休眠时间(申请加锁时,其他线程加的锁还未过期,休眠一段时间后再重试加锁)。

申请加锁步骤:

  1. 申请加锁( SETNX string_key timestamp_expired ),如果加锁成功,设置锁的过期时间并返回成功。否则进入步骤 2;
  2. 如果加锁失败原因为 string_key 刚刚被其他进程删除(释放锁),返回步骤 1 重新申请加锁,否则进入步骤 3;
  3. 如果加锁失败原因为 string_key 还未过期(此时也会得到旧的过期时间戳),休眠一段时间后返回步骤 1 重新申请加锁,否则进入步骤 4;
  4. 执行 GETSET string_key timestamp_expired_new 命令,如果命令返回时间戳与旧的过期时间戳相等,加锁成功,设置锁的过期时间并返回成功。否则进入步骤 5;
  5. 锁被其他进程删除了或者被其他进程抢先执行了 GETSET string_key timestamp_expired_new 命令,返回步骤 1 重新申请加锁。

释放锁:

如果 string_key 键未被删除且还未过期,执行删除键操作( DEL string_key )。


实现

主要类文件有 2 个,为文件 Redis.php(获取 redis 操作客户端) 和 Lock.php(加、释放锁),内容分别如下:

<?phpnamespace app\service;use Predis\Client;
use think\facade\Env;class Redis
{/*** @var Client $client*/protected $client;public function __construct(){$this->client = new Client(['host' => Env::get('redis.host'),'port' => Env::get('redis.port'),]);$this->client->auth(Env::get('redis.auth'));$this->client->select(Env::get('redis.database'));}
}
<?phpnamespace app\service;class Lock extends Redis
{// 锁的有效期(s)const LOCK_EXPIRE = 3;// 加锁失败后的重试次数const LOCK_RETRY_MAX = 3;// 休眠时间(s):申请加锁时,其他线程加的锁还未过期,休眠一段时间后再重试加锁const LOCK_SLEEP = 0.3;/*** @var string $lockKey 被当作锁的键*/private $lockKey;public function __construct($lockKey){parent::__construct();$this->lockKey = $lockKey;}/*** 获取加锁脚本* KEYS:被当作锁的键名* ARGVS:锁的过期时间戳、锁的有效期、当前时间戳** @return string*/private function getLockScript(){return <<<LUA
-- 日志
local function log_debug(msg)redis.log(redis.LOG_DEBUG, string.format("[申请加锁]%s", msg))
endlog_debug('开始')-- 被当作锁的键名
local key_lock = KEYS[1]
-- 锁的过期时间戳
local key_lock_expired = tonumber(ARGV[1])
-- 锁的有效期
local key_lock_expire = tonumber(ARGV[2])
-- 当前时间戳
local time_now = tonumber(ARGV[3])local res = redis.call('SETNX', key_lock, key_lock_expired)
-- 获取锁成功
if (res == 1) thenredis.call('EXPIRE', key_lock, key_lock_expire + 1)log_debug('成功')return 0
endlocal expired = redis.call('GET', key_lock)
-- 锁被其他进程删除
if (not expired) thenlog_debug('锁被其他进程删除,请再重新申请')return 1
endexpired = tonumber(expired)
-- 其他进程加的锁还未过期
if (expired > time_now) thenlog_debug('锁还未过期,请休眠后再申请')return 2
endlocal key_lock_expired_new = key_lock_expired + key_lock_expire + 1
local expired_old = tonumber(redis.call('GETSET', key_lock, key_lock_expired_new))
if (expired_old == expired) thenredis.call('EXPIRE', key_lock, key_lock_expire + 1)log_debug('锁过期,getset加锁成功')return 0
end-- 锁被其他进程删除或被其他进程抢占先机执行了 redis 的 getset 方法
log_debug('锁被其他进程删除或抢占先机,请再重新申请')
return 3
LUA;}/*** 获取释放锁脚本* KEYS:被当作锁的键名* ARGVS:当前时间戳** 客户端连接 redis server 加锁成功后,可能会出现以下情况:* 1. 客户端挂掉,锁自动过期;* 2. 客户端执行业务时间过长,锁过期后被其他进程再加锁;* ……** @return string*/private function getUnLockScript(){return <<<LUA
-- 被当作锁的键名
local key_lock = KEYS[1]
-- 当前时间戳
local time_now = tonumber(ARGV[1])
-- 锁的过期时间戳
local key_lock_expired = redis.call('GET', key_lock)
if (key_lock_expired and tonumber(key_lock_expired) <= time_now) thenredis.call('DEL', key_lock)
end
LUA;}private function acquire(){return $this->client->eval($this->getLockScript(),1,$this->lockKey,time() + self::LOCK_EXPIRE + 1,self::LOCK_EXPIRE,time());}/*** 加锁** @return bool*/public function lock(){// 加锁申请次数$retry = 1;while (0 != ($res = $this->acquire()) && (self::LOCK_RETRY_MAX > $retry)) {// 其他线程加的锁还未过期,休眠一段时间后再重试加锁2 == $res && sleep(self::LOCK_SLEEP);$retry++;}return 0 == $res;}/*** 释放锁*/public function unlock(){$this->client->eval($this->getUnLockScript(), 1, $this->lockKey, time());}
}

可以打开 redis server 的日志文件,跟踪加、释放锁的过程。


使用

  1. Lock 类中的 lock()unlock() 方法需成对使用,即在业务逻辑执行结束后主动释放锁。
  2. 在阿里云 redis 服务中使用上述编程时,可能需要取消 LUA 脚本中对调用传参 KEYSARGV 的局部变量赋值。如 Lock 类 getLockScript() 方法中 local key_lock = KEYS[1] ,在需要调用 key_lock 的地方替换为 KEYS[1]
  3. 在 cluster 模式的 redis 集群(主从复制、哨兵、cluster)下,注意在加锁调用传参时 key 的格式。如要传递的键名为 lock_goods:300 ,可以修改为 {lock_goods}:300,这样,redis 集群在查找这个键时只会对字符串 lock_goods 进行哈希计算,从而得出存放此键的一个确定 slot 。即我们都要到一个 slot 中申请加锁,而不是各自到不同的地方申请一把锁来操作同一个资源。

【redis】redis 锁相关推荐

  1. redis分布式锁 在集群模式下如何实现_收藏慢慢看系列:简洁实用的Redis分布式锁用法...

    在微服务中很多情况下需要使用到分布式锁功能,而目前比较常见的方案是通过Redis来实现分布式锁,网上关于分布式锁的实现方式有很多,早期主要是基于Redisson等客户端,但在Spring Boot2. ...

  2. 快来学习Redis 分布式锁的背后原理

    以前在学校做小项目的时候,用到Redis,基本也只是用来当作缓存.可阿粉在工作中发现,Redis在生产中并不只是当作缓存这么简单.在阿粉接触到的项目中,Redis起到了一个分布式锁的作用,具体情况是这 ...

  3. Redis分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击关注公众号,Java干货及时送达 来源:juejin.cn/post/6854573212831842311 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本篇文章主要是基于我们实际项目 ...

  4. Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本 ...

  5. 秒杀商品超卖事故:Redis分布式锁请慎用!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 来源:juejin.im/post/6854573 ...

  6. 记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 juejin.im/post/5f159cd8f2 ...

  7. 简单介绍redis分布式锁解决表单重复提交的问题

    在系统中,有些接口如果重复提交,可能会造成脏数据或者其他的严重的问题,所以我们一般会对与数据库有交互的接口进行重复处理.本文就详细的介绍一下redis分布式锁解决表单重复提交,感兴趣的可以了解一下 假 ...

  8. Redis 分布式锁没这么简单,网上大多数都有 bug

    Redis 分布式锁这个话题似乎烂大街了,不管你是面试还是工作,随处可见,「码哥」为啥还写? 因为看过很多文章没有将分布式锁的各种问题讲明白,所以准备写一篇,也当做自己的学习总结. 在进入正文之前,我 ...

  9. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

    ‍‍‍‍‍‍‍‍‍‍‍‍阅读本文大约需要 20 分钟. 大家好,我是 Kaito. 这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题. Redis 分布式锁的话题,很多文章已经写烂了 ...

  10. SpringBoot + Redis 分布式锁:模拟抢单

    作者:神牛003 cnblogs.com/wangrudong003/p/10627539.html 本篇内容主要讲解的是redis分布式锁,这个在各大厂面试几乎都是必备的,下面结合模拟抢单的场景来使 ...

最新文章

  1. 【NLP】含10,000 Python问题,伯克利联合团队推出新的代码生成评价基准
  2. 20180429 xlVBA套打单据自适应列宽
  3. python类2继承-抽象-多态
  4. 2014新浪研发project师实习笔试(哈尔滨站)
  5. 认识HTML5的WebSocket 1
  6. UE4 使用蓝图进行编辑器扩展
  7. Basler相机实时图像显示--Qt代码
  8. 制作网页所需的一些简单ps技巧
  9. 为什么海底捞员工很少离职
  10. Linux命令--tac(倒序查看文件所有内容)
  11. 分享免端口访问群晖的方法,可以顶级域名
  12. 基于JAVA游泳馆信息管理系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
  13. spring boot整合MySQL数据库
  14. 乐学成语(HappyIdiom)
  15. L1正则化进行特征选择
  16. 搭建直播平台源码用到的云技术到底是什么
  17. Vimeo上传功能中的SSRF
  18. XP设置屏保密码,但不设置开机密码问题
  19. eventlet并发读写socket出现Second simultaneous问题
  20. 沟通感悟—之沟通原则

热门文章

  1. JQuery遍历数组的方法
  2. 联邦学习论文笔记——FedFair: Training Fair Models In Cross-Silo Fedrated Learning
  3. NBA名人堂之-沙奎尔·奥尼尔|大卫·罗宾逊|卡尔·马龙|克莱德·德雷克斯勒|约翰·斯托克顿
  4. 关于list集合拷贝工具类
  5. 微信小程序 上传视频方法
  6. Windows下Emacs安装evil插件
  7. 网页上ftp服务器修改,ftp服务器怎么修改网页
  8. win10设置过的锁屏壁纸桌面壁纸
  9. php手机网页弹出软键盘代码,Html5页面上如何禁止手机虚拟键盘弹出
  10. 北京传智播客网页UI设计学院