教程: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\TestControllerpublic 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"typemc:default:m:articles:id:1hashhgetall 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.phpreturn [……"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\Managerpublic 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\Cacheablepublic 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\Modelpublic 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\Buildernamespace 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\Builderpublic function __construct(QueryBuilder $query) {$this->query = $query;}

4.3 设置默认值

#Hyperf\DbConnection\Listener\InitTableCollectorListeneruse 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\TableCollectornamespace 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\EagerLoadListeneruse 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\EagerLoaderuse 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\HandlerInterfaceinterface HandlerInterface extends CacheInterface{public function getConfig(): Config;public function incr($key, $column, $amount): bool;}
#Psr\SimpleCache\CacheInterfacenamespace 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\Managerpublic 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;}