技术研究 | 高并发列表查询优化实践
2026-03-20 · Henry
engine_group 高并发列表查询优化实践:版本化缓存 + singleflight + MQ 失效。本文总结了 plugin/engine_group/service 中 news/paper/project/member 列表接口在高并发场景下的优化方案。核心目标是降低 Preload 带来的数据库压力,同时保证多实例部署下缓存一致性。
摘要 本文总结了 plugin/engine group/service 中 news/paper/project/member 列表接口在高并发场景下的优化方案。核心目标是降低 Preload 带来的数据库压力,同时保证多实例部署下缓存一致性。最终落地为: 1. Redis 版本化列表缓存(Versioned Namespace Cache) 2. singleflight 防击穿(进程内请求合并) 3. MQ 异步失效(跨实例一致) 4. 同步立即失效兜底(削减消息消费延迟窗口) 5. 删除链路竞态修复(避免“先失效后删除”导致脏回填) 1. 背景与痛点 1.1 业务现状 news/paper/project/member 都有 GetXXXList,且大量使用 Preload。在高并发下会出现: 1. 缓存 miss 时大量并发直接打 DB 2. 复杂筛选 + 分页导致缓存 key 数量多,难以精准删除 3. 多实例场景下,单机缓存无法保证一致失效 4. 写后读可能看到旧缓存(消息延迟导致) 1.2 优化目标 1. 显著降低数据库查询峰值 2. 避免缓存击穿 3. 失效策略可扩展、低运维成本 4. 兼容 RedisMQ / RabbitMQ 2. 总体方案 2.1 版本化缓存 不做 SCAN + DEL,改为“域版本号 + 参数哈希”的缓存 key: 版本 key:engine group:list cache:version:<domain 数据 key:engine group:list cache:<domain :v<version :<queryHash 当发生增删改时,只需要对对应 domain 执行 INCR version,后续读请求自动进入新命名空间。旧缓存不再被命中,依赖 TTL 自然回收。 2.2 singleflight 防击穿 每个“当前版本 + 当前查询条件”对应一个 singleflight key。 流程: 1. 第一次读缓存 miss 2. 进入 singleflight 3. 在 singleflight 内二次读缓存(防并发窗口) 4. leader 查 DB 并回填缓存,follower 复用结果 2.3 MQ 失效广播 写请求触发失效任务,广播给各实例消费,保证跨实例一致性。 新增任务:ListCacheInvalidationTask,包含失效 domains。 2.4 同步立即失效 为减少“入队成功但消费尚未完成”的脏读窗口,写路径先本地同步失效,再发 MQ。 MQ 消费端通过 AlreadyInvalidated 跳过重复递增,避免版本过快抖动。 3. 关键实现细节 3.1 缓存工具层 文件:plugin/engine group/utils/list cache.go 1. BuildListCacheKey:读取域 version + 参数哈希生成最终 key 2. GetListCache / SetListCache:统一读写 Redis 3. InvalidateListCacheDomains:按 domain 批量 INCR version 3.2 失效任务层 文件:plugin/engine group/utils/list cache invalidation.go 1. EmitListCacheInvalidationTask: 先同步失效 再发 MQ 入队失败时兜底重试失效 2. listCacheCallbackFunc: 如果 AlreadyInvalidated=true,跳过重复失效 否则执行 InvalidateListCacheDomains 3.3 MQ 扩展 文件:plugin/engine group/utils/task mq.go 1. 扩展 taskPublisher,新增 list cache 任务发布方法 2. RedisMQ 新增 stream/group/consumer 3. RabbitMQ 新增 queue/routing key/dlq 绑定 3.4 Service 读路径改造 GetNewsList/GetPaperInfoList/GetProjectInfoList/GetMemberInfoList 已接入: 1. 首次读缓存 2. miss 进入 singleflight 3. 二次读缓存 4. 回源 DB + 回填缓存 3.5 Service 写路径改造 写接口统一触发 EmitListCacheInvalidationTask,并处理跨域影响(例如 member 变更会影响 paper/project preload 结果)。 4. 竞态问题与解决方案 4.1 问题:先失效后删除 原删除链路存在顺序风险: 1. 先失效(版本 +1) 2. 并发读回源 DB,此时记录还未删 3. 旧数据回填到新版本缓存 4. 删除完成但未再次失效,导致脏缓存 4.2 修复策略 将 paper/project 删除链路改为: 1. 清理关联时可选择不发失效 2. 仅在最终 DELETE 成功后统一发失效 这样保证“失效点”发生在最终一致状态之后。 5. 配置变更 6. 压力测试与结果分析 6.1 测试目标 验证本次优化后,在高并发下 member/paper/project 列表接口的可用性与稳定性,重点关注: 1. HTTP 成功率 2. 响应时延分位数(P90/P95) 3. 极端慢请求与错误占比 6.2 测试概况 1. 测试时长:约 2m00.9s 2. 最大并发虚拟用户:2500 VUs 3. 完成迭代:91863 4. 总请求数:275589 5. 平均吞吐:2279.54 req/s 6.3 核心结果 1. 健康检查总数:275589 2. 通过:275587(99.99%) 3. 失败:2(0.00%) 接口级检查: 1. member list is 200:91862 成功,1 失败(约 99.99%) 2. paper list is 200:全部通过 3. project list is 200:91862 成功,1 失败(约 99.99%) HTTP 指标: 1. http req duration: 平均:671.65ms 中位数:559.82ms P90:1.65s P95:1.97s 最大:27.57s 2. http req failed:2 / 275589(0.00%) 执行指标: 1. iteration duration: 平均:1.74s 中位数:1.61s P90:2.76s P95:3.09s 最大:31.6s 网络指标: 1. 接收流量:4.0 GB(约 33 MB/s) 2. 发送流量:50 MB(约 413 kB/s) 6.4 结果解读 1. 系统在 2500 VUs 峰值并发下保持了极高可用性(99.99% 成功率)。 2. P95 小于 2 秒,说明大多数请求响应稳定。 3. 存在极少量长尾(max 27.57s)和 2 次失败,建议结合网关日志、慢查询日志与 Redis/MQ 指标做关联排查。 4. 从吞吐与成功率看,本次“版本化缓存 + singleflight + 失效优化”已有效支撑高并发场景。 6.5 后续压测建议 1. 增加分场景压测:纯读、读写混合、突发写入(验证版本递增抖动)。 2. 增加观测维度:缓存命中率、DB QPS、MQ 消费延迟、版本增长速率。 3. 对长尾请求进行抽样追踪(Tracing),定位慢点在 DB、Redis、MQ 还是应用层。