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

概念

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

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

应用场景

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

分布式锁的特点

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

可用的存储组件选择

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的监控过期,以及重入性问题。

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

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

相关文章

力扣每日一题 2024/3/24 零钱兑换

题目描述 用例说明 思路讲解 动态规划五步法 第一步确定dp数组的含义&#xff1a;dp[i]为凑到金额为i所用最少的硬币数量 第二步确定动态规划方程&#xff1a;凑足金额为j-coins[i]所需最少的硬币个数为dp[j-coins[i]]&#xff0c;那凑足金额为j所用的最少硬币数为dp[j-coin…

怎么将文件快速生成二维码?文件二维码的在线生成技巧

现在越来越多的人都开始通过二维码的方式来传递文件&#xff0c;将word、pdf、excel、pdf等格式的文件通过扫码的方式展示或者下载文件&#xff0c;这种方式有很多的优势&#xff0c;包括传播速度快成本低&#xff0c;只需要生成一张二维码图片&#xff0c;就可以让其他人能够同…

Prompt-RAG:在特定领域中应用的革新性无需向量嵌入的RAG技术

论文地址&#xff1a;https://arxiv.org/ftp/arxiv/papers/2401/2401.11246.pdf 原文地址&#xff1a;https://cobusgreyling.medium.com/prompt-rag-98288fb38190 2024 年 3 月 21 日 虽然 Prompt-RAG 确实有其局限性&#xff0c;但在特定情况下它可以有效地替代传统向量嵌入 …

QTableWidget删除单元格

如果单元格内有内容&#xff0c;可以使用函数selectedItems() 获取有内容行的一个链表 QList<QTableWidgetItem *> items ui->qtableWidget->selectedItems(); //选中有内容的行可选择有内容的行int count items.count();for(int i 0 ; i < count; i){ …

搭建vite项目

文章目录 Vite 是一个基于 Webpack 的开发服务器&#xff0c;用于开发 Vue 3 和 Vite 应用程序 一、创建一个vite项目二、集成Vue Router1.安装 vue-routernext插件2.在 src 目录下创建一个名为 router 的文件夹&#xff0c;并在其中创建一个名为 index.js 的文件。在这个文件中…

Docker的Ubuntu上的安装教程及相关命令

一、简介 Docker 是一个开源的应用容器引擎&#xff0c;可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;这个容器是完全使用沙箱机制&#xff08;限制容器内部对系统资源的访问&#xff09;&#xff0c;更重要的是容器性能开销极低。 正是因为…

element-ui radio-group 组件源码分享

接着上篇的 radio 组件源码分享&#xff0c;继续探索 radio-group 源码部分的实现过程&#xff0c;主要从以下四个方面来讲解&#xff1a; 1、el-radio-group 页面结构 2、el-radio-group 组件属性 3、el-radio-group 组件方法 4、核心代码部分 一、页面结构&#xff0c;如…

docker 不同架构镜像融合问题解决

1、背景 docker 作为目前容器的标准之一&#xff0c;但是对于多种架构的平台的混合编译支撑不是很好。因此衍生了镜像融合&#xff0c;分别将多种不同的架构构建好&#xff0c;然后将镜像进行融合上传。拉取镜像的会根据当前系统的架构拉取不同的镜像&#xff0c;也可以通过 -…

Linux内核err.h文件分析

在阅读和编写内核相关的代码时&#xff0c;经常会看到IS_ERR、ERR_PTR等函数。这些函数在内核头文件的err.h中。以我服务器的代码为例&#xff0c;内核版本为5.15。 这个文件的代码如下&#xff1a; /* SPDX-License-Identifier: GPL-2.0 */ #ifndef _LINUX_ERR_H #define _L…

基于nodejs+vue在线作业管理系统的设计与实现python-flask-django-php

这种个性化的网络系统管理更重视相互协调和管理合作,能激发管理者的创造性和主动性,这对在线作业管理系统来说非常有益。 关键词&#xff1a;在线作业管理系统&#xff0c;nodejs语言&#xff0c;express框架&#xff0c; 前端技术&#xff1a;nodejsvueelementui, Express 框架…

2、Spring CLI安装

安装 Spring CLI 提供了多种格式,让您选择自己喜欢的安装方法。可下载的制品可从发布页面获取。 二进制发行版 WindowsLinuxMac手动安装(Windows,其他自己看) spring-cli-standalone-<version>-windows.x86_64.zip - 打包了 x86 JDKspring-cli-installer-<versi…

Rust常用库之序列化和反序列化库serde(使用 Serde 处理json)

文章目录 Rust常用库之序列化和反序列化库serde&#xff08;使用 Serde 处理json&#xff09;什么是serde库设计使用 Serde 处理jsonr# 的使用 参考 Rust常用库之序列化和反序列化库serde&#xff08;使用 Serde 处理json&#xff09; 什么是serde库 官网&#xff1a;https:/…

易源堂梵仕哲品牌新品发布会

祥龙启新&#xff0c;非凡无际&#xff01;2024年3月16日&#xff0c;上海易源堂集团梵仕哲品牌新品发布会在有着“山水宁海,寿者之乡”称号的浙江宁海顺利召开&#xff1b;易源堂集团联合创始人集团副总经理李振雨、易源堂集团营销策划总监曹斌&#xff0c;易源堂副总经理姚军…

缓存Caffine

缓存Caffine Caffine介绍添加手动加载自动加载异步加载 驱逐基于容量基于时间基于引用 移除显式移除移除监听器 刷新计算Interner规范 Caffine介绍 ​ Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。 缓存和ConcurrentMap有点相似&#xff0c;但还是有…

mysql 用户管理-账户管理

学习了《mysql 用户管理-权限表》。接着学习更常用的的账户管理。 2&#xff0c;账户管理 MySQL提供许多语句用来管理用户账号,这些语句可以用来管理包括登录和退出MySQL服务器、创建用户、删除用户、密码管理和权限管理等内容。MySQL 数据库的安全性&#xff0c;需要通过账户管…

注册马来西亚商标常见问题

马来西亚商标法于1983年9月1日正式生效。这部商标法废除了马来亚、沙巴和沙捞越三地区各自的商标法规和申请程序&#xff0c;使马来西亚有了一部统一商标法。此外&#xff0c;马来西亚有关商标的法规包括1983年9月1日同时生效的《1983年商标法实施细则》。在马来西亚&#xff0…

Xcode-双架构arm64 x86_64编译

要启用通用构建&#xff0c;在最新版本的 Xcode 中&#xff0c;请打开您的项目设置&#xff0c;然后依次选择&#xff1a; 1. “Build Settings” 选项卡。 2. 在顶部输入框中输入 “Architectures”。 3. 在 “Architectures” 下拉列表中选择 “Other”。 4. 在输入框中输入 …

国内git最新版本下载链接2.44

git官网地址:Git - Downloading Package (git-scm.com) 蓝奏云: ​​​​​​gGit-2.44.0-64-bit.exe - 蓝奏云 git仓库地址:git/git: Git Source Code Mirror - This is a publish-only repository but pull requests can be turned into patches to the mailing list via …

有哪些方式适合保存token

保存token的最佳方式取决于具体的应用场景和需求。以下是几种常见的保存token的方式及其优缺点&#xff0c;以便您根据实际情况进行选择&#xff1a; HTTP Only Cookies&#xff1a; 优点&#xff1a;Cookies可以自动随HTTP请求发送&#xff0c;且HTTP Only属性可以防止JavaS…

2024常用接口抓包以及接口测试工具总结【建议收藏】

接口 统称为API&#xff0c;程序与程序之间的对接、交接。 接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点&#xff0c;主要是为了检验不同组件&#xff08;模块&#xff09;之间数据的传递是否正确&#xff0c;同时接口测试还要测试当前系统与第三方…