当前位置: 首页 > news >正文

【黑马点评|2 Redis缓存 面试题】

正文内容给出的还是关键代码逻辑,结尾附录给了完整可执行的代码 细节教学推荐看:这篇

前言

核心目标🎯:

  1. 基础缓存接入

  2. 缓存更新策略落地

  3. 双写一致(先库后删)

  4. 缓存穿透(缓存空对象 + 布隆过滤)

  5. 缓存雪崩(TTL 随机值)

  6. 缓存击穿(互斥锁 + 逻辑过期)

  7. 通用 Redis 缓存工具类封装


核心技术栈

Spring Boot、Redis、StringRedisTemplate、Hutool(JSON 序列化)、Redisson(布隆过滤)、线程池(缓存重建)


一、核心基础:Redis 缓存工具类封装

工具类作用

统一封装缓存序列化、反序列化、TTL 存储、逻辑过期存储、缓存查询等操作,简化业务层代码,标准化缓存处理逻辑。

核心代码与说明

java

@Component
public class RedisCacheUtil {@Resourceprivate StringRedisTemplate stringRedisTemplate;private static final String CACHE_NULL_VALUE = ""; // 空值统一标识
​// 1. TTL过期存储(适用于普通缓存、空值缓存)public void setWithTTL(String key, Object value, long ttl, TimeUnit timeUnit) {String jsonStr = serialize(value); // 统一序列化stringRedisTemplate.opsForValue().set(key, jsonStr, ttl, timeUnit);}
​// 2. 逻辑过期存储(适用于热点Key,解决缓存击穿)public void setWithLogicalExpire(String key, Object value, long expireSeconds) {RedisData redisData = new RedisData(); // 封装数据+过期时间redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}
​// 3. 穿透防护查询(缓存空对象)public <T> T getWithPassThrough(String key, Class<T> type) {String jsonStr = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(jsonStr)) return null; // 未缓存if (CACHE_NULL_VALUE.equals(jsonStr)) return null; // 缓存空值return deserialize(jsonStr, type); // 正常数据反序列化}
​// 4. 逻辑过期查询(解决缓存击穿)public <T> T getWithLogicalExpire(String key, Class<T> type) {String jsonStr = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(jsonStr)) return null; // 未缓存// 反序列化逻辑过期数据RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);T data = deserialize(JSONUtil.toJsonStr(redisData.getData()), type);// 未过期返回数据,已过期返回null(触发缓存重建)return redisData.getExpireTime().isAfter(LocalDateTime.now()) ? data : null;}
​// 通用序列化(null→空字符串)private String serialize(Object obj) {return obj == null ? CACHE_NULL_VALUE : JSONUtil.toJsonStr(obj);}
​// 通用反序列化private <T> T deserialize(String jsonStr, Class<T> type) {return JSONUtil.toBean(jsonStr, type);}
​// 逻辑过期数据封装类public static class RedisData {private Object data;private LocalDateTime expireTime;// getter+setter}
}

二、基础缓存接入(工具类调用示例)

核心逻辑

先查缓存→缓存命中直接返回→未命中查库→库中存在则写入缓存→库中不存在则缓存空值。

业务层实现

java

@Override
public Result queryById1(Long id) {String key = CACHE_SHOP_KEY + id;// 1. 工具类查询缓存(自动处理空值穿透)Shop shop = redisCacheUtil.getWithPassThrough(key, Shop.class);if (shop != null) return Result.ok(shop);
​// 2. 缓存未命中,查数据库shop = getById(id);if (shop == null) {// 3. 库中不存在,缓存空值(2分钟过期)redisCacheUtil.setWithTTL(key, null, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("店铺不存在");}
​// 4. 库中存在,写入缓存(30分钟基础TTL)redisCacheUtil.setWithTTL(key, shop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);
}

三、缓存更新与双写一致

核心策略:先库后删

  • 逻辑:修改数据库数据后,删除对应缓存(而非更新缓存),下次查询自动从库加载最新数据重建缓存。

  • 优势:避免并发场景下,多个线程同时更新缓存导致的脏数据问题。

业务层实现(店铺更新示例)

java

@Override
public Result update(Shop shop) {if (shop.getId() == null) return Result.fail("店铺ID不能为空");// 1. 先更新数据库updateById(shop);// 2. 后删除缓存(触发后续重建)String key = CACHE_SHOP_KEY + shop.getId();stringRedisTemplate.delete(key);return Result.ok();
}

四、缓存穿透防护(双重方案)

问题定义

恶意请求不存在的 Key(如店铺 ID=-1),缓存未命中则持续冲击数据库。

方案 1:缓存空对象(已通过工具类实现)

  • 逻辑:库中查询为 null 时,缓存空字符串(""),过期时间较短(如 2 分钟)。

  • 关键:工具类getWithPassThrough自动识别空值标识,直接返回 null,阻断后续查库。

方案 2:布隆过滤(终极防护)

  • 逻辑:项目启动时加载所有有效店铺 ID 到布隆过滤器,查询前先判断 ID 是否 “可能存在”,不存在直接返回。

  • 优势:比缓存空对象更高效,从源头拦截无效请求。

实现步骤
  1. 引入 Redisson 依赖

xml

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.17.6</version>
</dependency>
  1. 布隆过滤器初始化(项目启动时)

java

@Component
public class ShopBloomFilterInit implements CommandLineRunner {@Resourceprivate RedissonClient redissonClient;@Resourceprivate ShopMapper shopMapper;private static final String BLOOM_KEY = "bloom:filter:shop:id";
​@Overridepublic void run(String... args) {// 初始化:预计10万数据,误判率0.01RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(BLOOM_KEY);bloomFilter.tryInit(100000, 0.01);// 加载所有有效店铺IDList<Long> ids = shopMapper.selectList(null).stream().map(Shop::getId).collect(Collectors.toList());ids.forEach(bloomFilter::add);}
}
  1. 业务层集成过滤

java

public Result queryWithBloomFilter(Long id) {// 1. 布隆过滤器拦截无效IDRBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("bloom:filter:shop:id");if (!bloomFilter.contains(id)) {return Result.fail("店铺不存在");}// 2. 后续流程:工具类查询缓存→查库(同queryById1)return queryById1(id);
}

五、缓存雪崩防护

问题定义

大量缓存 Key 同时过期,导致请求集中冲击数据库。

解决方案:TTL 随机值偏移

在基础过期时间上添加 ±5 分钟随机值,分散缓存过期时间,避免集中失效。

实现代码(工具类调用改造)

java

// 写入缓存时添加随机TTL
long baseTTL = RedisConstants.CACHE_SHOP_TTL; // 30分钟基础值
long random = new Random().nextInt(10) - 5; // ±5分钟随机偏移
redisCacheUtil.setWithTTL(key, shop, baseTTL + random, TimeUnit.MINUTES);

六、缓存击穿防护(两种方案)

问题定义

热点 Key(如高频访问的店铺)过期时,大量并发请求直击数据库。

方案 1:互斥锁(强一致性)

核心逻辑

热点 Key 过期后,仅允许一个线程查库重建缓存,其他线程获取锁失败后休眠重试,确保并发安全。

业务层实现

java

运行

@Override
public Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;String lockKey = LOCK_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);
​// 缓存命中(正常/空值)直接返回if (StrUtil.isNotBlank(shopJson)) return JSONUtil.toBean(shopJson, Shop.class);if ("".equals(shopJson)) return null;
​Shop shop = null;int retryCount = 3; // 限制重试次数try {// 循环获取锁while (retryCount-- > 0) {if (tryLock(lockKey)) {// 二次查缓存(避免重试期间已重建)shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) return JSONUtil.toBean(shopJson, Shop.class);// 查库重建缓存shop = getById(id);if (shop == null) {redisCacheUtil.setWithTTL(key, null, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}redisCacheUtil.setWithTTL(key, shop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return shop;}Thread.sleep(50); // 未获锁,休眠重试}return null;} catch (InterruptedException e) {throw new RuntimeException(e);} finally {unlock(lockKey); // 必须释放锁}
}
​
// 互斥锁实现(带唯一标识防误删)
private boolean tryLock(String key) {String uuid = UUID.randomUUID().toString();Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {String uuid = stringRedisTemplate.opsForValue().get(key);if (uuid != null && uuid.equals(stringRedisTemplate.opsForValue().get(key))) {stringRedisTemplate.delete(key);}
}

方案 2:逻辑过期(高可用性,允许短期脏数据)

核心逻辑

热点 Key 物理永不过期,缓存中存储 “数据 + 逻辑过期时间”。过期后启异步线程重建缓存,当前线程直接返回旧数据,无锁等待。

业务层实现

java

运行

// 线程池(专门用于缓存重建)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​
// 1. 预热热点缓存(初始化/更新时调用)
public void saveShop2Redis(Long id, Long expiredSeconds) {Shop shop = getById(id);RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expiredSeconds));stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
​
// 2. 逻辑过期查询
public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(json)) return null; // 非热点Key,直接返回
​// 反序列化逻辑过期数据RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();
​if (expireTime.isAfter(LocalDateTime.now())) {return shop; // 未过期,返回旧数据}
​// 已过期,异步重建缓存if (tryLock(LOCK_SHOP_KEY + id)) {CACHE_REBUILD_EXECUTOR.submit(() -> {try {saveShop2Redis(id, 20L); // 重建缓存,20秒逻辑过期} finally {unlock(LOCK_SHOP_KEY + id);}});}return shop; // 返回旧数据,不阻塞
}

七、关键注意事项

  1. 空值标识统一为"",避免与"null"字符串混淆,确保工具类判断一致。

  2. 互斥锁必须添加唯一标识(如 UUID),释放前校验归属权,防止误删其他线程的锁。

  3. TTL 随机值范围需合理(如 ±5 分钟),避免过期时间过短导致缓存频繁重建。

  4. 逻辑过期的线程池核心线程数需根据业务调整(如 10 个),避免线程耗尽。

  5. 布隆过滤器需在店铺 ID 新增 / 删除时同步更新,否则会出现误判。

  6. 缓存删除操作需在数据库事务提交后执行,避免事务回滚导致缓存脏数据。

八、方案对比总结

缓存问题解决方案核心思路适用场景
穿透布隆过滤 + 缓存空对象源头拦截 + 兜底防护所有查询场景
雪崩TTL 随机值分散过期时间,避免集中失效普通缓存场景
击穿互斥锁单线程查库重建,强一致性不允许脏数据(如订单查询)
击穿逻辑过期异步重建,返回旧数据,高可用允许短期脏数据(如商品详情)
双写一致先库后删保证数据库正确性,触发缓存重建读多写少场景

Redis 缓存 10 道难度递增面试题(含场景 + 答案)

1. 基础题:Redis 缓存的核心作用是什么?请结合电商商品详情页场景说明为什么需要用缓存。

场景

某电商平台商品详情页日均访问量 100 万次,直接查询 MySQL 数据库时,单库 QPS 峰值超 5 万,导致数据库响应缓慢,页面加载延迟 3 秒以上。

问题

Redis 缓存的核心作用的是什么?该场景下用缓存能解决什么问题?

答案

  • 核心作用:加速查询、减轻数据库压力、提升系统吞吐量(本质是 “空间换时间”,将热点数据存入内存)。

  • 场景解决:

    1. 商品详情属于热点数据(同一商品被大量用户访问),存入 Redis 后,查询响应时间从 3 秒降至 10ms 内;

    2. 90% 以上的查询请求直接命中 Redis,MySQL QPS 降至 5000 以下,避免数据库过载宕机;

    3. 支持高并发访问,应对促销活动时的流量峰值。

2. 基础进阶:缓存更新的三种核心策略(Cache-Aside、Read-Through、Write-Through)分别是什么?电商场景中商品库存更新,优先选哪种策略?

场景

电商平台商品库存实时变动(用户下单减库存、退货加库存),需保证缓存与数据库库存数据一致,同时兼顾查询性能。

问题

三种缓存更新策略的核心逻辑是什么?该场景下优先选择哪种?为什么?

答案

  • 三种策略核心逻辑:

    1. Cache-Aside(旁路缓存):查询先查缓存,未命中查数据库;更新先更数据库,再删缓存(不直接更缓存);

    2. Read-Through(读透):缓存代理数据库查询,未命中时自动查库并写入缓存,业务层无需关注 DB;

    3. Write-Through(写透):更新时先写缓存,缓存再同步写数据库,业务层无需关注 DB。

  • 优先选择:Cache-Aside 策略

  • 原因:

    1. 电商库存更新频繁,Write-Through 会导致缓存与 DB 双写,性能开销大;

    2. Read-Through 依赖缓存中间件支持,灵活性低;

    3. Cache-Aside 实现简单,先库后删能保证数据一致性,删除缓存而非更新可避免并发冲突,适配库存高频变动场景。

3. 中级:什么是缓存穿透?请结合 “恶意查询不存在的商品 ID” 场景,说明 “缓存空对象” 方案的实现逻辑、优点和缺点。

场景

某黑产通过脚本批量查询不存在的商品 ID(如 ID=-1、999999),导致所有请求穿透缓存直击 MySQL,数据库连接池耗尽。

问题

  1. 缓存穿透的定义是什么?

  2. “缓存空对象” 方案如何解决该问题?

  3. 该方案的优缺点是什么?

答案

  • 缓存穿透定义:请求查询的 Key 在缓存和数据库中均不存在,导致所有请求直接穿透到数据库,引发数据库压力过大

  • 方案实现逻辑:

    1. 业务层查询商品 ID 时,若 DB 返回 null(商品不存在),则向 Redis 缓存一个空值标识(如 ""或"null");

    2. 后续相同 ID 查询时,缓存直接返回空值,阻断对 DB 的请求。

  • 优点:实现简单,无需额外中间件,能快速解决大部分穿透场景;

  • 缺点:

    1. 浪费 Redis 内存(存储大量无效空值);

    2. 若后续该商品 ID 被创建(如商家新增商品 999999),会出现缓存空值与 DB 实际数据不一致(需设置空值过期时间,如 2 分钟)。

4. 中级:缓存击穿和缓存穿透的核心区别是什么?请结合 “秒杀商品热点 Key 过期” 场景,说明互斥锁方案解决缓存击穿的实现步骤和关键注意事项。

场景

电商秒杀活动中,某爆款商品的缓存 Key(如 "cache:shop:1001")过期,瞬间有 10000 + 并发请求查询该商品,直接冲击 MySQL。

问题

  1. 缓存击穿和缓存穿透的核心区别是什么?

  2. 互斥锁方案如何解决该场景的缓存击穿?

  3. 实现时需注意哪些问题?

答案

  • 核心区别:

    1. 缓存穿透:Key 在缓存和 DB 中均不存在;

    2. 缓存击穿:Key 在 DB 中存在,但缓存 Key 过期 / 失效,导致大量并发请求直击 DB。

  • 互斥锁方案实现步骤:

    1. 查询缓存,若未命中,尝试通过 Redis 的setIfAbsent方法获取互斥锁(如 "lock:shop:1001");

    2. 若获取锁成功,查询 DB 并将数据写入缓存,然后释放锁;

    3. 若获取锁失败,休眠 50ms 后重试查询缓存,直到锁释放或重试次数耗尽。

  • 关键注意事项:

    1. 锁需设置过期时间(如 10 秒),避免线程异常导致死锁;

    2. 释放锁前需校验锁的归属权(如用 UUID 作为锁值),防止误删其他线程的锁;

    3. 限制重试次数(如 3 次),避免无限阻塞消耗资源。

5. 中级偏上:什么是缓存雪崩?请结合 “电商大促零点大量商品缓存同时过期” 场景,说明至少 3 种解决方案,并分析各自的适用场景。

场景

电商双 11 零点,平台上 10 万 + 商品的缓存 Key 同时过期,导致海量请求集中穿透到 MySQL,数据库瞬间宕机。

问题

  1. 缓存雪崩的定义是什么?

  2. 该场景下有哪些解决方案?各自适用什么情况?

答案

  • 缓存雪崩定义:大量缓存 Key 在同一时间过期,或 Redis 集群宕机,导致请求集中冲击数据库,引发系统级故障

  • 解决方案及适用场景:

    1. TTL 随机值:给每个缓存 Key 的过期时间添加随机偏移量(如基础 30 分钟 ±5 分钟),分散过期时间。适用场景:普通商品缓存,无特殊一致性要求;

    2. 热点 Key 永不过期:对秒杀、爆款等热点商品,不设置物理过期时间,通过业务逻辑定期更新缓存。适用场景:热点数据,允许短期脏数据;

    3. Redis 集群部署:采用主从 + 哨兵或 Redis Cluster 架构,避免单节点宕机导致整个缓存失效。适用场景:高可用要求高的核心业务;

    4. 限流降级:在网关层对请求限流,缓存失效时触发降级策略(如返回默认页面),保护数据库。适用场景:流量峰值不可控的大促场景。

6. 高级:双写一致中 “先更新数据库,后删除缓存” 是主流方案,但存在 “缓存删除失败” 的潜在问题。请结合 “商品信息更新” 场景,说明该问题的具体表现、原因及解决方案。

场景

商家更新商品价格(DB 中价格从 199 元改为 299 元),执行 “更新 DB→删除缓存” 流程时,缓存删除操作失败(如 Redis 网络波动),导致后续查询仍返回旧缓存(199 元)。

问题

  1. 该场景下 “缓存删除失败” 会导致什么问题?

  2. 失败的常见原因有哪些?

  3. 如何解决该问题?

答案

  • 问题表现:数据库数据已更新,但缓存未删除,导致后续查询返回旧数据,出现缓存与 DB 不一致(脏数据)。

  • 失败原因:Redis 网络波动、Redis 节点宕机、业务线程异常中断。

  • 解决方案:

    1. 重试机制:缓存删除失败时,通过本地重试(如 3 次)确保删除成功;

    2. 消息队列补偿:将缓存删除操作写入消息队列(如 RabbitMQ),若删除失败,消费者重试删除,直到成功;

    3. 定时任务校验:后台启动定时任务,对比缓存与 DB 数据的一致性,发现不一致则同步更新缓存。

7. 高级:布隆过滤器为什么能解决缓存穿透?请结合 “海量用户 ID 查询用户信息” 场景,说明其原理、实现步骤及优缺点。

场景

社交 APP 有 1 亿注册用户,大量非注册用户 ID(如随机生成的 ID)被用于查询,导致缓存穿透。需用布隆过滤器优化。

问题

  1. 布隆过滤器的核心原理是什么?

  2. 如何结合 Redis 实现布隆过滤器解决该场景的穿透问题?

  3. 优缺点是什么?

答案

  • 核心原理:基于多个哈希函数和位数组,快速判断一个元素是否 “可能存在” 于集合中(不存在则 100% 准确,存在则有极小误判率)。

  • Redis 实现步骤:

    1. 项目启动时,将所有注册用户 ID 加载到 Redis 布隆过滤器(如 Redisson 的 RBloomFilter);

    2. 配置布隆过滤器参数:预计存储 1 亿个 ID,误判率 0.01(需提前计算位数组大小和哈希函数个数);

    3. 用户查询时,先通过布隆过滤器判断 ID 是否存在,不存在则直接返回 “用户不存在”,存在则继续查缓存→DB。

  • 优点:

    1. 内存效率极高(1 亿个 ID,误判率 0.01,仅需约 12MB 内存);

    2. 查询速度快(O (k),k 为哈希函数个数);

  • 缺点:

    1. 存在误判率(无法完全避免穿透,需配合缓存空对象兜底);

    2. 不支持删除操作(用户注销后,无法从布隆过滤器中移除 ID)。

8. 高级:基于 Redis 实现分布式锁时,如何避免 “死锁”“误删他人锁”“锁过期释放” 三大问题?请结合 “秒杀下单库存扣减” 场景,写出完整的实现代码思路。

场景

秒杀活动中,多个服务实例同时扣减同一商品库存,需通过分布式锁保证库存不超卖。

问题

  1. 如何解决分布式锁的三大问题?

  2. 完整的实现代码思路是什么?

答案

  • 三大问题解决方案:

    1. 死锁:给锁设置过期时间(如 10 秒),即使线程异常,锁也会自动释放;

    2. 误删他人锁:给锁值设置唯一标识(如 UUID),释放锁前校验标识是否匹配;

    3. 锁过期释放:若业务执行时间超过锁过期时间,通过 “看门狗机制”(后台线程定时续期锁的过期时间)。

  • 实现代码思路:

java

运行

// 1. 获取锁(带唯一标识+过期时间)
public boolean tryLock(String lockKey, String uuid, long expireTime, TimeUnit unit) {return BooleanUtil.isTrue(stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuid, expireTime, unit));
}
​
// 2. 看门狗机制(续期锁)
public void startWatchDog(String lockKey, String uuid, long expireTime) {ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();executor.scheduleAtFixedRate(() -> {// 每3秒续期一次,续期为原过期时间的1/3if (uuid.equals(stringRedisTemplate.opsForValue().get(lockKey))) {stringRedisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);} else {executor.shutdown(); // 锁已释放,停止续期}}, 0, expireTime / 3, TimeUnit.SECONDS);
}
​
// 3. 释放锁(校验唯一标识)
public void unlock(String lockKey, String uuid) {String value = stringRedisTemplate.opsForValue().get(lockKey);if (uuid.equals(value)) {stringRedisTemplate.delete(lockKey);}
}
​
// 4. 秒杀库存扣减场景使用
public boolean seckillStock(Long productId) {String lockKey = "lock:stock:" + productId;String uuid = UUID.randomUUID().toString();try {// 获取锁boolean isLock = tryLock(lockKey, uuid, 10, TimeUnit.SECONDS);if (!isLock) return false;// 启动看门狗续期startWatchDog(lockKey, uuid, 10);// 扣减库存(查DB→减库存→更DB)Product product = productMapper.selectById(productId);if (product.getStock() <= 0) return false;product.setStock(product.getStock() - 1);productMapper.updateById(product);return true;} finally {// 释放锁unlock(lockKey, uuid);}
}

9. 高级偏架构:逻辑过期方案和互斥锁方案解决缓存击穿的核心差异是什么?请结合 “商品详情页” 和 “订单查询” 两个场景,说明如何选择方案,并分析原因。

场景

  1. 电商商品详情页:日均访问量 10 万 +,允许 5 分钟内的价格、库存等数据脏读;

  2. 订单查询页:用户查询自己的订单状态,不允许任何脏读(订单状态必须实时准确)。

问题

  1. 逻辑过期和互斥锁方案的核心差异是什么?

  2. 两个场景分别选择哪种方案?为什么?

答案

  • 核心差异对比:

    | 维度 | 互斥锁方案 | 逻辑过期方案 |

    |--------------|---------------------------|-----------------------------|

    | 一致性 | 强一致性(单线程查库重建) | 最终一致性(允许短期脏数据) |

    | 可用性 | 低(锁竞争导致请求阻塞) | 高(无锁,返回旧数据) |

    | 性能 | 中等(阻塞重试消耗资源) | 高(无阻塞,直接返回) |

    | 实现复杂度 | 中等(锁管理 + 重试) | 较高(异步重建 + 逻辑过期封装)|

  • 场景选择:

    1. 商品详情页:选择逻辑过期方案。原因:允许短期脏数据,追求高并发、低延迟,逻辑过期方案无锁阻塞,能支撑 10 万 + 日均访问量;

    2. 订单查询页:选择互斥锁方案。原因:订单状态不允许脏读,互斥锁能保证单线程查库重建缓存,确保数据实时一致。

10. 架构级:某高并发电商平台(日均访问 1 亿次,热点商品占比 20%),请设计一套 Redis 缓存架构,解决缓存穿透、击穿、雪崩、双写一致问题,并说明核心组件和优化策略。

场景

平台包含商品、订单、用户三大核心模块,商品模块热点集中(20% 商品占 80% 访问量),订单模块要求强一致性,用户模块需避免穿透。

问题

  1. 画出核心架构图(文字描述);

  2. 说明各组件的作用;

  3. 如何解决四大缓存问题?

答案

  • 核心架构图(文字描述):

    客户端 → 网关(限流降级) → 应用层(本地缓存 Caffeine) → Redis Cluster(主从 + 哨兵) → 布隆过滤器 → MySQL

  • 各组件作用:

    1. 网关:对恶意请求限流,缓存失效时触发降级(返回默认页面);

    2. 本地缓存 Caffeine:缓存 Top20% 热点商品,减少 Redis 访问压力(本地缓存响应时间 < 1ms);

    3. Redis Cluster:3 主 3 从架构,分片存储缓存数据,保证高可用;

    4. 布隆过滤器:Redis 集群级布隆过滤器,拦截无效 Key(如不存在的商品 ID);

    5. MySQL:存储核心业务数据,保证数据持久化。

  • 四大缓存问题解决方案:

    1. 缓存穿透:网关拦截 + 布隆过滤器 + 缓存空对象(三级防护);

    2. 缓存击穿:本地缓存 Caffeine 缓存热点商品(物理永不过期)+ Redis 逻辑过期(异步重建);

    3. 缓存雪崩:Redis Cluster 高可用(避免集群宕机)+ TTL 随机值(分散过期时间);

    4. 双写一致:

      • 商品模块(读多写少):先库后删 + 消息队列补偿;

      • 订单模块(强一致性):先删缓存 + 分布式锁 + 数据库事务(确保更新原子性)。

  • 额外优化策略:

    1. 热点数据隔离:将 Top20% 热点商品缓存到独立的 Redis 分片,避免热点冲击整个集群;

    2. 缓存预热:大促前通过定时任务将热点商品数据加载到本地缓存和 Redis;

    3. 监控告警:通过 Prometheus 监控 Redis 命中率、过期 Key 数量,异常时及时告警。


源码

package com.hmdp.service.impl;
​
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisCacheUtil;
import com.hmdp.utils.RedisConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
​
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
​
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
​
/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
​@Resourceprivate StringRedisTemplate stringRedisTemplate;
​@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {
​Shop shop = JSONUtil.toBean(shopJson, Shop.class);log.debug("缓存中查找成功🏅🏅");return Result.ok(shop);}//解决缓存穿透问题//todo:使用布隆过滤
​if (shopJson == null) {return Result.fail("店铺不存在");}Shop shop = getById(id);if (shop == null) {//stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr("null"), 30L, TimeUnit.MINUTES);
​//不存在,返回错误信息return Result.fail("店铺不存在");}stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
​
​return Result.ok(shop);}//解决缓存击穿问题
​/*** 尝试获取分布式锁(基于Redis实现)* 核心:利用Redis的setIfAbsent原子操作,确保并发场景下只有一个线程能获取锁** @param key 锁的唯一标识(如"lock:shop:1",通常包含业务ID)* @return true-获取锁成功;false-获取锁失败*/private boolean tryLock(String key) {// setIfAbsent:原子操作,等价于Redis的SET key value NX PX 10000// 作用:当key不存在时,设置key=1,同时指定10秒过期时间;若key已存在,不做操作// 过期时间目的:防止线程获取锁后因异常崩溃,导致锁永久无法释放(避免死锁)Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
​// 处理Redis返回的Boolean可能为null的情况(如Redis连接异常),确保返回boolean类型且避免空指针return BooleanUtil.isTrue(flag);}
​/*** 释放分布式锁* 注意:此实现为基础版本,存在潜在风险(如可能误删其他线程持有的锁)** @param key 锁的唯一标识(需与获取锁时的key一致)*/private void unlock(String key) {// 直接删除Redis中的锁标识,释放锁资源供其他线程竞争stringRedisTemplate.delete(key);}
​@Overridepublic Shop queryWithPassThrough(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);
​// 1. 缓存命中非空值:直接返回if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}
​// 2. 缓存命中空值(已存入""):直接返回null(避免查库)if ("".equals(shopJson)) {return null;}
​// 3. 缓存未命中:查数据库Shop shop = getById(id);if (shop == null) {// 缓存空值为""(而非"null"),明确区分空值stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}
​// 4. 数据库存在:写入缓存(使用正常数据过期时间)stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, // 修复:使用正常数据的过期时间TimeUnit.MINUTES);
​return shop;}
​@Overridepublic Shop queryWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);
​// 1. 缓存命中非空值:直接返回if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}
​// 2. 缓存命中空值(统一用""标识):直接返回nullif ("".equals(shopJson)) {return null;}
​String lockKey = LOCK_SHOP_KEY + id;Shop shop = null;int retryCount = 3; // 限制最大重试次数(避免无限递归)
​try {// 3. 循环重试获取锁(替代递归,减少栈溢出风险)while (retryCount-- > 0) {boolean isLock = tryLock(lockKey);if (isLock) {// 3.1 二次检查缓存(避免重复查库)shopJson = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}if ("".equals(shopJson)) {return null;}
​// 3.2 查数据库shop = getById(id);// 模拟业务耗时(方便测试锁机制)
//                    Thread.sleep(100);
​// 3.3 处理数据库结果if (shop == null) {// 统一存入""作为空值标识stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 写入正常缓存stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);return shop;}
​// 3.4 锁获取失败:休眠后重试Thread.sleep(50);}
​// 重试次数耗尽仍未获取锁:返回空(或降级处理)return null;
​} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 释放锁(假设tryLock已使用唯一标识,unlock时校验)unlock(lockKey);}}
​public void saveShop2Redis(Long id, Long expiredSeconds) {//1.查询店铺数据Shop shop = getById(id);
​//2.封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expiredSeconds));
​//3.写入redisstringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}
​private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return shop;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock) {CACHE_REBUILD_EXECUTOR.submit(() -> {
​try {//重建缓存this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;}
​@Resourceprivate RedisCacheUtil redisCacheUtil;/*** 基于缓存工具类的查询方法(解决缓存穿透)* 使用RedisCacheUtil封装的缓存穿透解决方案,简化代码逻辑*/@Overridepublic Result queryById1(Long id) {// 1. 定义缓存keyString key = CACHE_SHOP_KEY + id;
​// 2. 调用工具类查询缓存(自动处理空值和反序列化)Shop shop = redisCacheUtil.getWithPassThrough(key, Shop.class);
​// 3. 缓存命中:直接返回结果if (shop != null) {log.debug("缓存中查找成功🏅🏅");return Result.ok(shop);}
​// 4. 缓存未命中:查询数据库shop = getById(id);
​// 5. 数据库不存在该店铺:缓存空值(解决缓存穿透)if (shop == null) {// 调用工具类存储空值,设置空值过期时间(30分钟)redisCacheUtil.setWithTTL(key, null, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("店铺不存在");}
​// 6. 数据库存在该店铺:写入缓存(设置正常过期时间)redisCacheUtil.setWithTTL(key, shop, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
​// 7. 返回数据库结果return Result.ok(shop);}
}
​

package com.hmdp.utils;
​
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
​
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
​
/*** 通用Redis缓存工具类(基于StringRedisTemplate)* 支持TTL过期、逻辑过期、缓存穿透、缓存击穿解决方案*/
@Component
public class RedisCacheUtil {
​@Resourceprivate StringRedisTemplate stringRedisTemplate;
​// 缓存空值标识(统一用空字符串,避免JSON解析问题)private static final String CACHE_NULL_VALUE = "";
​/*** 方法1:存储缓存(TTL过期时间)* @param key 缓存key* @param value 任意Java对象(需支持JSON序列化)* @param ttl 过期时间* @param timeUnit 时间单位*/public void setWithTTL(String key, Object value, long ttl, TimeUnit timeUnit) {String jsonStr = serialize(value);stringRedisTemplate.opsForValue().set(key, jsonStr, ttl, timeUnit);}
​/*** 方法2:存储缓存(逻辑过期时间,用于解决缓存击穿)* @param key 缓存key* @param value 任意Java对象(需支持JSON序列化)* @param expireSeconds 逻辑过期秒数*/public void setWithLogicalExpire(String key, Object value, long expireSeconds) {// 封装逻辑过期数据RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 序列化后存入Redis(无物理过期时间)String jsonStr = JSONUtil.toJsonStr(redisData);stringRedisTemplate.opsForValue().set(key, jsonStr);}
​/*** 方法3:查询缓存(解决缓存穿透)* @param key 缓存key* @param type 目标对象类型* @return 目标对象(不存在返回null)*/public <T> T getWithPassThrough(String key, Class<T> type) {// 1. 从Redis查询缓存String jsonStr = stringRedisTemplate.opsForValue().get(key);// 2. 缓存未命中:返回null(让调用方查库)if (StrUtil.isBlank(jsonStr)) {return null;}// 3. 缓存命中空值:返回null(解决缓存穿透)if (CACHE_NULL_VALUE.equals(jsonStr)) {return null;}// 4. 缓存命中非空值:反序列化返回return deserialize(jsonStr, type);}
​/*** 方法4:查询缓存(逻辑过期解决缓存击穿)* @param key 缓存key* @param type 目标对象类型* @return 目标对象(未命中/已过期返回null,未过期返回数据)*/public <T> T getWithLogicalExpire(String key, Class<T> type) {// 1. 从Redis查询缓存String jsonStr = stringRedisTemplate.opsForValue().get(key);// 2. 缓存未命中:返回nullif (StrUtil.isBlank(jsonStr)) {return null;}// 3. 反序列化为RedisData(包含数据和逻辑过期时间)RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);T data = deserialize(JSONUtil.toJsonStr(redisData.getData()), type);LocalDateTime expireTime = redisData.getExpireTime();// 4. 判断是否过期:未过期返回数据,已过期返回null(让调用方重建缓存)if (expireTime.isAfter(LocalDateTime.now())) {return data;}return null;}
​/*** 通用序列化:Java对象 → JSON字符串*/private String serialize(Object obj) {if (obj == null) {return CACHE_NULL_VALUE;}return JSONUtil.toJsonStr(obj);}
​/*** 通用反序列化:JSON字符串 → 目标Java类型*/private <T> T deserialize(String jsonStr, Class<T> type) {if (StrUtil.isBlank(jsonStr) || CACHE_NULL_VALUE.equals(jsonStr)) {return null;}return JSONUtil.toBean(jsonStr, type);}
​/*** 逻辑过期缓存数据封装类* 需与工具类配合使用,存储数据本身和过期时间*/public static class RedisData {private Object data;private LocalDateTime expireTime;
​// getter + setterpublic Object getData() {return data;}
​public void setData(Object data) {this.data = data;}
​public LocalDateTime getExpireTime() {return expireTime;}
​public void setExpireTime(LocalDateTime expireTime) {this.expireTime = expireTime;}}
}
http://www.dtcms.com/a/541001.html

相关文章:

  • 大学学院教授委员会制度研究(二)理论基础与分析框架-杨立恒毕业论文
  • Nginx基础入门篇-基础配置
  • 雅可比SVD算法:高精度矩阵分解的经典方法
  • 在 Python 中测试中assert断言和 if分支的区别
  • 【题解】洛谷 P1169 [ZJOI2007] 棋盘制作 [思维 + dp]
  • 音频限幅器D2761使用手册
  • 网站金融模版wordpress轮播代码
  • 【工具推荐】电脑手机多端互通协作实用
  • 一般网站的跳出率dede做双语网站
  • 自己制作的网站如何发布建筑设计公司经营范围有哪些
  • 51c大模型~合集39
  • 操作【GM3568JHF】FPGA+ARM异构开发板 使用指南:串口
  • 【牛客CM11】链表分割
  • .NET 对象转Json的方式
  • 广西住建局官方网站大数据营销的应用领域
  • Linux ioctl 深度剖析:从原理到实践
  • 网站备案流程解答做最漂亮的网站
  • LED驱动电路(三)
  • Keil工程编译垃圾清理
  • 同城跑腿APP源码开发技术全景:即时订单、骑手定位与路线优化算法
  • 【数据工程】15. Stream Query Processing
  • 鄂州网站设计效果wordpress comment_form_after
  • 爱网站关键词查询工具潍坊营销网站
  • java程序生成pdf或wod乱码
  • 做网站和游戏是如何赚钱crm系统开发
  • 网页pdf下载攻略--以混元上传的pdf为例
  • AI在处理扫描版PDF时准确率低,如何提升?
  • 网站做成软件免费wordpress 首页制作
  • 所有网站打不开深圳做app网站的公司名称
  • centos 7 redhat7 升级内核 升级内核到5.4版本 202510试过可以成功