本篇文章给大家分享了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监控的键值)

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

/**
  * 解单据锁
  * @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防止在与被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
  $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防止在与被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
  $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,解锁失败

以上就是本次给大家整理的全部内容,感谢大家对爱安网的支持。

最新资讯
人民日报海外版:知识付费缘何“遇冷”

人民日报海外版:知识付

随着用户在使用知识付费产品过程中对内容的辨别和筛选
“双11”再爆发显消费潜力 中国消费升级路径如何?

“双11”再爆发显消费

“数据上看,‘双十一’的爆发性增长仍在延续,这显示只要
苏宁拼购发力社交电商 新进者如何差异化破局?

苏宁拼购发力社交电商

在2018年苏宁智慧零售的版图中,苏宁拼购就是标志性产品
WeWork从软银获得30亿美元的新增投资

WeWork从软银获得30亿

根据路透社的一份投资者报告显示,美国共享办公空间提供
微软重新发布Windows 10系统2018年10月更新

微软重新发布Windows

微软在美国当地时间11月13日宣布,该公司将重新发布Wind
FF困兽犹斗 贾跃亭的IPO故事能否圈粉

FF困兽犹斗 贾跃亭的I

过去几个月,FF遇到了极大的资金流动性困难,公司不得不采
最新文章
php实现微信企业付款到个人零钱功能

php实现微信企业付款

这篇文章主要为大家详细介绍了php实现微信企业付款到
PHP实现微信对账单处理

PHP实现微信对账单处

这篇文章主要为大家详细介绍了PHP实现微信对账单处理,
php提取微信账单的有效信息

php提取微信账单的有

这篇文章主要为大家详细介绍了php提取微信账单的有效
php-fpm.conf配置文件中文说明详解及重要参数说明

php-fpm.conf配置文件

本文主要介绍了php-fpm.conf配置文件的中文说明详解以
PHP实现微信退款功能

PHP实现微信退款功能

这篇文章主要为大家详细介绍了PHP实现微信退款功能,具
PHP实现微信申请退款功能

PHP实现微信申请退款

这篇文章主要为大家详细介绍了PHP实现微信申请退款功