PHP實現Redis單據鎖,防止並發重複寫入____PHP

來源:互聯網
上載者:User

一、寫在前面:
在整個供應鏈系統中,會有很多種單據(採購單、入庫單、到貨單、運單等等),在涉及寫單據資料的介面時(增刪改操作),即使前端做了相關限制,還是有可能因為網路或異常操作產生並發重複調用的情況,導致對相同單據做相同的處理;

為了防止這種情況對系統造成異常影響,我們通過Redis實現了一個簡單的單據鎖,每個請求需先擷取鎖才能執行商務邏輯,執行結束後才會釋放鎖;保證了同一單據的並發重複操作請求只有一個請求可以擷取到鎖(依賴Redis的單線程),是一種悲觀鎖的設計;

註:Redis鎖在我們的系統中一般只用於解決並發重複請求的情況,對於非並發的的重複請求一般會去資料庫或日誌校正資料的狀態,兩種機制結合起來才能保證整個鏈路的可靠。

二、加鎖機制:
主要依賴Redis setnx指令實現:

但使用setnx有一個問題,即setnx指令不支援設定到期時間,需要使用expire指令另行為key設定逾時時間,這樣整個加鎖操作就不是一個原子性操作,有可能存在setnx加鎖成功,但因程式異常退出導致未成功設定逾時時間,在不及時解鎖的情況下,有可能會導致死結(即使業務情境中不會出現死結,無用的key一直常駐記憶體也不是很好的設計);

這種情況可以使用Redis事務解決,把setnx與expire兩條指令作為一個原子性操作執行,但這樣做相對而言會比較麻煩,好在Redis 2.6.12之後版本,Redis set指令支援了nx、ex模式,並支援原子化地設定到期時間:

三、加鎖實現(完整測試代碼會貼在最後):

  /**     * 加單據鎖     * @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;    }

四、解鎖機制:
解鎖即比對加鎖時的唯一lock id,如果比對成功,則刪除key;需要注意的是,解鎖整個過程中同樣需要保證原子性,這裡依賴redis的watch與事務實現;

WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之後的事務就不會執行。監控一直持續到EXEC命令(事務中的命令是在EXEC之後才執行的,所以在MULTI命令後可以修改WATCH監控的索引值)
Redis watch與事務可參看簡書文章:https://www.jianshu.com/p/361cb9cd13d5

五、解鎖實現(完整測試代碼會貼在最後):

/**     * 解單據鎖     * @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;    }

六、附整體測試代碼(此代碼僅為簡易版本)

<?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,解鎖失敗

部落格搬家:https://segmentfault.com/blog/leeonfancy

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.