根据 ProgramService 中的代码逻辑,getDetail (V1) 和 getDetailV2 (V2) 代表了系统在不同并发量级下的两种架构选择。以下是对这两个版本的深度对比与解析。
1. 核心架构区别总结
| 特性 | V1 版本 (getDetail) | V2 版本 (getDetailV2) |
|---|---|---|
| 缓存架构 | Redis + 数据库 (二级架构) | 本地缓存(JVM) + Redis + 数据库 (三级架构/多级缓存) |
| 防并发策略 | 分布式锁 (Redisson) | 本地锁/缓存框架机制 + 分布式锁 |
| 网络开销 | 每次查询(即使命中缓存)都需要一次 Redis 网络IO | 命中本地缓存时 零网络IO,响应极快 |
| 适用场景 | 常规高并发,防止缓存击穿 | 极热点数据 (Hot Key),防止 Redis 单节点带宽被打满 |
| 代码调用 | getById | getByIdMultipleCache |
2. V1 版本深度解析:分布式缓存与防击穿
V1 的核心逻辑是 “Redis 集中式缓存 + 分布式锁兜底”。
2.1 实现流程
- 查询 Redis:优先查询 Redis 缓存。
- 分布式锁保护:若 Redis 无数据(缓存未命中),为了防止海量请求瞬间击穿数据库,使用
@ServiceLock或serviceLockTool.getLock获取分布式锁。 - 查询数据库:成功获取锁的线程查询数据库,并将数据回写至 Redis。
2.2 代码逻辑证据
调用了 getById 方法,其伪代码逻辑如下:
java
@ServiceLock(lockType= LockType.Read, name = PROGRAM_LOCK, keys = {"#programId"})
public ProgramVo getById(Long programId, ...) {
// 1. 查 Redis
ProgramVo programVo = redisCache.get(..., ProgramVo.class);
if (Objects.nonNull(programVo)) return programVo;
// 2. 加分布式锁 (Redisson RLock)
RLock lock = serviceLockTool.getLock(...);
lock.lock();
try {
// ... 双重检查 + 查数据库 + 回写Redis
} ...
}2.3 技术焦点:双重检查锁(Double-Check Locking)
在 V1 版本的锁逻辑中,采用了一个经典的双重检查机制,这是解决缓存击穿和惊群效应的关键。
为什么要二次检查?
假设有一个热点节目(如周杰伦演唱会),Redis 缓存刚好失效,瞬间涌入 1000 个请求。
- 第一重检查(锁外):1000 个请求查询 Redis 均为空,全部尝试获取分布式锁。
- 竞争锁:线程 A 抢到锁,其余 999 个请求(线程 B、C...)被阻塞排队。
- 线程 A 执行:查库 -> 写 Redis -> 释放锁。
- 线程 B 获取锁:线程 A 释放后,线程 B 获得锁进入
try块。- 如果不加检查:线程 B 不知道线程 A 已经处理过了,会再次查库、写 Redis。后续 998 个线程同理。这会导致 1000 次数据库查询,锁失去了意义。
- 加上二次检查:线程 B 进锁后,再次查询 Redis。发现已有数据(线程 A 写入的),直接返回,不再查库。
逻辑伪代码
java
// 1. 第一次检查:挡住绝大部分已经有缓存的流量
val = redis.get(key);
if (val != null) return val;
lock.lock();
try {
// 2. 第二次检查:挡住刚才在门外排队、现在进来的流量
// 这里的 val 是前一个持有锁的线程(线程A)写入的
val = redis.get(key);
if (val != null) return val;
// 3. 真的去查库
val = db.get(key);
redis.set(key, val);
} finally {
lock.unlock();
}2.4 缺点
对于超级热点数据,虽然保护了数据库,但所有请求流量都会涌向 Redis。这可能导致 Redis 该分片的带宽被占满,影响其他业务。
3. V2 版本深度解析:多级缓存与极致性能
V2 的核心逻辑是 “多级缓存(Multi-Level Cache)”,引入了 JVM 级别的本地缓存(如 Caffeine)。
3.1 实现流程
- 查询本地缓存:优先查询当前应用进程的内存(LocalCache)。如果命中,不经过网络,直接返回。
- 查询 Redis:本地缓存未命中,再查 Redis。
- 查询数据库:Redis 未命中,最后查数据库。
3.2 代码逻辑证据
V2 版本不仅主对象使用多级缓存,关联对象也全面升级为 ...MultipleCache 系列方法:
java
public ProgramVo getByIdMultipleCache(Long programId, Date showTime){
// 使用 localCacheProgram (本地缓存组件)
return localCacheProgram.getCache(
RedisKeyBuild.createRedisKey(RedisKeyManage.PROGRAM, programId).getRelKey(),
key -> {
// 本地缓存没命中时的加载逻辑(通常再去查 Redis -> DB)
// ...
});
}组件同步升级列表:
programShowTimeService.selectProgramShowTimeByProgramIdMultipleCacheprogramService.getProgramGroupMultipleCacheticketCategoryService.selectTicketCategoryListByProgramIdMultipleCache
3.3 优势
通过在 JVM 内部缓存数据,实现了毫秒级甚至纳秒级的响应速度,极大降低了 Redis 集群的负载,是解决“热点 Key”问题的终极方案。
4. 详细代码调用差异对比
| 业务步骤 | V1 代码调用 | V2 代码调用 | 差异点分析 |
|---|---|---|---|
| 演出时间 | selectProgramShowTimeByProgramId | ...MultipleCache | V2 优先查本地内存 |
| 节目详情 | getById | getByIdMultipleCache | V2 优先查本地内存 |
| 节目分组 | getProgramGroup | getProgramGroupMultipleCache | V2 优先查本地内存 |
| 分类信息 | getProgramCategory | ...MultipleCache | V2 优先查本地内存 |
| 票档列表 | selectTicketCategoryListByProgramId | ...MultipleCache | V2 优先查本地内存 |
5. 总结建议
- V1 (Standard Cache):标准的高并发解决方案。
- 适用性:适合大多数常规业务场景。
- 特点:利用 Redis 抗压,利用分布式锁保护 DB。开发复杂度适中。
- V2 (Extreme Performance):针对极端高并发场景(如大麦网抢票、秒杀)的优化。
- 适用性:极热点数据(Hot Key)。
- 特点:利用本地内存抗压,牺牲了一定的数据一致性(本地缓存更新有延迟),但换取了极致的吞吐量和 Redis 的安全性。