一文详解Cache Aside(旁路缓存模式)
最经典、最常用的缓存设计模式业务代码自己维护缓存缓存组件不感知数据库缓存和 DB 相互独立所以叫旁路。适用Redis MySQL 这类组合几乎所有业务系统都在用。旁路 缓存不在数据库读写的主链路里面只是一条 “旁边的辅助支路”。请求 →业务代码在主路上业务自己做判断查缓存命中直接返回缓存没命中业务自己去查数据库拿到数据之后业务再额外往旁边的缓存里写一份。数据库的读写主干流程本身不依赖缓存缓存只是挂在主干旁边的一条支路。数据库不知道缓存存在缓存也不知道数据库。缓存是旁路的附属业务代码主动去旁路操作缓存。一、核心思想缓存不主动和数据库联动读写逻辑全部由应用程序代码控制。数据库是权威数据源缓存只是副本。二、读流程查询数据先查 Cache命中 → 返回未命中 → 查询 DB拿到数据业务手动写 Cache返回三、写流程更新数据重点两种方案对比Cache Aside 的坑基本都在更新策略。✅ 方案 A先更新数据库再删除缓存业界推荐更新数据库删除缓存不是更新缓存为什么是删除缓存而不是更新缓存如果更新缓存高并发下多次写会出现缓存被旧值覆盖而且很多数据不一定会被读取更新缓存是浪费。删除缓存下次读的时候自动加载最新数据更合理。可能存在的极小概率并发问题线程 A读缓存失效 → 查 DB拿到旧数据线程 B写更新 DB → 删除缓存线程 A读把旧数据写入缓存结果缓存是旧数据DB 是新数据数据不一致。✅ 怎么解决给缓存设置过期时间兜底就算不一致到期自动刷新或者使用延迟双删更新 DB → 删除缓存 → 等待一小段时间几百 ms→ 再次删除缓存Canal binlog 方案大厂优化缺点不能 100% 根除只是降低概率一般业务过期时间兜底足够。1. 这个问题本质问题发生的根本原因读请求的 DB 查询 RT 写请求更新 DB 删缓存的总 RT。这个场景本身就属于比较罕见的正常情况下 DB 查询很快很难出现这种时序。2. 方案对比TTL / 延迟双删 / 锁① TTL最常用推荐给缓存 key 设置过期时间。就算出现脏数据最多等 TTL 时间脏数据自动消失。优点零侵入、性能几乎无损实现最简单缺点短暂不一致窗口等于 TTL绝大多数互联网业务商品、用户信息、订单基础信息都接受几秒几分钟的短暂不一致。② 延迟双删次选进一步缩小概率更新 DB → 删除缓存 → 延迟几百 ms 再删一次。延迟时间要大于正常读请求从查 DB 到写入缓存的最大耗时。优点大幅降低脏数据存活概率缺点依然不是 100% 杜绝延迟任务需要线程池 / 消息队列依然存在极极端场景③ 加锁不推荐作为通用方案思路对同一个业务 key比如 user:100加分布式锁读写互斥。读流程获取锁 → 查缓存miss 则查 DB 写缓存 → 释放锁写流程获取锁 → 更新 DB → 删除缓存 → 释放锁锁带来的问题很关键面试常问性能断崖下跌同一个 key 串行化并发读写变成排队热点 key 直接瓶颈。死锁风险锁超时、业务异常、节点宕机需要锁自动过期处理复杂。锁本身也有失败概率分布式锁不是绝对可靠网络抖动、主从切换丢锁。收益很低为了堵一个极低概率的时序 bug牺牲系统吞吐得不偿失。❗ 注意这里的锁是为了解决数据不一致不是解决缓存击穿。 缓存击穿大量请求同时 miss 打 DB可以用互斥锁和这个脏数据场景是两个问题不要混淆。延迟双删伪代码// 更新 public void updateUser(User user) { // 1. 更新数据库 db.updateById(user); // 2. 删除缓存 cache.del(user: user.getId()); // 3. 延迟一段时间再次删除 ThreadPool.schedule(() - { cache.del(user: user.getId()); }, 500, TimeUnit.MILLISECONDS); }延迟时间设置大于业务读取 DB 写入缓存的耗时。❌方案 B先删除缓存再更新数据库不推荐四、Cache Aside 优缺点✅ 优点适合读多写少场景最通用缓存失效策略灵活可以 TTL 过期、主动删除❌ 缺点存在短暂的数据不一致最终一致性不是强一致缓存失效瞬间大量请求同时查 DB →缓存击穿需要互斥锁 / 热点永不过期处理方案 1互斥锁核心思路缓存失效时只放行一个请求去查 DB 回写缓存其他请求等待等缓存建好之后直接读缓存避免大量请求同时访问 DB。伪代码业务逻辑// 查询流程 public Object getHotData(String key) { Object cacheVal cache.get(key); if (cacheVal ! null) { return cacheVal; } // 缓存为空尝试加锁 String lockKey lock: key; boolean locked redis.tryLock(lockKey, 30, TimeUnit.SECONDS); //tryLock( 锁key, 锁持有过期时间, 时间单位 ) if (locked) { try { // ✅ 抢到锁查询DB Object dbVal db.select(key); // 回写缓存 cache.set(key, dbVal, 60, TimeUnit.SECONDS); return dbVal; } finally { // ✅ 内部自带lua校验只有本线程持有锁才释放 redis.unLock(lockKey); } } else { // ❌ 没抢到锁短暂休眠后重试读取缓存 Thread.sleep(50); return getHotData(key); } }关键点锁粒度按缓存 key 加锁不是全局锁。不同 key 互不影响。锁必须设置过期时间防止拿到锁的服务宕机锁永远不释放死锁。重试策略不要无限循环设置最大重试次数避免线程卡死。推荐用 Redis 分布式锁Redisson不要自己手写简单 SETNX容易有释放锁的 bug。✅优点实时性好数据更新后缓存过期能读到最新 DB 数据不需要提前预判热点 key通用方案❌缺点会有等待并发高时请求排队接口延迟上升锁本身有开销如果锁设计不当会有死锁、锁失效风险大量请求等待重试极端情况容易堆积请求方案 2热点永不过期逻辑过期不设置 Redis 的 TTL重点物理上缓存 key 不删除、不设过期时间在 value 里面自己存一个过期时间叫逻辑过期。核心思路热点 keyRedis 不自动过期。缓存永远存在。 缓存 value 结构{ data: 业务数据, expireTime: 1798888888000 // 逻辑过期时间戳 }流程请求先读缓存拿到对象判断当前时间 expireTime逻辑过期没过期直接返回缓存数据逻辑过期返回旧缓存数据同时单独开一个异步线程去更新 DB 更新缓存里的 data 和逻辑过期时间⚠️ 重点不会阻塞用户请求用户拿到的是旧数据异步后台更新伪代码示意//约定这个方法只给热点 key 调用 public Object getHotKey(String key) { CacheItem item cache.get(key); if (item null) { // 缓存不存在场景才走查DB写缓存项目预热阶段一次性加载热点 return loadDbAndSetCache(key); } // 判断逻辑是否过期 if (System.currentTimeMillis() item.expireTime) { // 尝试获取更新锁只有一个异步线程去更新 if (redis.tryLock(updateLock:key)) { // 异步线程执行更新不阻塞当前请求 threadPool.submit(() - { Object newData db.select(key); CacheItem newItem new CacheItem(newData, System.currentTimeMillis()30*1000); cache.set(key, newItem); redis.unLock(updateLock:key); }); } } // 无论是否逻辑过期都立刻返回当前缓存数据旧数据也返回 return item.data; }关键点热点 key 要提前预热项目启动或者定时任务预先加载进 Redis保证缓存永远不为空逻辑过期更新是异步用户请求不等待不会击穿 DB数据是短暂不一致的读到旧数据后台慢慢更新适合允许短暂旧数据的业务同样加分布式锁保证同一时刻最多一个异步线程去更新缓存防止多线程并发更新缓存✅优点完全杜绝缓存击穿缓存 key 永远存在不会大量请求打 DB用户接口响应快没有等待、无排队延迟❌缺点数据会读到旧值牺牲强一致性只能最终一致只能针对提前识别出来的热点 key不能通用所有 key需要预热如果 DB 数据被删除缓存里的旧数据需要额外处理清理五、场景问题假设说是一个涉及到查询库存和库存扣减的业务场景通常会怎样设计库存扣减 查询场景设计结合 Cache Aside库存业务特点读多写少但写操作强敏感不能超卖库存数据一致性要求远高于普通用户信息。普通 Cache Aside先更新 DB 再删缓存 TTL可以用但不能直接裸奔因为一旦出现脏库存会直接超卖造成资损。先明确库存分两类设计思路不一样实时可售库存扣减、下单核心强敏感不能随便出现脏数据展示库存商品详情页展示仅供参考可以容忍短暂不一致普通 Cache Aside 就够用。面试高频坑商品页展示库存 ≠ 下单扣减的真实库存两者要分开方案整体思路✅ 商品详情页展示库存Cache AsideRedis 缓存允许短暂不一致✅ 下单扣减真实库存不读缓存做扣减库存扣减以数据库为准缓存只做查询展示核心原则扣减动作不走缓存缓存只用来抗查询流量库存扣减的判断必须落到数据库或者分布式锁 Redis 预扣两种路线路线一数据库扣减中小流量简单稳妥推荐优先1查询展示库存Cache Aside读流程查 Redis 展示库存缓存命中直接返回给前端商品页miss → 查询 DB 库存 → 写入 Redis返回写流程库存变更下单、取消订单、补货数据库执行库存扣减SQL 乐观锁-- 乐观锁扣减防止超卖 UPDATE stock SET available available - #{num}, version version 1 WHERE id #{stockId} AND available #{num} AND version #{oldVersion};看 update 影响行数大于 0 代表扣减成功0 库存不足 / 版本不对扣减失败。DB 更新成功后删除 Redis 里的展示库存缓存Cache Aside先更 DB再删缓存缓存自带 TTL 兜底比如 10s就算极端时序脏数据最多 10s 过期这里的脏缓存只会影响前端页面展示不会导致超卖因为下单扣减判断不走这个缓存。 就算页面显示还有 100 件真实 DB 已经卖完用户点下单DB 乐观锁直接扣减失败不会超卖。时序风险回顾放到库存场景理解极端时序请求 A缓存失效 → 查询 DB 拿到旧库存100正在回写缓存请求 B下单DB 扣减成功变成 99删除缓存请求 A把旧值 100 写回 Redis 缓存 结果前端页面看到库存 100但真实 DB 是 99。影响页面展示不准不会超卖所以这个场景不需要分布式锁去解决这个 Cache Aside 的脏数据问题。TTL 兜底足够。这就是为什么前面说普通 Cache Aside 脏数据问题库存展示场景不用锁。路线二Redis 预扣库存高并发秒杀大流量场景DB 扛不住大量下单请求把库存放到 Redis 做预扣DB 做最终落地。 这个就不再是简单的 Cache AsideRedis 不再只是旁路缓存变成库存计数器。流程系统预热把 DB 库存加载到 Redis库存 key用户下单Redis lua 脚本原子预扣库存预扣成功生成订单进入异步队列异步消费队列在数据库里完成真实库存扣减 订单落库取消订单 / 超时未支付lua 脚本回补 Redis 库存定时任务Redis 库存和 DB 库存对账修复差异⚠️ 风险点 Redis 预扣 异步落库会有数据不一致风险Redis 扣成功DB 宕机没扣成功需要对账补偿机制。这种场景下Redis 是业务计数器不是单纯旁路缓存已经脱离标准 Cache Aside。扩展Binlog 订阅异步删缓存大厂常用推荐业务代码DB 库存扣减成功 → 主动删除缓存。同时用 Canal 监听 stock 表 binlog只要库存字段发生变更消费端再次删除缓存 key。业务代码删缓存失败网络异常binlog 兜底删除极端时序产生脏缓存一旦 DB 库存发生变更binlog 会再次触发删除优势不阻塞读写无锁不影响接口性能本质双重删除兜底极大降低脏缓存存在的概率。缺点引入 Canal 组件架构变重需要维护 binlog 消费、重试、故障。

相关新闻

从 failed to load plugins 看插件系统:加载失败根因与排查

从 failed to load plugins 看插件系统:加载失败根因与排查

我不止一次在启动日志里被一行failed to load plugins或plugins did not activate的告警搞得头皮发麻。尤其是那些把插件机制做得比较“野”的工具,装了一堆插件,最后启动时某个不显眼的报错让你排查一整个下午。这次不聊某个具体产品,而是从…

2026/10/4 21:10:15 阅读更多 →
西门子AF框架UMAC用户权限配置与实战排错指南

西门子AF框架UMAC用户权限配置与实战排错指南

1. 项目概述:为什么“AF框架翻译”不是简单的文字搬运,而是西门子自动化工程师的必修课“西门子AF框架翻译-第十七章”这个标题乍看像是一份普通的文档翻译任务,但如果你在TIA Portal环境下调试过S7-1500 PLC的用户管理功能,或者被…

2026/10/4 21:09:15 阅读更多 →
GLM 5.3 深度排查只读重入:跨合约价格预言机瞬时汇率失真挖掘

GLM 5.3 深度排查只读重入:跨合约价格预言机瞬时汇率失真挖掘

GLM 5.3 深度排查只读重入:跨合约价格预言机瞬时汇率失真挖掘在所有智能合约漏洞类型中,“只读重入(Read-only Reentrancy)”被很多顶级安全专家公认为隐蔽性最强、破坏力最大的幽灵杀手。它不同于 The DAO 时代那种在一个函数内反…

2026/10/4 21:09:15 阅读更多 →

最新新闻

YOLO监控场景行人检测实战:数据集训练全流程与避坑指南

YOLO监控场景行人检测实战:数据集训练全流程与避坑指南

简介:面向街道监控场景的行人检测数据集,共包含1200张监控视角抓拍图片,标注类别为person,最大特点是已按YOLO系列要求生成txt标签,可直接用于YOLOv3至YOLOv10等全系列算法训练。资源包内总计2000个文件,由…

2026/10/4 21:53:04 阅读更多 →
OpenClaw 实战总结:从WSL2部署到Skill开发的开源AI助手指南

OpenClaw 实战总结:从WSL2部署到Skill开发的开源AI助手指南

OpenClaw 这个项目我从年初一直折腾到现在,中间经历了好几轮版本重写。标题写成“终章”不是说项目没了,而是我想把这一阶段的使用心得收个尾:从 Windows Companion 到 WSL2 环境校验,从 Node.js 安装到 Ollama 本地模型&#xff…

2026/10/4 21:53:04 阅读更多 →
从2小时到10分钟:发票批量录入的OCR与Excel自动化实操指南

从2小时到10分钟:发票批量录入的OCR与Excel自动化实操指南

1. 从2小时到10分钟,我到底改了什么干过律所行政或财务的都知道,业务管理系统里录发票这事儿,看着不起眼,干起来真要命。重庆这边的律所,业务量一大,每个案子结案要归档案卷、登记费用,发票录入…

2026/10/4 21:53:04 阅读更多 →
Claude Code部署实战:从零生成Landing Page完整指南

Claude Code部署实战:从零生成Landing Page完整指南

今天是学习 AI 编程的第四天。前三天我基本在“用”AI:写提示词让聊天机器人解释代码,开个编辑器插件让它补全函数,把报错信息粘贴出去求助。工具换了好几轮,但对 AI 编程的认知始终停留在一问一答的层面。第四天终于不一样了&…

2026/10/4 21:53:04 阅读更多 →
手写PLY文件+Open3D实现工业级点云可视化

手写PLY文件+Open3D实现工业级点云可视化

1. 项目概述:为什么一个能自己生成并可视化PLY点云的人,比只会调库的工程师更值钱点云可视化不是炫技,是三维感知落地的第一道门槛。我带过三届校招实习生,发现一个扎眼现象:90%的人能跑通Open3D官网示例,但…

2026/10/4 21:53:04 阅读更多 →
Qwen2-VL图像识别微调实战:Python实现LoRA训练与部署指南

Qwen2-VL图像识别微调实战:Python实现LoRA训练与部署指南

简介:面向图像识别与多模态大模型应用开发者,这份Python工程源码完整演示了基于千问Qwen2-VL从COCO 2014 Caption图片数据准备、模型训练到checkpoint加载推理的落地路径,适合已有Python基础、希望掌握视觉语言模型微调及图像识别工程化流程的…

2026/10/4 21:52:03 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 20:14:29 阅读更多 →