首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【黑马点评日记】封装Redis缓存工具类:三大方案全解析

【黑马点评日记】封装Redis缓存工具类:三大方案全解析

作者头像
北极的代码
发布2026-04-22 13:51:05
发布2026-04-22 13:51:05
1150
举报

摘要:整体来说就是把我们之前对一个业务的操作抽象出一个类,然后从特殊性到普遍性。

本文介绍了Redis缓存工具类的设计与实现,重点解决缓存穿透、击穿和雪崩三大问题。 通过泛型化和函数式接口实现通用性,提供三种核心方案:空值缓存应对穿透、互斥锁防止击穿、逻辑过期优化性能。 工具类采用包装类存储逻辑过期时间支持异步更新和双检机制,并详细对比了各方案的适用场景。 文章还包含线程池配置、死锁预防等注意事项,以及形象化的快递柜比喻帮助理解。该工具类可灵活应用于店铺查询等场景,实现高性能缓存管理。

Redis 缓存工具类全解析

一、整体架构概览 这个工具类解决三个核心问题:

  • 缓存穿透:查询不存在的数据,绕过缓存直接打数据库
  • 缓存击穿:热点数据过期,大量请求同时打到数据库
  • 缓存雪崩:大量缓存同时过期(工具类未直接处理,通过设置随机过期时间解决)

二、核心数据结构

RedisData 包装类
代码语言:javascript
复制
java

public class RedisData {
    private LocalDateTime expireTime;  // 逻辑过期时间
    private Object data;               // 实际数据
}

为什么需要这个包装类?

传统 Redis 过期是物理删除(时间到了自动删),逻辑过期是自己维护过期标志:

代码语言:javascript
复制
json

// 存入Redis的实际格式
{
    "expireTime": "2024-12-31T23:59:59",
    "data": {"id": 1, "name": "海底捞"}
}

好处

  • 过期不删除数据,还能继续用旧数据
  • 可以异步刷新,不影响用户请求

三、泛型详解

泛型方法签名
代码语言:javascript
复制
java

public <R, ID> R queryWithMutex(
    String keyPrefix,
    ID id,                    // 泛型ID:可以是Long、String、Integer
    Class<R> type,            // 泛型R:返回类型
    Function<ID, R> dbFallback,  // 函数式接口
    Long timeout,
    TimeUnit unit
)
泛型的作用

泛型

含义

实际使用示例

<R, ID>

声明两个泛型类型

Shop, Long

R

返回的数据类型

Shop, User, Product

ID

查询ID的类型

Long, String, Integer

为什么要用泛型?
代码语言:javascript
复制
java

// 没有泛型:每个类型都要写一个方法
public Shop queryShopById(Long id) { ... }
public User queryUserById(Long id) { ... }
public Product queryProductById(String id) { ... }

// 有泛型:一个方法通吃
public <R, ID> R queryById(ID id, Class<R> type) { ... }

// 调用示例
Shop shop = queryById(1L, Shop.class);
User user = queryById(100L, User.class);
Product product = queryById("P001", Product.class);
Function 函数式接口
代码语言:javascript
复制
java

Function<ID, R> dbFallback
// 参数类型 ID → 返回值类型 R

// 使用时传入方法引用
this::getById        // getById(Long id) 返回 Shop
this::getUserById    // getUserById(Long id) 返回 User

三种解决方案详解

方案一:缓存穿透解决(queryWithPassThrough)

问题场景

代码语言:javascript
复制
text

攻击者请求 id = -1 的数据
→ 缓存没有
→ 数据库也没有
→ 每次都查数据库
→ 数据库压力剧增

解决原理:空值缓存

代码语言:javascript
复制
java

// 流程图
请求 id = -1
    ↓
查缓存 → 没有
    ↓
查数据库 → 返回 null
    ↓
缓存空值:set key "" 过期2分钟
    ↓
下次请求 → 查到空值 → 直接返回 null

代码关键点

代码语言:javascript
复制
java

// 判断空值缓存
if (json != null) {
    return null;  // 说明缓存的是空字符串
}

// 缓存空值
if (result == null) {
    stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
    return null;
}

注意:空字符串和 null 的区别

  • null:缓存不存在
  • "":缓存存在,但值是空的(表示数据库没有这条数据)
方案二:互斥锁方案(queryWithMutex)

问题场景

text

代码语言:javascript
复制
热点数据过期瞬间
1000个请求同时发现缓存失效
全部去查数据库
数据库压力爆炸

解决原理:只让一个线程去查数据库,其他线程等待

代码语言:javascript
复制
java

// 流程图
请求1 ──→ 发现缓存失效 ──→ 获取锁成功 ──→ 查DB ──→ 写缓存 ──→ 释放锁 ──→ 返回
请求2 ──→ 发现缓存失效 ──→ 获取锁失败 ──→ 休眠50ms ──→ 递归重试
请求3 ──→ 发现缓存失效 ──→ 获取锁失败 ──→ 休眠50ms ──→ 递归重试

DoubleCheck 的必要性

代码语言:javascript
复制
java

// 场景:请求1重建缓存期间,请求2在等待
// 请求1释放锁后,请求2拿到锁
// 如果没有DoubleCheck,请求2会再次查数据库

// 有DoubleCheck:
String newJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(newJson)) {
    return JSONUtil.toBean(newJson, type);  // 直接返回,不再查DB
}

锁的细节

代码语言:javascript
复制
java

private boolean tryLock(String key) {
    // setIfAbsent = SETNX(不存在才设置)
    // 过期时间10秒:防止死锁(如果线程挂了,锁自动释放)
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}
方案三:逻辑过期方案(queryWithLogicalExpire)

问题场景: 互斥锁方案中,等待的线程会被阻塞,影响性能

解决原理:永远不阻塞,直接返回旧数据,后台异步更新

代码语言:javascript
复制
java

// 流程图
请求 ──→ 查缓存
    ↓
未过期 ──→ 返回最新数据
    ↓
已过期 ──→ 尝试获取锁
    ├── 获取锁成功 ──→ 提交异步任务 ──→ 立即返回旧数据
    └── 获取锁失败 ──→ 立即返回旧数据

关键代码

代码语言:javascript
复制
java

// 异步重建(不等待)
CACHE_REBUILD_EXECUTOR.submit(() -> {
    try {
        R newData = dbFallback.apply(id);
        setWithLogicalExpire(key, newData, expireSeconds);
    } finally {
        unlock(lockKey);  // 必须在finally释放
    }
});

// 立即返回旧数据(不等待重建完成)
return data;

五、三种方案对比

维度

穿透方案

互斥锁方案

逻辑过期方案

核心思想

空值缓存

只让一个线程查DB

异步刷新,先返回旧数据

响应时间

可能等待(最坏100ms)

极快(无等待)

数据一致性

强一致

强一致

最终一致

数据库压力

极小

极小

适用场景

恶意请求

一致性要求高

高并发、容忍短暂不一致

缺点

浪费一点内存

等待线程会阻塞

可能返回旧数据

六、使用示例

1. 在 Service 中注入工具类
代码语言:javascript
复制
java

@Service
public class ShopServiceImpl implements IShopService {
    
    @Autowired
    private RedisCacheClient redisCacheClient;
    
    @Autowired
    private ShopMapper shopMapper;
    
    private static final String CACHE_SHOP_KEY = "cache:shop:";
}
2. 查询店铺(互斥锁方案)
代码语言:javascript
复制
java

public Shop queryShopById(Long id) {
    return redisCacheClient.queryWithMutex(
        CACHE_SHOP_KEY,           // key前缀
        id,                       // 查询id
        Shop.class,               // 返回类型
        this::getById,            // 数据库查询方法
        30L,                      // 过期时间
        TimeUnit.MINUTES          // 时间单位
    );
}

// 数据库查询方法
private Shop getById(Long id) {
    return shopMapper.selectById(id);
}
3. 查询店铺(逻辑过期方案)
代码语言:javascript
复制
java

public Shop queryShopWithLogicalExpire(Long id) {
    // 注意:逻辑过期需要提前预热缓存
    return redisCacheClient.queryWithLogicalExpire(
        CACHE_SHOP_KEY,
        id,
        Shop.class,
        this::getById,
        30L  // 逻辑过期时间(秒)
    );
}

// 缓存预热
@PostConstruct
public void initCache() {
    List<Shop> shops = shopMapper.selectList(null);
    for (Shop shop : shops) {
        redisCacheClient.setWithLogicalExpire(
            CACHE_SHOP_KEY + shop.getId(), 
            shop, 
            30L
        );
    }
}

七、重要注意事项

1. 线程池配置
代码语言:javascript
复制
java

// 当前是固定大小线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

// 生产环境建议使用自定义线程池
@Bean
public ThreadPoolTaskExecutor cacheRebuildExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("cache-rebuild-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
}
2. 死锁问题
代码语言:javascript
复制
java

// 锁必须在finally中释放
try {
    // 业务逻辑
} finally {
    unlock(lockKey);  // 确保释放
}
3. 递归调用风险
代码语言:javascript
复制
java

// 互斥锁方案中的递归调用
if (!isLock) {
    Thread.sleep(50);
    return queryWithMutex(...);  // 递归
}

风险:可能无限递归 解决:设置重试次数限制

代码语言:javascript
复制
java

private static final int MAX_RETRY = 10;
int retryCount = 0;

if (!isLock && retryCount++ < MAX_RETRY) {
    Thread.sleep(50);
    return queryWithMutex(...);
}
4. 空值缓存时间
代码语言:javascript
复制
java

// 空值缓存不能太长,避免真正数据写入后还返回空
stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
5. 逻辑过期缓存预热

逻辑过期方案要求缓存必须提前存在,否则会返回 null:

代码语言:javascript
复制
java

// 系统启动时预热
@PostConstruct
public void warmUpCache() {
    // 加载热点数据到缓存
    List<Long> hotShopIds = getHotShops();
    for (Long id : hotShopIds) {
        Shop shop = shopMapper.selectById(id);
        redisCacheClient.setWithLogicalExpire(key, shop, 30L);
    }
}

八、总结

你要解决什么问题

用哪个方法

有人用不存在的ID恶意请求

queryWithPassThrough

热点数据过期,要求数据绝对一致

queryWithMutex

超高并发,允许短暂不一致

queryWithLogicalExpire

核心记忆点

  1. 泛型让工具类通用
  2. DoubleCheck避免重复查询
  3. 逻辑过期的核心是"先返回旧数据,后台慢慢更新"
  4. 锁必须在finally释放,否则会死锁

Redis 缓存工具类 - 形象化解释

一、把 Redis 想象成“快递柜”

想象一下,你开了一家店,店里有个快递柜(Redis),里面放着顾客常买的东西(缓存数据)。


二、三个问题的形象比喻

问题1:缓存穿透 → “捣乱的小孩”

场景:有个捣乱的小孩,不停地按快递柜上不存在的柜子号码(比如第100号,但只有1-50号)。

  • 每次按一下 → 你要去仓库(数据库)找一遍
  • 找100次 → 仓库管理员累死了

解决方案(空值缓存)

text

代码语言:javascript
复制
在那些不存在的柜子上贴张纸条:“这个柜子不存在,别找了!”
下次小孩再来按 → 看到纸条 → 直接走人,不麻烦仓库管理员
问题2:缓存击穿 → “抢购明星产品”

场景:店里最火的商品(比如最新款iPhone)的快递柜刚好到期打开,外面1000个顾客同时伸手去拿。

  • 第一个顾客:柜子是空的!→ 跑去仓库拿
  • 后面999个:柜子也是空的!→ 全部跑去仓库
  • 仓库被挤爆了 💥

解决方案1(互斥锁)

代码语言:javascript
复制
text

给柜子装一把锁 🔒
- 第一个顾客:拿到锁 → 去仓库取货 → 放进柜子 → 开锁
- 其他顾客:看到锁着 → 在门口排队等
- 等第一个放好货 → 大家直接从柜子拿

像排队买票,慢但不会乱

解决方案2(逻辑过期)

代码语言:javascript
复制
text

柜子上贴个标签:“商品已过期,但还能用,新货正在补”
- 第一个顾客:发现过期 → 通知店员去仓库补货 → 自己先拿旧货走
- 后面999个:发现过期 → 看到已经有店员去了 → 也直接拿旧货走
- 店员后台慢慢补货,不影响顾客

像外卖APP显示“30分钟送达”,先让你下单,后台慢慢做

问题3:缓存雪崩 → “大批商品同时过期”

场景:快递柜里100个商品设置了同一个过期时间,到点了全部一起打开。

  • 100个柜子同时空 → 100×N个顾客同时冲进仓库
  • 仓库直接宕机

解决方案

代码语言:javascript
复制
text

设置过期时间时加点随机数:
- 商品A:30分钟 + 随机1-5分钟
- 商品B:30分钟 + 随机3-7分钟
→ 不会同时过期

三、三种方案的形象对比

场景:网红奶茶店开业,招牌奶茶非常火

方案

比喻

流程

优缺点

缓存穿透

有人点不存在的“星空奶茶”

第一次:查菜单没有 → 贴告示“没有这款” 以后:看到告示直接说没有

防止捣乱

互斥锁

招牌奶茶卖完了,店员去仓库补货

第1个顾客:拿到“补货牌” → 等5分钟拿到奶茶 后面顾客:看到有人拿着牌 → 排队等

强一致,但要等

逻辑过期

奶茶显示“已售罄,但可预订”

所有顾客:直接预订(立即拿到预订券) 后台:慢慢做奶茶

快,但拿到的不是真正的奶茶


四、泛型的形象解释

泛型 = “万能容器”

没有泛型:你要准备3个不同的盒子

代码语言:javascript
复制
text

BoxForApple 盒子(只能装苹果)
BoxForBanana 盒子(只能装香蕉)
BoxForOrange 盒子(只能装橘子)

有泛型:只准备1个万能盒子

代码语言:javascript
复制
text

Box<T> 万能盒子
- Box<Apple> → 装苹果
- Box<Banana> → 装香蕉
- Box<Orange> → 装橘子

代码对应

代码语言:javascript
复制
java

// 没有泛型:每种类型写一个方法
Shop getShop(Long id) { ... }
User getUser(Long id) { ... }
Product getProduct(String id) { ... }

// 有泛型:一个方法搞定
<R, ID> R getData(ID id, Class<R> type) { ... }

// 使用
Shop shop = getData(1L, Shop.class);     // 装的是Shop
User user = getData(100L, User.class);   // 装的是User

五、DoubleCheck 的形象解释

场景:两个人同时发现柜子空了
代码语言:javascript
复制
text

时间线:
T1: 小明和小红同时看到柜子空
T2: 小明拿到锁,去仓库取货
T3: 小红没拿到锁,在门口等
T4: 小明取完货,放好,开锁
T5: 小红拿到锁

【如果没有DoubleCheck】
T6: 小红:柜子空吗?不知道 → 再去仓库取一次(浪费时间)

【如果有DoubleCheck】
T6: 小红:先看看柜子 → 发现有货了 → 直接拿,不去仓库 ✅

DoubleCheck = 拿到锁后,再看一眼柜子有没有货


六、逻辑过期的核心思想

比喻:牛奶的“最佳饮用期”

普通过期(物理过期):

代码语言:javascript
复制
text

牛奶到保质期 → 直接扔掉 → 下次买要等

逻辑过期:

代码语言:javascript
复制
text

牛奶过最佳饮用期 → 贴标签“已过期,但还能喝”
- 顾客:看到标签 → 直接拿走喝(不等新牛奶)
- 店员:后台慢慢补新牛奶

核心过期不等同于不可用,旧数据还能用,新数据慢慢补


七、完整流程图解

互斥锁方案(等一等)
代码语言:javascript
复制
text

顾客 → 看柜子
    ├─ 有货 → 拿走 ✅
    └─ 没货 → 看谁拿着锁
            ├─ 没人拿 → 拿锁 → 去仓库取货 → 放柜子 → 开锁 → 拿走
            └─ 有人拿 → 排队等待 → 等锁释放 → 从柜子拿
逻辑过期方案(不等待)
代码语言:javascript
复制
text

顾客 → 看柜子
    ├─ 没过期 → 拿走 ✅
    └─ 已过期 → 看谁拿着锁
            ├─ 没人拿 → 拿锁 → 喊店员去补货(不等待)→ 拿旧货走
            └─ 有人拿 → 直接拿旧货走

需要普遍性处理的9大方面

1. ID 类型的泛型化

问题:ID 不一定是 Long 类型

代码语言:javascript
复制
java

// ❌ 只支持Long
public Shop query(Long id) { ... }

// ✅ 支持任意类型
public <R, ID> R query(ID id, Class<R> type) { ... }

// 使用示例
query(1L, Shop.class);           // Long类型
query("P001", Product.class);    // String类型
query(1001, User.class);         // Integer类型
2. 返回类型的泛型化

问题:不能只为 Shop 服务

代码语言:javascript
复制
java

// ❌ 只能返回Shop
public Shop queryShop(Long id) { ... }

// ✅ 返回任意类型
public <R, ID> R query(ID id, Class<R> type) { ... }

// 使用示例
Shop shop = query(1L, Shop.class);
User user = query(100L, User.class);
Product product = query("P001", Product.class);
3. 数据库查询方法的泛型化

问题:每个表的查询方法不同

代码语言:javascript
复制
java

// ❌ 写死查询方法
Shop shop = shopMapper.selectById(id);

// ✅ 通过函数式接口传入
Function<ID, R> dbFallback  // 调用方自己决定怎么查

// 使用示例
this::getById          // Shop表
this::getUserById      // User表  
this::getProductById   // Product表
4. 缓存 Key 的前缀处理

问题:不同业务用不同前缀

代码语言:javascript
复制
java

// ❌ 写死前缀
String key = "cache:shop:" + id;

// ✅ 作为参数传入
String keyPrefix  // 调用方自己定义

// 使用示例
queryWithMutex("cache:shop:", id, ...);     // 店铺缓存
queryWithMutex("cache:user:", id, ...);     // 用户缓存
queryWithMutex("cache:product:", id, ...);  // 商品缓存
5. 过期时间的灵活配置

问题:不同数据过期时间不同

代码语言:javascript
复制
java

// ❌ 写死30分钟
Long timeout = 30L;

// ✅ 作为参数传入
Long timeout, TimeUnit unit

// 使用示例
queryWithMutex(..., 30L, TimeUnit.MINUTES);   // 30分钟
queryWithMutex(..., 10L, TimeUnit.SECONDS);   // 10秒
queryWithMutex(..., 2L, TimeUnit.HOURS);      // 2小时
6. 空值缓存的普遍性 ⚠️ 需要增强

问题:空值缓存时间写死了2分钟

代码语言:javascript
复制
java

// 当前代码(写死)
stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);

// 改进:作为参数
public <R, ID> R queryWithPassThrough(
    ...,
    Long nullTimeout,      // 空值过期时间
    TimeUnit nullUnit
) {
    if (result == null) {
        stringRedisTemplate.opsForValue().set(key, "", nullTimeout, nullUnit);
        return null;
    }
}
7. 锁的超时时间 ⚠️ 需要增强

问题:锁的过期时间写死了10秒

代码语言:javascript
复制
java

// 当前代码(写死)
private boolean tryLock(String key) {
    setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
}

// 改进:可配置
private boolean tryLock(String key, Long timeout, TimeUnit unit) {
    setIfAbsent(key, "1", timeout, unit);
}

// 或者使用常量
private static final long LOCK_TIMEOUT = 10;
private static final TimeUnit LOCK_UNIT = TimeUnit.SECONDS;
8. 重试机制 ⚠️ 需要增强

问题:互斥锁方案中无限递归,可能栈溢出

代码语言:javascript
复制
java

// 当前代码(危险)
if (!isLock) {
    Thread.sleep(50);
    return queryWithMutex(...);  // 无限递归
}

// 改进:限制重试次数
public <R, ID> R queryWithMutex(
    ...,
    int maxRetry      // 最大重试次数
) {
    return queryWithMutex(..., maxRetry, 0);  // 重试计数器
}

private <R, ID> R queryWithMutex(
    ...,
    int maxRetry,
    int retryCount
) {
    if (!isLock) {
        if (retryCount >= maxRetry) {
            // 超过重试次数,直接查数据库(降级)
            return dbFallback.apply(id);
        }
        Thread.sleep(50);
        return queryWithMutex(..., maxRetry, retryCount + 1);
    }
}
9. 线程池的通用配置 ⚠️ 需要增强

问题:写死的固定大小线程池

代码语言:javascript
复制
java

// 当前代码
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

// 改进:可配置 + 使用Spring线程池
@Component
public class RedisCacheClient {
    
    @Autowired
    private ThreadPoolTaskExecutor cacheRebuildExecutor;  // 从配置文件读取
    
    // 或者使用配置文件
    @Value("${redis.cache.thread-pool-size:10}")
    private int threadPoolSize;
    
    @PostConstruct
    public void init() {
        CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(threadPoolSize);
    }
}

application.yml

代码语言:javascript
复制
yaml

redis:
  cache:
    thread-pool-size: 20      # 线程池大小
    lock-timeout: 10          # 锁超时时间(秒)
    null-cache-timeout: 2     # 空值缓存时间(分钟)
    max-retry: 3              # 最大重试次数

核心要点总结(面试/复习用)

问题

原因

解决方案

黑马代码体现

缓存穿透

查询不存在的数据

1. 缓存空对象 2. 布隆过滤器

ShopServiceImpl.queryById 缓存 null 对象

缓存雪崩

大量key同时失效

1. TTL随机 2. 集群 3. 多级缓存

商铺缓存设置不同TTL

缓存击穿

热点key失效

1. 互斥锁 2. 逻辑过期

RedisData 对象 + logicalExpire 字段

缓存更新一致性

数据变更后缓存脏读

更新DB后删除缓存

updateShop 方法中先更新DB,再 delete 缓存

工具封装

重复造轮子

泛型 + 函数式接口

CacheClient 类(setWithLogicalExpire、queryWithMutex 等)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis 缓存工具类全解析
    • 二、核心数据结构
      • RedisData 包装类
    • 三、泛型详解
      • 泛型方法签名
      • 泛型的作用
      • 为什么要用泛型?
      • Function 函数式接口
    • 三种解决方案详解
      • 方案一:缓存穿透解决(queryWithPassThrough)
      • 方案二:互斥锁方案(queryWithMutex)
      • 方案三:逻辑过期方案(queryWithLogicalExpire)
    • 五、三种方案对比
    • 六、使用示例
      • 1. 在 Service 中注入工具类
      • 2. 查询店铺(互斥锁方案)
      • 3. 查询店铺(逻辑过期方案)
    • 七、重要注意事项
      • 1. 线程池配置
      • 2. 死锁问题
      • 3. 递归调用风险
      • 4. 空值缓存时间
      • 5. 逻辑过期缓存预热
    • 八、总结
  • Redis 缓存工具类 - 形象化解释
    • 一、把 Redis 想象成“快递柜”
    • 二、三个问题的形象比喻
      • 问题1:缓存穿透 → “捣乱的小孩”
      • 问题2:缓存击穿 → “抢购明星产品”
      • 问题3:缓存雪崩 → “大批商品同时过期”
    • 三、三种方案的形象对比
      • 场景:网红奶茶店开业,招牌奶茶非常火
    • 四、泛型的形象解释
      • 泛型 = “万能容器”
    • 五、DoubleCheck 的形象解释
      • 场景:两个人同时发现柜子空了
    • 六、逻辑过期的核心思想
      • 比喻:牛奶的“最佳饮用期”
    • 七、完整流程图解
      • 互斥锁方案(等一等)
      • 逻辑过期方案(不等待)
    • 需要普遍性处理的9大方面
      • 1. ID 类型的泛型化
      • 2. 返回类型的泛型化
      • 3. 数据库查询方法的泛型化
      • 4. 缓存 Key 的前缀处理
      • 5. 过期时间的灵活配置
      • 6. 空值缓存的普遍性 ⚠️ 需要增强
      • 7. 锁的超时时间 ⚠️ 需要增强
      • 8. 重试机制 ⚠️ 需要增强
      • 9. 线程池的通用配置 ⚠️ 需要增强
    • 核心要点总结(面试/复习用)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档