PHP+Redis併發下單(代碼全篇)

<?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 ;
    }
}
相關文章
相關標籤/搜索