基于Eloquent ORM的缓存实时更新、读取封装

基于Eloquent ORM的缓存实时更新、读取封装

一、设计背景:

有不少公司的后台系统在操作更新数据库的同时会实时刷新缓存(永久),而且缓存一般都采用hash(存表的行数据)+有序集合(指定排序字段存放行数据的id)。但是每个模型都加这些操作缓存的方法实在是繁琐的很,其实都大同小异。那我们有没有办法去设计一套规则去吃掉这些繁琐的操作呢?答案是肯定有的,接下来我会提出我的解决思路并用代码实现这个过程。我一直是秉着用最少的代码去实现同样的功能的原则去设计每一样东西,这样我们的coding质量才能得到大大的提高。

二、设计思路

只要我们能够监听到操作完数据库后,我们具体做了哪些操作(新增、更新、删除)并且能够拿到操作时对应的数据,我们就能对这些数据去做相应的缓存处理。都统一封装起来,作为模型基类去被其他模型类继承使用即可,同时考虑到一些比较特殊的情况,可能需要特殊处理的,在合适的位置开放方法让模型类实现一下即可。

Laravel ORM自带的事件监听就能监听并拿到数据。
所以我们只要在相应的事件中做相应的处理就好了。

三、Coding Time

1.这是我的模型基类(Base.php),分别监听不同事件做了不同的缓存处理

<?php

namespace App\Models;

use App\Models\Cache\BaseCache;
use App\Models\Cache\QueryCache;
use App\Models\Cache\RelationCache;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Model;

class Base extends Model
{
    protected $guarded = [];

    public $timestamps = false;

    /**
     * @var null
     * 缓存实例
     */
    protected static $cache = null;

    /**
     * @var bool
     * ORM缓存开关
     */
    protected static $cacheSwitch = false;

    use BaseCache,RelationCache,QueryCache;


    public function __construct(array $data = [])
    {
        parent::__construct($data);
        $this->initCache();
    }

    protected static function booted()
    {
        parent::booted(); // TODO: Change the autogenerated stub
        if (static::$cacheSwitch) {
            $calledClass = get_called_class();

            static::saved(function ($saved) use ($calledClass){

                $original = $saved->original;
                if (empty($original)) {
                    var_dump('created');
                    //新增缓存
                    $createdBaseRes = static::createdBase($saved);
                    $createdRelationRes = true;
                    if (static::$relationCacheKey) {
                        $createdRelationRes = static::createdRelation($saved);
                    }
                    //更新缓存后的回调处理
                    if (method_exists($calledClass, 'createdCacheCallBack')) {
                        static::createdCacheCallBack($saved, $createdBaseRes && $createdRelationRes);
                    }
                } else {
                    var_dump('updated');
                    //更新缓存
                    $updatedBaseRes = static::updatedBase($saved);
                    $updatedRelationRes = true;
                    if (static::$relationCacheKey) {
                        $updatedRelationRes = static::updatedRelation($saved);
                    }
                    if (method_exists($calledClass, 'updatedCacheCallBack')) {
                        static::updatedCacheCallBack($saved, $updatedBaseRes && $updatedRelationRes);
                    }
                }
            });

            static::deleted(function ($deleted) use ($calledClass){
                var_dump('deleted');
                //删除缓存
                $deletedBaseRes = static::deletedBase($deleted);
                $deletedRelationRes = true;
                if (static::$relationCacheKey) {
                    $deletedRelationRes = static::deletedRelation($deleted);
                }
                if (method_exists($calledClass, 'deletedCacheCallBack')) {
                    static::deletedCacheCallBack($deleted, $deletedBaseRes && $deletedRelationRes);
                }
            });
        }
    }

    /**
     * @param array $data
     * @param string $cacheKey
     * @return string
     * @throws \Exception
     * 获取缓存key
     */
    public static function getCacheKey(array $data, string $cacheKey, bool $rewriteData = false): string
    {
        $pattern = '/{\$(.*?)}/';
        preg_match_all($pattern, $cacheKey, $matches);
        $variables = $matches[1];
        $find = $replace = [];
        if ($rewriteData) {
            foreach ($variables as $vk => $vv) {
                if (isset($data[$vk])) {
                    $data[$vv] = $data[$vk];
                }
            }
        }
        foreach ($variables as $variable) {
            if (!isset($data[$variable])) {
                throw new \Exception("获取缓存key失败:缺失{\${$variable}}");
            }
            $find[] = "{\$$variable}";
            $replace[] = $data[$variable];
        }
        return $find && $replace ? str_replace($find,$replace,$cacheKey) : $cacheKey;
    }

    /**
     * @param array $data
     * @return void
     * 过滤缓存字段
     */
    public static function filterCacheDataField(array &$data)
    {
        if (static::$cacheFields) {
            foreach ($data as $k => $v) {
                if(!in_array($data, static::$cacheFields)) {
                    unset($data[$k]);
                }
            }
        }
    }

    /**
     * @param bool $paging
     * @return void
     * 开启分页
     */
    public function setPaging(bool $paging)
    {
        static::$paging = $paging;
    }

    /**
     * @int $pageSize
     * @return void
     * 设置每页记录数
     */
    public function setPageSize(int $pageSize)
    {
        static::$pageSize = $pageSize;
    }

    /**
     * @int $pageSize
     * @return void
     * 设置当前页码
     */
    public function setPage(int $page)
    {
        static::$page = $page;
    }

    /**
     * @param array $fields
     * @return void
     * 设置获取字段
     */
    public function setGetFields(array $fields)
    {
        static::$getFields = $fields;
    }

    /**
     * @param string $field
     * @return void
     * 设置排序字段
     */
    public function setSortField(string $field)
    {
        static::$sortField = $field;
    }

    /**
     * @param string $type
     * @return void
     * 设置排序顺序
     */
    public function setSortType(string $type)
    {
        static::$sortType = $type;
    }

    /**
     * @return null
     * 初始化缓存实例
     */
    protected function initCache()
    {
        if (self::$cache == null) {
            self::$cache = Cache::store('redis')->getRedis();
        }
        return  self::$cache;
    }
}

2.这是处理表行数据的缓存类BaseCache.php

<?php

namespace App\Models\Cache;
use App\Models\Base;
trait BaseCache
{
    /**
     * @var string 缓存key
     */
    protected static $cacheKey = '';

    /**
     * @var array 缓存字段 空数组则缓存表所有字段
     */
    protected static $cacheFields = [];

    /**
     * @var string 缓存的数据结构 默认哈希
     */
    protected static $cacheDataStructure = 'hash';

    /**
     * @param $created
     * @return bool
     * @throws \Exception
     * ORM设置缓存数据
     */
    public static function createdBase ($created): bool
    {
        self::checkBaseCacheConf();
        $cache = Base::initCache();
        $data = $created->attributes;
        $key = Base::getCacheKey($data,static::$cacheKey);
        Base::filterCacheDataField($data);
        $cacheMethod = 'hMset';
        if (static::$cacheDataStructure == 'string') {
            $cacheMethod = 'set';
            $data = json_encode($data,JSON_UNESCAPED_UNICODE);
        }
        return $cache->$cacheMethod($key,$data);
    }

    /**
     * @param $updated
     * @return bool
     * @throws \Exception
     * ORM更新缓存数据
     */
    public static function updatedBase ($updated): bool
    {
        self::checkBaseCacheConf();
        $cache = Base::initCache();
        $data = $updated->attributes;
        $key = Base::getCacheKey($data,static::$cacheKey);
        Base::filterCacheDataField($data);
        $cacheMethod = 'hMset';
        if (static::$cacheDataStructure == 'string') {
            $cacheMethod = 'set';
            $oldData = json_decode($cache->get($key),true) ?: [];
            $data = array_merge($oldData,$data);
            $data = json_encode($data,JSON_UNESCAPED_UNICODE);
        }
        return $cache->$cacheMethod($key,$data);
    }

    /**
     * @param $deleted
     * @return bool
     * ORM删除缓存数据
     */
    public static function deletedBase ($deleted): bool
    {
        self::checkBaseCacheConf();
        $cache = Base::initCache();
        $data = $deleted->attributes;
        $key = Base::getCacheKey($data,static::$cacheKey);
        return $cache->del($key);
    }

    /**
     * @return bool
     * @throws \Exception
     * 校验参数配置
     */
    private static function checkBaseCacheConf(): bool
    {
        if (empty(static::$cacheKey)) {
            throw new \Exception('缓存错误:缓存key必填');
        }

        if (!in_array(static::$cacheDataStructure, ['hash','string'])) {
            throw new \Exception('缓存的数据结构错误:暂时只支持string和hash');
        }
        return true;
    }
}

3.这是处理关联表数据的缓存类RelationCache.php

<?php

namespace App\Models\Cache;
use App\Models\Base;
trait RelationCache
{
    /**
     * @var string 关联缓存key 为空则不开启关联缓存
     */
    protected static $relationCacheKey = '';

    /**
     * @var string 关联缓存的数据结构 默认有序集合
     */
    protected static $relationCacheDataStructure = 'zset';

    /**
     * @var array 关联缓存排序字段 数据结构为zset时必填
     */
    protected static $relationCacheSortFields = [];

    /**
     * @param $created
     * @return bool
     * @throws \Exception
     * ORM设置缓存数据
     */
    public static function createdRelation ($created): bool
    {
        $data = $created->attributes;
        $primaryKey = $created->primaryKey;
        if (!isset($data[$primaryKey])) {
            throw new \Exception('关联缓存错误:主键数据不能为空');
        }
        self::checkRelationCacheConf();
        $cache = Base::initCache();
        $cacheKey = Base::getCacheKey($data,static::$relationCacheKey);
        $keys = static::$relationCacheDataStructure == 'zset' ? self::getSortedKeys($cacheKey) : [];
        $res = true;
        if ($keys) {
            //开启事务
            $cache->multi();
            foreach ($keys as $kk => $kv) {
                if (isset($data[$kk])) {
                    $cache->zAdd($kv, $data[$kk], $data[$primaryKey]);
                }
            }
            //执行事务
            $exec = $cache->exec();
            foreach ($exec as $v) {
                if($v === false){
                    $res = false;
                    break;
                }
            }
        } else {
            $res = $cache->sAdd($cacheKey, $data[$primaryKey]);
        }
        return $res;
    }

    /**
     * @param $updated
     * @return bool
     * @throws \Exception
     * ORM更新缓存数据
     */
    public static function updatedRelation ($updated): bool
    {
        self::checkRelationCacheConf();
        //如果是有序集合
        $res = true;
        if (static::$relationCacheDataStructure == 'zset') {
            $cache = Base::initCache();
            $data = $updated->attributes;
            $original = $updated->original;
            $diff = array_diff_assoc($data,$original);
            $intercept = array_intersect(array_keys($data), static::$relationCacheSortFields);
            if ($intercept && $diff) {
                $primaryKey = $updated->primaryKey;
                $cacheKey = Base::getCacheKey($original,static::$relationCacheKey);
                $keys = self::getSortedKeys($cacheKey);
                $cache->multi();
                foreach ($keys as $kk => $kv) {
                    if (isset($diff[$kk])) {
                        $cache->zAdd($kv, $diff[$kk], $original[$primaryKey]);
                    }
                }
                //执行事务
                $exec = $cache->exec();
                foreach ($exec as $v) {
                    if($v === false){
                        $res = false;
                        break;
                    }
                }
            }
        }
        return $res;
    }

    /**
     * @param $deleted
     * @return bool
     * @throws \Exception
     * ORM删除缓存数据
     */
    public static function deletedRelation ($deleted): bool
    {
        self::checkRelationCacheConf();
        $cache = Base::initCache();
        $res = true;
        $original = $deleted->original;
        $primaryKey = $deleted->primaryKey;
        $cacheKey = Base::getCacheKey($original,static::$relationCacheKey);
        if (static::$relationCacheDataStructure == 'zset') {
            $keys = self::getSortedKeys($cacheKey);
            $cache->multi();
            foreach ($keys as $kk => $kv) {
                $cache->zRem($kv, $original[$kk], $original[$primaryKey]);
            }
            //执行事务
            $exec = $cache->exec();
            foreach ($exec as $v) {
                if($v === false){
                    $res = false;
                    break;
                }
            }
        } else {
            $res = $cache->sRem($cacheKey,$original[$primaryKey]);
        }
        return $res;
    }

    /**
     * @param $key
     * @return array
     * @throws \Exception
     * 获取有序集合排序字段对应的key
     */
    private static function getSortedKeys($key): array
    {
        $keys = [];
        foreach (static::$relationCacheSortFields as $sortField) {
            $keys[$sortField] = "{$sortField}_{$key}";
        }
        return $keys;
    }

    /**
     * @return bool
     * @throws \Exception
     * 检验参数配置
     */
    private static function checkRelationCacheConf (): bool
    {
        if (!in_array(static::$relationCacheDataStructure, ['zset','set'])) {
            throw new \Exception('关联缓存错误:数据结构错误,暂时只支持zset和set');
        }
        if (static::$relationCacheDataStructure == 'zset' && empty(static::$relationCacheSortFields)) {
            throw new \Exception('关联缓存错误:数据结构为zset时排序字段必填');
        }
        return true;
    }
}

4.QueryCache.php提供缓存查询详情、列表(支持分页、排序设置)

<?php

namespace App\Models\Cache;
use App\Models\Base;

trait QueryCache
{
    /**
     * @var bool
     * 开启分页
     */
    protected static $paging = false;

    /**
     * @var int
     * 设置当前页码
     */
    protected static $page = 1;

    /**
     * @var int
     * 设置每页记录数
     */
    protected static $pageSize = 10;

    /**
     * @var array
     * 获取的字段 为空默认返回所有
     */
    protected static $getFields = [];

    /**
     * @var string
     * 排序字段
     */
    protected static $sortField = '';

    /**
     * @var string
     * 排序方式
     */
    protected static $sortType = 'desc';

    /**
     * @param ...$args
     * @return array|mixed
     * @throws \Exception
     * 获取缓存详情 按缓存key的{$xxx}顺序传参即可
     */
    public function getInfo(...$args)
    {
        $cache = Base::initCache();
        $cacheKey = Base::getCacheKey($args,static::$cacheKey,true);
        if (static::$cacheDataStructure == 'string') {
            $info = json_decode($cache->get($cacheKey),true) ?? [];
        } else {
            $info = static::$getFields ? $cache->hmget($cacheKey,static::$getFields) : $cache->hgetAll($cacheKey);
        }
        return $info;
    }

    /**
     * @param ...$args
     * @return array
     * @throws \Exception
     * 获取缓存列表 按关联缓存key的{$xxx}顺序传参即可
     */
    public function getList(...$args)
    {
        $cahce = Base::initCache();
        $relationCacheKey = Base::getCacheKey($args,static::$relationCacheKey,true);
        $list = [];
        if (static::$relationCacheDataStructure == 'zset') {
            $key = static::$sortField ? static::$sortField.'_'.$relationCacheKey : static::$relationCacheSortFields[0].'_'.$relationCacheKey;
            $start = 0;
            $end = -1;
            if (static::$paging) {
                //开启分页
                $start = (static::$page - 1) * static::$pageSize;
                $end = $start + static::$pageSize - 1;
            }
            $elements = strtolower(static::$sortType) == 'asc' ? $cahce->zRange($key, $start, $end) : $cahce->zRevRange($key, $start, $end);
        } else {
            $elements = [];
            if (static::$paging) {
                //开启分页
                $start = (static::$page - 1) * static::$pageSize;
                $end = $start + static::$pageSize - 1;
                // 使用 SSCAN 迭代获取指定范围内的元素
                $cursor = '0'; // 初始游标值

                do {
                    $result = $cahce->sScan($relationCacheKey, $cursor, 'MATCH', '*', 'COUNT', static::$pageSize);
                    $cursor = $result[0]; // 获取下一次迭代的游标值
                    $elements = array_merge($elements, $result[1]); // 将获取到的元素合并到结果数组中
                } while ($cursor !== '0' && count($elements) < $end); // 继续迭代直到游标为 '0' 或达到结束索引
                // 截取指定范围内的元素
                $elements = array_slice($elements, $start, static::$pageSize);
            } else {
                $elements = $cahce->sMembers($relationCacheKey);
            }
        }
        foreach ($elements as $member) {
            $list[] = self::getInfo($member);
        }

        return $list;
    }
}

四、DEMO测试
1.建个模型AdmUser.php继承Base.php模型

<?php

namespace App\Models;

class AdmUser extends Base
{
    protected $table = 'adm_user';

    protected static $cacheSwitch = true;

    protected static $cacheKey = 'au:{$id}';

    protected static $relationCacheKey = 'aur';

    protected static $relationCacheSortFields = ['id','buy', 'view'];
    /**
     * @param $created
     * @return void
     * ORM设置缓存回调处理
     */
    protected static function createdCacheCallBack($created, $cacheRes){
        var_dump($cacheRes);
    }

    /**
     * @param $updated
     * @return void
     * ORM更新缓存回调处理
     */
    protected static function updatedCacheCallBack($updated, $cacheRes){
        var_dump($cacheRes);
    }

    /**
     * @param $deleted
     * @return void
     * ORM删除缓存回调处理
     */
    protected static function deletedCacheCallBack($deleted, $cacheRes){
        var_dump($cacheRes);
    }
}

2.建个控制器IndexController.php 分开执行cud、查看缓存更新情况!

<?php

namespace App\Http\Controllers\Web;
use App\Models\AdmUser;
use Laravel\Lumen\Routing\Controller;
class IndexController extends Controller
{
    public function index()
    {
        //模拟插入数据
        for ($i = 0; $i < 30; $i++) {
            (AdmUser::query()->create([
                'name'=>uniqid(),
                'age'=>mt_rand(18,50),
                'buy'=>mt_rand(0,3000),
                'view'=>mt_rand(0,30000),
                'created_at'=>time()
            ]));
        }

        //查询缓存数据
        //设置返回字段
//        AdmUser::setGetFields(['age']);
        //查询详情
        $info = AdmUser::getInfo(1);
        var_dump($info);
        //查询列表 开启分页
        AdmUser::setPaging(true);
        //设置当前页码
        AdmUser::setPage(1);
        //设置每页记录数
        AdmUser::setPageSize(5);
        //设置排序字段
        AdmUser::setSortField('view');
        //设置排序方式
        AdmUser::setSortType('asc');
        $list = AdmUser::getList();
        var_dump($list);

        //更新数据
        $au = AdmUser::find(1);
        $au->name = 123456;
        $au->age = 11;
        $au->buy = 22;
        $au->created_at = time();
        $au->save();

        //删除数据
        AdmUser::destroy([1]);
    }
}

3.这是我本地跑的一些数据

基于Eloquent ORM事件的缓存实时更新设计

以上就是这次的ORM缓存实时更新设计、后期有新想法还可以拓展自己想要的东西!!!谢谢观看!!!

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 6

想法很棒,建议看下 watson/rememberable

1年前 评论
yzbfeng 1年前
seebyyu (作者) 1年前

这里是不是写错了 file

1年前 评论
提桶跑路了 (楼主) 1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!