Laravel 實現二級快取 提高快取的命中率和細粒化快取 key

lar_cainiao發表於2019-08-13
  快取在web應用中有著很重要的地位,應用比較廣範。傳統的快取使用方式有兩種方式:
  1.先寫資料庫後寫快取。
  2.先寫快取後將sql寫佇列批次執行。
  後一種方式明顯比上一種方式 執行效率要高  提高了應用的qps  
  但是第一種方式雖然犧牲了一些效能但是保證了資料的一致性,後一種方式在資料不一致性要做一些措施。
  本人使用的lumen 框架 首先感謝你使用laravel或者lumen 框架 因為你正在用Java的方式 寫php laravel優雅迷人,程式碼格式比較風騷, 她是那樣迷人有魅力 ,就像十八歲的姑娘渾身散發著迷人青春的資訊。
  下面先說一下本人應用結構 本人公分四層結構 
  1.model層  維護資料表結構 表與表之間的關聯關係 類似於spring和 symfony的Entity;
  2.repository層 輔助model層對維護的資料表進行curd操作 
  3.service層 輔助controller 實現業務邏輯 
  4.controller 負責接收引數 service 排程
  本人使用的快取雙寫的第一種方式 即先入資料庫後入快取的方式 
 model層:
namespace App\Models\Platform;
use Illuminate\Database\Eloquent\SoftDeletes;
class HotWordModel extends BaseModel
{
//設定軟刪除
use SoftDeletes;
// 定義偽刪除欄位名稱
const DELETED_AT = 'delete_time';
// 表名
protected $table = 'hotword';
// 表主鍵
protected $primaryKey = 'id';

// 不可被批次賦值的屬性
protected $guarded = [];

// 不啟用時間自動維護,
public $timestamps = false;

// 可以被批次賦值的屬性。
protected $fillable = ['id', 'title', 'sort', 'is_show','create_uid','create_time','update_time','update_uid','delete_time'];

/**
* 新增商品 詳細資料
* @param $data array 商品詳細資料
*/
public function add($data)
{
$data['create_uid'] = $this->loginUser['id'];
$data['create_time']= $data['update_time'] = time();
return $this->create($data);
}

/**
* 編輯商品 詳細資料
* @param $goodsId int 要編輯的商品id
* @param $data  array 商品資料
*/
public function edit($goodsId, $data)
{
$data = array_only($data, $this->fillable);         //欄位過濾
unset($data['id']);    //避免id 注入
return $this->where($this->primaryKey, '=', $goodsId)->update($data);
}

/**
* 透過商品id集合批次刪除
* @param $goodsIds
* @return mixed
*/
public function deleteByGoodsIds($goodsIds)
{
    return $this->whereIn('id', $goodsIds)->delete();
}

public function getTableName(){
    return $this->table;
}

public function getPrimaryKey(){
    return $this->primaryKey;
}

public function getFillAble(){
    return $this->fillable;
}
model層其實按照正常的邏輯是沒有資料庫操作的 
快取處理邏輯 本人使用的是快取門面 
cache層:
namespace App\Cache\Platform;
use App\Common\Base\BaseSingleton;
use Illuminate\Support\Facades\Cache;
use App\Constants\Nomal\Consistent;

class HotWordCache extends BaseSingleton
{
    protected $data;

/**
 * 快取key 字首
 * @var string
 */
public static $cacheKeyPx;

/**
 * 快取型別
 * @var string
 */
public static $cacheType = 'goods';     //快取連線(對應config/cache.php 檔案中)

/**
 * @var int 快取過期時間
 */
protected static $cacheExpire = 0;     //0為永久儲存,>0 為設定時間 (分鐘數)

private static $consistent;
// 建構函式注入hash
public function __construct(Consistent $consistents)
{
    self::$consistent = $consistents;
    self::setHashKey(array('goods'));

}

//獲cache
static public function getCache($cacheName)
{
    return Cache::store(self::getHashValue($cacheName))->get($cacheName);
}
//寫cache
static public function setCache($cacheName, $value, $expire_time = NULL)
{
    $expire_time =  strtotime(date('Y-m-d')) + 97200 - time();
    if($expire_time<0){
        Cache::store(self::getHashValue($cacheName))->forever($cacheName, $value);
    }else{
        Cache::store(self::getHashValue($cacheName))->put($cacheName, $value,$expire_time);
    }
}
//清cache
static public function delCache($cacheName)
{
    if (static::$cacheExpire <= 0) {     //刪除永久儲存
        Cache::store(self::getHashValue($cacheName))->forget($cacheName);
    } else {
        Cache::store(self::getHashValue($cacheName))->pull($cacheName);
    }
}
 // 獲取快取種子
static public function getCacheSeed($key)
{
    $result = self::getCache($key);
    if (!empty($result)) return $result;
    return self::setCacheSeed($key);
}
// 設定快取種子
static public function setCacheSeed($key, $timesec = 0)
{
    $expire_time = 0<$timesec? $timesec: strtotime(date('Y-m-d')) + 97200 - time();
    $data = uniqid();
    if(self::setCache($key, uniqid(), $expire_time)) {
        return $data;
    } else {
        return false;
    }
}
// 分散式鎖的實現
static public function getCacheLock($key){
    Cache::lock($key, 10)->block(5, function ($key) {
        return $key;
    });
}
// 傳入需要hash的key
static public function setHashKey(array $Hashkey){
     if(!empty($Hashkey) && is_array($Hashkey)){
         foreach($Hashkey as $value){
             self::$consistent->addNode($value);
         }
     }
}
// 返回一致性hash後的value
static public function getHashValue(string $hashValue){
    return self::$consistent->lookup($hashValue);
}
// 設定二級快取
static public function setSecondCache(string $cacheName,string $value,int $time = 86400){
    $expire_time = strtotime(date('Y-m-d')) + $time - time() + rand(1,1000);
    Cache::store(self::getHashValue($cacheName))->put($cacheName,$value,$expire_time);
}
// 獲取二級快取
static public function getSecondCache(string $cacheName){
    return Cache::store(self::getHashValue($cacheName))->get($cacheName);
}
// 返回一級快取的key
static public function getFirstCacheKeyById($onlyKey,$className,$id){
    return $onlyKey.'_'.md5(serialize($className)).'_'.$id;
}
}   
    快取主要針對於傳統的一致性雜湊演算法將需要入快取的資料存到某個快取資料庫(本人目前使用一致性hash),現在redis 高版本支援Cluster叢集(3.0以後)比一致性雜湊演算法更為優秀, 你們抽空可以看一下 laravel對redis 叢集支援也比較優秀 ,深感技術更新比較快 ,朕的大清藥丸了。
  下面講一下 本人二級快取的實現思路: 業務場景 維護一個表的快取資料 ,多條件查詢各種分頁的查詢出來的資料這些, 常規做法 ,每一個查詢都針對於查詢條件和分頁維護一個新的快取key, 我之前寫的邏輯 ,一旦資料表裡的資料 ,有更新或者, 新增就把這個生成的 唯一的快取更新掉,再用唯一key,拼接查詢條件和分頁條件,生成新的快取key, 去維護這個結果集 。這樣有個弊端 每次更新或者新增 所有列表裡的key 都會失效 所有請求列表的資料 都會訪問資料庫 這是低效的 增加資料庫的壓力。 
   二級快取:維護兩個不同的時間快取時間 ,一個是短期有效 ,一個長期有效  短期有效的維護的是長期有效的id結果集 那麼每次從列表裡取資料 都是從根據二級快取裡的id 去訪問一級快取的資料。
   這樣有兩個明顯的有點 :1.一級混存的命中提高了  redis 或者memche lru 或者lazy 演算法不會那麼快的更新掉一級快取 2.快取的資料的size 更加合理 之前維護列表資料表達 即使分10條為一頁 如果是大型快取叢集 其佔用的記憶體 是非常可怕的 關係型資料儲存 這裡不做解釋 。
 repository層:
namespace App\Repository\Platform;
use App\Models\Platform\HotWordModel;
use App\Cache\Platform\HotWordCache;
use App\Constants\Key\Cache\Platform;

class HotWordRepository
{
protected  $HotWordRepository,$loginUser,$HotWordCache;
// 依賴注入模型 快取
public function __construct(HotWordModel $HotWordRepository,HotWordCache $HotWordCache)
{
    $this->HotWordRepository = $HotWordRepository;
    $this->HotWordCache      = $HotWordCache;
    $this->loginUser = app('authUser')->getUser();
}
// 獲取一條資料
public function BaseGetInfoById($id){
    if($this->HotWordCache::getCache($this->HotWordCache::getFirstCacheKeyById(Platform::PLATFORM_HOT_WORD,__CLASS__,$id))){
               return $this->HotWordCache::getCache($this->HotWordCache::getFirstCacheKeyById(Platform::PLATFORM_HOT_WORD,__CLASS__,$id));
            }else{
                $ret = $this->HotWordRepository->find($id)->toArray();
                 if(is_array($ret)){
                    $this->HotWordCache::setCache($this->HotWordCache::getFirstCacheKeyById(Platform::PLATFORM_HOT_WORD,__CLASS__,$id),$ret);
                    return $ret;
                 }else{
                     return false;
                 }
            }
        }
// 新增一條資料
public function BaseAdd($data){

$data['create_uid'] = $this->loginUser['id'];
$data['create_time']= $data['update_time'] = time();
$res = $this->HotWordRepository->insertGetId($data);
if($res){
    $this->HotWordCache::setCacheSeed(Platform::PLATFORM_HOT_WORD.__CLASS__.md5(serialize($this->HotWordRepository->getTableName())));
    return $res;
}else{
    return false;
}
}
// 修改一條資料
public function BaseEdit($id,$data){

$data['update_uid'] = $this->loginUser['id'];
$data['update_time'] = time();
unset($data['id']);
$res = $this->HotWordRepository->where($this->HotWordRepository->getPrimaryKey(), '=', $id)->update($data);

if($res){
    $this->HotWordCache::setCacheSeed(Platform::PLATFORM_HOT_WORD.__CLASS__.md5(serialize($this->HotWordRepository->getTableName())));
        return $res;
    }else{
        return false;
    }
}
/**
 * @Notes:移除一條資料
 * @User: 張狀
 * @param $id  string id
 * @Date: 2019/07/24
 * @return boolean
 */
public function baseRemove($id)
{
    $res = $this->HotWordRepository->where($this->HotWordRepository->getPrimaryKey(), '=', $id)->delete();
    if($res){
        $this->HotWordCache::delCache(Platform::PLATFORM_HOT_WORD.md5(__CLASS__.$id));
        $this->HotWordCache::setCacheSeed(Platform::PLATFORM_HOT_WORD.__CLASS__.md5(serialize($this->HotWordRepository->getTableName())));
        return $res;
    }else{
        return false;
    }
}
/**
 * @Notes:分頁獲取列表
 * @User: 張狀
 * @Date: 2019/07/24
 * @param $fields  string 欄位
 * @param $condition  array 篩選條件
 * @param $pageIndex  int   當前頁
 * @param $pageSize  int  每頁條數
 * @return array
 */
public function baseGetListInfoByCondition(string $fields,array $condition,int $pageIndex = 1,int $pageSize = 20)
{
    $fields = !empty($fields) ? $fields : $this->HotWordRepository->getFillAble();// 獲取欄位
    $conditions = array();// 欄位校驗
    if(isset($condition['title'])&&!empty($condition['title'])) $conditions[] = array('title','=',$condition['title']);
    if(isset($condition['sort'])&&!empty($condition['sort']))   $conditions[] = array('sort','=',$condition['sort']);
    if(isset($condition['is_show'])&&!empty($condition['is_show'])) $conditions[] = array('is_show','=',$condition['is_show']);
    if(isset($condition['create_uid'])&&!empty($condition['create_uid'])) $conditions[] = array('create_uid','=',$condition['create_uid']);
    if(isset($condition['create_time'])&&!empty($condition['create_time'])) $conditions[] = array('create_time','>=',strtotime($condition['create_time']));
        if(isset($condition['update_uid'])&&!empty($condition['update_uid'])) $conditions[] = array('update_uid','=',$condition['update_uid']);
        if(isset($condition['update_time'])&&!empty($condition['update_time'])) $conditions[] = array('update_time','=',strtotime($condition['update_time']));
        // 獲取快取key
        $cacheKey  = $this->HotWordCache::getCacheSeed(Platform::PLATFORM_HOT_WORD.__CLASS__.md5(serialize($this->HotWordRepository->getTableName())));
        $cache_key = Platform::PLATFORM_HOT_WORD.'_'.md5(serialize(implode($fields,',').'_'.implode($condition,',').'_'.$pageIndex.'_'.$pageSize).'_'.$cacheKey);

$offset =($pageIndex - 1) * $pageSize;//分頁處理
$ret['pageIndex'] = $pageIndex;
$ret['pageSize']  = $pageSize;

if(!empty($this->HotWordCache::getCache($cache_key.'_'.'count'))){
    $ret['total_count'] = $this->HotWordCache::getCache($cache_key.'_'.'count');
}else{
    $ret['total_count'] = $this->HotWordRepository->select($fields)->where($conditions)->count();
}
$ret['total_pages'] = $pageIndex <= 0 ? 1 : intval(ceil($ret['total_count'] / $pageSize));

//從快取裡去資料
if($this->HotWordCache::getSecondCache($cache_key)){
    $ret['rows'] = iterator_to_array($this->getCacheListInfo(explode(',',$this->HotWordCache::getSecondCache($cache_key))));
    return $ret;
}else{
    $ret['rows'] = $this->HotWordRepository->select($fields)->where($conditions)->offset($offset)->limit($pageSize)->get()->toArray();
    if(is_array($ret['rows'])){
        // 入快取
        $this->HotWordCache::setSecondCache($cache_key,implode(',',array_column($ret['rows'],'id')));
        return $ret;
    }else{
        return array();
    }
   }
}
// 根據快取
public function getCacheListInfo(array $data){
  foreach ($data as $info){
      yield $this->BaseGetInfoById($info);
   }
}
    service層:

namespace App\Services;
use App\Repository\Platform\HotWordRepository;

class HotWordService
{
protected $HotWordService;
// 依賴注入倉庫
public function __construct(HotWordRepository $HotWordService)
{
$this->HotWordService = $HotWordService;
}
// 獲取一條熱搜詞資料
public function getInfoById($id){
return $this->HotWordService->BaseGetInfoById($id);
}
// 獲取熱搜詞列表邏輯
public function getListInfoByCondition(string $fields,array $condition,int $pageIndex = 1,int $pageSize = 20){
return $this->HotWordService->baseGetListInfoByCondition($fields,$condition,$pageIndex,$pageSize);
}
// 新增編輯熱搜詞邏輯
public function addOneHotWord($data){
if(empty($data['id'])){
   return  $this->HotWordService->BaseAdd($data);
}else{
   return  $this->HotWordService->BaseEdit($data['id'],$data);
}
}
// 刪除一條熱搜詞邏輯
public function deleteHotWordById($id){
return $this->HotWordService->baseRemove($id);
}
     controller層:

namespace App\Http\Controllers\Platform;
use Illuminate\Http\Request;
use App\Services\HotWordService;
use App\Constants\Nomal\RedisList;

class HotWordController extends BaseController
{
private $hotword,$pageIndex,$pageSize,$redisPage;
public function __construct(Request $request,HotWordService $hotword,RedisList $redisPage)
{
    parent::__construct($request);
    $this->hotword = $hotword;
    $this->pageIndex = 1;
    $this->pageSize  = 20;
    $this->redisPage = $redisPage;
}
/**
 * @Notes: 熱搜詞儲存
 * @User: 張狀
 * @Date: 2019/7/5
 * @return \Illuminate\Http\JsonResponse
 * @throws PlatformException
 */
public function save()
{
    $this->verification();    //基礎驗證資訊
    $res = $this->hotword->addOneHotWord($this->request->post());
   //if($res)
    //$this->addLog('新增分類', BaseConstant::LOG_ACTION_TYPE_ADD);        //操作日誌
    return $this->responseSuccess($res);
}

/**
 * @Notes: 修改熱搜詞
 * @User: 張狀
 * @Date: 2019/7/5
 * @return \Illuminate\Http\JsonResponse
 * @throws PlatformException
 * @throws \Illuminate\Validation\ValidationException
 */
public function edit()
{
    $this->verification();    //驗證資料
    $this->validate($this->request, ['id' => 'required|integer|min:1']);     //單獨對id驗證

    $data = $this->request->post();
    $data['id'] = $this->request->get('id');

    $this->hotword->addOneHotWord($data);
   // $this->addLog('修改分類', BaseConstant::LOG_ACTION_TYPE_EDIT);        //操作日誌
    return $this->responseSuccess(true);
}

/**
 * @Notes: 熱搜詞列表
 * @User: 張狀
 * @Date: 2019/7/5
 * @return \Illuminate\Http\JsonResponse
 * @throws \Illuminate\Validation\ValidationException
 */
public function index()
{
    $data = $this->hotword->getListInfoByCondition('',$this->request->all(),
        !empty($this->request->get('pageIndex'))?intval($this->request->get('pageIndex')):$this->pageIndex,
        !empty($this->request->get('pageSize'))?intval($this->request->get('pageSize')):$this->pageSize);
    return $this->responseSuccess($data);
}

/**
 * @Notes: 熱搜詞詳情
 * @User: 張狀
 * @Date: 2019/7/5
 * @return \Illuminate\Http\JsonResponse
 * @throws \Illuminate\Validation\ValidationException
 */
public function info()
{
    $this->validate($this->request, ['id' => 'required|integer|min:1']);
    if($this->hotword->getInfoById($this->request->get('id')))  {
        return $this->responseSuccess($this->hotword->getInfoById($this->request->get('id')));
    }else{
        return $this->responseSuccess(false);
    }
}

/**
 * @Notes:刪除
 * @User: 張狀
 * @Date: 2019/7/5
 * @return \Illuminate\Http\JsonResponse
 * @throws PlatformException
 * @throws \Illuminate\Validation\ValidationException
 */
public function delete()
{
    $this->validate($this->request, ['id' => 'required|integer|min:1']);
    $this->hotword->deleteHotWordById($this->request->get('id'));
    //$this->addLog('修改分類', BaseConstant::LOG_ACTION_TYPE_DELETE);        //操作日誌
    return $this->responseSuccess(true);
}

/**
 * 需要驗證的欄位
 * @return array
 */
public function rules()
{
    return [
        'title'=>'required|unique:platform.hotword|string|max:100',
        'sort'  => 'required|integer|between:0,100000',
        'is_show'  => 'required|integer|between:0,100'
    ];
}
}    
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章