PHP实现Redis单据锁以及防止并发重复写入


Posted in PHP onApril 10, 2018

一、写在前面:

在整个供应链系统中,会有很多种单据(采购单、入库单、到货单、运单等等),在涉及写单据数据的接口时(增删改操作),即使前端做了相关限制,还是有可能因为网络或异常操作产生并发重复调用的情况,导致对相同单据做相同的处理;

为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的单据锁,每个请求需先获取锁才能执行业务逻辑,执行结束后才会释放锁;保证了同一单据的并发重复操作请求只有一个请求可以获取到锁(依赖Redis的单线程),是一种悲观锁的设计;

注:Redis锁在我们的系统中一般只用于解决并发重复请求的情况,对于非并发的的重复请求一般会去数据库或日志校验数据的状态,两种机制结合起来才能保证整个链路的可靠。

二、加锁机制:

主要依赖Redis setnx指令实现:

PHP实现Redis单据锁以及防止并发重复写入

但使用setnx有一个问题,即setnx指令不支持设置过期时间,需要使用expire指令另行为key设置超时时间,这样整个加锁操作就不是一个原子性操作,有可能存在setnx加锁成功,但因程序异常退出导致未成功设置超时时间,在不及时解锁的情况下,有可能会导致死锁(即使业务场景中不会出现死锁,无用的key一直常驻内存也不是很好的设计);

这种情况可以使用Redis事务解决,把setnx与expire两条指令作为一个原子性操作执行,但这样做相对而言会比较麻烦,好在Redis 2.6.12之后版本,Redis set指令支持了nx、ex模式,并支持原子化地设置过期时间:

PHP实现Redis单据锁以及防止并发重复写入

三、加锁实现(完整测试代码会贴在最后):

/**
  * 加单据锁
  * @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监控的键值)

五、解锁实现(完整测试代码会贴在最后):

/**
  * 解单据锁
  * @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 相关文章推荐
PHP5 的对象赋值机制介绍
Aug 02 PHP
PHP生成随机用户名和密码的实现代码
Feb 27 PHP
linux命令之调试工具strace的深入分析
Jun 03 PHP
解析使用ThinkPHP应该掌握的调试手段
Jun 20 PHP
php汉字转拼音的示例
Feb 27 PHP
php生成固定长度纯数字编码的方法
Jul 09 PHP
解决PHP里大量数据循环时内存耗尽的方法
Oct 10 PHP
php实现购物车功能(下)
Jan 05 PHP
PHP GD库相关图像生成和处理函数小结
Sep 30 PHP
PHP实现基于面向对象的mysqli扩展库增删改查操作工具类
Jul 18 PHP
PHP基于递归实现的约瑟夫环算法示例
Aug 27 PHP
使用PHPUnit进行单元测试并生成代码覆盖率报告的方法
Mar 08 PHP
PHP使用zlib扩展实现GZIP压缩输出的方法详解
Apr 09 #PHP
基于CI(CodeIgniter)框架实现购物车功能的方法
Apr 09 #PHP
PHP缓存工具XCache安装与使用方法详解
Apr 09 #PHP
PHP+Session防止表单重复提交的解决方法
Apr 09 #PHP
PHP创建自己的Composer包方法
Apr 09 #PHP
Bootstrap+PHP实现多图上传功能实例详解
Apr 08 #PHP
PHP实现的获取文件mimes类型工具类示例
Apr 08 #PHP
You might like
WordPress中用于创建以及获取侧边栏的PHP函数讲解
2015/12/29 PHP
基于Jquery的文字自动截取(提供源代码)
2011/08/09 Javascript
js判断FCKeditor内容是否为空的两种形式
2013/05/14 Javascript
JS中自定义定时器让它在某一时刻执行
2014/09/02 Javascript
JS实现文字链接感应鼠标淡入淡出改变颜色的方法
2015/02/26 Javascript
javascript实现dom元素可拖动
2016/03/21 Javascript
JS原型链怎么理解
2016/06/27 Javascript
Javascript中获取浏览器类型和操作系统版本等客户端信息常用代码
2016/06/28 Javascript
BootStrap Table前台和后台分页对JSON格式的要求
2017/06/28 Javascript
详解Vue2中组件间通信的解决全方案
2017/07/28 Javascript
vue 引入公共css文件的简单方法(推荐)
2018/01/20 Javascript
vuex操作state对象的实例代码
2018/04/25 Javascript
Vue中在新窗口打开页面及Vue-router的使用
2018/06/13 Javascript
Vue项目添加动态浏览器头部title的方法
2018/07/11 Javascript
Bootstrap Table 双击、单击行获取该行及全表内容
2018/08/31 Javascript
jquery树形插件zTree高级使用详解
2019/08/16 jQuery
Vue.js组件props数据验证实现详解
2019/10/19 Javascript
vue3 源码解读之 time slicing的使用方法
2019/10/31 Javascript
vue-cli3中配置alias和打包加hash值操作
2020/09/04 Javascript
Python代码的打包与发布详解
2014/07/30 Python
Python爬取京东的商品分类与链接
2016/08/26 Python
详解使用pymysql在python中对mysql的增删改查操作(综合)
2017/01/18 Python
浅谈Python中的zip()与*zip()函数详解
2018/02/24 Python
关于python pycharm中输出的内容不全的解决办法
2020/01/10 Python
Tensorflow进行多维矩阵的拆分与拼接实例
2020/02/07 Python
pytorch实现MNIST手写体识别
2020/02/14 Python
python实现字符串和数字拼接
2020/03/02 Python
解决运行出现'dict' object has no attribute 'has_key'问题
2020/07/15 Python
matplotlib交互式数据光标mpldatacursor的实现
2021/02/03 Python
CSS3实现王者荣耀匹配人员加载页面的方法
2019/04/16 HTML / CSS
Saks Fifth Avenue澳洲/亚太地区:萨克斯第五大道精品百货店
2019/06/09 全球购物
幼儿园园长自我鉴定
2013/10/22 职场文书
违反学校规则制度检讨书
2015/01/01 职场文书
表扬通报怎么写
2015/01/16 职场文书
综合测评自我评价
2015/03/06 职场文书
用Python爬虫破解滑动验证码的案例解析
2021/05/06 Python