<?php /** * Class redisConcurrent */ class RedisConcurrent { /** lock key * @var string */ public $_lockKey = 'redis_lock'; /** Redis Class * @var Redis */ private $_redis ; /** ip * @var mixed|string */ private $ip ='127.0.0.1' ; /** port * @var string */ private $port = '6379' ; /** init redis connect * redisConcurrent constructor. * @param array $config */ public function __construct( $config = [] ) { if(!empty($config)) { if(isset($config['ip'])) { $this->ip = $config['ip']; } if(isset($config['port'])){ $this->ip = $config['port']; } } /** * Redis鏈接信息能夠用原生,也能夠用其它的框架集成 */ $this->_redis = new Redis(); $this->_redis->connect($this->ip,$this->port); } /** 鎖定 * @param int $intTimeout 默認過時時間(避免死鎖) * @return bool */ private function lock($intTimeout = 8) { #新版set,已經集成了大多數集成操做 $strRet = $this->_redis->set($this->_lockKey, time().rand(10000,99999).rand(1000,9999).rand(100,999), 'ex', $intTimeout, 'nx'); if($strRet) { return true; }else{ return false; } } /** 解鎖 * @throws \Exception */ private function unlock() { $strRet = $this->_redis->del($this->_lockKey); if($strRet) { return true; }else{ if($this->_redis->get($this->_lockKey)) { return false ; }else{ return false ; } } } /** * 業務相關的key,能夠是庫存,物品數等 */ const ORDER_KEY = 'order_num'; /** * 用戶相關的key */ const USER_KEY = 'user_num'; /** Redis下單 * @param int $num 下單個數 * @param string $userId 用戶ID * * 場次是爲了方便異常處理,方便數據查找 * @param string $bout 商品場次 => order_num:1 , order_num:2 * @return bool * @throws Exception */ public function order( string $userId ,string $bout = '1' ,int $num = 1) { $orderKey = self::ORDER_KEY.':'.$bout ; $userKey = self::USER_KEY.':'.$bout ; //此方法不具有原子性 併發處理是不能作條件判斷 //$len = $this->_redis->llen(); #實際爲n+1次觸發完結,這裏只作Redis自減 $check = $this->_redis->lpop($orderKey); if(!$check){ #當前order_num已經爲0! //自動補貨爲 100 ,$bout有必定的處理規則,不能亂傳 self::autoBuild(100,$bout); return false ; } //特殊處理,避免n+1次的狀況 $len = $this->_redis->llen($orderKey) ; if($len == 0) { //自動補貨爲 100 ,$bout有必定的處理規則,不能亂傳 self::autoBuild(100,$bout); return false ; } //添加用戶數據 $result = $this->_redis->lpush($userKey,$userId); if($result){ return true ; }else{ return false ; } } /** 失敗處理 * #增長當前庫存 * #減小用戶庫存 * @param string $num * @param string $userId * @param $bout * @return bool * @throws Exception */ public function _out(string $num,string $userId,$bout) { #併發參與時,總庫存有5個,一共10次請求,成功5次,退款1次,實際庫存1次 #失敗處理時和_buildOrder加上同一把鎖,避免更新下次庫存時,上次庫存累積 #_out 和 _buildOrder 同時只能有一個在執行,否則鎖會報錯,也避免下沒必要要的死鎖 self::lock(); //減用戶庫存 $user = $this->_redis->lpop(self::USER_KEY.':'.$bout); if(!$user) { return false ; } //增長商品庫存 $all = $this->_redis->lpush(self::ORDER_KEY.':'.$bout,$userId); if(!$all) { //TODO::這裏須要作容錯處理,即再商品庫存增長失敗時,作記錄 return false ; } self::unlock(); } /** 自動構建 * @param int $num * @param $bout * @throws Exception */ private function autoBuild( int $num ,$bout) { $a = $this->_redis->get(self::ORDER_KEY.':'.$bout); if(!$a) { //庫存已完結 $this->_buildOrder(self::ORDER_KEY.':'.$bout,$num); } } /** 物品庫存規則 * @param $orderKey * @param $num * @return string * @throws Exception */ private function _buildOrder($orderKey,$num) { //鎖定 self::lock(); $ckNum = '0' ;#Redis操做後返回爲string類型 #總數 與$ckNum要相同類型 否則可能會出現判斷錯誤 if($num < 0) { throw new \Exception('商品數量錯誤!'); } $beforeNum = 0 ; //上一次庫存判斷 () if($beforeNum > 0) { throw new \Exception('商品未售罄!'); } //當前庫存判斷 $length = $this->_redis->llen($orderKey); if($length > 0) { throw new \Exception('商品已經存在!'); } //生成當前庫存 while ($ckNum < $num) { if($ckNum == $num) { break ; }else if($ckNum > $num){ break ; }else{ $ckNum = $this->_redis->lpush($orderKey,1) ; if($ckNum >=$num) { break ; } } } //併發時 循環成功 redis不必定成功 /*for ($i=1;$i<=$num ;$i++) { $ckNum = $this->_redis->lpush(self::$_allCoin.self::getNum().':'.$coin,1); if($ckNum >= $num) { break ; } }*/ //解鎖 self::unlock(); return $ckNum ; } }