教程:Hyperf
一 安装及配置
1.1 安装
目前仅支持redis。
composer require hyperf/model-cache
1.2 配置
配置位置:config/autoload/databases.php
配置 | 类型 | 默认值 | 备注 |
---|---|---|---|
handler | string | Hyperf\ModelCache\Handler\RedisHandler::class | 无 |
cache_key | string | mc:%s:m:%s:%s:%s | mc:缓存前缀:m:表名:主键 KEY:主键值 |
prefix | string | db connection name | 缓存前缀 |
pool | string | default | 缓存池 |
ttl | int | 3600 | 超时时间 |
empty_model_ttl | int | 60 | 查询不到数据时的超时时间 |
load_script | bool | true | Redis 引擎下 是否使用 evalSha 代替 eval |
use_default_value | bool | false | 是否使用数据库默认值 |
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
hashhgetall mc:default:m:articles:id:11) "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:11) "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;}