分享

使用redis解决一些并发访问的问题

 WindySky 2018-02-25

redis的一些锁机制以及事务机制,可以高效地解决并发访问以及抢购问题,这里举例说明一下

这里模拟并发抢购的实现思路:

1.竞拍的物品预先设定一个订单号

2.很多用户针对该订单的物品下单

3.先下单的能抢购成功、后下单的抢购失败

4.先下单的如果处理失败,则别人可以继续抢购


  1. <?php  
  2. header('Content-Type: text/html;charset=utf-8');  
  3.     //操作redis的类  
  4. class Test_redis   
  5. {  
  6.     private $redis = null;  
  7.     public function __construct() {  
  8.         $this->redis = new Redis();  
  9.         $this->redis->connect('192.168.1.102',6379);  
  10.     }  
  11.       
  12.     public function test() {      
  13.         $this->redis->set('name','areyouok');  
  14.         $output = $this->redis->get('name');  
  15.         var_dump($output);  
  16.     }  
  17.     /** 
  18.      * 测试方法是否存在 
  19.      * @param $methodName string 方法名 
  20.      */  
  21.     public function isMethodExist($methodName)  
  22.     {  
  23.         $refObj = new ReflectionClass($this->redis);  
  24.         //var_dump($refObj->getMethods());  
  25.         $result = $refObj->hasMethod($methodName);  
  26.         var_dump($result);  
  27.     }  
  28.       
  29.     /** 
  30.      * 使用setnx模拟并发抢购加锁机制 
  31.      * @param int $orderId 拍卖的单据号 
  32.      */  
  33.     public function  buy_lock($orderId,$userId)  
  34.     {         
  35.         $ttl  = 1800;//针对此订单号在30分钟后会关闭,关闭后不允许下单  
  36.         $orderId = (int) $orderId;  
  37.         $userId = (int) $userId;  
  38.         $key = 'lock_order_'.$orderId;  //针对该订单号加锁  
  39.   
  40.         $ok = $this->redis->setnx($key, 1); //如果key写入值则返回false  
  41.         if ($ok) {  
  42.             $this->redis->expire($key,$ttl);      //设置key在30分钟后失效,假定针对此次抢购共30分钟  
  43.             echo "redis get({$key})".$this->redis->get($key)."<br/>";  
  44.             echo "setnx {$key} is successful<br/>";  
  45.                 $ret = $this->update();       //模拟将订单写入数据库,执行方法后成功返回true,失败返回false  
  46.                 var_dump($ret);  
  47.                 if (!$ret) {                  
  48.                     $this->redis->del($key); //执行失败后删除当前key,释放锁,其他用户可以继续下单  
  49.                 } else {  
  50.                              //todo:关闭该订单,关闭改单后不允许再下单  
  51.                         }         
  52.         } else {  
  53.             echo "setnx {$key} is failed<br/>";//抢购失败  
  54.         }  
  55.     }  
  56.       
  57.   
  58.     /** 
  59.      * 测试redis消息订阅功能,先订阅一个频道才能收到该频道的消息 
  60.      */  
  61.     public function test_subscribe()  
  62.     {  
  63.         $this->redis->setOption(Redis::OPT_READ_TIMEOUT, -1);  
  64.         echo '222';  
  65.         $this->redis->subscribe(array('redischat'), array($this, 'process_msg'));  
  66.     }  
  67.   
  68.     public function process_msg($redis, $chan, $msg)  
  69.     {  
  70.         echo $chan.'|'.$msg."\n";  
  71.     }  
  72.   
  73.     /** 
  74.      * 测试redis发布消息功能 
  75.      */  
  76.     public function test_publish()  
  77.     {  
  78.         for($i=1; $i<10; ++$i) {  
  79.             $this->redis->publish('redischat', 'user'.$i.' is bidding, price is:'.(7000+$i*200));  
  80.         }  
  81.     }  
  82.   
  83.     /** 
  84.      * 模拟将竞拍出价写入数据库的过程 
  85.      */  
  86.     private function update()  
  87.     {  
  88.         //usleep(1000);  
  89.         $random = mt_rand(1,100000);  
  90.         if($random % 2 === 0) {  
  91.             return true;  
  92.         }else {  
  93.             return false;  
  94.         }  
  95.     }  
  96.       
  97.     /** 
  98.      * 操作list类型 
  99.      */  
  100.     public function operateList()  
  101.     {  
  102.         $task1Key = 'needToProcess';  
  103.         $task2Key = 'Processing';  
  104.           
  105.         $taskArr = array(['exec_time'=>1,'orderId'=>'123'],['exec_time'=>1,'orderId'=>'124'],['exec_time'=>1,'orderId'=>'125'],['exec_time'=>1,'orderId'=>'126']);  
  106.         foreach($taskArr as $task) {  
  107.             $this->redis->rpush($task1Key,json_encode($task));  
  108.         }  
  109.         $taskList = $this->redis->lRange($task1Key,0,-1);  
  110.         foreach($taskList as $taskJson) {  
  111.             $this->redis->rPush($task2Key,$taskJson);     //将内容插入另一个队列  
  112.             $this->redis->lRem($task1Key,$taskJson,1);        //从队列中删除一项  
  113.         }  
  114.         echo 'after processing<br/>';  
  115.         $taskList = $this->redis->lRange($task1Key,0,-1);  
  116.         var_dump($taskList);  
  117.         echo '<br/>';  
  118.         $task2List = $this->redis->lRange($task2Key,0,-1);  
  119.         var_dump($task2List);                 
  120.     }  
  121.   
  122.     /** 
  123.      * 模拟用户1竞拍出价并发场景 
  124.      * 在watch与multi操作之间如果有其他人竞拍出价,则本次出价不能成功 
  125.      * sleep(3) 这个操作模拟用户1出价处理比较慢的场景 
  126.      * @param $orderId 订单Id 
  127.      * @param $price  用户1的竞拍报价 
  128.      */  
  129.     public function user1_buy($orderId, $price)  
  130.     {  
  131.         $key = 'order:'.$orderId;  
  132.         $value = $this->redis->get($key);  
  133.         echo 'in '.__METHOD__.',before providing price:'.$price.',the price is:'.$value.'<br/>';  
  134.   
  135.         $this->redis->watch($key);//添加对该订单当前报价的监控  
  136.         sleep(3);                         
  137.         $result = $this->redis->multi()->set($key, $price)->exec(); //从multi()方法到exec()方法的一系列步骤可以看做是一个原子操作  
  138.         var_dump($result);  
  139.         if ($result) {  
  140.             echo '竞拍出价成功<br/>';  
  141.         } else {  
  142.             echo '竞拍出价失败,出价过程中已经有其他人出价<br/>'; //在watch()与multi()中间有其他人成功出价的话,则本次操作会失败回滚  
  143.         }  
  144.         //$this->redis->unwatch();                                    //unwatch()方法此时不再需要,因为执行EXEC命令后redis会取消对所有键的监控  
  145.         $value = $this->redis->get($key);  
  146.         echo 'in '.__METHOD__.',after providing price,the price is:'.$value.'<br/>';  
  147.     }  
  148.   
  149.     /** 
  150.      * 模拟用户2拍卖出价并发场景 
  151.      * @param $orderId 订单Id 
  152.      * @param $price  用户1的竞拍报价 
  153.      */  
  154.     public function user2_buy($orderId, $price)  
  155.     {  
  156.         $key = 'order:'.$orderId;  
  157.         $value = $this->redis->get($key);  
  158.         echo 'in '.__METHOD__.',before providing price:'.$price.',the price is:'.$value.'<br/>';  
  159.   
  160.         $this->redis->watch($key);  
  161.         $result = $this->redis->multi()->set($key, $price)->exec();  
  162.         var_dump($result);  
  163.         if ($result) {  
  164.             echo '竞拍出价成功<br/>';  
  165.         } else {  
  166.             echo '竞拍出价失败,出价过程中已经有其他人出价<br/>';  
  167.         }  
  168.         $value = $this->redis->get($key);  
  169.         echo 'in '.__METHOD__.',after providing price,the price is:'.$value.'<br/>';  
  170.     }  
  171.       
  172. }  
  173.   
  174.   
  175. $testRedis  = new Test_redis();  
  176.   
  177. if(isset($argv) && count($argv)>1) {  
  178.     $action = $argv[1];  
  179. }else {  
  180.     $action = htmlentities($_GET['action']);  
  181. }  
  182.   
  183. switch($action) {  
  184.     case 'test':  
  185.         $testRedis->test();  
  186.         break;  
  187.     case 'buy_lock':  
  188.         $testRedis->buy_lock();  
  189.         break;  
  190.     case 'session1':  
  191.         $testRedis->write_lock_session1();  
  192.         break;  
  193.     case 'session2':  
  194.         $testRedis->write_lock_session2();  
  195.         break;  
  196.     case 'ismethodexist':   //测试方法是否存在  
  197.         $methodName = $_GET['method'];  
  198.         echo $methodName.':';  
  199.         $testRedis->isMethodExist($methodName);  
  200.         break;  
  201.     case 'subscribe':   //测试订阅消息  
  202.         $testRedis->test_subscribe();  
  203.         break;  
  204.     case 'publish':     //测试发布消息  
  205.         $testRedis->test_publish();  
  206.         break;  
  207.     case 'operateList': //测试操作redisList  
  208.         $testRedis->operateList();  
  209.         break;  
  210.     case 'user1_buy':   //模拟用户1竞拍出价  
  211.         $testRedis->user1_buy(2,650);      
  212.         break;  
  213.     case 'user2_buy':   //模拟用户2竞拍出价  
  214.         $testRedis->user2_buy(2,660);  
  215.         break;  
  216. }  

1.模拟抢购场景:如果有一批商品正在抢购,可以先对这些商品生成一个抢购的订单,后续如果用户抢到该订单,可以只更新订单表中的一些字段

可以开启两个终端页面,同时访问:http://127.0.0.1/test_redis.php?action=buy_lock

会发现结果如下:

页面1:

redis get(lock_order_1)1
setnx lock_order_1 is successful
return:

页面2:

setnx lock_order_1 is failed

就是说加入一个用户抢到了该订单,那么其他用户必然抢单失败。


2.模拟两个用户同时竞拍出价的过程

如果用户1的出价过程中已经有其他用户出价成功,则A用户出价会失败

具体例子也需要开启两个终端页面,分别同时访问 http://127.0.0.1/test_redis.php?action=user1_buy 和 http://127.0.0.1/test_redis.php?action=user2_buy

访问结束后页面分别如下:

页面1:

    in Test_redis::user1_buy,before providing price:650,the price is:660
    bool(false)竞拍出价失败,出价过程中已经有其他人出价
    in Test_redis::user1_buy,after providing price,the price is:660

页面2:

    in Test_redis::user2_buy,before providing price:660,the price is:660
    array(1) { [0]=> bool(true)}竞拍出价成功
    in Test_redis::user2_buy,after providing price,the price is:660


具体应用的方法主要有: watch() multi()  exec() 这几个方法,其中watch() 会监控某一个key值  multi() exec()  会针对一系列操作作为原子操作


3.模拟消息订阅和消息发布的过程

可以后台开启一个任务,php命令执行某一个页面,具体可以在项目根目录下:

使用以下命令开启任务:web101-php-dev-bj1:~/www/site >$ php other/test_redis.php subscribe

然后在浏览器地址栏中输入URL :http://127.0.0.1/other/test_redis.php?action=publish

然后再切换到php命令行界面,会发现结果如下图所示:


从图上可以看到,发布的消息在这里都列举出来了

实际场景中,需要先订阅某一个消息频道,然后当其他人发布消息的时候,后台任务都能够收到。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多