KEY_REGISTRY_REV
KEY_REGISTRY_REV = 'cm:registry_rev'
注册表 rev 版本号(td_sys_cache 变更时 INCR,跨进程失效信号)
缓存管理器(集中式缓存治理,可携带模块)。
职责:全站缓存的唯一治理入口——缓存 key 集中注册(注册表)、统一读写、自动失效与 改名迁移都经此类,调用方不写裸 key、不自行清缓存。跨应用 key 带应用前缀 (admin:/client:...),主控(admin)为权威,其他端参与。
注册表结构(流程:① 逻辑名映射到含 {占位} 的 key 模板 ② 按分层/TTL/依赖表/连接 登记进 td_sys_cache ③ 进程内缓存 + Redis rev 版本号保证跨进程一致): 数据库表 td_sys_cache(SysCache 模型)全部缓存项集中登记,管理页可治理 (key 模板可改=清旧自动重建、TTL/分层/依赖表/说明可改、审批); 分层:global(全局规则,事件失效)/ user(用户维度,事件失效+模型关系推导受影响用户)/ ttl(短时,自然过期)/ permanent(永久,丢失重建)。
失效协议(流程:① 表数据变更 ② 遍历注册表找依赖该表的缓存项 ③ global 整组清、 user 层按表解析受影响用户精确清 ④ 受保护项豁免): 自动失效 = 数据变更 → invalidateByTable 沿模型关系反向推导受影响缓存并清理; 保护锁 = protected 项不受任何清理操作删除(全清/单删/事件失效/改名迁移均豁免)。
deferred 事务语义(流程:① 事务内触发 → 失效计划即时解析、Redis 删除挂 DB::afterCommit 提交后执行、回滚丢弃 ② 事务外触发 → 立即执行): 模型事件走 invalidateByTableDeferred;任何手动 forget/forgetAll/invalidateByTable 一律放在事务提交之后,禁止事务内清缓存——事务中途清缓存会留「并发读以提交前旧数据 重建缓存 → 提交后脏缓存残留至 TTL」的竞态窗口。
双连接:注册项用 connection 标记所在 Redis 连接—— default(db0):Redis::setex/redis_set 直连项(user_permissions/token/captcha 等) cache(db1):Cache::store 项(tag 存储,key = 连接前缀+store前缀+sha1:业务key) 管理面扫描/删除一律按项选连接,并用 SCAN 游标迭代(不用 KEYS 全库阻塞)。
┌────────────────── 分区导航(按此顺序阅读)──────────────────┐ 分区 1 · 常量与进程缓存 KEY_REGISTRY_REV / KEY_TTL_OVERRIDES KEY_SNAPSHOT / 三个静态缓存字段 分区 2 · 注册表 registry / loadRegistryFromDb / invalidateRegistry resetLocalCaches / has / item / isProtected ttl / key / autoRegister / ensureRegistered 分区 3 · 连接归属 connectionOf / isDirect 分区 4 · 读写 remember / get / put / forget / normalizeArgs / tagsFor 分区 5 · SCAN 与原始 key scanConnectionKeys / scanInstanceKeys / scanTemplateKeys deleteRawKeys / ttlOfRaw 分区 6 · 改名迁移 snapshotRegistry / migrateKeys / clearByPrefixPublic 分区 7 · 发现与自动登记 discoverAndRegister / inferNameTemplate / vocab dictValues / dictItems / dictEntries / vocabDictCode keyTemplateCompliant / logicalNameCompliant 分区 8 · 自动失效引擎 invalidateByTable / invalidateByTableDeferred buildInvalidationPlan / applyInvalidationPlan usersOfRole / usersOfPost / resolveAffectedUsers affectsAllUsers / forgetAll / forgetConfigGroup └────────────────────────────────────────────────────────────┘
$registryCache : ?array
进程内静态缓存:避免每次缓存读都回源 Redis
$registryRevCache : ?string
$registryRevCheckedAt : ?float
rev 检查时间窗口(微秒时间戳):窗口内不重复发 Redis GET(注册表变更极低频)
$registryRevLatest : ?string
最近一次 rev 检查值(窗口内复用)
$ttlOverridesCache : ?array
registry() : array
全部缓存注册项(数据库表 td_sys_cache 全量,含已审批与待审批)。
流程:① 读 Redis rev 版本号 ② 与本地缓存的 rev 对比,变化才重查 DB 重建 ③ 加载失败不缓存空结果(下次访问重试),避免进程存活期内误报「缓存未注册」
进程内静态缓存 + Redis rev 版本号:每次调用 Redis GET rev 对比本地, 变化才重查 DB(php -S 单进程跨请求、php-fpm 多进程均一致); SysCache 增删改 → invalidateRegistry() 递增 rev。
remember(string $logicalName, array<string,int|string>|int|string|null $params = [], ?callable $callback = null) : mixed
读缓存;未命中则回调重建并写入(读多写少,标准范式)。
参数三种形态(极简使用): remember('sys:site:info', fn () => $info) // 无参数缓存,直接省参数数组 remember('user:menu:tree', $userId, fn () => $tree) // 单值自动映射注册表唯一占位符({userId}) remember('user:menu:tree', ['userId' => 1], fn () => $tree) // 多参数显式数组
| string | $logicalName | |
| array |
$params | |
| ?callable | $callback |
scanConnectionKeys(string $connection, string $pattern) : string[]
SCAN 游标迭代某连接上匹配 pattern 的原始 key(含连接前缀)。
注意前缀行为差异(实测):
| string | $connection | |
| string | $pattern |
原始完整 key(含连接前缀)
systemMarkers() : string[]
系统内部 key 豁免标记单源:discover 与 flush 共用, 防「两处豁免清单不一致 + CACHE_PREFIX 自定义后 'cache:' 字面量失效 → 误删/误登记」。
成员:
discoverAndRegister() : int
手动全扫:扫描两个 Redis 连接的全部 key,把未登记的裸写缓存**自动登记为「待审批」**。
认知模型(docs/缓存协议.md §0 缓存准入治理):任何缓存写入(无论走 CacheManager 还是裸写) 都会被发现进入「待审批」,脱离治理的自然回到治理——不靠约定强制。 逻辑名/key 模板按裸写 key 推断(初始猜测),管理员审批时修正(审核通过前一切可改)。 自动定时扫描待计划任务模块(见 docs/待办清单.md)。
新登记条数(已存在同逻辑名待审批项的重扫不计)
keyTemplateCompliant(string $template) : array
缓存 key 模板合规判定(五层标准,词表全部出自字典)。
结构:{领域}:{实体}:{动作}[:{标识}][:{归属}];cm 域豁免(治理元数据,内部 key)。 三层校验: 1 格式层:全小写、冒号分隔、花括号占位 2 词表层:领域/实体/动作命中字典词表 3 语义层:标识必须是精确型参数名(Id/Code/Key/md5/账号类,模糊键禁止);归属为白名单或省略
合规 = 可直接使用(自动纳入治理 status=1);不合规 = 待审批(审批时必须改到标准内)。
| string | $template |
invalidateByTable(string $table, int|null $rowId = null) : void
表数据变更 → 自动失效相关缓存(核心:精确推导受影响用户)。
规则:
执行时机:立即执行。模型事件链路请走 invalidateByTableDeferred—— 事件在事务内触发时若立即清,并发读会以「提交前旧数据」重建缓存, 提交后该脏缓存残留至 TTL(缓存失效必须发生在提交之后,见该方法的论证)。
| string | $table | 变更的表名(td_sys_*) |
| int|null | $rowId | 变更的行 id(精确推导用;缺省整表推导) |
invalidateByTableDeferred(string $table, int|null $rowId = null) : void
表数据变更 → 延迟到事务提交后失效(模型事件专用:saved/deleting 在事务内触发)。
流程:① 调用当下即时解析失效计划(受影响缓存名与用户集)② 事务内:挂 DB::afterCommit, 提交后执行 Redis 删除、回滚丢弃 ③ 事务外:立即执行(与 invalidateByTable 等价)
失效计划(受影响缓存名与用户集)在调用当下即时解析——deleting 事件时行仍在, 回查 user_id/role_id/post_id 可得;提交后行已删,届时再解析必漏清。 仅 Redis 删除动作延迟:
残余窗口(已论证可接受):提交语句与 afterCommit 清理之间的微秒级间隙内, 并发读会命中旧缓存(旧缓存 = 提交前已提交状态,不脏);以及并发读在 「缓存缺失 → 读库 → 写回」跨越提交边界的 ABA 写回(写回旧值被提交后清理覆盖, 除非写回恰落在清理之后——后台写并发近零,残留自愈于下次失效/TTL)。
| string | $table | 变更的表名(td_sys_*) |
| int|null | $rowId | 变更的行 id(精确推导用;缺省整表推导) |
autoRegister(string $logicalName, array $params) : array
未登记缓存自动登记(治理闭环:谁都能用缓存,使用产生的名字必须可治理)。 自动 key 模板 = 逻辑名 + 参数键拼 {占位}(remember('x', ['userId'=>1]) → x:{userId}); 默认配置 global 层 / 无 TTL / cache 连接 / 空依赖表。 **合规即用**:key 模板符合五层命名标准(CacheManager::keyTemplateCompliant)→ 直接 status=1 自动纳入治理; 不合规 → status=2 待审批(管理员审批时必须改到标准内)。 firstOrCreate:已存在(含已审批项)绝不覆盖其 key 模板/状态/治理配置。
| string | $logicalName | |
| array | $params |
ensureRegistered(string $logicalName, array $params) : array
确保逻辑名可用:已登记直接用;未登记自动登记(治理闭环,同 remember); 登记写库失败(DB 不可用/表未建)→ 以「逻辑名+参数」推导临时 key 模板兜底。
缓存准入治理的可用性边界:治理缺口只降级、不阻断—— 任何业务链路(含登录/限流/验证码)都不因注册表缺行或不可写而失败; 缺行会在可写时自动登记进治理(待审批),管理员在管理页照常补治理信息。
| string | $logicalName | |
| array | $params |
normalizeArgs(string $logicalName, mixed $params, ?callable $callback = null) : array
参数归一化(极简使用):支持 无参数省略 / 单值自动映射 / 数组 三种形态。 单值 → 从注册表 key 模板找唯一占位符名自动映射(remember('x', $id) → ['userId' => $id]); 未登记项单值 → 用通用占位 'param'(自动登记生成 name:{param},审批时改标准名)。
| string | $logicalName | |
| mixed | $params | |
| ?callable | $callback |
scanTemplateKeys(string $template, bool $direct, string $connection) : array
按 key 模板扫描实例原始 key。 实际 key 结构:连接前缀 + [store前缀 + sha1:] + 业务key—— 直连项 pattern = 业务前缀*;tag 项 = store前缀*:业务前缀*(sha1 段通配)。 无前导 *:按尾段精确匹配,避免 token:/captcha: 与 user_*_captcha:/user_register_code:/user_repass_token: 等业务前缀串扰。 tag 项另扫一遍 store前缀+业务前缀*(裸 Cache::put 不带 sha1 段,两套 pattern 不重叠可安全合并)。
| string | $template | |
| bool | $direct | |
| string | $connection |
dictEntries(string $dictCode) : array
字典启用项基础读取(直查库,不走 sys:dict:list remember 缓存)。
返回全字段数组(id/type_id/type_code/parent_id/label/value/sort/status 等),上层按需取字段。 刻意不套缓存:本方法被 keyTemplateCompliant 调用(合规判定),而 remember 在缓存未登记时 会触发 autoRegister → 再次合规判定 → 再次调用本方法,形成无限递归(测试环境 RefreshDatabase 空注册表必现,生产靠「已登记」侥幸不炸)。词表是合规判定的只读依赖、量小(个位数~几十条), 直查代价可忽略。
| string | $dictCode |