6.2 缓存驱动与策略 核对台放行的请求,还有一类可以省力:同样的榜单、同样的配置、同样的热点书目,没必要每次都跑一趟数据站台。缓存在总线旁铺了一条抄近的岔路——命中时几毫秒回程,未命中才走全程。本节承接第 4 章的查询构造器(岔路尽头取数靠它),讲驱动选型、读写姿势、有效期设计,以及击穿、雪崩、污染三种失效现场的处置;这些策略在第 8 章性能调优里会被编排进完整的瓶颈定位方法。 驱动选型:先看数据去向 框架把缓存抽象成统一接口,驱动决定数据实际住在哪: 选型判断只有两问:部署形态与数据共享需求。单机小站、缓存内容少——File 驱动零依赖、够用;多台机器负载均衡、或要给队列复用同一套 Redis——Redis 驱动,数据集中在第三方进程,任何一台应用机都能读到同一份。
核对台放行的请求,还有一类可以省力:同样的榜单、同样的配置、同样的热点书目,没必要每次都跑一趟数据站台。缓存在总线旁铺了一条抄近的岔路——命中时几毫秒回程,未命中才走全程。本节承接第 4 章的查询构造器(岔路尽头取数靠它),讲驱动选型、读写姿势、有效期设计,以及击穿、雪崩、污染三种失效现场的处置;这些策略在第 8 章性能调优里会被编排进完整的瓶颈定位方法。
框架把缓存抽象成统一接口,驱动决定数据实际住在哪:
// config/cache.php:默认驱动与连接 return [ 'default' => env('cache.driver', 'file'), 'stores' => [ 'file' => [ 'type' => 'file', 'path' => '', 'expire' => 0, // 0 表示永久,按需覆盖 'prefix' => 'shop:', ], 'redis' => [ 'type' => 'redis', 'host' => env('redis.host', '127.0.0.1'), 'port' => 6379, 'password' => env('redis.password', ''), 'select' => 0, 'prefix' => 'shop:', ], ], ];
选型判断只有两问:部署形态与数据共享需求。单机小站、缓存内容少——File 驱动零依赖、够用;多台机器负载均衡、或要给队列复用同一套 Redis——Redis 驱动,数据集中在第三方进程,任何一台应用机都能读到同一份。Memcached 与 Redis 二选一时,纯键值缓存前者更轻,需要 list、set、过期策略精细控制则后者胜出。判断的陷阱是「为未来过度设计」:日活三位数的站点上 Redis,维护成本高于收益——File 起步,压测(第 8 章)证明瓶颈在缓存时再迁移,驱动切换对业务代码透明。
统一接口下,日常操作是一组门面方法:
use think\facade\Cache; // 基础四件 Cache::set('hot_books', $list, 600); // 写,有效期 600 秒 $list = Cache::get('hot_books'); // 读,未命中返回 null Cache::delete('hot_books'); // 删 Cache::remember('hot_books', function () { // 记:读,没有就回源并写入 return Db::name('book')->order('sales desc')->limit(10)->select()->toArray(); }, 600); // 自增与标签:计数器与批量失效 Cache::inc('coupon_used_2024'); Cache::tag('book_list')->set('hot_books', $list, 600); Cache::tag('book_list')->clear(); // 图书相关缓存一键清空
remember 是读穿模式的日常写法,配合标签能把「同一业务域的键」批量管理——图书数据更新后清掉 book_list 标签下的所有副本,比手工罗列键名可靠。键名设计给两条约定:前缀区分业务域(shop: 已在配置里统一加),键内含参数要可控(book_detail_1024 可以,含未过滤的用户输入不行——既可能撞键,也可能被用来污染别人的键)。

有效期是缓存的核心参数,设长的收益是命中率、代价是旧数据窗口。给三档参考:配置类与类目树——变更极少,小时到天级,靠「写后删副本」保一致;榜单与详情——分钟级,接受短暂旧读;库存与价格——要么不缓存走直查,要么极短时效并在写操作后主动失效。最后一条是纪律性的:一致性策略优先选「写后删」而不是「写后改」——删除让下次读取按最新逻辑重建,改副本则要把写路径的每个字段变更都同步进缓存逻辑,漏一处就是长期脏数据。账目算到这里,多数业务的正确答案朴素得很:能接受旧读的才上缓存,不能接受的老老实实直查。
背景:练习项目的畅销榜单每次请求都聚合查询数据站台,压测时数据库最先扛不住。操作:用 remember 给榜单铺 300 秒旁路并打上 book_list 标签;把有效期临时改成同一时刻批量到期,用并发脚本压出一次击穿;再按互斥锁方案改造(第一个回源请求持有短锁,其余等待后读副本),复压对比。结果示例:改造后数据库查询频次从每请求一次降到每五分钟几次,击穿尖峰被削平。解读:注意锁的粒度与时长——锁比回源动作先过期就形同虚设,比副本寿命长就变成新瓶颈。变式:把下单成功后「删榜单副本」接到订单写路径上,体验写后删的一致性策略,并思考为什么这里不用标签清空(代价与精度)。
⚠️ 常见坑:把 Session 也存进 File 缓存目录再给缓存做整体清理——一键
clear会把在线用户的登录态全数清空。缓存与会话是两套存储,清理策略必须分开(会话见 6.3)。
缓存策略好不好,最终落在一个数字上:命中率。算法朴素——命中数除以总查询数;生产环境从 Redis 的信息面板或应用侧埋点取数。判读的经验值:配置类缓存命中率应显著偏高,榜单类随有效期内访问集中度波动,整体命中率长时间低于五成,说明有效期设计或键设计有问题,先查「该缓存的没缓存、缓存的秒过期」。仪表要常态化看:命中率是缓存的体温,掉下来一定有原因,找到原因之前不要急着加机器。
配套一块简单的埋点:在读穿封装里各加一行计数(命中加一、回源加一),按小时落进既有日志通道——十几行代码换来一块长期仪表,是本节性价比最高的投入。
remember 是读穿骨架:配合标签做批量失效,键名前缀与参数白名单先行。旁路铺完,最后一道闸口管跨请求的记忆:登录态与购物车如何随票取用——Session 与 Cookie 见 6.3。