深入理解PHP+Redis实现分布式锁的相关问题

news/2024/4/29 14:37:49/文章来源:https://blog.csdn.net/qq_42342282/article/details/137117631

概念

PHP使用分布式锁,受语言本身的限制,有一些局限性。

  • 通俗理解单机锁问题:自家的锁锁自家的门,只能保证自家的事,管不了别人家不锁门引发的问题,于是有了分布式锁。
  • 分布式锁概念:是针对多个节点的锁。避免出现数据不一致或者并发冲突的问题,让每个节点确保在任意时刻只有一个节点能够对公共资源进行操作,单机的锁只能够单节点使用,多节点防不住。
  • 核心原理:分布式锁的核心原理,就是在每个节点执行时,先去一个公共的地方判断是否持有锁,如果有锁就说明资源被占用,没锁就可以持有该资源。
  • 通俗举例:多个部门,开部门会议,需要占用会议室的位置,发现会议室门关着,不知道里面有没有人,此时门外面有个牌子说明是会议中,还是会议结束,离老远就知道会议室是不是被占用了,避免会议竞争引起的错乱。

应用场景

  • 分布式排它:保证只有一个节点被访问,常用于秒杀,等并发问题的处理。
  • 分布式任务调度:在分布式任务调度系统中,多个节点可能会竞争执行同一个任务,使用分布式锁可以确保只有一个节点能够执行该任务,避免重复执行和冲突。
  • 并发下数据库事务幻读问题:并发下的MySQL事务当中,插入数据前先判断有没有,没有再插入,从而避免重复,但是其它事务未提交,就检测不到(RR的隔离级别导致的),但是插入相同数据,又会导致唯一约束起作用从而报错,添加分布式锁,从而避免报错。(这场景适用于唯一约束冲突报错很多的场景功能,否则使用了会影响性能)。

分布式锁的特点

  • 互斥性,相同时间,只能有一个节点会获取该锁,其它节点要么等待要么直接返回失败。
  • 可重入(单个节点可重复获取该锁且不会发生阻塞)。
  • 安全(获取锁的节点崩溃或失去连接、锁资源会释放)。

可用的存储组件选择

  • Redis、MySQL(乐观锁、或悲观锁)、ZooKeeper、Etcd、Memcache等存储组件都可以实现分布式锁。
  • ZooKeeper、Etcd是Java生态,PHP几乎不用。
  • Memcache很少用了,一般都会用redis。
  • MySQL性能比不了Redis,高并发过来容易被夯住,数据不会自动过期删除,需要逻辑判断。所以也不用。
  • 分布式锁要求高性能,和自动过期的兜底特性,所以用Redis的set命令刚好。
  • Redis分布式锁,又称为Redis Distributed Lock,也叫RedLock。

用Redis手动实现分布式锁(示例)

这是花十分钟写出来的例子,不建议商用!!!

class RedLock {//声明redisprivate $redis;/*** @function 构造方法初始化redis* @other    void*/public function __construct() {$redis = new Redis();$redis->connect('127.0.0.1', 6379);$this->redis = $redis;}/*** @function 非阻塞分布式锁* @param    $key string 锁名称* @param    $ttl int    key自动过期时间,单位毫秒* @return   array       返回操作的结果* @other    void*/public function addLock($lock_name, $ttl = 10000) {$lock_name = 'red_lock_' . $lock_name;$val = base64_encode(openssl_random_pseudo_bytes(32));$set = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);if($set === false) {return ['status' => false, 'msg' => '锁设置失败', 'key' => '', 'val' => ''];}return ['status' => true, 'msg' => '', 'key' => $lock_name, 'val' => $val];}/*** @function 阻塞式分布式锁* @param    $key string 锁名称* @param    $ttl int    key自动过期时间,单位毫秒* @param    $ttl int    超时时间,单位毫秒* @return   array       成功返回数组,失败返回false* @other    void*/public function addLockSpin($lock_name, $ttl = 10000, $timeout = 3000) {$lock_name = 'red_lock_' . $lock_name;$start = bcmul(microtime(true), 1000, 2);$val = base64_encode(openssl_random_pseudo_bytes(32));$set = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);if($set === false) {while(true) {//超时$start_loop = bcmul(microtime(true), 1000, 2);if(bcadd($start, $timeout, 2) <= $start_loop) {return ['status' => false, 'msg' => '超时', 'key' => '', 'val' => ''];}//尝试获取锁$set_loop = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);if($set_loop) {return ['status' => true, 'msg' => '', 'key' => $lock_name, 'val' => $val];}usleep(50000);}}return ['status' => true, 'msg' => '', 'key' => $lock_name, 'val' => $val];}/*** @function 释放锁资源* @param    $key array 锁资源* @return   bool* @other    void*/public function unLock($lock) {if($lock['status'] === false) {return false;}$script = <<<LUA_DEL_LOCKif redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0endLUA_DEL_LOCK;return $this->redis->eval($script, [$lock['key'], $lock['val']], 1) ? true : false;}
}
//调用端-------------------------------------------------------------------------------
$redLock = new RedLock();$lock = $redLock->addLockSpin('test_key', 10000, 3000);
if(! $lock['status']) {echo '锁没有抢到,原因:' . $lock['msg'];
} else {echo '抢到锁了,处理一些业务逻辑,然后释放锁资源';$redLock->unLock($lock);
}

现有的解决方案

java实现分布式锁有redisson,PHP也有自己的包。
看过一些博主的用PHP实现分布式锁,好多没有使用Lua,这没办法保证多条

Redis语句原子性的执行。

项目中能用到这种东西的,对于高可用、原子性、稳定性有很强的依赖,所以推荐使用成熟的扩展包。

安装

composer require signe/redlock-php
文档:https://packagist.org/packages/signe/redlock-php
执行之后看使用redis的monitor指令查看,发现用了Lua,说明这个包,兼顾了原子性的操作。
我这个是示例,记得无论最后执行成功还是失败,都记得及时释放锁资源。

非自旋写法

$server = new \Redis;
$server->connect('127.0.0.1', 6379);
$servers = [$server,];$redLock = new \RedLock\RedLock($servers);
$lock = $redLock->lock('my_resource_name', 10000);if($lock) {echo '加锁成功';$redLock->unlock($lock);
} else {echo '加锁失败';
}

自旋写法

$server = new \Redis;
$server->connect('127.0.0.1', 6379);
$servers = [$server,];$redLock = new \RedLock\RedLock($servers);
$lock = $redLock->lock('my_resource_name', 10000);if($lock) {echo '加锁成功';
//    $redLock->unlock($lock);
} else {while(true) {$lock2 = $redLock->lock('my_resource_name', 10000);if($lock2) {echo '加锁成功2';//运行某些代码$redLock->unlock($lock2);return '';} }
}

如果需要:拿到锁后,释放锁前,业务逻辑代码块再对拿到锁的分布式锁续期。

因为redis的key与val值都不变,只变动过期时间,所以使用PEXPIRE指令,也可使用PSETEX指令。

又需要防止这个锁自动过期,已经被其它节点占用,已经改成了其它节点的数据,所以value值需要验证是不是当前锁的value值。

两个操作为了保证原子性,就用到了Lua。

//$redLock = new \RedLock\RedLock($servers);
//$lock = $redLock->lock('my_resource_name', 20000);$script = 'if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("PEXPIRE", KEYS[1], KEYS[2])elsereturn 0end';
$server->eval($script, [$lock['resource'], '毫秒过期时间', $lock['token']], 2);

PHP使用分布式锁的局限性问

  • 超时问题没有监控机制:没有像redisson一样的watch dog看门狗的机制,去监控业务执行过长导致redis分布式锁自动释放,被其它锁占用的问题。 可能需要Swoole的异步才支持
    PHP使用分布式锁,有种照猫画虎的感觉。

为什么加锁时set指令要加NX

set指令加nx表示,只有在key不存在的情况下才能设置键值对。
多个节点加锁,获取分布式锁资源,实质就是在redis中设置一条值。因为分布式锁的排它性,同一时间内只能有一个节点可以拿到该锁。
若用set,不加nx,就会产生覆盖,造成业务错乱。

客户端宕机导致锁资源无法释放的死锁问题

redis单线程通常不会发生死锁问题。
Redis在客户端挂掉的情况的情况,会导致分布式锁锁资源无法及时释放,这可能会导致其它节点无法加锁从而阻塞,类似死锁的效果。
添加过期时间做兜底即可。

对高可用:MySQL可以主从,Redis也可以,从而保证分布式锁存储的高可用性。

分布式锁redis操作的原子性问题

用redis做抢购秒杀仍旧超卖,问题也是出在这里。你写的多条redis,看起来是针对同一条数据的操作,其实在并发情况下,是有间隙的

就算是redis事务(multi)也是弱事务,仍旧会出现并发安全问题,最好使用Lua+Redis的方式去实现原子性的分布式锁,这会把一些指令集当做一个任务队列去处理,保证原子性。
注意这里说的原子性,不是Redis事务的原子性,而是说操作同一数据要么都成功或者都失败,没有lua的加持,高并发情况下,分布式锁的释放,无法保证get和del的是同一条数据。
若不用Lua,举个例子:
高并发情况下:
get库存为10, decr库存你以为是9,实际上可能小于9,因为你get之后再decr库存,中间有间隙,可能已经被其它并发过来的请求decr过了,超卖的实质就是这样产生的。
用Lua写成一个整体,则可以保证这两个语句没有间隙。至于,成功则都成功,失败则都失败的原子性,靠的是Lua脚本的判断逻辑。

难道Redis事务没办法保证ACID吗,非要用Lua

无法保证。
redis的事务是弱事务BASE,ACID无法保证。
multi声明的事务,我认为叫批量执行命令(批处理)更好。官方给他起了个名称,叫管道(避免频繁的命令往来,造成的性能问题)。
管道的极简类比:买10瓶水,去10次超市,1次买1瓶的开销。和一次性买10瓶水,只去1次的开销。如果不利用管道,redis需要多次io,用户态到内核态的转变,对于进程上下文有影响,管道用来解决这个问题。
把multi当做乐观锁来用,那就是弱事务,当做批处理来用,那就是管道。

从底层分析MySQL与Redis事务:

  • 原子性(A):MySQL是由undo log实现的,也叫做回滚日志,支持原子性。Redis事务不支持(multi中遇到语法错误会整体回滚,但是遇到执行错误,例如incr string就不会全部回滚,正确的语句仍执行)。
  • 一致性(C):一致性是指从一个合法的状态变为另一个合法的状态,执行自增10,不会变成11,12,这一点MySQL可以保证,redis可以保证。
  • 隔离性(I):多个事务可以并发执行,各个事务之间的操作互相隔离。MySQL有MVCC机制对4种隔离级别提供的底层支撑,所以有快照读和当前读之分。Redis只有Watch实现的乐观锁可以保证,没有隔离级别的概念。
  • 持久性(D):MySQL持久化数据,数据是持久化到了页上,为了保证高可用出现了redo log(重做日志)机制,而redis虽然有AOF和RDB,但持久化机制不是实时的,实时对持久化是高可用,但会降低性能,而redis就是为了快,所以持久性有,无法保证高可用。

如何设置拿到锁资源后的超时时间

对于Java,redisson有watch dog的自动监控机制,但是PHP没有。
PHP也很难实现,原因有2:

  • 不知道自动续期的时机:业务流程没走完,分布式锁临近过期才续期,业务流程走完了还续什么期?这个时机,高并发场景下难以获取,净增加复杂度。
  • PHP语言本身缺少锁机制:就算知道了要续期,加锁与续期监控,缺少锁机制的强关联,加锁一个进程,监控又一个进程,进程间通信是一个问题,PHP进程间通信与Redis操作无法原子执行又是一个问题,也就是说就算被通知要续期了,再续期时,锁资源超时自动释放后,可能都被别的节点占用了。

PHP能做的只能是设置更多的超时时间,来防止锁资源自动释放被其它节点抢走。
缺点也很明显,一旦这个节点挂掉,锁资源需要很长时间才能释放,这个时间段的分布式锁无法被任意一个节点使用。

锁资源的错误释放问题

时序图:
在这里插入图片描述
为了避免这个问题,val值可设置为节点标识。
所以redis在get值的时候,需要判断,val值是不是当前的节点标识。
为了保证原子性,查询和删除两个操作需要用Lua脚本。

其次要注意,不管节点程序执行成功或者失败,只要该走的流程走完了,都需要及时释放锁。

分布式锁的可重入问题

  • 极简概括:单个进程(或线程),单节点可重复上锁,不用等待,避免死锁。这种机制是为了避免,在循环或者递归获取锁时,第一层循环成功,之后失败的问题。(大部分场景不需要这个重入性,某些场景才需要)。
  • 动作分析:看我自行封装的的代码,假设同一个节点,递归或循环获取分布式锁,就算是同一节点,获取分布式锁后,再次获取分布式锁,也得自旋,等自家节点的分布锁释放后,再获取锁,这个地方可以改进。
  • 实现思路:重入问题,还需要再维持一个redis hash,key为锁名,field为节点的唯一标识,value为重入次数,重入1次次数加1,释放重入1次次数减1,为了避免因业务逻辑耗时而导致锁过期,还需要给当前的锁续期。期间的多个操作,也需要在Lua脚本中执行。

不过对于PHP而言,以当前的认知来看,重入性用不上。 因为重入,相当于在获取锁的情况下,多次获取同一把锁,那直接在if(拿到锁){}这里写逻辑就行了是不是,何必多次拿到一样的锁。

分布式锁的自旋机制

自旋可以理解为内部死循环,内部不断重试,直到满足条件,直观感受就是被阻塞。
如果没有自旋,10个节点,只有1个能加锁成功,其余9个失败,如果这9个全部失败掉,看起来差点意思。

因此可以选择被阻塞,期间不断重试,所谓的自旋方案,其实很好理解,重试伪代码如下:

while(加锁失败) {usleep(10000);重新尝试加锁代码if(加锁成功) {return '加锁成功';}
}此处也可以添加一个次数限制,防止永久死循环的兜底策略
$retry_count = 0;
while(true) {$retry_count ++;if('加锁成功') {return '加锁成功';}if($retry_count > 20) {echo 1;return '重试次数过多';}usleep(30000);
}也可以根据时间去做限制,防止永久死循环的兜底策略
$start_time = microtime(true);
while(true) {if($start_time + 5 <= microtime(true)) {return '超时';	}if('加锁成功') {return '加锁成功';}usleep(30000);
}

Redis主从架构对分布式锁的高可用问题

节点1再master上获取到了分布式锁,叫lock1,此时master还没有同步到slave,结果master挂掉了。
此时故障转移,slave做顶梁柱,节点2也获取到了slave的分布式锁,也叫lock1。
这种情况违背了分布式锁的排它性。概率很小,但是有可能发生。
setnx无法解决分布式场景下的锁排它性问题。
这个是运维层面要考虑的东西。

手动实现分布式锁容易被忽略的问题

分布式锁这种工程化的东西,每个零件都有用,虽然RedLock底层用redis set指令实现。

  • 若忘记加超时时间:上锁的节点挂掉没有释放锁资源,其它节点会一直拿不到锁,严重影响业务。
  • 若忘记加value值判断去释放锁:A节点在执行业务逻辑超时,自动释放锁资源被B节点抢去,等A节点执行完业务代码后释放锁,会把B节点的锁删除。
  • 若忘记用Lua脚本:这导致redis在执行任务期间,同一客户端的多个脚本不会在一个Redis内置的任务队列处理,保证不了原子性,超卖的并发安全问题就是这样产生的。
  • 覆盖问题:redis分布式锁设置值时,用的setnx思想(有值则不设置,避免覆盖),若用set,整不好把原先的覆盖掉了。
  • 可能缺少过期锁自动续期机制:就对PHP而言,手动实现可能缺少key的监控过期,毕竟PHP没有像Java Redisson中那样的watch dog机制。
  • 若忘记重入性问题:这会导致多节点多次添加分布式锁,有阻塞或者失败的可能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1027737.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过Caliper进行压力测试程序,且汇总压力测试问题解决

环境要求 第一步. 配置基本环境 部署Caliper的计算机需要有外网权限;操作系统版本需要满足以下要求:Ubuntu >= 16.04、CentOS >= 7或MacOS >= 10.14;部署Caliper的计算机需要安装有以下软件:python 2.7、make、g++(gcc-c++)、gcc及git。第二步. 安装NodeJS # …

RegSeg 学习笔记(待完善)

论文阅读 解决的问题 引用别的论文的内容 可以用 controlf 寻找想要的内容 PPM 空间金字塔池化改进 SPP / SPPF / SimSPPF / ASPP / RFB / SPPCSPC / SPPFCSPC / SPPELAN &#xfffc; ASPP STDC&#xff1a;short-term dense concatenate module 和 DDRNet SE-ResNeXt …

初识React(一)从井字棋游戏开始

写在前面&#xff1a; 磨磨唧唧了好久终于下定决心开始学react&#xff0c;刚刚接触感觉有点无从下脚...新的语法新的格式跟vue就像两种物种...倒是很好奇路由和store是怎么实现的了~v~&#xff0c;一点一点来吧&#xff01;&#xff01;&#xff01; (一)创建项目 使用vite…

Reactor设计模式和Reactor模型

Reactor设计模式 翻译过来就是反应堆&#xff0c;所以Reactor设计模式本质是基于事件驱动。 角色 Handle&#xff08;事件&#xff09;EventHandler&#xff08;事件处理器&#xff09;ConcreteEventHandler&#xff08;具体事件处理器&#xff09;Synchronous Event Demult…

QT实现蒙层效果

一.蒙层的作用 1.为了其他窗口不被误操作&#xff0c;禁止对其他窗口操作 二.应用场景 1.一些触摸屏设备上弹出一个dialog窗口&#xff0c;在操作这个窗口的时候不希望后面的窗口被误操作 2.之前做一个医疗设备就曾有过这种需求&#xff0c;因为医疗设备对安全性要求非常高&…

利用 Scapy 库编写 ARP 缓存中毒攻击脚本

一、ARP 协议基础 参考下篇文章学习 二、ARP 缓存中毒原理 ARP&#xff08;Address Resolution Protocol&#xff09;缓存中毒是一种网络攻击&#xff0c;它利用了ARP协议中的漏洞&#xff0c;通过欺骗或篡改网络中的ARP缓存来实施攻击。ARP协议是用于将IP地址映射到物理MAC…

各大pdf转word软件都用的哪家的ocr引擎?

国内一般的PDF软件一般都调用某国际PDF原厂的OCR接口&#xff0c;但这家公司是主要做PDF&#xff0c;在OCR方面并不专注&#xff0c;一些不是很复杂的场景还能应付得过来&#xff0c;复杂一点的效果就强差人意了&#xff0c;推荐用金鸣表格文字识别系统&#xff0c;它主要有以下…

基于树莓派实现 --- 智能家居

最效果展示 演示视频链接&#xff1a;基于树莓派实现的智能家居_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Tr421n7BM/?spm_id_from333.999.0.0 &#xff08;PS&#xff1a;房屋模型的搭建是靠纸板箱和淘宝买的家居模型&#xff0c;户型参考了留学时短租的公寓~&a…

Linux repo基本用法: 搭建自己的repo仓库[服务端]

概述 Repo的使用离不开Git, Git 和 Repo 都是版本控制工具&#xff0c;但它们在使用场景和功能上有明显区别… Git 定义&#xff1a;Git 是一个分布式的版本控制系统&#xff0c;由 Linus Torvalds 为 Linux 内核开发而设计&#xff0c;现已成为世界上最流行的版本控制软件之…

【详细讲解PostCSS如何安装和使用】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d; 希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

Leetcode146. LRU 缓存

Every day a Leetcode 题目来源&#xff1a;146. LRU 缓存 解法1&#xff1a;哈希表 链表 代码&#xff1a; /** lc appleetcode.cn id146 langcpp** [146] LRU 缓存*/// lc codestart class LRUCache { private:unordered_map<int, list<pair<int, int>>:…

图解Kafka架构学习笔记(二)

kafka的存储机制 https://segmentfault.com/a/1190000021824942 https://www.lin2j.tech/md/middleware/kafka/Kafka%E7%B3%BB%E5%88%97%E4%B8%83%E5%AD%98%E5%82%A8%E6%9C%BA%E5%88%B6.html https://tech.meituan.com/2015/01/13/kafka-fs-design-theory.html https://feiz…

华为防火墙配置指引超详细(包含安全配置部分)以USG6320为例

华为防火墙USG6320 华为防火墙USG6320是一款高性能、高可靠的下一代防火墙,适用于中小型企业、分支机构等场景。该防火墙支持多种安全功能,可以有效抵御网络攻击,保护网络安全。 目录 华为防火墙USG6320 1. 初始配置 2. 安全策略配置 3. 防火墙功能配置 4. 高可用性配…

四种常用限流算法、固定窗口限流算法、滑动窗口限流算法、漏桶限流算法和令牌桶限流算法

什么是限流&#xff1f; 限流可以被视为服务降级的一种形式&#xff0c;其核心目标是通过控制输入和输出流量来保护系统。通常&#xff0c;一个系统的处理能力是可以预估的&#xff0c;为了确保系统的稳定运行&#xff0c;当流量达到预定的阈值时&#xff0c;必须采取措施限制进…

在宝塔面板中,为自己的云服务器安装SSL证书,为所搭建的网站启用https(主要部分攻略)

前提条件 My HTTP website is running Nginx on Debian 10&#xff08;或者11&#xff09; 时间&#xff1a;2024-3-28 16:25:52 你的网站部署在Debain 10&#xff08;或者11&#xff09;的 Nginx上 安装单域名证书&#xff08;默认&#xff09;&#xff08;非泛域名&#xf…

数据结构与算法(二)优先队列

数据结构与算法&#xff08;二&#xff09; 优先队列 一、优先队列的基本概念 我们的电脑总是运行着多个程序&#xff0c;电脑会给每个程序分配一个优先级&#xff0c;并首先执行下一个优先级更高的程序。在此情况下&#xff0c;可将其抽象为一个数据结构&#xff0c;该数据结构…

鸿蒙HarmonyOS开发-FA模型访问Stage模型DataShareExtensionAbility

无论FA模型还是Stage模型&#xff0c;数据读写功能都包含客户端和服务端两部分。 FA模型中&#xff0c;客户端是由DataAbilityHelper提供对外接口&#xff0c;服务端是由DataAbility提供数据库的读写服务。 Stage模型中&#xff0c;客户端是由DataShareHelper提供对外接口&…

【JavaEE】_Spring MVC项目获取URL中的参数

目录 1. 单参数 2. 多参数 1. 单参数 .java文件如下&#xff1a; package com.example.demo.controller;import com.example.demo.Person; import org.springframework.web.bind.annotation.*;import java.util.Arrays; import java.util.List;RequestMapping("/Para&…

SpringBoot Redis 之Lettuce 驱动

一、前言 一直以为SpringBoot中 spring-boot-starter-data-redis使用的是Jredis连接池&#xff0c;直到昨天在部署报价系统生产环境时&#xff0c;因为端口配置错误造成无法连接&#xff0c;发现报错信息如下&#xff1a; 一了解才知道在SpringBoot2.X以后默认是使用Lettuce作…

jmeter中参数加密

加密接口常用的方式有&#xff1a; MD5&#xff0c;SHA&#xff0c;HmacSHA RSA AES&#xff0c;DES&#xff0c;Base64 压测中有些参数需要进行加密&#xff0c;加密方式已接口文档为主。 MD5加密 比如MD5加密的接口文档&#xff1a; 请求URL&#xff1a;http://101.34.221…