深入理解PHP+Redis实现分布式锁的相关问题
概念
PHP使用分布式锁,受语言本身的限制,有一些局限性。
- 通俗理解单机锁问题:自家的锁锁自家的门,只能保证自家的事,管不了别人家不锁门引发的问题,于是有了分布式锁。
- 分布式锁概念:是针对多个节点的锁。避免出现数据不一致或者并发冲突的问题,让每个节点确保在任意时刻只有一个节点能够对公共资源进行操作,单机的锁只能够单节点使用,多节点防不住。
- 核心原理:分布式锁的核心原理,就是在每个节点执行时,先去一个公共的地方判断是否持有锁,如果有锁就说明资源被占用,没锁就可以持有该资源。
- 通俗举例:多个部门,开部门会议,需要占用会议室的位置,发现会议室门关着,不知道里面有没有人,此时门外面有个牌子说明是会议中,还是会议结束,离老远就知道会议室是不是被占用了,避免会议竞争引起的错乱。
应用场景
- 分布式排它:保证只有一个节点被访问,常用于秒杀,等并发问题的处理。
- 分布式任务调度:在分布式任务调度系统中,多个节点可能会竞争执行同一个任务,使用分布式锁可以确保只有一个节点能够执行该任务,避免重复执行和冲突。
- 并发下数据库事务幻读问题:并发下的MySQL事务当中,插入数据前先判断有没有,没有再插入,从而避免重复,但是其它事务未提交,就检测不到(RR的隔离级别导致的),但是插入相同数据,又会导致唯一约束起作用从而报错,添加分布式锁,从而避免报错。(这场景适用于唯一约束冲突报错很多的场景功能,否则使用了会影响性能)。
分布式锁的特点
- 互斥性,相同时间,只能有一个节点会获取该锁,其它节点要么等待要么直接返回失败。
- 可重入(单个节点可重复获取该锁且不会发生阻塞),PHP的语言特性不支持。
- 安全(获取锁的节点崩溃或失去连接、锁资源会释放)。
可用的存储组件选择
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|false 成功返回数组,失败返回false* @other void*/public function addLock($lock_name, $ttl = 10000) {$val = base64_encode(openssl_random_pseudo_bytes(32));$set = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);if($set === false) {return false;}return ['key' => $lock_name, 'val' => $val];}/*** @function 阻塞式分布式锁* @param $key string 锁名称* @param $ttl int key自动过期时间,单位毫秒* @param $ttl int 超时时间,单位毫秒* @return array|false 成功返回数组,失败返回false* @other void*/public function addLockSpin($lock_name, $ttl = 10000, $timeout = 3000) {$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 false;}//尝试获取锁$set_loop = $this->redis->set($lock_name, $val, ['NX', 'PX' => $ttl]);if($set_loop) {return ['key' => $lock_name, 'val' => $val];}usleep(50000);}}return ['key' => $lock_name, 'val' => $val];}/*** @function 释放锁资源* @param $key array|false 锁资源* @return bool* @other void*/public function unLock($lock) {if($lock === false) {return false;}$script = 'if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0end';if(! $this->redis->eval($script, [$lock['key'], $lock['val']], 1)) {return false;}return true;}
}$redLock = new RedLock();
if($lock = $redLock->addLockSpin('test_key')) {echo '抢到锁了,处理一些业务逻辑';$redLock->unLock($lock); //记得及时释放锁资源
} else {echo '锁没有抢到';
}
现有的解决方案
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使用分布式锁的局限性问题
- 重入性无法实现:PHP这门语言有局限性,不适合和redis结合做分布式锁,分布式锁的重入性无法实现,因为脚本能执行完内存就被回收了,无法像C/C++那样轻松操控进程和线程。
- 超时问题没有监控机制:没有像redisson一样的watch dog看门狗的机制,去监控业务执行过长导致redis分布式锁自动释放,被其它锁占用的问题。
PHP使用分布式锁,有种东施效颦的感觉。
为什么加锁时set指令要加NX
set指令加nx表示,只有在key不存在的情况下才能设置键值对。
多个节点加锁,获取分布式锁资源,实质就是在redis中设置一条值。因为分布式锁的排它性,同一时间内只能有一个节点可以拿到该锁。
若用set,不加nx,就会产生覆盖,造成业务错乱。
客户端宕机导致锁资源无法释放的死锁问题
redis单线程通常不会发生死锁问题。
Redis在客户端挂掉的情况的情况,会导致分布式锁锁资源无法及时释放,这可能会导致其它节点无法加锁从而阻塞,类似死锁的效果。
添加过期时间做兜底即可。
对高可用:MySQL可以主从,Redis也可以,从而保证分布式锁存储的高可用性。
分布式锁redis操作的原子性问题
就算是redis事务(multi)也是弱事务,仍旧会出现并发安全问题,最好使用Lua+Redis的方式去实现原子性的分布式锁,这会把一些指令集当做一个任务队列去处理,保证原子性。
如何设置拿到锁资源后的超时时间
对于Java,redisson有watch dog的自动监控机制,但是PHP没有。
PHP也很难实现,原因有2:
- 不知道自动续期的时机:业务流程没走完,分布式锁临近过期才续期,业务流程走完了还续什么期?这个时机,高并发场景下难以获取,净增加复杂度。
- PHP语言本身缺少锁机制:就算知道了要续期,加锁与续期监控,缺少锁机制的强关联,加锁一个进程,监控又一个进程,进程间通信是一个问题,PHP进程间通信与Redis操作无法原子执行又是一个问题,也就是说就算被通知要续期了,再续期时,锁资源超时自动释放后,可能都被别的节点占用了。
PHP能做的只能是设置更多的超时时间,来防止锁资源自动释放被其它节点抢走。
缺点也很明显,一旦这个节点挂掉,锁资源需要很长时间才能释放,这个时间段的分布式锁无法被任意一个节点使用。
锁资源的错误释放问题
时序图:
| 步骤 | 客户端1 | 客户端2 | 补充 |
|---|---|---|---|
| 1 | 获取锁成功 | / | / |
| 2 | 执行中 | 获取锁失败 | 客户端1的锁阻塞了客户端2 |
| 3 | 执行中 | 获取锁失败 | 客户端2自旋,不断尝试获取锁 |
| 4 | 锁资源到期自动释放 | 获取锁成功 | 由于客户端1的锁资源过期,才导致客户端2拿到的分布式锁 |
| 5 | 释放锁 | 执行中 | 这一步才是客户端1真正释放(删)锁的时刻,但是由于没做验证,这个释放(删)的过程,会把会话2创建的锁给释放(删)掉,造成误删除 |
为了避免这个问题,val值可设置为节点标识。
所以redis在get值的时候,需要判断,val值是不是当前的节点标识。
为了保证原子性,查询和删除两个操作需要用Lua脚本。
其次要注意,不管节点程序执行成功或者失败,只要该走的流程走完了,都需要及时释放锁。
分布式锁的可重入问题
PHP解决不了。
假设同一个节点,递归或循环添加分布式锁,是否让同一节点重复加同一把锁,大部分场景不需要,但是也得看业务场景。
这种机制是为了避免第一层循环添加成功,之后失败的问题。
对于非PHP而言,重入问题,还需要再维持一个redis hash,key为锁名,field为节点的唯一标识,value为重入次数,重入1次次数加1。因为重入相当于重新获取锁,但是不会新增锁资源,如果这个时间被删掉,那么重入时会加锁成功,但锁资源被强制释放,此时重入后的业务逻辑还不一定执行完毕。所以删除时需要判断value值是否为0,如果不为0,说明有重入,这两步操作,也是需要再一个Lua脚本中。
分布式锁的自旋机制
自旋可以理解为内部死循环,内部不断重试,直到满足条件,直观感受就是被阻塞。
如果没有自旋,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('加锁成功') {return '加锁成功';}if($start_time + 5 <= microtime(true)) {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,整不好把原先的覆盖掉了。
- 对于像Java(PHP不行)语言:手动实现可能缺少key的监控过期,以及重入性问题。
相关文章:
深入理解PHP+Redis实现分布式锁的相关问题
概念 PHP使用分布式锁,受语言本身的限制,有一些局限性。 通俗理解单机锁问题:自家的锁锁自家的门,只能保证自家的事,管不了别人家不锁门引发的问题,于是有了分布式锁。分布式锁概念:是针对多个…...
perl:获取同花顺数据--业绩预告
perldoc LWP::UserAgent 如果没有安装,则安装模块,运行 cpanm LWP::UserAgent 。 编写 get_yjyg_10jqka.pl 如下 #!/usr/bin/perl # perl 获取同花顺数据--业绩预告 use LWP::UserAgent; use Encode qw(decode encode); use POSIX; use Data::Dump…...
如何对比引用传参和值传参两者的效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或…...
探索软件工程:构建可靠、高效的数字世界
软件工程是一门涵盖了设计、开发、测试、维护和管理软件的学科,它在如今数字化时代的发展中扮演着至关重要的角色。随着科技的不断进步和社会的不断变迁,软件工程的意义也愈发凸显。本文将探索软件工程的重要性、原则和实践,以及其对当今社会…...
超越肉眼:深入计算机视觉的奇妙之旅
揭秘计算机视觉的奥秘:从基础到前沿的探索之旅 引言:一、计算机视觉的基础1. 图像处理基础2. 特征提取与描述3. 基本模式识别 二、机器学习在计算机视觉中的应用1. 深度学习革命2. 迁移学习与多任务学习3. 强化学习与主动学习4. 无监督学习和自监督学习 …...
mac 安装 nvm 【真解决问题】
前提 没有node环境已有git 下载 我用的gitee极速下载 git clone https://gitee.com/mirrors/nvm.git ~/.nvm && cd ~/.nvm && git checkout git describe --abbrev0 --tags配置 1. 配置变量 在用户的目录下新增文件 .zshrc export NVM_DIR"$HOME/…...
【Godot 3.5控件】用TextureProgress制作血条
说明 本文写自2022年11月13日-14日,内容基于Godot3.5。后续可能会进行向4.2版本的转化。 概述 之前基于ProgressBar创建过血条组件。它主要是基于修改StyleBoxFlat,好处是它几乎可以算是矢量的,体积小,所有东西都是样式信息&am…...
第十届蓝桥杯大赛个人赛省赛(软件类)真题- CC++ 研究生组
第十届蓝桥杯大赛个人赛省赛(软件类)真题- C&C 研究生组-立方和 第十届蓝桥杯大赛个人赛省赛(软件类)真题- C&C 研究生组-字串数字 第十届蓝桥杯大赛个人赛省赛(软件类)真题- C&C 研究生组-质数…...
Linux:Gitlab:16.9.2 创建用户及项目仓库基础操作(2)
我在上一章介绍了基本的搭建以及邮箱配置 Linux:Gitlab:16.9.2 (rpm包) 部署及基础操作(1)-CSDN博客https://blog.csdn.net/w14768855/article/details/136821311?spm1001.2014.3001.5501 本章介绍一下用户的创建,组内设置用户&…...
【数据挖掘】实验5:数据预处理(1)
实验5:数据预处理(1) 一:实验目的与要求 1:熟悉和掌握数据预处理,学习数据清洗、数据集成、数据变换、数据规约、R语言中主要数据预处理函数。 二:实验内容 【缺失值分析】 第一步࿱…...
383.赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以,返回 true ;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。 思路:将magazine 中字…...
Python 3 教程(8)
heisenbug601 601***902@qq.com 参考地址 311 tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字: 代码如下: >>> classmates = (Michael, Bob, Tracy) 现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法…...
Mysql数据库深入理解
目录 一、什么是数据库 二、Mysql基本架构图 1.Mysql客户端/服务器架构 2.客户端与服务器的连接过程 3.服务器处理客户端请求 4.一条查询SQL执行顺序 4.1连接器 4.2查询缓存 4.3解析器 4.4执行器 4.4.1预处理阶段 4.4.2优化阶段 4.4.3执行阶段 5.一条记录如何存…...
android 音频焦点,音频策略梳理
音频焦点和音频策略两个不同的概念,容易搞混 先来看下音频焦点和音频策略直接的区别和联系 音频策略的主要功能是为该音频找到合适的硬件设备播放 1 音频策略流程: (从usage->device) attributesBuilder.setUsage--->audioservice.mCarAudioCont…...
go语言-基础元素与结构的使用
go基础元素与结构的使用,快速上手 编译go文件 编译为可执行文件 go build 文件名.go运行文件 ./文件名输入/输出 引用fmt库(关于输入输出的库) 输入 scanf按照给定的格式依次读取数据(包括非法数据),不…...
【leetcode热题】 二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出: [1,3,4]示例 2: 输入: [1,null,3] 输出: [1,3]示例 3: 输入: [] 输出: []解法一 题…...
康奋威科技邀您到场参观2024长三角快递物流展
参展企业介绍 杭州康奋威科技股份有限公司创立于2005年,由国家“万人计划”专家任天挺先生创立并担任法人,是一家专业从事智能装备研发与制造的国家级高新技术企业。专注于自动化控制、机械设计、信息化方面的技术研究,主要为太阳能光伏、智…...
linux centos 安装jenkins,并构建spring boot项目
首先安装jenkins,使用war包安装,比较简单,注意看下载的版本需要的JDK版本,官网下载https://www.jenkins.io/download/ 把下载好的war包放到服务器上,然后运行,注意8080端口的放行 # 前台运行并指定端口 ja…...
是德科技keysight DSOX3024T示波器
181/2461/8938产品概述: DSOX3024T 示波器 要特性与技术指标 使用电容触摸屏进行简洁的触控操作: •提高调试效率 •触控设计可以简化文档记录 •使用起来就像您喜欢的智能手机或平板电脑一样简单 使用 MegaZoom IV 技术揭示偶发异常: •超快…...
C#获取HTML源码
C#获取HTML源码 2024年03月23日记录 以前的那个从网上找到的方法, 在一些网站上用不了,如17K,取出来的是乱码,要么就是一坨JS,好像是用JS又重新加载了什么的 using System; using System.Collections.Generic; using System.We…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
【深度学习新浪潮】什么是credit assignment problem?
Credit Assignment Problem(信用分配问题) 是机器学习,尤其是强化学习(RL)中的核心挑战之一,指的是如何将最终的奖励或惩罚准确地分配给导致该结果的各个中间动作或决策。在序列决策任务中,智能体执行一系列动作后获得一个最终奖励,但每个动作对最终结果的贡献程度往往…...
Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践
前言:本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中,跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南,你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案,并结合内网…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
Vue 3 + WebSocket 实战:公司通知实时推送功能详解
📢 Vue 3 WebSocket 实战:公司通知实时推送功能详解 📌 收藏 点赞 关注,项目中要用到推送功能时就不怕找不到了! 实时通知是企业系统中常见的功能,比如:管理员发布通知后,所有用户…...
【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...
