教程:Hyperf
目前仅支持redis。
composer require hyperf/model-cache
配置位置: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连接。
#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值,没有则向缓存设置。
模型中使用的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()实现缓存控制。
根据文档,设置默认值适用于数据库新加字段和缓存数据的适配。
#新加数据库字段
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等修改方法,应该就能刷新缓存。
大概流程是查出表结构,和查出的数据集做对比,然后设置。
Hyperf\ModelCache\Manager设置缓存时使用Manager::getCacheTTL(),设置缓存值的过期时间。
Hyperf\ModelCache\Manager::getCacheTTL()获取缓存时间,其中调用Hyperf\ModelCache\Cacheable::getCacheTTL(),根据其返回值判断。若Cacheable::getCacheTTL()返回null则使用配置文件的值,否之使用Cacheable::getCacheTTL()的值。
根据文档是修改Cacheable::getCacheTTL()返回值,或者直接改配置文件的ttl的值。Cacheable::getCacheTTL()系统文件中未修改返回null,即默认使用配置文件。
用于解决多次查询问题,组后调用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,还在研究。学习差不多之后,这个内容打算之后再开一篇文章。
#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);
}
#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;
}
#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.');
}
}
}
}
#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());
}
}
#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;
}