应选用rediscache或memcachedcache以保障命中率,arraycache和filesystemcache不适用于生产环境;需规范缓存键设计、正确配置cacheregion、隔离元数据缓存干扰进行基准测试。

缓存驱动选型直接影响命中率
Doctrine 二级缓存(即实体/查询结果缓存)的命中率不是靠调参堆出来的,第一步得选对 cache driver。ArrayCache 在 CLI 测试里看着快,但进程一结束就清空,HTTP 请求间完全不共享,生产环境基本等于没开;FilesystemCache 虽然持久,但文件锁和磁盘 I/O 会拖慢高并发场景;真正能撑住命中率的只有 RedisCache 或 MemcachedCache——它们支持原子操作、分布式共享、毫秒级响应。
关键点:
- 使用
RedisCache时,务必确认 Redis 实例启用了maxmemory-policy(推荐allkeys-lru),否则缓存写满后会拒绝写入,导致命中率断崖下跌 -
MemcachedCache不支持过期时间精确控制(只支持相对时间),若业务依赖 TTL 精度(比如权限缓存必须 15 分钟失效),优先选 Redis - 不要在
dev环境用生产级缓存驱动做“模拟测试”,debug 模式下 Doctrine 会绕过部分缓存逻辑,测出来不准
缓存键设计决定失效粒度与复用能力
Doctrine 默认用 DQL 字符串 + 参数哈希生成缓存键,看似简单,但极易导致“伪未命中”:两个语义等价的查询(如字段顺序不同、别名不同、WHERE 条件位置交换)会产生不同键,哪怕结果完全一样也不复用。
实操建议:
- 强制统一 DQL 风格:固定
SELECT字段顺序、禁用动态别名、参数全部用命名占位符(:id而非?) - 对高频查询手动指定缓存键:
$query->setHint(Query::HINT_CACHE_KEY, 'user_profile_by_id_' . $userId),避免解析器生成不可控键 - 涉及关联实体的查询,注意
fetch="EAGER"会改变查询结构,导致键变更——要么全用 LAZY + 后续加载,要么统一开启 EAGER 并固化关联路径
命中率低?先查 CacheRegion 是否被绕过
Doctrine 的二级缓存不是全局开关,而是按 Region 划分:每个实体类或查询有自己的 CacheRegion。如果配置里漏了某实体的 region 声明,或者该 entity 的 @Cache 注解没加 usage="READ_WRITE"(或 "NONSTRICT_READ_WRITE"),那它根本不会进二级缓存,自然没有命中率可言。
常见错误现象:
- 日志里反复出现
Cache lookup for region "App\Entity\User" returned null—— 表示 region 存在但没命中;如果是Cache region "App\Entity\User" not configured,说明压根没注册 - 启用
debug模式时,Query::HINT_CACHE_REGION必须显式设置,否则即使 entity 有@Cache,查询也不会走 region 缓存 - 使用
EntityManager::find()时,只走实体缓存(identity map),不触发二级缓存;要强制走二级缓存,得用$em->createQuery()->useResultCache(true)->getResult()
性能基准测试必须隔离元数据缓存干扰
很多团队测出“缓存后变慢”,其实是把 metadata_cache_driver 和 result_cache_driver 混在一起压测了。元数据缓存(entity structure、mapping info)一旦 warm up 完就几乎不耗时,但结果缓存的读写延迟才是瓶颈所在。
正确做法:
- 单独压测结果缓存:关闭
metadata_cache_driver和query_cache_driver,只保留result_cache_driver,用ab或hey对同一查询发起 1000 次请求,观察 P95 响应时间变化 - 对比指标看
cacheHits和cacheMisses计数(可通过Redis的INFO commandstats或自定义CacheProvider统计),而不是只看平均响应时间 - 测试数据量要贴近真实:小表(OFFSET/LIMIT)验证缓存穿透风险
缓存键的稳定性、region 的显式声明、驱动本身的共享能力——这三个点没对齐,再调 ttl 或加节点都没用。











