缓存键必须带业务版本号或时间戳以实现优雅过期;推荐用cache('user_123_v2')或cache('user_123_'.floor(time()/3600)),避免固定键导致无法预热;注意key合法性、原子写入、防穿透雪崩、禁用不可靠tag机制。

缓存键设计必须带业务版本号或时间戳
ThinkPHP 自身不提供「自动刷新」能力,所谓优雅过期,本质是靠缓存键(key)的可预测性 + 外部触发逻辑来实现。如果直接用 cache('user_123') 这种固定键,就只能等 TTL 到期或手动删,没法做到“快过期前自动更新”。
常见错误:把缓存逻辑全塞进控制器,每次请求都查库再写缓存,完全没利用缓存的「读多写少」特性。
- 推荐做法:在缓存键里嵌入一个可升级的业务版本标识,比如
cache('user_123_v2'),版本号由配置项或数据库字段控制 - 更轻量的替代:用时间戳分段,如
cache('user_123_' . floor(time() / 3600))(按小时切片),天然具备滚动过期+预热能力 - 注意
think\Cache默认不校验 key 合法性,含特殊字符(如空格、斜杠)会导致静默失败或写入异常路径
用 Cache::remember() 配合自定义过期策略
Cache::remember() 是 ThinkPHP 提供的「查缓存→未命中则执行回调→写入并返回」原子操作,但它默认只管「首次写入时设 TTL」,不解决「缓存快过期时怎么提前刷」的问题。
实际场景中,你往往希望:缓存还剩 10% 时间时,后台悄悄异步重算,而用户拿到的仍是旧值;等新值写入后,下次请求才切换——这需要自己补一层判断逻辑。
- 不要直接传死值给
$ttl参数,改用动态计算:比如$ttl = 3600 - (time() - $cacheTime)(需先getMetadata获取写入时间,但 TP6+ 不暴露该接口,得自己存时间戳) - 更稳的做法:缓存值本身带
expire_at字段,每次读取先比对time() > $data['expire_at'] - 300,满足则丢进队列异步刷新,不阻塞当前请求 - TP 的
File驱动对高并发写入缓存文件有锁竞争,Redis驱动下用SET key value EX 3600 NX才真正原子,别依赖cache()表面的“线程安全”
用中间件或事件监听器拦截「缓存穿透+临界刷新」
用户请求一个刚过期的 key,多个并发进来,全都击穿到数据库,又全都去写缓存——这就是典型的临界刷新雪崩。ThinkPHP 没内置防穿透机制,得自己卡住第一请求,其余等待。
错误示范:在模型里用 if (!cache($key)) { cache($key, $db->find()); } —— 并发下会反复查库。
- 用
Redis::set($key . '_lock', 1, ['ex' => 3, 'nx'])做简易分布式锁,成功拿到锁的进程查库+写缓存+删锁;失败的进程 sleep(50ms) 后重试读缓存 - TP 的
Hook或Event可监听CacheWrite事件,在写入前注入刷新钩子,但要注意事件回调里不能再调cache(),否则递归死锁 - 别在
__construct()或initialize()里预热缓存,CLI 和 HTTP 请求生命周期不同,容易漏刷或重复刷
Redis 驱动下慎用 Tag 清除,它不保证原子性
很多人想靠 cache('user_123', $data, ['tag' => 'user']) 然后 clear('user') 实现批量失效,但 TP 的 tag 实现是「记录 key 列表 → 遍历删除」,中间任何一步失败都会导致残留。
尤其 Redis 集群模式下,KEYS 命令被禁用,tag 功能直接不可用,且 TP 不报错,只是静默跳过。
- 生产环境禁用
tag,改用业务层约定 key 前缀,如user:123、user:profile:123,用SCAN+DEL脚本清理(注意 SCAN 的游标和超时) - 如果必须用 tag,确保驱动是
Redis且单机部署,并在clear()后加日志确认删除数量,避免误以为清掉了 -
File驱动的 tag 是靠写额外索引文件,IO 开销大,且并发写入时索引文件可能损坏,线上务必关掉
真正难的不是怎么设 TTL,而是怎么让「过期」这件事对业务透明——键要可推导、刷新要可收敛、清除要可验证。这些细节没对齐,自动过期就成了自动翻车。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











