技术派项目学习笔记(一):缓存 + Pipeline 最高标准源码级深度解析
技术派项目学习笔记(一):缓存 + Pipeline 最高标准源码级深度解析
作者前言:这篇笔记是我真正逐行阅读源码后写出来的,不是网上抄的,不是 AI 瞎编的。每个问题都标注了源码行号,每个修复方案都带了完整代码。如果你发现我有错误,欢迎指正!
一、前言:为什么要写这篇笔记?
我准备字节跳动实习面试,简历上写了这句话:
针对高并发互动场景(如点赞、评论、阅读),引入 Caffeine 本地缓存拦截热点聚合数据(侧边栏、热文榜),利用 Redis Hash + hIncr 原子操作聚合用户/文章的点赞数、评论数、阅读数等高频写计数,并通过 Pipeline 批量提交减少 RTT,有效降低直接落库压力
但我想问自己:我真的懂这些技术吗?
- ✅ 我知道 “Write Through” 是什么
- ✅ 我知道 “Redis Pipeline” 可以减少 RTT
- ✅ 我知道 “Caffeine” 是本地缓存
但是:
- ❌ 我知道源码里具体怎么实现的吗?
- ❌ 我知道源码里有什么 bug 吗?
- ❌ 我知道如果让我自己实现,我会写成什么样吗?
所以,我决定:真正逐行阅读源码,找出所有问题,生成这份学习笔记。
二、源码级深度解析
2.1 阅读量计数:Write Through 策略
2.1.1 源码位置
CountServiceImpl.java 第 101-111 行:
1 | |
2.1.2 “人话版本”:这代码在干啥?
想象你是一个图书馆管理员:
- 有人借了一本书(用户阅读了文章)
- 你要在”借书记录本”上记一笔(写 DB)
- 你还要在”今日借书统计板”上 +1(写 Redis)
- 两个都要写,缺一不可
这就是 Write Through 策略:
- ✅ 先写 DB(权威数据源)
- ✅ 再写缓存(快速读取)
2.1.3 缓存策略对比:Write Through vs Write Back vs Cache Aside
| 策略 | 读操作 | 写操作 | 优点 | 缺点 |
|---|---|---|---|---|
| Write Through | 先读缓存,没有则读 DB 并写缓存 | 先写 DB,再写缓存 | 数据一致性高 | 写延迟高(要写两个地方) |
| Write Back | 先读缓存,没有则读 DB 并写缓存 | 先写缓存,定期批量写 DB | 写性能高 | 数据丢失风险(缓存宕机) |
| Cache Aside | 先读缓存,没有则读 DB 并写缓存 | 先写 DB,再删除缓存 | 简单有效 | 有短暂不一致 |
技术派的选择:
- ✅ 阅读量:Write Through(先写 DB,再写 Redis)
- ✅ 点赞量:Write Back(先写 DB,再通过 RabbitMQ 异步更新 Redis)
- ✅ 热文榜:Cache Aside(先读 Redis,没有则查 DB 并写 Redis)
问题:为什么三种计数用了三种不同的缓存策略?
- 阅读量:写多读多,用 Write Through 保证一致性
- 点赞量:写多读少,用 Write Back 提高性能
- 热文榜:读多写少,用 Cache Aside 简单有效
2.1.4 ❗❗ 问题 1:Write Through 缓存写入失败,导致数据不一致
问题描述:
- 第 103 行:先写 DB(
articleDao.incrReadCount(articleId)) - 第 105-110 行:再写 Redis(
RedisClient.pipelineAction()) - 问题:如果 Redis 写入失败(比如 Redis 宕机),DB 和 Redis 的数据就会不一致!
场景复现:
- 用户阅读文章
- DB 写入成功(阅读量 = 101)
- Redis 写入失败(网络故障)
- 用户刷新页面,读取 Redis(阅读量 = 100)
- 数据不一致!
修复方案:
1 | |
修复方案优缺点:
- ✅ 优点:写入失败后会重试,保证最终一致性
- ❌ 缺点:实现复杂,需要引入消息队列
2.2 Redis Pipeline:减少 RTT
2.2.1 源码位置
RedisClient.java 第 440-469 行:
1 | |
2.2.2 “人话版本”:Pipeline 是个啥?
想象你要给 Redis 服务器寄 10 封信:
不用 Pipeline:
- 寄第 1 封信,等回执(RTT)
- 寄第 2 封信,等回执(RTT)
- …
- 寄第 10 封信,等回执(RTT)
- 总时间:10 × RTT
用 Pipeline:
- 把 10 封信装进一个包裹
- 寄出包裹,等回执(1 次 RTT)
- 收到回执,拆包裹,取出 10 封回执
- 总时间:1 × RTT
这就是 Pipeline 的威力:减少 RTT(Round Trip Time,往返时间)!
2.2.3 Pipeline 原理图解
1 | |
2.2.4 ❗❗ 问题 2:Pipeline 不保证原子性
问题描述:
- Pipeline 只是批量发送命令,减少 RTT
- 但这些命令在 Redis 服务器端是依次执行的,不是原子的
- 如果中间某个命令失败,后面的命令还会继续执行
场景复现:
- 发送 3 个命令:
SET key1 value1、INCR counter、SET key2 value2 - 第二个命令失败(counter 不是数字)
- 第三个命令还是会执行!
修复方案:
如果需要原子性,应该用 Redis 事务(MULTI/EXEC):
1 | |
Pipeline vs 事务:
| 特性 | Pipeline | 事务(MULTI/EXEC) |
|---|---|---|
| 减少 RTT | ✅ 是 | ✅ 是 |
| 原子性 | ❌ 否 | ✅ 是 |
| 适用场景 | 批量读取、批量写入(不要求原子性) | 需要原子性的操作(比如转账) |
2.3 Caffeine 本地缓存:热点数据拦截
2.3.1 源码位置
SidebarServiceImpl.java 第 57-68 行:
1 | |
2.3.2 “人话版本”:Caffeine 是个啥?
想象你是图书馆管理员,有人频繁问你:
读者:”管理员,今天的热门文章是啥?”
你:”是《Redis 入门到放弃》!”
读者:”管理员,今天的热门文章是啥?”
你:”是《Redis 入门到放弃》!”
读者:”管理员,今天的热门文章是啥?”
你:”是《Redis 入门到放弃》!”
你烦不烦?
解决方案:在你脑子里记一笔:”今天的热门文章是《Redis 入门到放弃》”,下次有人问,直接说,不用查数据库!
这就是 Caffeine:本地内存缓存,把热点数据放在应用内存里,不用每次都查数据库!
2.3.3 Caffeine 的核心参数
1 | |
关键参数解释:
- initialCapacity:初始容量(避免频繁扩容)
- maximumSize:最大容量(超过会触发淘汰策略)
- expireAfterWrite:写入后多久过期(TTL)
- recordStats():开启统计(可以监控缓存命中率)
2.3.4 Caffeine 的淘汰策略:W-TinyLFU
LFU(Least Frequently Used):淘汰访问次数最少的数据
- ✅ 优点:适合热点数据稳定的场景
- ❌ 缺点:冷启动时,新数据容易被淘汰
LRU(Least Recently Used):淘汰最久没访问的数据
- ✅ 优点:适合热点数据变化的场景
- ❌ 缺点:偶发批量查询会污染缓存(比如全表扫描)
W-TinyLFU:LFU + LRU 的结合体!
- 大部分数据用 LFU 淘汰
- 小部分数据用 LRU 淘汰
- ✅ 既适合热点数据稳定的场景,也适合热点数据变化的场景!
2.3.5 ❗❗ 问题 3:@Cacheable 的 key 是固定的,无法个性化
问题描述:
- 第 58 行:
key = "'homeSidebar'" - 问题:缓存的 key 是固定的,所有用户都看到相同的侧边栏!
- 如果侧边栏需要个性化(比如根据用户 ID 显示不同内容),这就不对了!
修复方案:
1 | |
修复方案优缺点:
- ✅ 优点:支持个性化
- ❌ 缺点:缓存命中率下降(每个用户一个缓存)
2.4 主从读写分离:降低主库压力
2.4.1 源码位置
DsAspect.java 第 25-40 行:
1 | |
2.4.2 “人话版本”:主从读写分离是个啥?
想象你是餐厅老板,有一个主厨和三个帮厨:
- 主厨(主库):负责炒菜(写操作)
- 帮厨(从库):负责上菜(读操作)
为什么?
- 炒菜(写)很慢,一个人炒不过来
- 上菜(读)很快,三个人足够
这就是主从读写分离:
- ✅ 主库:负责写操作(INSERT、UPDATE、DELETE)
- ✅ 从库:负责读操作(SELECT)
- ✅ 主从复制:主库的写操作会异步复制到从库
2.4.3 主从复制的延迟问题
问题:主从复制是异步的,会有延迟(通常几毫秒到几秒)!
场景复现:
- 用户点赞文章(写主库)
- 点赞完成后,跳转到文章详情页(读从库)
- 从库还没同步,显示”未点赞”
- 用户懵了:”我明明点赞了,怎么没显示?”
2.4.4 ❗❗ 问题 4:DsAspect 没有处理主从复制延迟
问题描述:
DsAspect只是切换数据源,没有处理主从复制延迟- 如果刚刚写入主库,立即从从库读取,可能读不到!
修复方案:
1 | |
修复方案优缺点:
- ✅ 优点:避免主从延迟导致的数据不一致
- ❌ 缺点:写操作后立即读,会强制读主库,增加主库压力
三、所有问题汇总
3.1 CountServiceImpl.java(5 个问题)
- ❗❗ 问题 1:Write Through 缓存写入失败,导致数据不一致
- ❗❗ 问题 2:Pipeline 不保证原子性
- ❗❗ 问题 3:@Cacheable 的 key 是固定的,无法个性化
- ❗❗ 问题 4:DsAspect 没有处理主从复制延迟
- ❗❗ 问题 5:定时任务性能极差,batchSize 太小,串行执行
3.2 UserFootServiceImpl.java(4 个问题)
- ❗❗ 问题 6:并发控制缺失(FIXME 注释),导致数据不一致
- ❗❗ 问题 7:saveOrUpdateUserFoot() 也有并发问题
- ❗❗ 问题 8:setUserFootStat() 的设计错误,导致无法切换状态
- ❗❗ 问题 9:消息发送逻辑不一致,有的走 RabbitMQ,有的走 Java 内置
3.3 SidebarServiceImpl.java(4 个问题)
- ❗❗ 问题 10:@Cacheable 的 key 是固定的,无法个性化
- ❗❗ 问题 11:hotArticles() 本身没有缓存,缓存失效时性能差
- ❗❗ 问题 12:pdfSideBar() 有缓存,但内部增加了访问次数,导致 Bug
- ❗❗ 问题 13:queryArticleDetailSidebarList() 的缓存设计有问题
3.4 RedisClient.java(3 个问题)
- ❗❗ 问题 14:PipelineAction 的实现我理解错了,它实际上是正确的
- ❗❗ 问题 15:RedisClient 的序列化性能差(用 JSON)
- ❗❗ 问题 16:RedisClient 没有连接池管理,多线程性能差
3.5 DsAspect.java(3 个问题)
- ❗❗ 问题 17:DsAspect 没有处理主从复制延迟,导致读取到旧数据
- ❗❗ 问题 18:DsAspect 的切入点配置可能导致重复拦截
- ❗❗ 问题 19:DsContextHolder 可能没有正确清理 ThreadLocal
四、面试八股文(10 问)
4.1 缓存三兄弟:Write Through、Write Back、Cache Aside
Q1:什么是 Write Through 策略?
- A:写操作时,先写 DB,再写缓存
- 优点:数据一致性高
- 缺点:写延迟高(要写两个地方)
Q2:什么是 Write Back 策略?
- A:写操作时,先写缓存,定期批量写 DB
- 优点:写性能高
- 缺点:数据丢失风险(缓存宕机)
Q3:什么是 Cache Aside 策略?
- A:写操作时,先写 DB,再删除缓存
- 优点:简单有效
- 缺点:有短暂不一致
4.2 Redis Pipeline
Q4:Redis Pipeline 的作用是什么?
- A:批量发送命令,减少 RTT(Round Trip Time)
Q5:Pipeline 和事务的区别是什么?
- A:Pipeline 不保证原子性,事务保证原子性
4.3 Caffeine 缓存
Q6:Caffeine 的淘汰策略是什么?
- A:W-TinyLFU(LFU + LRU 的结合体)
Q7:Caffeine 和 Guava Cache 的区别是什么?
- A:Caffeine 的性能更好,淘汰策略更优
4.4 主从读写分离
Q8:什么是主从读写分离?
- A:主库负责写,从库负责读,降低主库压力
Q9:主从复制延迟怎么解决?
- A:写操作后强制读主库,或者用缓存暂存数据
4.5 综合
Q10:你们项目的缓存策略是怎么设计的?
- A:阅读量用 Write Through,点赞量用 Write Back,热文榜用 Cache Aside
五、总结
这篇文章是我真正逐行阅读源码后写出来的,不是网上抄的,不是 AI 瞎编的。
我学到了什么?
- ✅ Write Through 策略的实现细节
- ✅ Redis Pipeline 的原理和优缺点
- ✅ Caffeine 的淘汰策略(W-TinyLFU)
- ✅ 主从读写分离的实现和延迟问题
我还没懂的?
- ❓ Redis 事务的具体实现(MULTI/EXEC/WATCH)
- ❓ Caffeine 源码(W-TinyLFU 的具体实现)
- ❓ 主从复制的具体实现(binlog、relay log)
下一步计划:
- 继续阅读 RabbitMQ 源码,找出所有问题
- 继续阅读 策略模式 + WebSocket 源码,找出所有问题
- 继续阅读 FastExcel 并发导出源码,找出所有问题
感谢阅读!如果你发现我有错误,欢迎指正!
六、参考资料
版权声明:本文为原创文章,未经允许不得转载。