php+redis消息队列实现抢购功能


Posted in PHP onFebruary 08, 2018

本文实例为大家分享了php+redis消息队列实现抢购的具体代码,供大家参考,具体内容如下

实现功能:

1. 基于redis队列,防止高并发的超卖
2. 基于mysql的事务加排它锁,防止高并发的超卖

基于redis队列工作流程:

1. 管理员根据goods表中的库存,创建redis商品库存队列
2. 客户端访问秒杀API
3. web服务器先从redis的商品库存队列中查询剩余库存重点内容
4. redis队列中有剩余,则在mysql中创建订单,去库存,抢购成功
5. redis队列中没有剩余,则提示库存不足,抢购失败重点内容

基于mysql事务和排它锁工作流程:

1. 开启事务
2. 查询库存,并显示的设置写锁(排他锁):SELECT * FROM goods WHERE id = 1 FOR UPDATE
3. 生成订单
4. 去库存,隐示的设置写锁(排他锁):UPDATE goods SET counts = counts ? 1 WHERE id = 1
5. commit,释放锁

注意:第二步步可以设置共享锁,不然有可能会造成死锁。

代码:

<?php
/**********************************************
* 抢购模块
*
* @author liubin
* @date 2016-02-10
*
* ab -n 1000 -c 100 http://192.168.16.73/Seckill/buy.php
*
*/
class seckill extends common
{

 private $_orderModel = null;
 private $_goodsModel = null;
 private $_redis = null;
 /*
  * 错误信息
 */
 protected $_error = '';
 /**
  * 构造器
  *
 */
 public function __construct()
 {
  if($this->_orderModel === null){
   $this->_orderModel = new OrderModel();
  }
  if($this->_goodsModel === null){
   $this->_goodsModel = new GoodsModel();
  }
  if($this->_redis === null){
   $this->_redis = new QRedis(); 
  }
 }
 /*
  * 秒杀API
  * 
  * @author liubin
  * @date 2017-02-10
 */
 public function addQsec(){
  $gid = intval($_GET['gid']);
  $type = isset($_GET['type']) ? $_GET['type'] : 'mysql';
  switch ($type) {
   case 'mysql':
    $this->order_check_mysql($gid);
    echo $this->getError();
    break;
   case 'redis':
    $this->order_check_redis($gid);
    echo $this->getError();
    break;
   case 'transaction':
    $this->order_check_transaction($gid);
    echo $this->getError();
    break;
   default:
    echo '类型错误';
    break;
  }
 }
 /*
  * 获取错误信息
  * 
  * @author liubin
  * @date 2017-02-10
 */
 public function getError(){
  return $this->_error;
 }
 /*
  * 基于mysql验证库存信息
  * @desc 高并发下会导致超卖
  *
  * @author liubin
  * @date 2017-02-10
 */
 protected function order_check_mysql($gid){


  $model = $this->_goodsModel;
  $pdo = $model->getHandler();
  $gid = intval($gid);

  /*
   * 1:$sql_forlock如果不加事务,不加写锁:
   * 超卖非常严重,就不说了
   * 
   * 2:$sql_forlock如果不加事务,只加写锁:
   * 第一个会话读$sql_forlock时加写锁,第一个会话$sql_forlock查询结束会释放该行锁.
   * 第二个会话在第一个会话释放后读$sql_forlock的写锁时,会再次$sql_forlock查库存
   * 导致超卖现象产生
   *
  */
  $sql_forlock = 'select * from goods where id = '.$gid .' limit 1 for update';
  //$sql_forlock = 'select * from goods where id = '.$gid .' limit 1';
  $result = $pdo->query($sql_forlock,PDO::FETCH_ASSOC);
  $goodsInfo = $result->fetch();

  if($goodsInfo['counts']>0){

   //去库存
   $gid = $goodsInfo['id'];
   $sql_inventory = 'UPDATE goods SET counts = counts - 1 WHERE id = '.$gid;
   $result = $this->_goodsModel->exect($sql_inventory);
   if($result){
    //创订单
    $data    = [];
    $data['order_id'] = $this->_orderModel->buildOrderNo();
    $data['goods_id'] = $goodsInfo['id'];
    $data['addtime'] = time();
    $data['uid']  = 1;
    $order_rs = $this->_orderModel->create_order($data);
    if($order_rs){
     $this->_error = '购买成功';
     return true;
    }
   }
  }

  $this->_error = '库存不足';
  return false;

 }
 /*
  * 基于redis队列验证库存信息
  * @desc Redis是底层是单线程的,命令执行是原子操作,包括lpush,lpop等.高并发下不会导致超卖
  *
  * @author liubin
  * @date 2017-02-10
 */
 protected function order_check_redis($gid){
  $goodsInfo = $this->_goodsModel->getGoods($gid);
  if(!$goodsInfo){
   $this->_error = '商品不存在';
   return false;
  }
  $key = 'goods_list_'.$goodsInfo['id'];
  $count = $this->_redis->getHandel()->lpop($key);
  if(!$count){
   $this->_error = '库存不足';
   return false;
  }
  //生成订单
  $data    = [];
  $data['order_id'] = $this->_orderModel->buildOrderNo();
  $data['goods_id'] = $goodsInfo['id'];
  $data['addtime'] = time();
  $data['uid']  = 1;
  $order_rs = $this->_orderModel->create_order($data);

  //库存减少
  $gid = $goodsInfo['id'];
  $sql = 'UPDATE goods SET counts = counts - 1 WHERE id = '.$gid;
  $result = $this->_goodsModel->exect($sql);
  $this->_error = '购买成功';
  return true;
 }
 /*
  * 基于mysql事务验证库存信息
  * @desc 事务 和 行锁 模式,高并发下不会导致超卖,但效率会慢点
  * @author liubin
  * @date 2017-02-10


  说明:
  如果$sql_forlock不加写锁,并发时,$sql_forlock查询的记录存都大于0,可以减库存操作.
  如果$sql_forlock加了写锁,并发时,$sql_forlock查询是等待第一次链接释放后查询.所以库存最多就是5

 */
 protected function order_check_transaction($gid){

  $model = $this->_goodsModel;
  $pdo = $model->getHandler();
  $gid = intval($gid);

  try{
   $pdo->beginTransaction();//开启事务处理


   /*
    * 1:$sql_forlock如果只加事务,不加写锁:
    * 开启事务
    * 因为没有加锁,读$sql_forlock后,并发时$sql_inventory之前还可以再读。
    * $sql_inventory之后和commit之前才会锁定
    * 出现超卖跟事务的一致性不冲突
    * 
    *
    * 2:$sql_forlock如果加了事务,又加读锁:
    * 开启事务
    * 第一个会话读$sql_forlock时加读锁,并发时,第二个会话也允许获得$sql_forlock的读锁,
    * 但是在第一个会话执行去库存操作时(写锁),写锁便会等待第二个会话的读锁,第二个会话执行写操作时,写锁便会等待第一个会话的读锁,
    * 出现死锁

    * 3:$sql_forlock如果加了事务,又加写锁:
    * 开启事务
    * 第一个会话读$sql_forlock时加写锁,直到commit才会释放写锁,并发查询不会出现超卖现象。
    *
   */

   $sql_forlock = 'select * from goods where id = '.$gid .' limit 1 for update';
   //$sql_forlock = 'select * from goods where id = '.$gid .' limit 1 LOCK IN SHARE MODE';
   //$sql_forlock = 'select * from goods where id = '.$gid .' limit 1';
   $result = $pdo->query($sql_forlock,PDO::FETCH_ASSOC);
   $goodsInfo = $result->fetch();

   if($goodsInfo['counts']>0){

    //去库存
    $gid = $goodsInfo['id'];
    $sql_inventory = 'UPDATE goods SET counts = counts - 1 WHERE id = '.$gid;
    $result = $this->_goodsModel->exect($sql_inventory);

    if(!$result){
     $pdo->rollBack();
     $this->_error = '库存减少失败';
     return false;
    }

    //创订单
    $data    = [];
    $data['id']   = 'null';
    $data['order_id'] = $this->_orderModel->buildOrderNo();
    $data['goods_id'] = $goodsInfo['id'];
    $data['uid']  = 'abc';
    $data['addtime'] = time();

    $sql = 'insert into orders (id,order_id,goods_id,uid,addtime) values ('.$data['id'].',"'.$data['order_id'].'","'.$data['goods_id'].'","'.$data['uid'].'","'.$data['addtime'].'")';   
    $result = $pdo->exec($sql);
    if(!$result){
     $pdo->rollBack();
     $this->_error = '订单创建失败';
     return false;
    }
    $pdo->commit();//提交
    $this->_error = '购买成功';
    return true;

   }else{
    $this->_error = '库存不足';
    return false;
   }
  }catch(PDOException $e){
   echo $e->getMessage();
   $pdo->rollBack();
  }


 }
 /*
  * 创建订单
  * mysql 事物处理,也可以用存储过程
  *
 */
 private function create_order($goodsInfo){
  //生成订单
  $data    = [];
  $data['order_id'] = $this->_orderModel->buildOrderNo();
  $data['goods_id'] = $goodsInfo['id'];
  $data['addtime'] = time();
  $data['uid']  = 1;
  $order_rs = $this->_orderModel->create_order($data);

  //库存减少
  $gid = $goodsInfo['id'];
  $sql = 'UPDATE goods SET counts = counts - 1 WHERE id = '.$gid;
  $result = $this->_goodsModel->exect($sql);
  return true;
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
基于文本的搜索
Oct 09 PHP
PHP在特殊字符前加斜杠的实现代码
Jul 17 PHP
PHP 无限分类三种方式 非函数的递归调用!
Aug 26 PHP
PHP的MVC模式实现原理分析(一相简单的MVC框架范例)
Apr 29 PHP
PHP解码unicode编码的中文字符代码分享
Aug 13 PHP
php清空(删除)指定目录下的文件,不删除目录文件夹的实现代码
Sep 04 PHP
PHP截取IE浏览器并缩小原图的方法
Mar 04 PHP
Centos6.5和Centos7 php环境搭建方法
May 27 PHP
php日期操作技巧小结
Jun 25 PHP
PHP的PDO预定义常量讲解
Jan 24 PHP
PHP5.5基于mysqli连接MySQL数据库和读取数据操作实例详解
Feb 16 PHP
PHP+Apache实现二级域名之间共享cookie的方法
Jul 24 PHP
PHP多线程模拟实现秒杀抢单
Feb 07 #PHP
PHP设计模式之装饰器模式实例详解
Feb 07 #PHP
PHP使用星号替代用户名手机和邮箱的实现代码
Feb 07 #PHP
PHP unlink与rmdir删除目录及目录下所有文件实例代码
Feb 07 #PHP
php删除一个路径下的所有文件夹和文件的方法
Feb 07 #PHP
浅析PHP类的反射来实现依赖注入过程
Feb 06 #PHP
php打开本地exe程序,js打开本地exe应用程序,并传递相关参数方法
Feb 06 #PHP
You might like
十天学会php之第十天
2006/10/09 PHP
PHP Session_Regenerate_ID函数双释放内存破坏漏洞
2011/01/27 PHP
谨慎使用PHP的引用原因分析
2012/09/06 PHP
php设计模式之单例模式使用示例
2014/01/20 PHP
PHP生成指定长度随机数最简洁的方法
2014/07/14 PHP
PHP date函数常用时间处理方法
2015/05/11 PHP
php数字每三位加逗号的功能函数
2015/10/22 PHP
JavaScript AJAX之惰性载入函数
2014/08/27 Javascript
使用C++为node.js写扩展模块
2015/04/22 Javascript
js实现图片点击左右轮播
2015/07/08 Javascript
BootStrap响应式导航条实例介绍
2016/05/06 Javascript
vue2 前后端分离项目ajax跨域session问题解决方法
2017/04/27 Javascript
js es6系列教程 - 基于new.target属性与es5改造es6的类语法
2017/09/02 Javascript
vue中配置mint-ui报css错误问题的解决方法
2017/10/11 Javascript
vue基于mint-ui的城市选择3级联动的示例
2017/10/25 Javascript
vue底部加载更多的实例代码
2018/06/29 Javascript
[41:52]2018DOTA2亚洲邀请赛3月29日小组赛B组Effect VS Secret
2018/03/30 DOTA
python比较两个列表大小的方法
2015/07/11 Python
Python浅复制中对象生存周期实例分析
2018/04/02 Python
通过python爬虫赚钱的方法
2019/01/29 Python
Win10下Python3.7.3安装教程图解
2019/07/08 Python
Django REST framwork的权限验证实例
2020/04/02 Python
python如何写try语句
2020/07/14 Python
Python faker生成器生成虚拟数据代码实例
2020/07/20 Python
详解如何使用rem或viewport进行移动端适配
2020/08/14 HTML / CSS
Julep官网:美容产品和指甲油
2017/02/25 全球购物
几个Linux面试题笔试题
2012/12/01 面试题
《乡愁》教学反思
2014/02/18 职场文书
历史专业大学生职业生涯规划书
2014/03/13 职场文书
导航工程专业自荐信
2014/09/02 职场文书
领导干部“四风”问题批评与自我批评材料
2014/09/24 职场文书
运动会表扬稿
2015/01/16 职场文书
2015年小学生国庆节演讲稿
2015/07/30 职场文书
师德师风培训感言
2015/08/03 职场文书
早恋主题班会
2015/08/14 职场文书
浅析Redis Sentinel 与 Redis Cluster
2021/06/24 Redis