PHP 使用 Redis 实现分布式锁

Last-Modified: 2019年6月5日15:59:34php

参考连接

锁实现的注意点

  1. 互斥: 任意时刻, 只能有一个客户端得到锁
  2. 不会死锁: 客户端持有锁期间崩溃, 没有主动解除锁, 能保证后续的其余客户端得到锁
  3. 锁归属标识: 加锁和解锁的必须是同一个客户端, 客户端不能解掉非本身持有的锁(锁应具有标识)

若是是Redis集群, 还得考虑具备容错性: 只要大部分Redis节点正常运行, 客户端就能够加锁和解锁.git

如下只考虑 Redis单机部署的 场景.github

若是是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])

执行上面代码结果:数据库

  1. $key 对应的锁不存在, 进行加锁操做
  2. $key 对应的锁已存在, 什么也不作

加锁容易错误的点:缓存

  • 使用 setnxexpire 的组合

    缘由: 若在 setnx 后脚本崩溃会致使死锁服务器

$value 客户端标识的:分布式

  • 简单点就用 毫秒级unix时间戳 + 客户端标识(大部分状况下够用了)
  • 使用其余算法确保生成惟一随机值

connect 与 pconnect

在php中, 若使用 pconnect 链接redis, 则在当前脚本声明周期结束后, 与redis创建的链接仍会保留, 直到对应fpm进程的生命周期结束, 同时在下一次请求时, fpm会重用该链接.ide

即该链接的生命周期是 fpm 进程的生命周期, 而非一次php脚本的执行.

若代码使用 pconnect, close 的做用仅是使当前php脚本不能再进行redis请求, 并无真正关闭与redis的链接, 链接在后续请求中仍然会被重用.

pconnect函数在线程版本中不能被使用

clipboard.png

clipboard.png

上图中, php-fpm 与redis创建的链接并未随请求结束后立刻断开

解锁

php解锁示例: 使用lua脚本

$key = "...";
$identification = "...";
// KEYS 和 ARGV 是lua脚本中的全局变量
$script = <<< EOF
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    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 脚本中执行 getdel 是原子性的, 整个lua脚本会被当作一条命令来执行
  • 即便 get 后锁恰好过时, 此时也不会被其余客户端加锁
eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,而且直到eval命令执行完成,Redis才会执行其余命令。

因为 script 执行的原子性, 因此不要在script中执行过长开销的程序,不然会验证影响其它请求的执行。

解锁容易错误的点:

  • 直接 del 删除键

    缘由: 可能移除掉其余客户端加的锁(在本身的锁已过时状况下)

  • get判断锁归属, 若符合再 del

    缘由: 非原子性操做, 若在 get 后锁过时了, 此时别的客户端进行加锁操做, 这里的 del 就会错误的将其余客户端加的锁解开.

Redis 中使用 Lua 脚本的注意点

↓ 这一段内容转载自 https://blog.csdn.net/zhouzme...

注意点:

  1. Redis 会把全部执行过的脚本都缓存在内存中
  2. Redis 在重启的时候会释放掉以前保存的脚本
  3. Lua 脚本中所须要用到的键名以及参数必定要使用 KEYS 和 ARGV 来替换,千万不要写死在代码中,除非你百分百肯定每次请求时他们是固定不变的值,特别是涉及到 时间,随机数的,必定要用参数代入,由于 Redis 每次使用 script 都会校验脚本缓存中是否已存在相同脚本,不然就会存储到缓存中,若是你的脚本很长,且每次请求存在不一样的变量值,则会生成无数多个脚本缓存,你将会发现Redis占用的内存会唰唰唰的往上涨,我一开始由于key 和 参数太多,分开写太麻烦了,就图省事方便,直接把变量拼接到脚本里面,结果发现内存不停的涨,非常抓狂,找了很久才发现是这么个缘由。

    clipboard.png

义变量必定要使用局部变量, 即 local var = 1, 局部变量只在所定义的块(指控制结构, 函数或chunk等)内有效, 使用局部变量能够避免命名冲突 而且访问更快(lua中局部变量和全局变量存储方式是不同的)

  1. 若是Lua脚本写的比较长,非本地或局域网的状况下,建议使用 SHA 签名的方法来调用,这样节省带宽,但对性能彷佛没什么直接的提高。这里对小白普及下我理解的原理就是 Redis 会把每一个脚本都生成惟一签名,把脚本做为函数体,并使用该签名做为脚本的函数名放到缓存中,因此后面调用就只须要传一个 SHA 签名就能够调用该函数了,精简不少了。同一个脚本生成的签名都是相同的,因此SHA签名能够先在本地生成,而后在服务器上 script load 一次脚本,程序中只需保存和使用该签名便可。另外须要注意的是,脚本若是被改动哪怕一个换行或一个空格(这些容易被忽略或误操做)都必须从新 load 来获取新的 SHA

    注意:获取 SHA 签名是单独的功能,不要放在你的正常流程中,当本地开发时就能够生成SHA,把字符串写死在流程中。一样的脚本,Reids是始终生成相同的签名的。

    clipboard.png

  2. 经过 eval 带入的 ARGV 参数若是原来是数字的,会被转换为字符串,若是你的逻辑中须要判断该变量 > 0 或 < 0 之类的数字判断则必须进行字符串到数字的转换,使用 tonumber() 方法 if (tonumber(ARGV[1]) > 0) then return 1; end;
  3. 我测试了几个 lua script 与 PIPELINE 处理对比,发现 script 的效率通常比 PIPELINE 高 30% ~ 40% 左右

    clipboard.png

Redis集群分布式锁

Redis 集群相对单机来讲, 须要考虑一个 容错性, 设计上更为复杂

因为这个我也从未实践过, 先贴一个官方的教程贴压压惊

https://github.com/antirez/re...

对应的翻译: http://ifeve.com/redis-lock/

RedLock 算法

官方给出了一个 RedLock 算法

情景: 当前有N个彻底独立的Redis master节点, 分别部署在不一样的主机上

客户端获取锁的操做:

  1. 使用相同key和惟一值(做为value)同时向这N个redis节点请求锁, 锁的超时时间应该 >> 超时时间(考虑到请求耗时), 若某个节点阻塞了了应尽快跳过
  2. 计算步骤1消耗的时间, 若总消耗时间超过超时时间, 则认为锁失败. 客户端需在大多数(超过一半)的节点上成功获取锁, 才认为是锁成功.
  3. 若是锁成功了, 则该锁有效时间就是 锁原始有效时间 - 步骤1消耗的时间
  4. 若是锁失败了(超时或没法获取超过一半 N/2 + 1 实例的锁), 客户端会到每一个节点释放锁(是每一个, 即便以前认为加锁失败的节点)
相关文章
相关标签/搜索