分层缓存调度:削峰控压下的 Feed 流高性能设计

发布时间:2026/5/22 20:32:32

分层缓存调度:削峰控压下的 Feed 流高性能设计 系统整体架构流程图用户请求与三级缓存架构简易版三级缓存架构一、业务背景与核心痛点针对首页公共Feed、我的文章双场景Feed流系统存在典型高并发读写痛点峰值压力大热点页面QPS极高数据库频繁面临击穿、洪峰压力延迟要求高用户侧需要秒级响应传统单级Redis缓存存在网络、序列化开销数据一致性难平衡高频更新、点赞收藏计数变更无法做到强一致需要秒级最终一致个性化与公共缓存冲突用户点赞、收藏等个人态会污染公共缓存导致命中率暴跌核心架构目标极致降低读延迟、收敛后端DB压力、保障秒级最终一致、解耦页面装配与个性化逻辑。二、核心架构思想摒弃传统单级缓存粗暴方案确立两大核心设计哲学支撑整套三级缓存体系动静解耦、分层装配页面骨架静态结构 条目碎片动态内容 用户个性化实时覆盖三层完全解耦缓存分层、成本逐级递增本地内存→Redis骨架→Redis碎片→DB按访问成本、速度、命中率分层治理缓存只读公共态个性化上层叠加彻底杜绝个性化数据污染公共缓存事件失效定时纠偏实现高性能下的秒级最终一致idsKey存当前页面文章ID[1234567890, 1234567891, 1234567892, ..., 1234567909]↑ ↑ ↑ ↑第1篇文章ID 第2篇文章ID 第3篇文章ID 第20篇文章ID三、三级缓存分层架构详解按照「速度从快到慢、成本从低到高、数据粒度从粗到细」分层设计1. L2 本地内存缓存Caffeine—— 热点极致加速层公共页面缓存key:private String cacheKey(int page, int size) { return feed:public: size : page :v LAYOUT_VER; }LAYOUY_VER 缓存版本号后续可以通过修改这个批量让缓存失效存储内容完整Feed页面响应条目数组(这一页的具体数据、分页、hasMore适用场景公共Feed热点页、个人近期知文热页核心优势无网络IO、无需序列化毫秒级返回扛峰值QPS策略短TTL热键动态续期热点常驻缓存// 对返回列表中的每个条目进行热度统计--并试图延长TTL for (FeedItemResponse item : local.items()) { recordItemHotKey(item.id()); }同时维护一个hasMore键判断是不是还有下一页这个键设置前端是否还显示下一页按钮String hasMoreKey feed:public:ids: safeSize : hourSlot : safePage :hasMore;隔离能力命中直接返回不再穿透Redis与DB2. L1Redis页面骨架缓存 —— 页面装配调度层存储内容页面ID列表、hasMore分页元数据、轻量索引核心定位解决页面结构复用稳定装配链路设计价值无需DB查询仅通过ID索引拼装页面规避重复排序、分页计算TTL策略短TTL随机抖动规避大面积同时失效3. L0 Redis碎片缓存 —— 数据最小粒度层存储内容单条目维度碎片作者、封面、时间、置顶标记、点赞/收藏计数核心定位可复用的最小数据单元支持跨页面、跨场景复用能力批量读取、缺片按需回源补齐提升装配成功率与命中率TTL策略三级最长保证碎片高可用四、完整读写链路流程贴合源码1. 读链路自上而下逐级命中优先查询 L2 本地缓存命中直接返回个性化叠加并尝试进行热度统计缓存时间延长本地缓存keyString localPageKey cacheKey(safePage, safeSize);private String cacheKey(int page, int size) { return feed:public: size : page :v LAYOUT_VER; }FeedPageResponse local feedPublicCache.getIfPresent(localPageKey); if (local ! null local.items() ! null) { // 对返回列表中的每个条目进行热度统计--并试图延长TTL for (FeedItemResponse item : local.items()) { recordItemHotKey(item.id()); } log.info(feed.public sourcelocal localPageKey{} page{} size{}, localPageKey, safePage, safeSize); ListFeedItemResponse enrichedLocal enrich(local.items(), currentUserIdNullable); return new FeedPageResponse(enrichedLocal, local.page(), local.size(), local.hasMore()); }L2未命中 → 查询 L1 页面骨架拿到ID列表批量拉取 L0 碎片拼装页面文章ID列表缓存key里面存的是当前小时这个里面存的是当前小时sagePage页的safeSize个文章ID列表long hourSlot System.currentTimeMillis() / 3600000L; String idsKey feed:public:ids: safeSize : hourSlot : safePage;是否还有下一页key:用来记录是否还有下一页过期时间很短记录前端是否显示下一页列表String hasMoreKey feed:public:ids: safeSize : hourSlot : safePage :hasMore;批量获取元数据将数据存到公共缓存中-----这里虽然存的是个性数据但每次用都会调用方法覆盖为调用用户的个人数据没有影响// 构造内容元数据标题内容等的 Redis Key ListString itemKeys new ArrayList(idList.size()); for (String id : idList) { itemKeys.add(feed:item: id); } // 批量获取知文 元数据 ListString itemJsons redis.opsForValue().multiGet(itemKeys);L1未命中 → 触发单航班机制唯一请求回源DB回源成功后自下而上回填 L0/L1/L2形成闭环加速2. 写回回填机制DB回源后优先写入L0碎片、再L1骨架、最后L2完整页面分层独立TTL抖动策略平滑失效流量3. 个性化叠加机制公共缓存只存公共状态不包含like/favor用户态返回前内存实时叠加用户个性化状态不写缓存彻底解决缓存碎片化、命中率下降问题synchronized (lock) { // 重查 L2 缓存避免重复回源 FeedPageResponse again assembleFromCache(idsKey, hasMoreKey, safePage, safeSize, currentUserIdNullable); if (again ! null) { feedPublicCache.put(localPageKey, again); // 对返回列表中的每个条目进行热度统计 if (again.items() ! null) { for (FeedItemResponse item : again.items()) { recordItemHotKey(item.id()); } } log.info(feed.public source3tier(after-flight) localPageKey{} page{} size{}, localPageKey, safePage, safeSize); singleFlight.remove(idsKey); return again; } // 数据库回源读取 size1 以判断是否有下一页后裁剪为当前页 int offset (safePage - 1) * safeSize; ListKnowPostFeedRow rows mapper.listFeedPublic(safeSize 1, offset); boolean hasMore rows.size() safeSize; if (hasMore) { rows rows.subList(0, safeSize); } // 构建基础列表计数已填充liked/faved 置为 null 以免污染用户维度缓存 ListFeedItemResponse items mapRowsToItems(rows, null, false); FeedPageResponse respForCache new FeedPageResponse(items, safePage, safeSize, hasMore); // 片段缓存ids/item/countTTL 更长并加入随机抖动降低同一时刻大量过期 int baseTtl 60; //使用高并发线程安全工具生成随机数0-29 int jitter ThreadLocalRandom.current().nextInt(30); Duration frTtl Duration.ofSeconds(baseTtl jitter); // 写入片段缓存与本地缓存 writeCaches(localPageKey, idsKey, hasMoreKey, safeSize, rows, items, hasMore, frTtl); feedPublicCache.put(localPageKey, respForCache); // 返回时覆盖用户维度状态不写回缓存 ListFeedItemResponse enriched enrich(items, currentUserIdNullable); log.info(feed.public sourcedb localPageKey{} page{} size{} hasMore{}, localPageKey, safePage, safeSize, hasMore); // 释放单航班锁允许后续请求正常进入 singleFlight.remove(idsKey); return new FeedPageResponse(enriched, safePage, safeSize, hasMore); }五、高并发防护体系1. Single-Flight 单航班机制 —— 防缓存击穿以页面骨架Key为航班锁并发同一页面仅一次DB回源其余请求等待缓存结果彻底解决缓存失效瞬间的DB击穿、惊群效应。举例来说1000 个用户同时请求第 1 页缓存都未命中↓999 个用户等待1 个用户去查数据库↓数据库查询完成写入缓存↓1000 个用户都获得相同的数据锁的竞争过程时间线─────┬──────────────────────────────────────────│0ms│ 请求 1 获得锁进入同步块│ 请求 2-1000 在锁外等待BLOCKED 状态││ ┌────────────────────────────────────┐│ │ 请求 1 执行 ││ │ 1. 重查 L2 缓存未命中 ││ │ 2. 查询数据库耗时 50ms ││ │ 3. 写入 Redis 缓存 ││ │ 4. 写入 Caffeine 缓存 ││ │ 5. 调用 enrich() 叠加个性化状态 ││ │ 6. 返回响应 ││ │ 7. singleFlight.remove(idsKey) ││ └────────────────────────────────────┘│55ms│ 请求 1 释放锁│56ms│ 请求 2 获得锁进入同步块│ ┌────────────────────────────────────┐│ │ 请求 2 执行 ││ │ 1. 重查 L2 缓存✅ 命中 ││ │ 2. 写入 Caffeine 缓存 ││ │ 3. 调用 enrich() 叠加个性化状态 ││ │ 4. 返回响应无需查数据库 ││ │ 5. singleFlight.remove(idsKey) ││ └────────────────────────────────────┘│57ms│ 请求 2 释放锁│58ms│ 请求 3 获得锁进入同步块│ ┌────────────────────────────────────┐│ │ 请求 3 执行 ││ │ 1. 重查 L2 缓存✅ 命中 ││ │ 2. 直接返回连 Caffeine 都不用写 ││ └────────────────────────────────────┘│59ms│ 请求 3 释放锁││ ... 请求 4-1000 依次快速返回│100ms│ 所有 1000 个请求都已完成 ✅─────┴──────────────────────────────────────────2. 双删失效策略 —— 解决数据不一致内容更新、置顶、可见性变更时执行「立即删除延迟二次删除」杜绝并发回源旧值覆盖问题保障秒级最终一致。/** * 为什么要先删除缓存 * 时间线 * T1: 线程 A 开始执行 confirmContent() * T2: 线程 A 执行第一次 delete → 清除旧缓存 ✅如果没有这个删除B会把数据库中旧数据写到缓存如果在高并发下此时有多个读取了旧数据 * T3: 线程 B 调用 getDetail() 查询同一条知文 * T4: 线程 B 发现缓存未命中 → 但此时数据库还未更新 * T5: 线程 B 等待...或读取到旧数据但不会写回缓存因为 SingleFlight 机制 * T6: 线程 A 执行 mapper.updateContent() → 数据库更新为【新数据】 * T7: 线程 A 执行第二次 delete → 清除可能在 T4-T6 期间产生的脏缓存 ✅ * T8: 线程 C 调用 getDetail() → 缓存未命中 → 从数据库读取【新数据】→ 写入缓存 ✅ * 防止并发污染在数据库更新前清除旧缓存避免并发请求在更新期间读取并回填旧数据 * 保证最终一致性通过更新前删除 更新后删除的双删策略确保缓存最终一定是最新数据 * 降低脏数据窗口期将缓存不一致的时间窗口压缩到最小 */3. 热键动态TTL扩缩容基于滑动窗口热度探测热点页面自动延长L2缓存TTL让高频流量永久滞留在缓存层极致削峰。4. 小时分片Key设计分页维度时间分片组合Key规避整点大面积缓存失效平滑流量波动。选择小时的原因合理的 Key 数量一天 24 个时段不会过多自然的业务周期用户行为通常以小时为单位变化足够的时间分散60-90 秒的 TTL 在一个小时内均匀分布便于清理过期的时段可以批量删除5. 随机防抖设计设计片段缓存和随机防抖缓存防止高并发下大量缓存同时过期造成缓存雪崩// 片段缓存ids/item/countTTL 更长并加入随机抖动降低同一时刻大量过期 int baseTtl 60; //使用高并发线程安全工具生成随机数0-29 int jitter ThreadLocalRandom.current().nextInt(30); Duration frTtl Duration.ofSeconds(baseTtl jitter);六、数据一致性方案秒级最终一致基础数据内容变更事件驱动双删实时失效计数数据增量事件入聚合桶 定时任务折叠纠偏缺片兜底碎片缺失时批量回源补齐自动修复缓存脏数据/缺失数据七、架构设计权衡与取舍为什么不做单级缓存单级本地缓存成本高、单级Redis延迟高、单级碎片装配复杂度高三级分层各司其职兼顾延迟、成本、稳定性。为什么个性化不进缓存避免缓存维度爆炸、命中率暴跌、雪崩风险抬升实现公共缓存全局复用。为什么用抖动TTL打散过期时间杜绝缓存集中失效洪峰。为什么采用事件定时双保障兼顾实时失效与兜底纠偏实现性能与一致性平衡。八、最终落地效果热点页面本地命中率极高P99延迟大幅下降绝大多数请求拦截在缓存层DB QPS极致收敛彻底解决峰值惊群、击穿、数据不一致问题实现「热点本地返回、普通Redis拼装、极少落DB」的最优流量模型九、总结与架构复用思想整套三级缓存架构通过分层存储、动静解耦、读写分离、个性后置、并发防护、最终一致纠偏完美解决高并发Feed流的性能与一致性矛盾可通用落地于所有信息流、列表页、推荐流等高读低改业务场景。

相关新闻