hyperf 二十四 模型缓存

教程:Hyperf

一 安装及配置

1.1 安装

目前仅支持redis。

composer require hyperf/model-cache

 1.2 配置

配置位置:config/autoload/databases.php

配置类型默认值备注
handlerstringHyperf\ModelCache\Handler\RedisHandler::class
cache_keystringmc:%s:m:%s:%s:%smc:缓存前缀:m:表名:主键 KEY:主键值
prefixstringdb connection name缓存前缀
poolstringdefault缓存池
ttlint3600超时时间
empty_model_ttlint60查询不到数据时的超时时间
load_scriptbooltrueRedis 引擎下 是否使用 evalSha 代替 eval
use_default_valueboolfalse是否使用数据库默认值
return [
    'default' => [
        ……
        'cache' => [
            'handler' => \Hyperf\ModelCache\Handler\RedisHandler::class,
            'cache_key' => 'mc:%s:m:%s:%s:%s',
            'prefix' => 'default',
            'ttl' => 3600 * 24,
            'empty_model_ttl' => 3600,
            'load_script' => true,
            'use_default_value' => false,
        ]
    ],
];

 该配置参数在Hyperf\ModelCache\Manager::__construct()中使用,该方法在调用Hyperf\ModelCache\Cacheable的成员方法时被调用。即每次查询和修改缓存执行一次redis连接。

二 使用

2.1 查询

#App1\Model\Article 
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
class Article extends Model implements CacheableInterface {
    use Cacheable;
   ……
}

#App\Controller\TestController
public function testmodelcache() {
        $model = Article::findFromCache(1)->toArray();
        var_dump($model);
        $models = Article::findManyFromCache([1, 2])->toArray();
        var_dump($models);
    }
}

 测试结果

array(6) {
  ["id"]=>
  int(1)
  ["user_id"]=>
  string(1) "1"
  ["title"]=>
  string(5) "test1"
  ["created_at"]=>
  string(19) "2024-01-13 10:05:51"
  ["updated_at"]=>
  string(19) "2024-01-13 10:05:53"
  ["deleted_at"]=>
  string(0) ""
}

array(2) {
  [0]=>
  array(6) {
    ["id"]=>
    int(1)
    ["user_id"]=>
    string(1) "1"
    ["title"]=>
    string(5) "test1"
    ["created_at"]=>
    string(19) "2024-01-13 10:05:51"
    ["updated_at"]=>
    string(19) "2024-01-13 10:05:53"
    ["deleted_at"]=>
    string(0) ""
  }
  [1]=>
  array(6) {
    ["id"]=>
    int(2)
    ["user_id"]=>
    string(1) "1"
    ["title"]=>
    string(5) "test2"
    ["created_at"]=>
    string(19) "2024-01-13 10:06:04"
    ["updated_at"]=>
    string(19) "2024-01-13 10:06:06"
    ["deleted_at"]=>
    string(0) ""
  }
}

 redis结果

keys *
1) "mc:default:m:articles:id:2"
2) "mc:default:m:articles:id:1"
3) "test"
4) "n0fsWPgnTRdnlB2VHFdhyPLAZlEZ4HgC1RdurOpV"


type  mc:default:m:articles:id:1
hash


hgetall mc:default:m:articles:id:1
 1) "id"
 2) "1"
 3) "user_id"
 4) "1"
 5) "title"
 6) "test1"
 7) "created_at"
 8) "2024-01-13 10:05:51"
 9) "updated_at"
10) "2024-01-13 10:05:53"
11) "deleted_at"
12) ""
13) "HF-DATA"
14) "DEFAULT"

 每次查询先获取链接,再判断缓存中是否有对应key值,没有则向缓存设置。

2.2 修改或删除

模型中使用的Cacheable,重写了修改和删除,以便处理缓存。

测试 缓存写入

 $model = Article::findFromCache(3)->toArray();
 var_dump($model);

 测试结果

array(6) {
  ["id"]=>
  int(3)
  ["user_id"]=>
  int(2)
  ["title"]=>
  string(5) "test3"
  ["created_at"]=>
  string(19) "2024-01-30 13:38:46"
  ["updated_at"]=>
  NULL
  ["deleted_at"]=>
  NULL
}


127.0.0.1:6379> keys *
1) "mc:default:m:articles:id:3"
2) "mc:default:m:articles:id:2"
3) "mc:default:m:articles:id:1"
4) "test"
127.0.0.1:6379> hgetall "mc:default:m:articles:id:3"
 1) "id"
 2) "3"
 3) "user_id"
 4) "2"
 5) "title"
 6) "test3"
 7) "created_at"
 8) "2024-01-30 13:38:46"
 9) "updated_at"
10) ""
11) "deleted_at"
12) ""
13) "HF-DATA"
14) "DEFAULT"

 测试 缓存删除

$res = Article::query(true)->where('id', '=', 3)->delete();
var_dump($res);

测试结果

int(1)


127.0.0.1:6379> keys *
1) "mc:default:m:articles:id:2"
2) "mc:default:m:articles:id:1"
3) "test"

 Cacheable复写的query()通过传入参数设置设否使用缓存。Cacheable复写的newModelBuilder()实现缓存控制。

2.3 使用默认值

根据文档,设置默认值适用于数据库新加字段和缓存数据的适配。

#新加数据库字段
ALTER TABLE `test`.`articles` 
ADD COLUMN `pv_num` int(4) NULL DEFAULT 0 COMMENT '浏览量' AFTER `deleted_at`;
#添加监听
#config\autoload\listeners.php
return [
    ……
    "Hyperf\DbConnection\Listener\InitTableCollectorListener",
];
#修改设置
return [
    'default' => [
        'cache' => [
            ……
            'use_default_value' => true,
        ],
    ]
]
$model = Article::findFromCache(1)->toArray();
var_dump($model);

 测试结果

array(7) {
  ["id"]=>
  int(1)
  ["user_id"]=>
  string(1) "1"
  ["title"]=>
  string(5) "test1"
  ["created_at"]=>
  string(19) "2024-01-13 10:05:51"
  ["updated_at"]=>
  string(19) "2024-01-13 10:05:53"
  ["deleted_at"]=>
  string(0) ""
  ["pv_num"]=>
  string(1) "0"
}
127.0.0.1:6379> hgetall mc:default:m:articles:id:1
 1) "id"
 2) "1"
 3) "user_id"
 4) "1"
 5) "title"
 6) "test1"
 7) "created_at"
 8) "2024-01-13 10:05:51"
 9) "updated_at"
10) "2024-01-13 10:05:53"
11) "deleted_at"
12) ""
13) "HF-DATA"
14) "DEFAULT"

再次获取数据,若缓存之中有数据则直接获取缓存数据。可以看见缓存中是没有浏览量字段,但是查出的结果中有对应字段。

为了证明不是从数据直接取值,第一可以查询数据库日志。

日志情况如下。可以看到仅查了一次之后删除,之后查的是数据库字段。

[2024-01-30 05:43:52] sql.INFO: [1.61] select `id` from `articles` where `id` = '3' and `articles`.`deleted_at` is null [] []
[2024-01-30 05:43:52] sql.INFO: [82.67] update `articles` set `deleted_at` = '2024-01-30 05:43:52', `articles`.`updated_at` = '2024-01-30 05:43:52' where `id` = '3' and `articles`.`deleted_at` is null [] []
[2024-01-30 06:45:17] sql.INFO: [195.56] select `table_schema`, `table_name`, `column_name`, `ordinal_position`, `column_default`, `is_nullable`, `data_type`, `column_comment` from information_schema.columns where `table_schema` = 'test' order by ORDINAL_POSITION [] []
[2024-01-30 06:45:17] sql.INFO: [186.61] select `table_schema`, `table_name`, `column_name`, `ordinal_position`, `column_default`, `is_nullable`, `data_type`, `column_comment` from information_schema.columns where `table_schema` = 'test' order by ORDINAL_POSITION [] []
[2024-01-30 06:45:17] sql.INFO: [260.73] select `table_schema`, `table_name`, `column_name`, `ordinal_position`, `column_default`, `is_nullable`, `data_type`, `column_comment` from information_schema.columns where `table_schema` = 'test' order by ORDINAL_POSITION [] []

 还可以先改数据再看运行结果。比如我直接改数据库对应id的pv_num值为1,但是查出的还是为0,但是调用update等修改方法,应该就能刷新缓存。

大概流程是查出表结构,和查出的数据集做对比,然后设置。

2.4 控制缓存时间

Hyperf\ModelCache\Manager设置缓存时使用Manager::getCacheTTL(),设置缓存值的过期时间。

Hyperf\ModelCache\Manager::getCacheTTL()获取缓存时间,其中调用Hyperf\ModelCache\Cacheable::getCacheTTL(),根据其返回值判断。若Cacheable::getCacheTTL()返回null则使用配置文件的值,否之使用Cacheable::getCacheTTL()的值。

根据文档是修改Cacheable::getCacheTTL()返回值,或者直接改配置文件的ttl的值。Cacheable::getCacheTTL()系统文件中未修改返回null,即默认使用配置文件。

2.5 预加载

用于解决多次查询问题,组后调用Hyperf\ModelCache\Manager::findManyFromCache()方法,使用whereIn查询。

官网提供两种方法,一个是使用监听,一个手动调用EagerLoader::load()。其实监听也是调用EagerLoader::load()。

model::loadCache()就是调用EagerLoader::load()。

EagerLoader::load()会执行查询对应关系数据的sql。

测试内容结合hyperf 二十三 分页-CSDN博客 中Article::author()设置。

$obj = Article::findManyFromCache([1, 2, 3]);
$obj->loadCache(['author']);
foreach ($obj as $item) {
     var_dump($item->toArray());
}

测试结果

array(8) {
  ["id"]=>
  int(1)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test1"
  ["created_at"]=>
  string(19) "2024-01-13 10:05:51"
  ["updated_at"]=>
  string(19) "2024-01-13 10:05:53"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(1)
  ["author"]=>
  array(4) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(3) "123"
    ["age"]=>
    int(22)
    ["deleted_at"]=>
    NULL
  }
}
array(8) {
  ["id"]=>
  int(2)
  ["user_id"]=>
  int(1)
  ["title"]=>
  string(5) "test2"
  ["created_at"]=>
  string(19) "2024-01-13 10:06:04"
  ["updated_at"]=>
  string(19) "2024-01-13 10:06:06"
  ["deleted_at"]=>
  NULL
  ["pv_num"]=>
  int(0)
  ["author"]=>
  array(4) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(3) "123"
    ["age"]=>
    int(22)
    ["deleted_at"]=>
    NULL
  }
}

 日志内容

[2024-01-30 09:49:46] sql.INFO: [40.81] select * from `articles` where `id` in ('1', '2', '3') and `articles`.`deleted_at` is null [] []
[2024-01-30 09:49:46] sql.INFO: [15.48] select * from `userinfo` where `userinfo`.`id` in (1) and `userinfo`.`deleted_at` is null [] []

三 缓存适配器

继承Hyperf\ModelCache\Handler\HandlerInterface,参考Hyperf\ModelCache\Handler\RedisHandler和Hyperf\ModelCache\Handler\RedisStringHandler。

自己写着练手的项目打算用PostgreSql,还在研究。学习差不多之后,这个内容打算之后再开一篇文章。

四 源码

4.1 配置使用

#Hyperf\ModelCache\Manager
public function __construct(ContainerInterface $container) {
        $this->container = $container;
        $this->logger = $container->get(StdoutLoggerInterface::class);
        $this->collector = $container->get(TableCollector::class);

        $config = $container->get(ConfigInterface::class);
        if (!$config->has('databases')) {
            throw new InvalidArgumentException('config databases is not exist!');
        }

        foreach ($config->get('databases') as $key => $item) {
            $handlerClass = $item['cache']['handler'] ?? RedisHandler::class;
            $config = new Config($item['cache'] ?? [], $key);

            /** @var HandlerInterface $handler */
            $handler = make($handlerClass, ['config' => $config]);

            $this->handlers[$key] = $handler;
        }
    }

#Hyperf\ModelCache\Cacheable
public static function findFromCache($id): ?Model
    {
        $container = ApplicationContext::getContainer();
        $manager = $container->get(Manager::class);

        return $manager->findFromCache($id, static::class);
    }

4.2 缓存数据设计及更新

#Hyperf\ModelCache\Cacheable 
use Hyperf\ModelCache\Builder as ModelCacheBuilder;
public static function query(bool $cache = false): Builder
    {
        return (new static())->newQuery($cache);
    }
public function newModelBuilder($query): Builder
    {
        if ($this->useCacheBuilder) {
            return new ModelCacheBuilder($query);
        }

        return parent::newModelBuilder($query);
    }

#Hyperf\Database\Mode\Model
public function newQuery() {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }
public function newQueryWithoutScopes() {
        return $this->newModelQuery()->with($this->with)->withCount($this->withCount);
    }
public function newModelQuery() {
        return $this->newModelBuilder($this->newBaseQueryBuilder())->setModel($this);
    }

#Hyperf\ModelCache\Builder
namespace Hyperf\ModelCache;

use Hyperf\Database\Model\Builder as ModelBuilder;
use Hyperf\Utils\ApplicationContext;

class Builder extends ModelBuilder
{
    public function delete()
    {
        return $this->deleteCache(function () {
            return parent::delete();
        });
    }

    public function update(array $values)
    {
        return $this->deleteCache(function () use ($values) {
            return parent::update($values);
        });
    }

    protected function deleteCache(\Closure $closure)
    {
        $queryBuilder = clone $this;
        $primaryKey = $this->model->getKeyName();
        $ids = [];
        $models = $queryBuilder->get([$primaryKey]);
        foreach ($models as $model) {
            $ids[] = $model->{$primaryKey};
        }
        if (empty($ids)) {
            return 0;
        }

        $result = $closure();

        $manger = ApplicationContext::getContainer()->get(Manager::class);

        $manger->destroy($ids, get_class($this->model));

        return $result;
    }
}

#Hyperf\Database\Model\Builder
public function __construct(QueryBuilder $query) {
        $this->query = $query;
    }

4.3 设置默认值

#Hyperf\DbConnection\Listener\InitTableCollectorListener
use Hyperf\DbConnection\Collector\TableCollector;
class InitTableCollectorListener implements ListenerInterface {
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var ConfigInterface
     */
    protected $config;

    /**
     * @var StdoutLoggerInterface
     */
    protected $logger;

    /**
     * @var TableCollector
     */
    protected $collector;

    public function __construct(ContainerInterface $container) {
        $this->container = $container;
        $this->config = $container->get(ConfigInterface::class);
        $this->logger = $container->get(StdoutLoggerInterface::class);
        $this->collector = $container->get(TableCollector::class);
    }

    public function listen(): array {
        return [
            BeforeHandle::class,
            AfterWorkerStart::class,
            BeforeProcessHandle::class,
        ];
    }

    public function process(object $event) {
        try {
            $databases = $this->config->get('databases', []);
            $pools = array_keys($databases);
            foreach ($pools as $name) {
                $this->initTableCollector($name);
            }
        } catch (\Throwable $throwable) {
            $this->logger->error((string) $throwable);
        }
    }

    public function initTableCollector(string $pool) {
        if ($this->collector->has($pool)) {
            return;
        }

        /** @var ConnectionResolverInterface $connectionResolver */
        $connectionResolver = $this->container->get(ConnectionResolverInterface::class);
        /** @var MySqlConnection $connection */
        $connection = $connectionResolver->connection($pool);

        /** @var \Hyperf\Database\Schema\Builder $schemaBuilder */
        $schemaBuilder = $connection->getSchemaBuilder();
        $columns = $schemaBuilder->getColumns();

        foreach ($columns as $column) {
            $this->collector->add($pool, $column);
        }
    }
}
#Hyperf\DbConnection\Collector\TableCollector
namespace Hyperf\DbConnection\Collector;

use Hyperf\Database\Schema\Column;

class TableCollector
{
    /**
     * @var array
     */
    protected $data = [];

    /**
     * @param Column[] $columns
     */
    public function set(string $pool, string $table, array $columns)
    {
        $this->validateColumns($columns);
        $this->data[$pool][$table] = $columns;
    }

    public function add(string $pool, Column $column)
    {
        $this->data[$pool][$column->getTable()][$column->getName()] = $column;
    }

    public function get(string $pool, ?string $table = null): array
    {
        if ($table === null) {
            return $this->data[$pool] ?? [];
        }

        return $this->data[$pool][$table] ?? [];
    }

    public function has(string $pool, ?string $table = null): bool
    {
        return ! empty($this->get($pool, $table));
    }

    public function getDefaultValue(string $connectName, string $table): array
    {
        $columns = $this->get($connectName, $table);
        $list = [];
        foreach ($columns as $column) {
            $list[$column->getName()] = $column->getDefault();
        }
        return $list;
    }

    /**
     * @throws \InvalidArgumentException When $columns is not equal to Column[]
     */
    protected function validateColumns(array $columns): void
    {
        foreach ($columns as $column) {
            if (! $column instanceof Column) {
                throw new \InvalidArgumentException('Invalid columns.');
            }
        }
    }
}

4.4 设置模型关系

#Hyperf\ModelCache\Listener\EagerLoadListener
use Hyperf\ModelCache\EagerLoad\EagerLoader;
class EagerLoadListener implements ListenerInterface
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function listen(): array
    {
        return [
            BootApplication::class,
        ];
    }

    public function process(object $event)
    {
        $eagerLoader = $this->container->get(EagerLoader::class);
        Collection::macro('loadCache', function ($parameters) use ($eagerLoader) {
            $eagerLoader->load($this, $parameters);
        });
    }
}
#Hyperf\ModelCache\EagerLoad\EagerLoader
use Hyperf\Database\Query\Builder as QueryBuilder;
class EagerLoader
{
    public function load(Collection $collection, array $relations)
    {
        if ($collection->isNotEmpty()) {
            /** @var Model $first */
            $first = $collection->first();
            $query = $first->registerGlobalScopes($this->newBuilder($first))->with($relations);
            $collection->fill($query->eagerLoadRelations($collection->all()));
        }
    }

    protected function newBuilder(Model $model): Builder
    {
        $builder = new EagerLoaderBuilder($this->newBaseQueryBuilder($model));

        return $builder->setModel($model);
    }

    /**
     * Get a new query builder instance for the connection.
     *
     * @return \Hyperf\Database\Query\Builder
     */
    protected function newBaseQueryBuilder(Model $model)
    {
        /** @var Connection $connection */
        $connection = $model->getConnection();

        return new QueryBuilder($connection, $connection->getQueryGrammar(), $connection->getPostProcessor());
    }
}

4.5 适配

#Hyperf\ModelCache\Handler\HandlerInterface
interface HandlerInterface extends CacheInterface
{
    public function getConfig(): Config;

    public function incr($key, $column, $amount): bool;
}

#Psr\SimpleCache\CacheInterface
namespace Psr\SimpleCache;

interface CacheInterface
{
    /**
     *从缓存中获取一个值。
     * @param string $key该项在缓存中的唯一键。
     * @param mixed $default键不存在时返回的默认值。
     * @return mix缓存项的值,如果缓存失败,则为$default。
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果$key字符串不是合法值,必须抛出。
     */
    public function get($key, $default = null);

    /**
     * 设置缓存字段和TTL过期时间
     *
     * @param string                 $key   存储键名
     * @param mixed                  $value 存储键值,必须可序列化
     * @param null|int|\DateInterval $ttl   过期时间,为空则使用配置文件(驱动)或redis过期时间
     *
     * @return bool 成功返回true,失败返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果$key字符串不是合法值,必须抛出。
     */
    public function set($key, $value, $ttl = null);

    /**
     * 根据唯一键删除缓存
     *
     * @param string $key 用于删除的唯一键名
     *
     * @return bool 成功返回true,失败返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果$key字符串不是合法值,必须抛出。
     */
    public function delete($key);

    /**
     * 擦除清除整个缓存的键。
     *
     * @return bool 成功返回true,失败返回false
     */
    public function clear();

    /**
     * 根据其唯一键获取多个缓存项。
     *
     * @param iterable $keys    在一次操作中可以获得的键的列表。
     * @param mixed    $default 对于不存在的键返回的默认值。
     *
     * @return 返回键值对形式的数组,过期数据使用默认值
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   任何$key字符串不是合法值,必须抛出。
     */
    public function getMultiple($keys, $default = null);

    /**
     * 设置键值对数组的缓存,并设置过期时间TTL.
     *
     * @param iterable               $values 键值对数组
     * @param null|int|\DateInterval $ttl    过期时间
     *                                       
     *
     * @return bool 成功返回true,失败返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   任何$key字符串不是合法值,必须抛出。
     */
    public function setMultiple($values, $ttl = null);

    /**
     * 在单个操作中删除多个缓存项。
     *
     * @param iterable $keys 要删除的基于字符串的键的列表。
     *
     * @return bool 成功返回true,失败返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   任何$key字符串不是合法值,必须抛出。
     */
    public function deleteMultiple($keys);

    /**
     * 确定项是否存在于缓存中。
     * 注意:建议has()仅用于缓存升温类型
     * 而不是在您的实时应用程序操作中使用get/set,就像这个方法一样
     * 受竞争条件的约束,其中has()将返回true,并立即返回。
     * 另一个脚本可以删除它,使你的应用程序的状态过时。
     *
     * @param string $key 键名
     *
     * @return bool 成功返回true,失败返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果$key字符串不是合法值,必须抛出。
     */
    public function has($key);
}

其中has()的注意事项没有看懂。官网说参考Hyperf\ModelCache\Handler\RedisStringHandler。但是框架中并没有使用,应该是可以替换配置的RedisHandler。

'handler' => \Hyperf\ModelCache\Handler\RedisHandler::class,

 但是能查到has()使用代码示例。

#Hyperf\ModelCache\Manager
public function increment($id, $column, $amount, string $class): bool {
        /** @var Model $instance */
        $instance = new $class();

        $name = $instance->getConnectionName();
        if ($handler = $this->handlers[$name] ?? null) {
            $key = $this->getCacheKey($id, $instance, $handler->getConfig());
            if ($handler->has($key)) {
                return $handler->incr($key, $column, $amount);
            }

            return false;
        }

        $this->logger->alert('Cache handler not exist, increment failed.');
        return false;
    }

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lsswear

感谢大佬打赏 q(≧▽≦q)

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值