不能。readonly仅控制save/update时忽略字段写入,与缓存穿透无关;防穿透需主动缓存空值占位符,并统一日志埋点识别“缓存未命中+db为空”组合。

模型里加 readonly 字段真能防缓存穿透?
不能。ThinkPHP 的模型 readonly 属性只控制写入时是否忽略该字段,和缓存、穿透完全无关——这是最常见的概念混淆点。它既不参与查询构造,也不影响缓存键生成,更不会拦截空结果回写。所谓“只读虚拟字段”在 TP 官方模型层压根不存在对应机制。
实操建议:
-
readonly仅在save()或update()时起作用:若字段设为readonly => true,即使传了值也不会写入数据库 - 想屏蔽查询字段?得用
field()显式指定要查的列,比如$model->field(['id', 'name'])->select() - “虚拟字段”需靠
getAttr或访问器(accessor)动态计算,但它们不改变原始 SQL,也不影响缓存逻辑
缓存穿透在 ThinkPHP 里实际怎么发生的?
典型场景是:用户高频请求一个数据库里根本不存在的 ID(比如 /user/9999999),每次请求都击穿缓存直查 DB,DB 压力陡增。ThinkPHP 自带的 cache() 方法默认对空结果不缓存,所以没做额外处理时,这就是裸奔状态。
关键点:
- TP 的
find()/select()默认不会自动缓存空结果,必须手动写cache(true)或指定过期时间才生效 - 缓存键由完整查询条件生成(含 where 参数),ID 变化 → 缓存键不同 → 无法复用
- Redis/Memcached 本身不识别“业务空值”,你得自己决定:该存
null、''还是特殊标记如__NOT_FOUND__
怎么给 ThinkPHP 加一层空值缓存兜底?
核心思路就一条:查不到数据时,主动往缓存里塞一个短时效的占位符。下次再查同一个 ID,先命中缓存,直接返回,不碰 DB。
示例(以 Redis 为例):
$user = $model->where('id', $id)->find();
if (empty($user)) {
// 写入空值缓存,过期时间比正常缓存短(比如 2 分钟)
cache('user_'.$id, '__NOT_FOUND__', 120);
return null;
}
cache('user_'.$id, $user, 3600); // 正常缓存 1 小时
注意:
- 别用
cache(true)自动缓存,它对空结果无效;必须显式调用cache($key, $value, $ttl) - 缓存键要和查询条件严格一致,否则形同虚设;ID 类查询建议直接拼接
'user_'.$id,别依赖 TP 自动生成的复杂键 - 如果用了模型事件(如
afterFind),确保空值分支也走同一套缓存逻辑,否则事件里可能又查一次 DB
日志里怎么记录穿透查询特征?
不是记“查了什么”,而是记“为什么判定为穿透”:即「缓存未命中 + DB 查询为空」这个组合动作。TP 没内置穿透日志,得自己埋点。
推荐方式:
- 封装一个查询方法,在
find()后立刻检查:缓存 key 是否存在(cache('user_'.$id) === null)且$user === null - 日志内容至少包含:
$id、$_SERVER['REQUEST_URI']、microtime(true)(用于分析集中时段) - 避免在生产环境用
trigger_error()或频繁写文件,优先走think\facade\Log::record()并配置独立通道(如penetration日志门面)
真正难的不是加日志,是区分“合理空查”(比如新注册用户还没填资料)和“恶意扫描”(连续请求 99999~100005)。这个边界得结合访问频率、来源 IP、User-Agent 综合判断,模型层解决不了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











