PHP+Redis解決實際問題二:快取擊穿

renxiaotu發表於2022-01-08

1、本系列文章每期都將解決一個Redis實際問題
2、每期問題將在每期的評論中選取
3、問題限Redis相關,其它問題如果本人感興趣也不排除開闢新系列
4、本人常用PHP所以解決方案以PHP為主
5、評論裡沒有合適的提問時我會自己給自己出題

問題描述:

本期為第二期,依舊是自己出題【狗頭】

PHP+Redis如何簡單避免快取擊穿

解決方案:

<?php

    /**
     * 防擊穿快取
     * @param string $key       快取key
     * @param int $expire       快取過期時間(s)
     * @param bool $refresh     是否強制重新整理資料
     * @param callable $func    獲取要快取的資料的回撥方法(僅支援返回型別:?string)
     * @return null|string      返回快取的值,沒有值時且沒有搶到重新整理權且快取已過期時返回null
     */
    function onceCache(string $key,int $expire,bool $refresh,callable $func): ?string
    {
        //記憶體,注:如果是cli模式不要使用
        static $dataStatic=[];

        //讀取記憶體
        if(isset(self::$dataStatic[$key])){
            return self::$dataStatic[$key];
        }

        //獲取laravel的獲取redis連線,其它框架需要修改
        $redis=Redis::connection();

        //原快取key加字尾"lock"為鎖的key
        $lockKey=$key.':lock';

        //鎖是否有效
        $lock=false;

        //讀取資料
        $data=$redis->get($key);

        //如果 非強制重新整理 且 快取非空 ,獲取鎖
        if(!$refresh && !is_null($data)){
            $lock = $redis->get($lockKey);
        }

        if(!$lock){//如果鎖過期或無資料
            $lock=$redis->setnx($lockKey,1);    //僅當鎖不存在時設定鎖,值1代表資料獲取中
            if($lock || $refresh){              //搶到新鎖 或 強制重新整理
                try {
                    $data=$func();              //從回撥函式獲取要快取的資料

                    //有資料則寫入快取,沒有則刪除資料
                    if(!is_null($data)){
                        $redis->set($key,$data);
                    }else{
                        $redis->del([$key]);
                    }

                    $redis->set($lockKey,2,'ex',$expire);   //設定鎖的過期時間,值2代表資料獲取完成
                }catch (Exception $exception){
                    $redis->del([$lockKey]);                //發生異常,刪除鎖
                }
            }else{
                //如果沒有資料,又沒有搶到鎖
                //30秒內每秒判斷搶到鎖的使用者是否執行完成,執行完成則從快取得到資料並返回
                $retry=0;
                do{
                    sleep(1);
                    $retry++;
                    $data = $redis->get($key);
                }while(is_null($data) && $redis->get($lockKey)==1 && $retry<30);
            }
        }

        //寫入記憶體
        if(!is_null($data)){
            $dataStatic[$key]=$data;
        }

        //返回資料
        return $data;
    }

程式碼解讀:

  1. 資料本體永不過期
  2. 設定鎖的過期時間,鎖過期則搶到鎖的使用者重新整理資料
  3. 未搶到鎖的使用者如果有拿到資料就直接返回(此時資料已經是過期資料,如果對資料及時性要求高的需要自己改造程式碼)
  4. 未搶到鎖的使用者如果沒有拿到資料則每秒判斷搶到鎖的使用者是否執行完成
  5. 鎖有極小機率變成死鎖,最好有定時任務定期處理,比如每天業務低谷期清理死鎖

以上,歡迎提問糾錯補充最佳化

本作品採用《CC 協議》,轉載必須註明作者和本文連結
世界最好語言的追隨者

相關文章