PHP实现Redis单据锁以及防止并发重复写入,如何利用Redis锁解决高并发问题详解

redis技巧的施用:

后生可畏、写在前边:

redis真的是一个很好的本事,它能够很好的在必然水平上消除网站曾几何时的并发量,举例商品抢购秒杀等运动。。。

在全部供应链系统中,会有比很多种单子(购销单、入库单、到货单、运单等等卡塔尔国,在提到写单据数据的接口时(增删改操作卡塔 尔(阿拉伯语:قطر‎,就算前端做了有关限量,依旧有希望因为网络或非常操作产生并发重复调用的情景,招致对肖似单据做相通的处理;

redis之所以能消释高并发的案由是它能够直接访谈内部存储器,而现在大家用的是数据库(硬盘),升高了寻访功用,消除了数据库服务器压力。

为了堤防这种情景对系统变成拾壹分影响,大家经过Redis完结了三个简便的单据锁,每种恳求需先获得锁能力履行专业逻辑,推行完成后才会放出锁;保证了平等单据的现身重复操作央浼唯有二个倡议能够拿到到锁(信任Redis的单线程卡塔尔国,是豆蔻梢头种消极锁的布署;

缘何redis的身份进一层高,大家怎么不选用memcache,那是因为memcache只好存款和储蓄字符串,而redis存款和储蓄类型很充足(比如有字符串、LIST、SET等卡塔 尔(阿拉伯语:قطر‎,memcache每一种值最大不能不存款和储蓄1M,存款和储蓄能源特别简单,拾贰分消耗内部存款和储蓄器财富,而redis能够积攒1G,最首要的是memcache它比不上redis安全,当服务器发生故障恐怕意想不到关机等情事时,redsi会把内部存款和储蓄器中的数据备份到硬盘中,而memcache所存款和储蓄的东西尽数错过;那也表明了memcache不符合做数据库来用,能够用来做缓存。

注:Redis锁在咱们的系统中貌似只用于缓和现身重复须要的图景,对于非并发的的再次须求一般会去数据库或日志校验数据的情状,两种机制结合起来技能保障全数链路的笃定。

引言

二、加锁机制:

此间大家第后生可畏选取Redis的setnx的命令来拍卖高并发。

注重注重Redis setnx指令完结:

setnx
有七个参数。第二个参数表示键。第3个参数表示值。借使当前键一纸空文,那么会插入当前键,将第2个参数做为值。再次回到1。如若当前键存在,那么会重回0。

图片 1

创建库存表

但利用setnx有二个主题材料,即setnx指令不扶助设置过期时间,需求使用expire指令另行为key设置超时时间,那样全方位加锁操作就不是三个原子性操作,有极大概率存在setnx加锁成功,但因程序特别退出变成未得逞安装超时时间,在不马上解锁的意况下,有希望会形成死锁(固然工作场景中不会身不由己死锁,无用的key一直常驻内部存款和储蓄器亦非很好的安插卡塔 尔(英语:State of Qatar);

CREATE TABLE `storage` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `number` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

这种景况能够使用Redis事务消除,把setnx与expire两条指令作为三个原子性操作施行,但如此做相对来说会相比麻烦,万幸Redis
2.6.12过后版本,Redis
set指令协助了nx、ex格局,并辅助原子化地安装过期时间:

安装发轫仓库储存为10

图片 2

创办订单表

三、加锁完结(完整测量检验代码会贴在最终卡塔 尔(英语:State of Qatar):

CREATE TABLE `order` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `number` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
 /**
  * 加单据锁
  * @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;
 }

测量检验不用锁的时候

四、解锁机制:

$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$sql="select `number` from storage where id=1 limit 1";
$res = $pdo->query($sql)->fetch();
$number = $res['number'];
if($number>0)
{
 $sql ="insert into `order` VALUES (null,$number)";

 $order_id = $pdo->query($sql);
 if($order_id)
 {

 $sql="update storage set `number`=`number`-1 WHERE id=1";
 $pdo->query($sql);
 }
}

解锁即比对加锁时的唯风华正茂lock
id,假设比对成功,则删除key;供给小心的是,解锁整个经过中同样需求确定保证原子性,这里注重redis的watch与职业完成;

ab测量检验模拟并发,开掘仓库储存是不易的。

WATCH命令能够监督二个或八个键,意气风发旦中间有三个键被修正(或删除卡塔 尔(阿拉伯语:قطر‎,之后的政工就不会试行。监察和控制向来声音在耳边不断鸣响到EXEC命令(事务中的命令是在EXEC之后才实行的,所以在MULTI命令后得以纠正WATCH监控的键值卡塔尔国

mysql> select * from storage;
+----+--------+
| id | number |
+----+--------+
| 1 | 0 |
+----+--------+
1 row in set (0.00 sec)

五、解锁完成(完整测验代码会贴在最终卡塔尔国:

在来看订单表

/**
  * 解单据锁
  * @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;
 }
mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 1 | 10 |
| 2 | 10 |
| 3 | 9 |
| 4 | 7 |
| 5 | 6 |
| 6 | 5 |
| 7 | 5 |
| 8 | 5 |
| 9 | 4 |
| 10 | 1 |
+----+--------+
10 rows in set (0.00 sec)

六、附全体测验代码(此代码仅为简易版本卡塔尔

发觉存在多少个订单都以操作的同二个仓库储存数量,那样就或许孳生超卖的场合。

<?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,解锁失败

矫正代码参预redis锁进行数量调节

以上正是此番给咱们收拾的全体内容,谢谢我们对剧本之家的援助。

<?php
/**
 * Created by PhpStorm.
 * User: daisc
 * Date: 2018/7/23
 * Time: 14:45
 */
class Lock
{
 private static $_instance ;
 private $_redis;
 private function __construct()
 {
 $this->_redis = new Redis();
 $this->_redis ->connect('127.0.0.1');
 }
 public static function getInstance()
 {
 if(self::$_instance instanceof self)
 {
  return self::$_instance;
 }
 return self::$_instance = new self();
 }

 /**
 * @function 加锁
 * @param $key 锁名称
 * @param $expTime 过期时间
 */
 public function set($key,$expTime)
 {
 //初步加锁
 $isLock = $this->_redis->setnx($key,time()+$expTime);
 if($isLock)
 {
  return true;
 }
 else
 {
  //加锁失败的情况下。判断锁是否已经存在,如果锁存在切已经过期,那么删除锁。进行重新加锁
  $val = $this->_redis->get($key);
  if($val&&$val<time())
  {
  $this->del($key);
  }
  return $this->_redis->setnx($key,time()+$expTime);
 }
 }


 /**
 * @param $key 解锁
 */
 public function del($key)
 {
 $this->_redis->del($key);
 }

}



$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$lockObj = Lock::getInstance();
//判断是能加锁成功
if($lock = $lockObj->set('storage',10))
{
 $sql="select `number` from storage where id=1 limit 1";
 $res = $pdo->query($sql)->fetch();
 $number = $res['number'];
 if($number>0)
 {
 $sql ="insert into `order` VALUES (null,$number)";

 $order_id = $pdo->query($sql);
 if($order_id)
 {

  $sql="update storage set `number`=`number`-1 WHERE id=1";
  $pdo->query($sql);
 }
 }
 //解锁
 $lockObj->del('storage');

}
else
{
 //加锁不成功执行其他操作。
}

您恐怕感兴趣的篇章:

  • PHP使用Redis长连接的点子安详严整
  • PHP基于redis计数器类定义与用法示例
  • php+redis完毕商号秒杀功用
  • php+redis消息队列完毕抢购功用
  • PHP+Redis 音信队列
    完成高并发下注册人数总括的实例
  • php
    Redis函数用法实例总括【附php连接redis单例类】
  • redis
    代替php文件存款和储蓄session的实例
  • yii框架redis结合php完成秒杀效果(实例代码)
  • ubuntu 系统上为php加上redis
    扩充的达成方式
  • PHP使用Redis实现幸免大并发下贰遍写入的章程
  • 详整thinkphp+redis+队列的落到实处代码
  • PHP数据库操作三:redis用法解析

双重开展ab测量检验,查看测量试验结果

mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 1 | 10 |
| 2 | 9 |
| 3 | 8 |
| 4 | 7 |
| 5 | 6 |
| 6 | 5 |
| 7 | 4 |
| 8 | 3 |
| 9 | 2 |
| 10 | 1 |
+----+--------+
10 rows in set (0.00 sec)

发掘订单表未有操作同三个仓库储存数量的情况。所以选取redis锁是足以有效的拍卖高并发的。

那边在加锁的时候其实是能够无需看清过期光阴的,这里大家为了防止变成死锁,所以加叁个逾期时光的论断。当过期的时候主动删除该锁。

总结

如上正是这篇小说的全部内容了,希望本文的剧情对咱们的读书恐怕干活富有自然的参阅学习价值,如若不寻常我们能够留言交换,感激我们对台本之家的协助。

您也许感兴趣的稿子:

  • 详整Java怎样促成基于Redis的布满式锁
  • Redis达成布满式锁的二种方法总计
  • redis达成加锁的三种格局事必躬亲详细明白
  • Redis上达成分布式锁以增加质量的方案研讨
  • Redis数据库中完毕分布式锁的措施
  • 解锁redis锁的科学姿势
  • php结合redis达成高并发下的抢购、秒杀功用的实例
  • Redis高并发难点的解决办法
  • Redis须臾时高并发秒杀方案总括

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注