\App\SupportCacheManager

缓存管理器(集中式缓存治理,可携带模块)。

职责:全站缓存的唯一治理入口——缓存 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 └────────────────────────────────────────────────────────────┘

Summary

Methods
Properties
Constants
registry()
invalidateRegistry()
resetLocalCaches()
has()
item()
isProtected()
ttl()
key()
connectionOf()
isDirect()
remember()
get()
put()
forget()
scanConnectionKeys()
scanInstanceKeys()
deleteRawKeys()
ttlOfRaw()
snapshotRegistry()
migrateKeys()
clearByPrefixPublic()
systemMarkers()
scanConnections()
discoverAndRegister()
vocab()
dictValues()
dictItems()
keyTemplateCompliant()
logicalNameCompliant()
invalidateByTable()
invalidateByTableDeferred()
usersOfRole()
usersOfPost()
forgetAll()
forgetConfigGroup()
No public properties found
KEY_REGISTRY_REV
KEY_TTL_OVERRIDES
KEY_SNAPSHOT
No protected methods found
No protected properties found
N/A
loadRegistryFromDb()
autoRegister()
ensureRegistered()
normalizeArgs()
tagsFor()
scanTemplateKeys()
inferNameTemplate()
dictEntries()
vocabDictCode()
buildInvalidationPlan()
applyInvalidationPlan()
resolveAffectedUsers()
affectsAllUsers()
$registryCache
$registryRevCache
$registryRevCheckedAt
$registryRevLatest
$ttlOverridesCache
N/A

Constants

KEY_REGISTRY_REV

KEY_REGISTRY_REV = 'cm:registry_rev'

注册表 rev 版本号(td_sys_cache 变更时 INCR,跨进程失效信号)

KEY_TTL_OVERRIDES

KEY_TTL_OVERRIDES = 'cm:ttl:overrides'

config 定义项的 TTL 覆盖(后台编辑 TTL 用,存 Redis)

KEY_SNAPSHOT

KEY_SNAPSHOT = 'cm:key:snapshot'

治理元数据快照 key(记录各逻辑名上次 key 模板,供改名迁移对比;DataCacheService::flush 需引用故 public)

Properties

$registryCache

$registryCache : ?array

进程内静态缓存:避免每次缓存读都回源 Redis

Type

?array

$registryRevCache

$registryRevCache : ?string

Type

?string

$registryRevCheckedAt

$registryRevCheckedAt : ?float

rev 检查时间窗口(微秒时间戳):窗口内不重复发 Redis GET(注册表变更极低频)

Type

?float

$registryRevLatest

$registryRevLatest : ?string

最近一次 rev 检查值(窗口内复用)

Type

?string

$ttlOverridesCache

$ttlOverridesCache : ?array

Type

?array

Methods

registry()

registry() : array

全部缓存注册项(数据库表 td_sys_cache 全量,含已审批与待审批)。

流程:① 读 Redis rev 版本号 ② 与本地缓存的 rev 对比,变化才重查 DB 重建 ③ 加载失败不缓存空结果(下次访问重试),避免进程存活期内误报「缓存未注册」

进程内静态缓存 + Redis rev 版本号:每次调用 Redis GET rev 对比本地, 变化才重查 DB(php -S 单进程跨请求、php-fpm 多进程均一致); SysCache 增删改 → invalidateRegistry() 递增 rev。

Returns

array —

invalidateRegistry()

invalidateRegistry() : void

注册表变更(增删改/审批/自动登记)后调用:递增 rev + 清本进程缓存,跨进程一致

resetLocalCaches()

resetLocalCaches() : void

清进程内静态缓存(注册表 rev 失效 / TTL 覆盖写后调用)

has()

has(string  $logicalName) : bool

注册表项是否存在

Parameters

string $logicalName

Returns

bool —

item()

item(string  $logicalName) : ?array

取注册项

Parameters

string $logicalName

Returns

?array —

isProtected()

isProtected(string  $logicalName) : bool

保护锁判定:true = 该缓存名所属的全部实例不受任何清理操作删除 (全清 flush / 单删 forget / 整组清 forgetAll / 事件失效 invalidateByTable / 改名迁移均豁免)。 想清理必须先解除保护(管理页编辑 protected=0)。

Parameters

string $logicalName

Returns

bool —

ttl()

ttl(string  $logicalName) : ?int

取逻辑名的有效 TTL:config 定义项先查后台 override,其次注册表声明。 运行时注册项(DataCacheService 建立)ttl 在注册项里,直接用。

Parameters

string $logicalName

Returns

?int —

key()

key(string  $logicalName, array<string,int|string>|int|string|null  $params = []) : string

逻辑名 + 参数 → 实际 key(替换 {xxx} 占位)。 参数支持极简形态:单值自动映射唯一占位符 / 数组(同 remember)。

Parameters

string $logicalName
array|int|string|null $params

Returns

string —

connectionOf()

connectionOf(array  $item) : string

注册项所在 Redis 连接:显式 connection 标记优先;redis_set 直连项默认 default; 其余(Cache::store tag 存储)归 cache。

Parameters

array $item

Returns

string —

isDirect()

isDirect(array  $item) : bool

是否直连项(Redis::setex / redis_set 裸写,不走 Cache::store tag 存储)

Parameters

array $item

Returns

bool —

remember()

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) // 多参数显式数组

Parameters

string $logicalName
array|int|string|null $params
?callable $callback

Returns

mixed —

get()

get(string  $logicalName, mixed  $params = []) : mixed

读缓存(无则 null);未注册名按「未命中」处理(治理缺口只降级不阻断,读路径不产生写入)

Parameters

string $logicalName
mixed $params

Returns

mixed —

put()

put(string  $logicalName, mixed  $params = [], mixed  $value = null) : void

写缓存(手动)

Parameters

string $logicalName
mixed $params
mixed $value

forget()

forget(string  $logicalName, mixed  $params = []) : void

删单个缓存

Parameters

string $logicalName
mixed $params

scanConnectionKeys()

scanConnectionKeys(string  $connection, string  $pattern) : string[]

SCAN 游标迭代某连接上匹配 pattern 的原始 key(含连接前缀)。

注意前缀行为差异(实测):

  • phpredis:不给 SCAN 的 MATCH pattern 加 OPT_PREFIX → 手动补连接前缀;返回原始 key
  • predis:KeyPrefixProcessor 自动给 MATCH 加连接前缀 → pattern 不带连接前缀

Parameters

string $connection
string $pattern

Returns

string[] —

原始完整 key(含连接前缀)

scanInstanceKeys()

scanInstanceKeys(string  $logicalName) : array

按注册项扫描其全部实例的原始 key(tag 项补 store 前缀 + sha1 段通配)

Parameters

string $logicalName

Returns

array —

deleteRawKeys()

deleteRawKeys(string  $connection, string[]  $rawKeys) : int

删原始完整 key(含连接前缀)——绕过连接自动前缀。 phpredis 临时关 OPT_PREFIX;predis 用 executeRaw(不经过前缀处理器)。

Parameters

string $connection
string[] $rawKeys

Returns

int —

ttlOfRaw()

ttlOfRaw(string  $connection, string  $rawKey) : int

查原始完整 key 的 TTL(绕过连接自动前缀;-2=不存在/-1=永久/>=0 剩余秒)

Parameters

string $connection
string $rawKey

Returns

int —

snapshotRegistry()

snapshotRegistry() : void

注册表快照:记录当前各逻辑名的 key 模板(部署后调用,改名迁移的依据)。

migrateKeys()

migrateKeys() : array

改名迁移:对比快照与当前注册表,key 模板变了的逻辑名, 清掉旧模板的所有缓存实例(新 key 首次访问自动查库重建,数据不丢),并更新快照。 部署后调用一次(php artisan tinker --execute 'CacheManager::migrateKeys()')。

原理:缓存本质是「丢失重建」——改名后旧 key 清掉,新 key 第一次 remember 自动重建。 不清旧值搬数据(tag 存储的 key 结构复杂,搬数据脆弱);清旧让新重建更可靠。

Returns

array —

clearByPrefixPublic()

clearByPrefixPublic(string  $template, array  $context = []) : int

按 key 模板的静态前缀清所有实例(SCAN + del);公开供改名/管理用。

Parameters

string $template
array $context

注册项(提供 connection/存储方式判定;缺省按 cache 连接 tag 存储)

Returns

int —

systemMarkers()

systemMarkers() : string[]

系统内部 key 豁免标记单源:discover 与 flush 共用, 防「两处豁免清单不一致 + CACHE_PREFIX 自定义后 'cache:' 字面量失效 → 误删/误登记」。

成员:

  • 'cm:' 治理元数据(注册表 rev/TTL 覆盖/快照,KEY_* 常量均为此前缀)
  • 'sessions:' 会话键
  • ':tag:'/'tag:' Redis tag 存储元数据
  • 实际 store 前缀:Laravel cache 普通键(CACHE_PREFIX 派生,如 tdadmin-cache-)
  • 'cache:' 历史直连形态兜底(兼容配置 CACHE_PREFIX=cache: 的场景)

Returns

string[] —

scanConnections()

scanConnections() : string[]

全清/发现的扫描连接清单cache.scan_connections 逗号分隔, 默认 default,cache;新增 Redis 连接(日志/队列专用库)无需改代码)。

Returns

string[] —

discoverAndRegister()

discoverAndRegister() : int

手动全扫:扫描两个 Redis 连接的全部 key,把未登记的裸写缓存**自动登记为「待审批」**。

认知模型(docs/缓存协议.md §0 缓存准入治理):任何缓存写入(无论走 CacheManager 还是裸写) 都会被发现进入「待审批」,脱离治理的自然回到治理——不靠约定强制。 逻辑名/key 模板按裸写 key 推断(初始猜测),管理员审批时修正(审核通过前一切可改)。 自动定时扫描待计划任务模块(见 docs/待办清单.md)。

Returns

int —

新登记条数(已存在同逻辑名待审批项的重扫不计)

vocab()

vocab(string  $type) : array

词表(领域/实体/动作):全部出自字典(缓存键名五层标准词表,管理页字典数据直接维护)。 返回 value => 中文名 关联数组;合规判定与 namingSpec 共用——管理侧「其他」自定义词保存时 自动写入对应字典类型(DataCacheService::extendVocab),词表可持续增长。

Parameters

string $type

Returns

array —

dictValues()

dictValues(string  $dictCode) : array

字典类型编码列表(标识参数/归属参数/归属端名白名单,出自字典)

Parameters

string $dictCode

Returns

array —

dictItems()

dictItems(string  $dictCode) : array

字典类型结构化词条 [{value, label}](标识/归属等下拉展示用,中文名出自字典)

Parameters

string $dictCode

Returns

array —

keyTemplateCompliant()

keyTemplateCompliant(string  $template) : array

缓存 key 模板合规判定(五层标准,词表全部出自字典)。

结构:{领域}:{实体}:{动作}[:{标识}][:{归属}];cm 域豁免(治理元数据,内部 key)。 三层校验: 1 格式层:全小写、冒号分隔、花括号占位 2 词表层:领域/实体/动作命中字典词表 3 语义层:标识必须是精确型参数名(Id/Code/Key/md5/账号类,模糊键禁止);归属为白名单或省略

合规 = 可直接使用(自动纳入治理 status=1);不合规 = 待审批(审批时必须改到标准内)。

Parameters

string $template

Returns

array —

logicalNameCompliant()

logicalNameCompliant(string  $name) : array

逻辑名(name)合规判定:逻辑名是「无参数抽象名」——{领域}:{实体}:{动作}[:{维度}], 领域/实体/动作命中字典词表,可选第 4 段为维度(如 login 限流的 ip/username); 不含花括号占位。cm: 前缀豁免(治理元数据)。审批时管理员确立逻辑名必须落在本标准内(定后不可改)。

Parameters

string $name

Returns

array —

invalidateByTable()

invalidateByTable(string  $table, int|null  $rowId = null) : void

表数据变更 → 自动失效相关缓存(核心:精确推导受影响用户)。

规则:

  • global 层:直接全局清(forgetAll)
  • user 层:affected_users 是 {表名: 解析器} 映射,按变更表选解析器, 精确推导「该表这行变更 → 哪些用户受影响」,只清他们的
  • 无该表解析器或行 id 缺失:保守全清(全用户)

执行时机:立即执行。模型事件链路请走 invalidateByTableDeferred—— 事件在事务内触发时若立即清,并发读会以「提交前旧数据」重建缓存, 提交后该脏缓存残留至 TTL(缓存失效必须发生在提交之后,见该方法的论证)。

Parameters

string $table

变更的表名(td_sys_*)

int|null $rowId

变更的行 id(精确推导用;缺省整表推导)

invalidateByTableDeferred()

invalidateByTableDeferred(string  $table, int|null  $rowId = null) : void

表数据变更 → 延迟到事务提交后失效(模型事件专用:saved/deleting 在事务内触发)。

流程:① 调用当下即时解析失效计划(受影响缓存名与用户集)② 事务内:挂 DB::afterCommit, 提交后执行 Redis 删除、回滚丢弃 ③ 事务外:立即执行(与 invalidateByTable 等价)

失效计划(受影响缓存名与用户集)在调用当下即时解析——deleting 事件时行仍在, 回查 user_id/role_id/post_id 可得;提交后行已删,届时再解析必漏清。 仅 Redis 删除动作延迟:

  • 事务内:挂 DB::afterCommit,提交后执行;回滚则丢弃(DB 未变,缓存本就无需清)
  • 事务外:立即执行(与 invalidateByTable 等价;此时 saved 在持久化后、deleting 在删除前触发)

残余窗口(已论证可接受):提交语句与 afterCommit 清理之间的微秒级间隙内, 并发读会命中旧缓存(旧缓存 = 提交前已提交状态,不脏);以及并发读在 「缓存缺失 → 读库 → 写回」跨越提交边界的 ABA 写回(写回旧值被提交后清理覆盖, 除非写回恰落在清理之后——后台写并发近零,残留自愈于下次失效/TTL)。

Parameters

string $table

变更的表名(td_sys_*)

int|null $rowId

变更的行 id(精确推导用;缺省整表推导)

usersOfRole()

usersOfRole(?int  $roleId) : int[]

角色 → 受影响用户(直授 td_sys_user_role 来源;岗位已独立建表,任职不再经角色)。

Parameters

?int $roleId

Returns

int[] —

usersOfPost()

usersOfPost(?int  $postId) : int[]

岗位 → 受影响用户(任职 td_sys_user_dept.post_id 来源)。

Parameters

?int $postId

Returns

int[] —

forgetAll()

forgetAll(string  $logicalName) : void

删某一逻辑名的全部实例(tag flush 整组;直连项按 key 模板前缀 SCAN + del)。

流程:① 查注册项(未登记直接返回)② 受保护缓存整组不清(先解锁再清) ③ 直连项按 key 模板前缀 SCAN 后删原始 key;tag 项 tag flush 整组清

Parameters

string $logicalName

forgetConfigGroup()

forgetConfigGroup(string  $groupCode) : void

按配置分组失效(自 ConfigService/MaintainConfigService 收敛为单源): 分组列表缓存 + 该组全部定义的 info:key/def/tab 缓存。

流程:① 清 sys:config:list 分组列表缓存 ② 取该组全部配置定义的 key,逐一清 info:key/def/tab 三项缓存

Parameters

string $groupCode

loadRegistryFromDb()

loadRegistryFromDb() : array

从数据库表加载注册表(异常向上抛,由 registry() 决定不缓存失败结果)

Returns

array —

autoRegister()

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 模板/状态/治理配置。

Parameters

string $logicalName
array $params

Returns

array —

ensureRegistered()

ensureRegistered(string  $logicalName, array  $params) : array

确保逻辑名可用:已登记直接用;未登记自动登记(治理闭环,同 remember); 登记写库失败(DB 不可用/表未建)→ 以「逻辑名+参数」推导临时 key 模板兜底。

缓存准入治理的可用性边界:治理缺口只降级、不阻断—— 任何业务链路(含登录/限流/验证码)都不因注册表缺行或不可写而失败; 缺行会在可写时自动登记进治理(待审批),管理员在管理页照常补治理信息。

Parameters

string $logicalName
array $params

Returns

array —

normalizeArgs()

normalizeArgs(string  $logicalName, mixed  $params, ?callable  $callback = null) : array

参数归一化(极简使用):支持 无参数省略 / 单值自动映射 / 数组 三种形态。 单值 → 从注册表 key 模板找唯一占位符名自动映射(remember('x', $id) → ['userId' => $id]); 未登记项单值 → 用通用占位 'param'(自动登记生成 name:{param},审批时改标准名)。

Parameters

string $logicalName
mixed $params
?callable $callback

Returns

array —

tagsFor()

tagsFor(array  $item) : \Illuminate\Contracts\Cache\Repository

该缓存项的 tag 存储(每逻辑名独立 tag,flush 只清本组)

Parameters

array $item

Returns

\Illuminate\Contracts\Cache\Repository —

scanTemplateKeys()

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 不重叠可安全合并)。

Parameters

string $template
bool $direct
string $connection

Returns

array —

inferNameTemplate()

inferNameTemplate(string  $businessKey) : array

从裸写业务 key 推断逻辑名与 key 模板(初始猜测,审批时管理员修正): - 冒号分隔且末段像参数(纯数字/长 hex)→ 逻辑名 = 静态前缀,模板 = 前缀 + :{param} - 否则 → 逻辑名 = 整个 key,模板 = key 原样

Parameters

string $businessKey

Returns

array —

dictEntries()

dictEntries(string  $dictCode) : array

字典启用项基础读取(直查库,不走 sys:dict:list remember 缓存)。

返回全字段数组(id/type_id/type_code/parent_id/label/value/sort/status 等),上层按需取字段。 刻意不套缓存:本方法被 keyTemplateCompliant 调用(合规判定),而 remember 在缓存未登记时 会触发 autoRegister → 再次合规判定 → 再次调用本方法,形成无限递归(测试环境 RefreshDatabase 空注册表必现,生产靠「已登记」侥幸不炸)。词表是合规判定的只读依赖、量小(个位数~几十条), 直查代价可忽略。

Parameters

string $dictCode

Returns

array —

vocabDictCode()

vocabDictCode(string  $type) : string

词类 → 字典类型编码

Parameters

string $type

Returns

string —

buildInvalidationPlan()

buildInvalidationPlan(string  $table, ?int  $rowId) : array

构建失效计划:遍历注册表解析受影响缓存,返回可延迟执行的动作清单。

Parameters

string $table
?int $rowId

Returns

array —

applyInvalidationPlan()

applyInvalidationPlan(array  $plan) : void

执行失效计划(invalidateByTable 与 afterCommit 延迟路径共用)。

Parameters

array $plan

resolveAffectedUsers()

resolveAffectedUsers(string  $logicalName, string  $table, ?int  $rowId) : int[]

解析「表变更 → 受影响用户」(user 层缓存)。 各缓存的推导规则集中在类内方法(config 不能存闭包——config:cache 序列化不了)。 rowId 缺失(整表变更/删后回查不到)时保守按全用户清,与「无解析器保守全清」一致。

Parameters

string $logicalName
string $table
?int $rowId

Returns

int[] —

affectsAllUsers()

affectsAllUsers(string  $table) : bool

结构类表(菜单/权限/部门/组织)变更是否影响全量用户(整组失效判定复用)。

Parameters

string $table

Returns

bool —