PHP实现Redis单据锁,防止并发重复写入
一、写在前面
在整个供应链系统中,会有很多种单据(采购单、入库单、到货单、运单等等),在涉及写单据数据的接口时(增删改操作),即使前端做了相关限制,还是有可能因为网络或异常操作产生并发重复调用的情况,导致对相同单据做相同的处理;
为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的单据锁,每个请求需先获取锁才能执行业务逻辑,执行结束后才会释放锁;保证了同一单据的并发重复操作请求只有一个请求可以获取到锁(依赖Redis的单线程),是一种悲观锁的设计;
注:Redis锁在我们的系统中一般只用于解决并发重复请求的情况,对于非并发的的重复请求一般会去数据库或日志校验数据的状态,两种机制结合起来才能保证整个链路的可靠。
二、加锁机制
主要依赖Redis setnx指令实现:
但使用setnx有一个问题,即setnx指令不支持设置过期时间,需要使用expire指令另行为key设置超时时间,这样整个加锁操作就不是一个原子性操作,有可能存在setnx加锁成功,但因程序异常退出导致未成功设置超时时间,在不及时解锁的情况下,有可能会导致死锁(即使业务场景中不会出现死锁,无用的key一直常驻内存也不是很好的设计);
这种情况可以使用Redis事务解决,把setnx与expire两条指令作为一个原子性操作执行,但这样做相对而言会比较麻烦,好在Redis 2.6.12之后版本,Redis set指令支持了nx、ex模式,并支持原子化地设置过期时间:
三、加锁实现(完整测试代码会贴在最后)
/**
* 加单据锁
* @param int $intOrderId 单据ID
* @param int $intExpireTime 锁过期时间(秒)
* @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false
*/
public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
{
//参数校验
if (empty($intOrderId) || $intExpireTime <= 0) {
return false;
} //获取Redis连接
$objRedisConn = self::getRedisConn(); //生成唯一锁ID,解锁需持有此ID
$intUniqueLockId = self::generateUniqueLockId(); //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的)
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间)
$bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]); //加锁成功返回锁ID,加锁失败返回false
return $bolRes ? $intUniqueLockId : $bolRes;
}
四、解锁机制
解锁即比对加锁时的唯一lock id,如果比对成功,则删除key;需要注意的是,解锁整个过程中同样需要保证原子性,这里依赖redis的watch与事务实现;
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)
Redis watch与事务可参看简书文章:https://www.jianshu.com/p/361...
五、解锁实现(完整测试代码会贴在最后)
/**
* 解单据锁
* @param int $intOrderId 单据ID
* @param int $intLockId 锁唯一ID
* @return bool
*/
public static function releaseLock($intOrderId, $intLockId)
{
//参数校验
if (empty($intOrderId) || empty($intLockId)) {
return false;
} //获取Redis连接
$objRedisConn = self::getRedisConn(); //生成Redis key
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
$objRedisConn->watch($strKey);
if ($intLockId == $objRedisConn->get($strKey)) {
$objRedisConn->multi()->del($strKey)->exec();
return true;
}
$objRedisConn->unwatch();
return false;
}
六、附整体测试代码(此代码仅为简易版本)
<?php /**
* Class Lock_Service 单据锁服务
*/
class Lock_Service
{
/**
* 单据锁redis key模板
*/
const REDIS_LOCK_KEY_TEMPLATE = 'order_lock_%s'; /**
* 单据锁默认超时时间(秒)
*/
const REDIS_LOCK_DEFAULT_EXPIRE_TIME = 86400; /**
* 加单据锁
* @param int $intOrderId 单据ID
* @param int $intExpireTime 锁过期时间(秒)
* @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false
*/
public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
{
//参数校验
if (empty($intOrderId) || $intExpireTime <= 0) {
return false;
} //获取Redis连接
$objRedisConn = self::getRedisConn(); //生成唯一锁ID,解锁需持有此ID
$intUniqueLockId = self::generateUniqueLockId(); //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的)
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间)
$bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]); //加锁成功返回锁ID,加锁失败返回false
return $bolRes ? $intUniqueLockId : $bolRes;
} /**
* 解单据锁
* @param int $intOrderId 单据ID
* @param int $intLockId 锁唯一ID
* @return bool
*/
public static function releaseLock($intOrderId, $intLockId)
{
//参数校验
if (empty($intOrderId) || empty($intLockId)) {
return false;
} //获取Redis连接
$objRedisConn = self::getRedisConn(); //生成Redis key
$strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
$objRedisConn->watch($strKey);
if ($intLockId == $objRedisConn->get($strKey)) {
$objRedisConn->multi()->del($strKey)->exec();
return true;
}
$objRedisConn->unwatch();
return false;
} /**
* Redis配置:IP
*/
const REDIS_CONFIG_HOST = '127.0.0.1'; /**
* Redis配置:端口
*/
const REDIS_CONFIG_PORT = 6379; /**
* 获取Redis连接(简易版本,可用单例实现)
* @param string $strIp IP
* @param int $intPort 端口
* @return object Redis连接
*/
public static function getRedisConn($strIp = self::REDIS_CONFIG_HOST, $intPort = self::REDIS_CONFIG_PORT)
{
$objRedis = new Redis();
$objRedis->connect($strIp, $intPort);
return $objRedis;
} /**
* 用于生成唯一的锁ID的redis key
*/
const REDIS_LOCK_UNIQUE_ID_KEY = 'lock_unique_id'; /**
* 生成锁唯一ID(通过Redis incr指令实现简易版本,可结合日期、时间戳、取余、字符串填充、随机数等函数,生成指定位数唯一ID)
* @return mixed
*/
public static function generateUniqueLockId()
{
return self::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY);
}
} //test
$res1 = Lock_Service::addLock('666666');
var_dump($res1);//返回lock id,加锁成功
$res2 = Lock_Service::addLock('666666');
var_dump($res2);//false,加锁失败
$res3 = Lock_Service::releaseLock('666666', $res1);
var_dump($res3);//true,解锁成功
$res4 = Lock_Service::releaseLock('666666', $res1);
var_dump($res4);//false,解锁失败
PHP实现Redis单据锁,防止并发重复写入的更多相关文章
- 使用Redis分布式锁处理并发,解决超卖问题
一.使用Apache ab模拟并发压测 1.压测工具介绍 $ ab -n 100 -c 100 http://www.baidu.com/ -n表示发出100个请求,-c模拟100个并发,相当是100 ...
- 使用redis分布式锁解决并发线程资源共享问题
众所周知, 在多线程中,因为共享全局变量,会导致资源修改结果不一致,所以需要加锁来解决这个问题,保证同一时间只有一个线程对资源进行操作 但是在分布式架构中,我们的服务可能会有n个实例,但线程锁只对同一 ...
- Redis修改数据多线程并发—Redis并发锁
本文版权归博客园和作者本人吴双共同所有 .转载爬虫请注明地址,博客园蜗牛 http://www.cnblogs.com/tdws/p/5712835.html 蜗牛Redis系列文章目录http:// ...
- redis锁处理并发问题
redis锁处理并发问题 redis锁处理高并发问题十分常见,使用的时候常见有几种错误,和对应的解决办法. set方式 setnx方式 setnx+getset方式 set方式 加锁:redis中se ...
- Java使用Redis实现分布式锁来防止重复提交问题
如何用消息系统避免分布式事务? - 少年阿宾 - BlogJavahttp://www.blogjava.net/stevenjohn/archive/2018/01/04/433004.html [ ...
- 用压测模拟并发、并发处理(synchronized,redis分布式锁)
使用工具:Apache an 测压命令: ab -n 100 -c 100 http://www.baidu.com -n代表模拟100个请求,-c代表模拟100个并发,相当于100个人同时访问 ab ...
- 应用Redis分布式锁解决重复通知的问题
研究背景: 这几天被支付宝充值后通知所产生的重复处理问题搞得焦头烂额, 一周连续发生两次重复充钱的杯具, 发事故邮件发到想吐..为了挽回程序员的尊严, 我用了Redis的锁机制. 事故场景: 支付宝下 ...
- 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...
- Redis 分布式锁的实现
0X00 测试环境 CentOS 6.6 + Redis 3.2.10 + PHP 7.0.7(+ phpredis 4.1.0) [root@localhost ~]# cat /etc/issue ...
随机推荐
- px转rem vue vscode
1.vscode中安装px2rem 2.打开settings.json ,新增 "px2rem.rootFontSize": 75, 3.重启vscode 4.可以转换了
- Python(一)对 meta class 的理解
1. 理解 class 对于 class 来说,表示一个代码块规定了实例化后的 object 的属性和方法 但是在 Python 中,class 本身也是对象.定义一个 class,就相当于在内存中 ...
- Gamma阶段第八次scrum meeting
每日任务内容 队员 昨日完成任务 明日要完成的任务 张圆宁 #91 用户体验与优化https://github.com/rRetr0Git/rateMyCourse/issues/91(持续完成) # ...
- prometheus添加自定义监控与告警(etcd为例)
一.步骤及注意事项(前提,部署参考部署篇) 一般etcd集群会开启HTTPS认证,因此访问etcd需要对应的证书 使用证书创建etcd的secret 将etcd的secret挂在到prometheus ...
- 第七节:Asp.Net Core内置日志和整合NLog(未完)
一. Asp.Net Core内置日志 1. 默认支持三种输出方式:控制台.调试(底部输出窗口).EventSource,当然也可以在Program类中通过logging.ClearProviders ...
- Effective.Java第56-66条(规范相关)
56. 为所有已公开的API元素编写文档注释 要正确地记录API,必须在每个导出的类.接口.构造方法.方法和属性声明之前加上文档注释.如果一个类是可序列化的,还需要记录它的序列化形式. 文档注释在源 ...
- Centos 使用kubeadm安装Kubernetes 1.15.3
本来没打算搞这个文章的,第一里面有瑕疵(没搞定的地方),第二在我的Ubuntu 18 Kubernetes集群的安装和部署 以及Helm的安装 也有安装,第三 和社区的问文章比较雷同 https:// ...
- SPA单页面应用和MPA多页面应用(转)
原文:https://www.jianshu.com/p/a02eb15d2d70 单页面应用 第一次进入页面时会请求一个html文件,刷新清除一下,切换到其他组件,此时路径也相应变化,但是并没有新的 ...
- FusionInsight大数据开发学习总结(1)
FusionInsight大数据开发 FusionInsight HD是一个大数据全栈商用平台,支持各种通用大数据应用场景. 技能需求 扎实的编程基础 Java/Scala/python/SQL/sh ...
- 如何判断两个IP地址是不是处于同一网段?
个人理解,欢迎指正. 一.要判断两个IP地址是不是在同一个网段,就将它们的IP地址分别与子网掩码做与运算,得到的结果-->网络号,如果网络号相同, 就在同一子网,否则,不在同一子网. 例:假定选 ...