Skip to content

根据 ProgramService 中的代码逻辑,getDetail (V1) 和 getDetailV2 (V2) 代表了系统在不同并发量级下的两种架构选择。以下是对这两个版本的深度对比与解析。

1. 核心架构区别总结

特性V1 版本 (getDetail)V2 版本 (getDetailV2)
缓存架构Redis + 数据库 (二级架构)本地缓存(JVM) + Redis + 数据库 (三级架构/多级缓存)
防并发策略分布式锁 (Redisson)本地锁/缓存框架机制 + 分布式锁
网络开销每次查询(即使命中缓存)都需要一次 Redis 网络IO命中本地缓存时 零网络IO,响应极快
适用场景常规高并发,防止缓存击穿极热点数据 (Hot Key),防止 Redis 单节点带宽被打满
代码调用getByIdgetByIdMultipleCache

2. V1 版本深度解析:分布式缓存与防击穿

V1 的核心逻辑是 “Redis 集中式缓存 + 分布式锁兜底”

2.1 实现流程

  1. 查询 Redis:优先查询 Redis 缓存。
  2. 分布式锁保护:若 Redis 无数据(缓存未命中),为了防止海量请求瞬间击穿数据库,使用 @ServiceLockserviceLockTool.getLock 获取分布式锁。
  3. 查询数据库:成功获取锁的线程查询数据库,并将数据回写至 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 个请求

  1. 第一重检查(锁外):1000 个请求查询 Redis 均为空,全部尝试获取分布式锁。
  2. 竞争锁线程 A 抢到锁,其余 999 个请求(线程 B、C...)被阻塞排队。
  3. 线程 A 执行:查库 -> 写 Redis -> 释放锁。
  4. 线程 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 实现流程

  1. 查询本地缓存:优先查询当前应用进程的内存(LocalCache)。如果命中,不经过网络,直接返回。
  2. 查询 Redis:本地缓存未命中,再查 Redis。
  3. 查询数据库: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.selectProgramShowTimeByProgramIdMultipleCache
  • programService.getProgramGroupMultipleCache
  • ticketCategoryService.selectTicketCategoryListByProgramIdMultipleCache

3.3 优势

通过在 JVM 内部缓存数据,实现了毫秒级甚至纳秒级的响应速度,极大降低了 Redis 集群的负载,是解决“热点 Key”问题的终极方案。


4. 详细代码调用差异对比

业务步骤V1 代码调用V2 代码调用差异点分析
演出时间selectProgramShowTimeByProgramId...MultipleCacheV2 优先查本地内存
节目详情getByIdgetByIdMultipleCacheV2 优先查本地内存
节目分组getProgramGroupgetProgramGroupMultipleCacheV2 优先查本地内存
分类信息getProgramCategory...MultipleCacheV2 优先查本地内存
票档列表selectTicketCategoryListByProgramId...MultipleCacheV2 优先查本地内存

5. 总结建议

  • V1 (Standard Cache):标准的高并发解决方案。
    • 适用性:适合大多数常规业务场景。
    • 特点:利用 Redis 抗压,利用分布式锁保护 DB。开发复杂度适中。
  • V2 (Extreme Performance):针对极端高并发场景(如大麦网抢票、秒杀)的优化。
    • 适用性:极热点数据(Hot Key)。
    • 特点:利用本地内存抗压,牺牲了一定的数据一致性(本地缓存更新有延迟),但换取了极致的吞吐量和 Redis 的安全性。

大麦项目-节目详情压测结果