一個簡單的基於redis的併發鎖(PHP語言)

在用redis做緩存時, 如果不考慮併發問題, 在緩存不存在或過期時, 會導致很多請求直接進入數據庫,造成很多"意外"的負載.

所以, 需要對緩存不存在->走數據庫查詢的處理過程中, 增加一個鎖, 來避免該問題, 這就是併發鎖.

加鎖的過程:

  • 請求的緩存不存在, 嘗試加鎖(必須使用redis的setnx), 開始循環處理.
  • 如果鎖存在, 則休眠, 等待下一次循環.
  • 如果鎖不存在
    • 加鎖成功, 則執行業務查詢(走數據庫),然後解鎖
    • 加鎖失敗, 休眠, 等待下一次循環(下一循環時, 鎖應該存在, 繼續循環)
  • 循環第二次開始, 如果鎖不存在, 則認爲緩存已經生成, 去查詢這個緩存. 如果查詢不到, 則認爲系統異常.

這裏的加鎖返回三個狀態:

  • 1: 獲取鎖成功(執行業務後需要unlock)
  • 2: 加鎖失敗,但是嘗試期間鎖已經不存在(可以判斷緩存數據是否存在)
  • 0: 獲取鎖失敗(或超時)

這裏的鎖的失效時間, 建議不要設置太長.

類庫文件: RedisLockUtility.php

/**
 * redis併發鎖
 * @author aben
 *
 * Class RedisLockUtility
 */
class RedisLockUtility {
    /**
     * 給key加鎖
     *
     * @param \ClsRedis $redis_connection redis連接對象
     * @param string $key_prefix 指定的key前綴
     * @param int $timeout 鎖定/超時時間, 默認2秒
     * @param float $retry_wait_sec 重新嘗試獲取key的時間間隔, 默認0.1秒
     * @return int 結果. 1:獲取鎖成功(執行業務後需要unlock), 2:加鎖失敗,但是嘗試期間鎖已經不存在(可以判斷緩存數據是否存在), 0: 獲取鎖失敗(或超時)
     */
    public static function lock($redis_connection, $key_prefix, $timeout = 2, $retry_wait_sec = 0.1){
        $cache_lock_key = self::getLockKey($key_prefix);

        //重試間隔 (秒轉換爲微秒)
        $retry_interval = $retry_wait_sec * 1000000;

        //重試次數
        $retry_times = $timeout / $retry_wait_sec;

        for($i=0; $i < $retry_times; $i++) {
            if($redis_connection->get($cache_lock_key)){
                //鎖存在, 則休眠
                usleep($retry_interval);
            }
            else {
                //(第一次循環)沒有鎖, 加鎖, 查詢數據
                if($i == 0){
                    //準備加鎖
                    $rt_lock = $redis_connection->setnx($cache_lock_key, 1);//使用setnx命令, 保證鎖的唯一
                    if($rt_lock === true) {
                        //加鎖成功
                        //設置`鎖的有效時間`
                        $redis_connection->expire($cache_lock_key, $timeout);

                        //加鎖成功後, 可以執行數據庫查詢, 然後解鎖(unlock) !!!

                        return 1;
                    }
                    else {
                        //加鎖失敗, 繼續嘗試
                        usleep($retry_interval);
                    }
                }
                else {
                    //鎖不存在了. 後續可以判斷有沒有緩存.
                    return 2;
                }
            }
        }

        return 0;
    }

    /**
     * 解鎖
     *
     * @param \ClsRedis $redis_connection redis連接對象
     * @param string $key_prefix
     */
    public static function unlock($redis_connection, $key_prefix){
        $redis_connection->del(self::getLockKey($key_prefix));
    }

    /**
     * 拼接完整的鎖的key
     *
     * @param string $key_prefix
     * @return string
     */
    public static function getLockKey($key_prefix){
        return $key_prefix.':lock';
    }
}

實際調用:

$cacheKey = 'index_products';
/** @var \ClsRedis $cache */
$cache = ClsRedis::getInstance();//redis連接實例
$res = $cache->get($cacheKey);

$getAllDataSuccess = false;
if ($res === false) {
    //數據緩存不存在
    
    //處理併發: 加鎖
    $lock_key_prefix = 'index_products';
    switch(RedisLockUtility::lock($cache, $lock_key_prefix, 2, 0.1)){
        case 1://獲取鎖成功
            //去數據庫查詢
            /**
            * 這裏執行數據庫查詢, 得到數據$res
            */

            //寫入緩存
            $cache->setex($cacheKey, 180, json_encode($res));
            //sleep(2); //併發測試時可以休眠一下, 注意: 這個時間不要超過`鎖的有效時間`!
            $getAllDataSuccess = true;

            //移除鎖
            RedisLockUtility::unlock($cache, $lock_key_prefix);

            break;
        case 2://加鎖失敗,但是嘗試期間鎖已經不存在(可以判斷緩存數據是否存在)
            //鎖不存在了, 判斷有沒有緩存.
            $data = $cache->get($cacheKey);
            if($data !== false){
                $res = json_decode($data, true);
                $getAllDataSuccess = true;
            }
            break;
        default://失敗
    }

    if($getAllDataSuccess === false){
        echo json_encode(['code' => 0, 'msg'=>'獲取數據失敗', 'res' => []]);
        exit;
    }
    if (empty($res)){
        echo json_encode(['code' => 200, 'msg'=>'沒有數據', 'res' => []]);
        exit;
    }
}
else {
    $res = json_decode($res, true);
}

這個方案有個問題. 因爲setnx和expire是2個操作, 如果expire操作失敗, 則這個鎖就會一致存在, 導致數據無法獲取.

本方案待完善....

關於分佈式鎖的問題, 可以參考: https://xie.infoq.cn/article/4d571787a3280ef3094338f9b

github上也有其他人寫的類, latrell/Lock等, 請自行搜索.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章