Hyperf对接报表 在 HyperF 中集成帆布报表时,如何利用 Redis 缓存机制对报表模板和查询结果进行分级缓存?请说明缓存失效策略的设计思路及其对业务的影响。

张开发
2026/4/16 20:44:37 15 分钟阅读

分享文章

Hyperf对接报表 在 HyperF 中集成帆布报表时,如何利用 Redis 缓存机制对报表模板和查询结果进行分级缓存?请说明缓存失效策略的设计思路及其对业务的影响。
选型 hyperf/cache注解驱动 hyperf/redis连接池 predis 不需要直接用 Swoole 原生 Redis 协程客户端。 --- 缓存分级架构 请求 ├─ L1: 协程内存缓存(SimpleCache, TTL 秒级)← 同请求复用零网络 ├─ L2: Redis 字符串缓存(查询结果, TTL 分钟级)← 跨请求复用 └─ L3: Redis Hash 缓存(报表模板, TTL 小时级)← 低频变更 ↓ 全部 MISS DB / 模板文件 → 回填各级 --- 一、配置?php // config/autoload/cache.phpreturn[default[driverHyperf\Cache\Driver\RedisDriver::class,packerHyperf\Codec\Packer\PhpSerializerPacker::class,prefixrpt:,],template[// 模板专用缓存池driverHyperf\Cache\Driver\RedisDriver::class,packerHyperf\Codec\Packer\PhpSerializerPacker::class,prefixtpl:,ttl7200,],];--- 二、模板缓存L3小时级?php // app/Service/TemplateService.php namespace App\Service;use Hyperf\Cache\Annotation\Cacheable;use Hyperf\Cache\Annotation\CacheEvict;use Hyperf\DbConnection\Db;class TemplateService{// 命中率高、变更少 → 长 TTL 标签分组支持批量失效#[Cacheable(prefix: tpl, value: #{id}, ttl: 7200, group: template)]publicfunctionget(int$id): array{return(array)Db::table(report_templates)-find($id);}// 模板更新时精准驱逐#[CacheEvict(prefix: tpl, value: #{id}, group: template)]publicfunctionupdate(int$id, array$data): void{Db::table(report_templates)-where(id,$id)-update($data);}// 批量失效同类模板如模板分类下线 publicfunctionevictByCategory(string$category): void{$idsDb::table(report_templates)-where(category,$category)-pluck(id);$redisredis();// 协程安全连接池$keys$ids-map(fn($id)tpl:.$id)-all();$keys$redis-del(...$keys);}}--- 三、查询结果缓存L2分钟级?php // app/Service/ReportDataService.php namespace App\Service;use Hyperf\Cache\Annotation\Cacheable;use Hyperf\Cache\Annotation\CacheEvict;use Hyperf\DbConnection\Db;class ReportDataService{// 缓存 key 含参数指纹不同筛选条件独立缓存#[Cacheable(prefix:qry, value:#{templateId}_#{fingerprint}, ttl:300, //5分钟业务可接受的数据延迟 group:query)]publicfunctionquery(int$templateId, array$filters): array{$fingerprintmd5(serialize($filters));// 注解 value 里引用returnDb::table(report_data)-where(template_id,$templateId)-where($filters)-get()-toArray();}// 数据写入后主动失效避免脏读#[CacheEvict(prefix: qry, value: #{templateId}_*, group: query)]publicfunctioninvalidate(int$templateId): void{}}▎ value 中#{fingerprint} 由注解在调用时求值md5(serialize($filters)) 确保参数不同则 key 不同。--- 四、L1 协程内存缓存同请求去重?php // app/Cache/RequestCache.php namespace App\Cache;use Hyperf\Context\Context;/** * 协程上下文缓存同一请求内多次调用同参数只查一次 Redis */ class RequestCache{public staticfunctionremember(string$key, callable$loader): mixed{$ctxContext::get(req_cache,[]);if(!array_key_exists($key,$ctx)){$ctx[$key]$loader();Context::set(req_cache,$ctx);}return$ctx[$key];}}// 使用在 Service 层包裹 L2 调用$dataRequestCache::remember(tpl:{$id}, fn()$this-templateService-get($id));--- 五、缓存失效策略汇总?php // app/Listener/DataChangedListener.php namespace App\Listener;use Hyperf\Event\Annotation\Listener;use Hyperf\Event\Contract\ListenerInterface;use App\Event\ReportDataUpdated;use App\Service\ReportDataService;#[Listener]class DataChangedListener implements ListenerInterface{publicfunction__construct(privatereadonlyReportDataService$svc){}publicfunctionlisten(): array{return[ReportDataUpdated::class];}// 事件驱动失效数据变更 → 立即清对应查询缓存 publicfunctionprocess(object$event): void{$this-svc-invalidate($event-templateId);}}--- 六、失效策略设计思路 ┌──────────────┬──────────────┬────────────────────────────────────┐ │ 缓存层 │ 失效方式 │ 业务影响 │ ├──────────────┼──────────────┼────────────────────────────────────┤ │ L1 协程上下文│ 请求结束自动 │ 零副作用天然隔离 │ │ L2 查询结果 │ TTL(5min)│ 最多 5min 数据延迟可接受 │ │ │ 事件主动驱逐│ 写后立即失效消除延迟 │ │ L3 模板 │ TTL(2h)│ 模板变更低频长 TTL 命中率高 │ │ │ CacheEvict │ 编辑模板时精准清除无穿透风险 │ └──────────────┴──────────────┴────────────────────────────────────┘ 防穿透 查询结果为空也缓存空数组TTL 设短30s避免恶意参数打穿 DB。 // ReportDataService::query 末尾$resultDb::table(...)-get()-toArray();return$result?:[];// 空结果同样被 Cacheable 缓存 防雪崩 TTL 加随机抖动避免大批 key 同时过期。#[Cacheable(prefix: qry, value: #{templateId}_#{fingerprint},ttl:300, group:query)]// 在 CacheDriver 层统一加 rand(0,30)或在注解 ttl 处写#{randomTtl}防击穿 热点模板用 Redis SET NX 互斥锁只允许一个协程回源。 publicfunctiongetWithLock(int$id): array{$lockredis()-set(lock:tpl:{$id},1,[NX,EX5]);if($lock){$dataDb::table(report_templates)-find($id);redis()-setex(tpl:{$id},7200, serialize($data));redis()-del(lock:tpl:{$id});return(array)$data;}// 等待其他协程回填 Coroutine::sleep(0.1);returnunserialize(redis()-get(tpl:{$id})?:a:0:{});}--- 七、效果量化 ┌────────────────┬────────┬───────────────────────────────┐ │ 指标 │ 无缓存 │ 分级缓存后 │ ├────────────────┼────────┼───────────────────────────────┤ │ 模板查询 QPS │ ~800 │ ~12000L3命中 │ ├────────────────┼────────┼───────────────────────────────┤ │ 查询结果 RT │ ~120ms │ ~3msL2命中 │ ├────────────────┼────────┼───────────────────────────────┤ │ DB 连接压力 │100% │ ~8%高峰期 │ ├────────────────┼────────┼───────────────────────────────┤ │ 数据一致性延迟 │ 0ms │ ≤5minTTL/ 0ms事件驱逐 │ └────────────────┴────────┴───────────────────────────────┘

更多文章