[原创] PHP 使用Redis实现锁
锁实现的注意点
- 互斥: 任意时刻, 只能有一个客户端获得锁
- 不会死锁: 客户端持有锁期间崩溃, 没有主动解除锁, 能保证后续的其他客户端获得锁
- 锁归属标识: 加锁和解锁的必须是同一个客户端, 客户端不能解掉非自己持有的锁(锁应具备标识)
如果是Redis集群, 还得考虑具有容错性: 只要大部分Redis节点正常运行, 客户端就可以加锁和解锁.
以下只考虑 Redis单机部署的 场景.
如果是Redis集群部署, 可以使用
加锁
php 加锁示例
$redis = new Redis();
$redis->pconnect("127.0.0.1", 6379);
$redis->auth("password"); // 密码验证
$redis->select(1); // 选择所使用的数据库, 默认有16个
$key = "...";
$value = "...";
$expire = 3;
// 参数解释 ↓
// $value 加锁的客户端请求标识, 必须保证在所有获取锁清秋的客户端里保持唯一, 满足上面的第3个条件: 加锁/解锁的是同一客户端
// "NX" 仅在key不存在时加锁, 满足条件1: 互斥型
// "EX" 设置锁过期时间, 满足条件2: 避免死锁
$redis->set($key, $value, ["NX", "EX" => $expire])
执行上面代码结果:
- $key 对应的锁不存在, 进行加锁操作
- $key 对应的锁已存在, 什么也不做
加锁容易错误的点:
使用
setnx
和expire
的组合原因: 若在
setnx
后脚本崩溃会导致死锁
$value
客户端标识的:
- 简单点就用 毫秒级unix时间戳 + 客户端标识(大部分情况下够用了)
- 使用其他算法确保生成唯一随机值
connect 与 pconnect
在php中, 若使用 pconnect
连接redis, 则在当前脚本声明周期结束后, 与redis建立的连接仍会保留, 直到对应fpm进程的生命周期结束, 同时在下一次请求时, fpm会重用该连接.
即该连接的生命周期是 fpm 进程的生命周期, 而非一次php脚本的执行.
若代码使用 pconnect
, close
的作用仅是使当前php脚本不能再进行redis请求, 并没有真正关闭与redis的连接, 连接在后续请求中仍然会被重用.
pconnect函数在线程版本中不能被使用
解锁
php解锁示例: 使用lua脚本
$key = "...";
$identification = "...";
// KEYS 和 ARGV 是lua脚本中的全局变量
$script = <<< EOF
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
then
return 0
end
EOF;
# $result = $redis->eval($script, [$key, $identification], 1);
// 返回结果 >0 表示解锁成功
// php中参数的传递顺序与标准不一样, 注意区分
// 第2个参数表示传入的 KEYS 和 ARGV, 通过第3个参数来区分, KEYS 在前, ARGV 在后
// 第3个参数表示传入的 KEYS 的个数
$result = $redis->evaluate($script, [$key, $identification], 1);
使用Lua脚本的原因:
避免误删其他客户端加的锁
eg. 某个客户端获取锁后做其他操作过久导致锁被自动释放, 这时候要避免这个客户端删除已经被其他客户端获取的锁, 这就用到了锁的标识.
lua 脚本中执行
get
和del
是原子性的, 整个lua脚本会被当做一条命令来执行即使
get
后锁刚好过期, 此时也不会被其他客户端加锁
eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
由于 script 执行的原子性, 所以不要在script中执行过长开销的程序,否则会验证影响其它请求的执行。
解锁容易错误的点:
直接
del
删除键原因: 可能移除掉其他客户端加的锁(在自己的锁已过期情况下)
get
判断锁归属, 若符合再del
原因: 非原子性操作, 若在
get
后锁过期了, 此时别的客户端进行加锁操作, 这里的del
就会错误的将其他客户端加的锁解开.
Redis 中使用 Lua 脚本的注意点
↓ 这一段内容转载自 https://blog.csdn.net/zhouzme/article/details/53046606
注意点:
Redis 会把所有执行过的脚本都缓存在内存中
Redis 在重启的时候会释放掉之前保存的脚本
Lua 脚本中所需要用到的键名以及参数一定要使用 KEYS 和 ARGV 来替换,千万不要写死在代码中,除非你百分百确定每次请求时他们是固定不变的值,特别是涉及到 时间,随机数的,一定要用参数代入,因为 Redis 每次使用 script 都会校验脚本缓存中是否已存在相同脚本,否则就会存储到缓存中,如果你的脚本很长,且每次请求存在不同的变量值,则会生成无数多个脚本缓存,你将会发现Redis占用的内存会唰唰唰的往上涨,我一开始因为key 和 参数太多,分开写太麻烦了,就图省事方便,直接把变量拼接到脚本里面,结果发现内存不停的涨,很是抓狂,找了好久才发现是这么个原因。
Lua 中脚本定义变量一定要使用局部变量, 即
local var = 1
, 局部变量只在所定义的块(指控制结构, 函数或chunk等)内有效, 使用局部变量可以避免命名冲突 并且访问更快(lua中局部变量和全局变量存储方式是不一样的)如果Lua脚本写的比较长,非本地或局域网的情况下,建议使用 SHA 签名的方法来调用,这样节省带宽,但对性能似乎没什么直接的提升。这里对小白普及下我理解的原理就是 Redis 会把每个脚本都生成唯一签名,把脚本作为函数体,并使用该签名作为脚本的函数名放到缓存中,所以后面调用就只需要传一个 SHA 签名就可以调用该函数了,精简很多了。同一个脚本生成的签名都是相同的,所以SHA签名可以先在本地生成,然后在服务器上 script load 一次脚本,程序中只需保存和使用该签名即可。另外需要注意的是,脚本如果被改动哪怕一个换行或一个空格(这些容易被忽略或误操作)都必须重新 load 来获取新的 SHA
注意:获取 SHA 签名是单独的功能,不要放在你的正常流程中,当本地开发时就可以生成SHA,把字符串写死在流程中。同样的脚本,Reids是始终生成相同的签名的。
通过 eval 带入的 ARGV 参数如果原来是数字的,会被转换为字符串,如果你的逻辑中需要判断该变量 > 0 或 < 0 之类的数字判断则必须进行字符串到数字的转换,使用
tonumber()
方法if (tonumber(ARGV[1]) > 0) then return 1; end;
我测试了几个 lua script 与 PIPELINE 处理对比,发现 script 的效率一般比 PIPELINE 高 30% ~ 40% 左右
Redis集群分布式锁
Redis 集群相对单机来说, 需要考虑一个 容错性, 设计上更为复杂
由于这个我也从未实践过, 先贴一个官方的教程贴压压惊
https://github.com/antirez/redis-doc/blob/master/topics/distlock.md
对应的翻译: http://ifeve.com/redis-lock/
RedLock 算法
官方给出了一个 RedLock 算法
情景: 当前有N个完全独立的Redis master节点, 分别部署在不同的主机上
客户端获取锁的操作:
- 使用相同key和唯一值(作为value)同时向这N个redis节点请求锁, 锁的超时时间应该 >> 超时时间(考虑到请求耗时), 若某个节点阻塞了了应尽快跳过
- 计算步骤1消耗的时间, 若总消耗时间超过超时时间, 则认为锁失败. 客户端需在大多数(超过一半)的节点上成功获取锁, 才认为是锁成功.
- 如果锁成功了, 则该锁有效时间就是 锁原始有效时间 - 步骤1消耗的时间
- 如果锁失败了(超时或无法获取超过一半 N/2 + 1 实例的锁), 客户端会到每个节点释放锁(是每个, 即使之前认为加锁失败的节点)
[原创] PHP 使用Redis实现锁的更多相关文章
- 死磕 java同步系列之redis分布式锁进化史
问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...
- C# Redis分布式锁的应用 - 叶子栈 - SegmentFault 思否
原文:C# Redis分布式锁的应用 - 叶子栈 - SegmentFault 思否 叶子 1 微信扫一扫 新浪微博 Twitter Facebook C# Redis分布式锁的应用 c#redis分 ...
- redis分布式锁原理与实现
分布式锁原理 分布式锁,是控制分布式系统之间同步访问共享资源的一种方式.在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候, ...
- Redis分布式锁的原理和实现
前言 我们之前聊过redis的,对基础不了解的可以移步查看一下: 几分钟搞定redis存储session共享--设计实现:https://www.cnblogs.com/xiongze520/p/10 ...
- Redis修改数据多线程并发—Redis并发锁
本文版权归博客园和作者本人吴双共同所有 .转载爬虫请注明地址,博客园蜗牛 http://www.cnblogs.com/tdws/p/5712835.html 蜗牛Redis系列文章目录http:// ...
- 分布式应用下的Redis单机锁设计与实现
背景 最近写了一个定时任务,期望是同一时间只有一台机器运行即可.因为是应用是在集群环境下跑的,所以需要自己实现类一个简陋的Redis单机锁. 原理 主要是使用了Redis的SET NX特性,成功设置的 ...
- 利用redis分布式锁的功能来实现定时器的分布式
文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...
- Redis分布式锁
Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...
- redis分布式锁和消息队列
最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...
随机推荐
- MVC5数据库迁移命令!
首先数据库迁移在上下文里设置要设置成为CreateDatabaseIfNotExists, 然后在Nuget控制平台输入命令 在“程序包管理器控制台”窗口中输入:Enable-Migrations ...
- [原创]分享本人自己PY写的BOOST编译程序(源码)
本程序WINDOWS专用,只做抛砖引玉,希望诸位按照各自需求自行修改,主要目的是为了让诸位编译时可以省一些组合指令的时间,只需要修改几个参数即可自动编译. 支持64位编译模式. 改进版本:http:/ ...
- spring 启动完成后事件监听器处理
有时候我们在spring容器启动完成后,我们需要做一些处理动作,这个时候怎么做呢? spring提供了事件监听器的处理机制. spring提供了内置的几类的事件: ContextClosedEvent ...
- 以json格式打印实体类信息
1.pom.xml <dependency> <groupId>com.google.code.gson</groupId> <artifactId>g ...
- jQuery框架-3.jQuery自定义封装插件和第三方插件
一.jQuery的封装扩展 1.jQuery中extend方法使用 (挂在到jQuery和jQuery.fn两对象身上的使用) 1.1.官方文档定义: jQuery.extend Merge th ...
- 利用Word发布文章到博客
目前大部分的博客作者在用Word写博客这件事情上都会遇到以下3个痛点: 1.所有博客平台关闭了文档发布接口,用户无法使用Word,Windows Live Writer等工具来发布博客.使用Word写 ...
- arean.c
glibc-2.14中的arean.c源代码,供研究malloc和free实现使用: /* Malloc implementation for multiple threads without loc ...
- MySQL性能调优与架构设计——第10章 MySQL数据库Schema设计的性能优化
第10章 MySQL Server性能优化 前言: 本章主要通过针对MySQL Server(mysqld)相关实现机制的分析,得到一些相应的优化建议.主要涉及MySQL的安装以及相关参数设置的优化, ...
- CentOS 6.6 MySQL 8.0详细安装步骤
1.备份服务器上MySQL数据库 [root@localhost ] # mysqldump -h localhost -u root -proot --databases Surpass --rou ...
- HRBUST1213 单词接龙 2017-03-06 15:53 67人阅读 评论(0) 收藏
单词接龙 单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的"龙"(每个单词都最多在"龙&quo ...