商户查询缓存
约 6446 字大约 21 分钟
2025-06-27
2.1 什么是缓存?
缓存(Cache) 是一种数据交换的缓冲区,用于临时存储数据,目的是提高数据访问速度。缓存通常存储从数据库中获取的数据,并将其保存在更快的存储介质中,例如内存。
就像自行车或越野车的避震器一样,缓存可以防止系统因高并发的数据访问而崩溃。如果没有缓存作为“避震器”,系统可能无法及时处理大量请求,导致瘫痪。
2.1.1 为什么要使用缓存?
使用缓存的主要原因是其速度快、好用。由于缓存数据存储在内存中,内存的读写速度远高于磁盘,因此可以大大降低用户访问并发量带来的服务器读写压力。
在实际开发中,企业的数据量可能非常大,如果没有缓存作为“避震器”,系统几乎无法承受。因此,企业会大量运用缓存技术。
然而,缓存也会增加代码的复杂度和运营成本。
2.1.2 如何使用缓存?
在实际开发中,通常会构建多级缓存来进一步提升系统运行速度,例如同时使用本地缓存和 Redis 中的缓存。
- 浏览器缓存: 主要存在于浏览器端。
- 应用层缓存: 可以是 Tomcat 本地缓存(如上述
map
示例),也可以使用 Redis 作为缓存。 - 数据库缓存: 数据库中有一块 buffer pool 区域,用于缓存经常访问的数据。
- CPU 缓存: 由于 CPU 性能提升快于内存读写速度,因此增加了 L1、L2、L3 级缓存。
从 CPU 缓存到数据库缓存,每一层缓存都旨在减少数据访问延迟。
2.2 添加商户缓存
在查询商户信息时,直接操作数据库效率较低。因此,需要增加缓存来提高查询速度。
标准操作方式是:先查询缓存,如果缓存数据存在,则直接从缓存中返回;如果缓存数据不存在,则查询数据库,然后将数据存入 Redis。

@Override
public Result queryById(Long id) {
// 1. 从 Redis 中查询缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
// 2. 若存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 3. 若不存在,根据 id 查询数据库
Shop shop = getById(id);
// 4. 若数据库也不存在,返回错误
if (shop == null) {
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
}
// 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
// 6. 返回
return Result.ok(shop);
}
2.3 缓存更新策略
缓存更新是为了节约内存。当 Redis 内存达到上限时,会自动触发淘汰机制,淘汰掉一些不重要的数据。
- 内存淘汰: Redis 自动进行,当 Redis 内存达到设定的
max-memory
时,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)。 - 超时剔除: 当给 Redis 设置了过期时间 TTL (Time To Live) 之后,Redis 会将超时的数据进行删除,方便继续使用缓存。
- 主动更新: 可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题。
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
2.3.1 数据库缓存不一致解决方案
由于缓存的数据源来自数据库,而数据库的数据会发生变化,因此可能出现缓存和数据库不一致的情况。为了解决这个问题,有以下几种方案:
- Cache Aside Pattern (人工编码方式): 缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。
- Read/Write Through Pattern: 由系统本身完成,数据库与缓存的问题交由系统本身去处理。
- Write Behind Caching Pattern: 调用者只操作缓存,其他线程去异步处理数据库,实现最终一致。
2.3.2 数据库和缓存不一致采用什么方案
综合考虑,通常选择 Cache Aside Pattern。
在操作缓存和数据库时,有三个问题需要考虑:
删除缓存还是更新缓存
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多。
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存。
如何保证缓存与数据库的操作同时成功或失败
- 单体系统:将缓存与数据库操作放在一个事务中。
- 分布式系统:利用 TCC 等分布式事务方案。
先操作缓存还是先操作数据库
- 先删除缓存,再操作数据库。
- 先操作数据库,再删除缓存。
前两个问题可以很快得出答案。对于第三个问题,考虑并发场景:假设线程 1 先来,删除了缓存。此时,线程 2 发起并发请求,由于缓存已被删除,线程 2 会从数据库读取数据并写入缓存。随后,线程 1 完成数据库更新操作。这时,缓存中的数据是旧的,与数据库中的数据不一致。

因此,如果在并发访问时,先删除缓存可能导致旧数据被写入缓存。
2.3.3 实现商铺缓存与数据库双写一致
核心思路:
- 根据 ID 查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间。
- 根据 ID 修改店铺时,先修改数据库,再删除缓存。
public Result queryById(Long id) {
// 1. 从 Redis 中查询缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
// 2. 若存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
} // 3. 若不存在,根据 id 查询数据库
Shop shop = getById(id);
// 4. 若数据库也不存在,返回错误
if (shop == null) {
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
} // 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 6. 返回
return Result.ok(shop);
}
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail(MessageConstants.SHOP_ID_EMPTY);
} // 1. 先更新数据库
updateById(shop);
// 2. 删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
2.4 缓存穿透
2.4.1 缓存穿透问题及解决思路
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在的情况。这会导致所有请求直接访问数据库,使得缓存失效。
常见的解决方案有两种:
- 缓存空对象:即使数据在数据库中不存在,也将空值存入 Redis,避免请求穿透缓存。
- 优点:实现简单,维护方便。
- 缺点:额外的内存消耗,可能造成短期的不一致。
- 布隆过滤:通过一个庞大的二进制数组,使用哈希思想来判断要查询的数据是否存在。如果布隆过滤器判断存在,则放行请求访问 Redis;如果判断不存在,则直接返回。
- 优点:内存占用较少,没有多余 key。
- 缺点:实现复杂,存在误判可能。
2.4.2 解决商品查询的缓存穿透问题
在原有的逻辑中,如果商品信息在 MySQL 中不存在,通常会直接返回 404 状态码,这会直接导致缓存穿透。
针对缓存穿透问题,考虑采用缓存空对象的策略来解决。 核心思路如下:
- 查询流程: 当收到商品查询请求时,首先在 Redis 缓存中查找对应的数据。
- 缓存未命中: 如果缓存未命中,则继续查询 MySQL 数据库。
- 数据库未找到数据: 如果在 MySQL 数据库中也未找到对应的数据,此时不直接返回 404,而是将一个空对象(例如,value 为
null
)写入 Redis 缓存中。 - 设置过期时间: 建议为空对象设置一个较短的过期时间,避免空对象长期占用缓存空间。
- 后续请求处理: 当后续请求再次查询该商品时,会命中 Redis 缓存,但 value 为
null
。 此时,程序需要识别出该null
值,并将其视为“缓存穿透”的结果,返回相应的提示信息,而不是继续查询数据库。 - 缓存命中: 如果缓存命中,并且 value 不为
null
,则表示缓存中存在有效数据,直接返回该数据。
@Override
public Result queryById(Long id) {
// 1. 从 Redis 中查询缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
// 2. 若存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if (shopJson != null) {
// 若是空值,返回错误
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
}
// 3. 若不存在,根据 id 查询数据库
Shop shop = getById(id);
// 4. 若数据库也不存在,返回错误
if (shop == null) {
// 将空值写入 Redis
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
} // 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 6. 返回
return Result.ok(shop);
}
2.5 缓存雪崩问题及解决思路
缓存雪崩是指在同一时段大量的缓存 Key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:
- 给不同的 Key 的 TTL 添加随机值。
- 利用 Redis 集群提高服务的可用性。
- 给缓存业务添加降级限流策略。
- 给业务添加多级缓存。
2.6 缓存击穿
2.6.1 缓存击穿问题及解决思路
缓存击穿问题也叫热点 Key 问题,指一个被高并发访问且缓存重建业务较复杂的 Key 突然失效,导致大量请求瞬间给数据库带来巨大冲击。
例如:假设线程 1 查询缓存未命中,需要查询数据库并将结果重新加载到缓存。在线程 1 重建缓存期间,线程 2、线程 3、线程 4 等并发访问该 Key,由于缓存中没有数据,它们会同时访问数据库,从而对数据库造成巨大压力。

常见的解决方案有两种:
互斥锁:使用锁的互斥性来保证只有一个线程可以访问数据库并重建缓存,避免大量请求同时冲击数据库。
线程 1 访问缓存未命中,成功获取锁,负责查询数据库并重建缓存。线程 2 随后访问,未能获取锁,进入休眠状态。当线程 1 释放锁后,线程 2 重新获取锁,此时可以从缓存中获取到数据。
逻辑过期:不给 Redis 的 Key 设置过期时间,而是在缓存的 Value 中存储一个逻辑过期时间。当发现缓存数据逻辑过期时,不是立即返回错误,而是允许少量的请求去异步重建缓存。
线程 1 访问缓存,发现数据逻辑过期,尝试获取互斥锁。获取成功后,线程 1 启动一个新线程异步重建缓存,然后立即返回旧的缓存数据。线程 3 随后访问,由于线程 1 持有锁,线程 3 无法获取锁,也直接返回旧的缓存数据。当新线程完成缓存重建后,后续的请求就可以获取到最新的数据。
方案对比:
- 互斥锁
- 优点:
- 没有额外的内存消耗
- 保证一致性
- 实现简单
- 缺点:
- 线程需要等待,性能受影响
- 可能有死锁风险
- 优点:
- 逻辑过期
- 优点:
- 线程无需等待,性能较好
- 缺点:
- 不保证一致性
- 有额外内存消耗
- 实现复杂
- 优点:
2.6.2 利用互斥锁解决缓存击穿问题
从缓存中查询不到数据后,先获取互斥锁,再查询数据库。如果获取锁失败,则休眠重试。获取锁成功后,查询数据库,将数据写入 Redis,释放锁,返回数据。

private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
public Result queryWithPassThrough(Long id) {
// 1. 从 Redis 中查询缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
// 2. 若存在,则直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
} if (shopJson != null) {
// 若是空值,返回错误
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
} // 3. 若不存在,根据 id 查询数据库
Shop shop = getById(id);
// 4. 若数据库也不存在,返回错误
if (shop == null) {
// 将空值写入 Redis
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
} // 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 6. 返回
return Result.ok(shop);
}
public Shop queryWithMutex(Long id) {
// 1. 从 Redis 中查询缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
// 2. 若存在,则直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
if (shopJson != null) {
// 若是空值,返回 null
return null;
}
// 3. 实现缓存重构
// 3.1 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 3.2 判断是否获取成功
if (!isLock) {
// 3.3 失败,则休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 3.4 成功,首先 Double Check: 再次检查 Redis 缓存是否存在
shopJson = stringRedisTemplate.opsForValue().get(key);
// 3.5 判断是否命中
if (StrUtil.isNotBlank(shopJson)) {
// 3.6 命中,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
if (shopJson != null) {
// 若是空值,返回 null return null;
}
// 3.7 缓存不存在,查询数据库
shop = getById(id);
// 模拟重建的延迟
Thread.sleep(200);
// 4. 不存在,则将空值写入 Redis,并返回 null
if (shop == null) {
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 6. 释放互斥锁
unlock(lockKey);
} return shop;
}
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null) {
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
}
return Result.ok(shop);
}
为什么获取锁成功后要进行 Double Check(再次检测 Redis 缓存是否过期)?
这里涉及到并发环境下的一个经典问题:避免多个线程重复重建缓存。 想象以下场景:
- 线程 A 查询 Redis,发现缓存逻辑过期。它尝试获取锁,并成功获取。
- 线程 B 在线程 A 获取锁 之前 也查询了 Redis,同样发现缓存过期。它也尝试获取锁,但因为线程 A 已经持有锁,所以线程 B 会自旋等待。
- 线程 A 获取锁后,立即开始重建缓存。
- 关键点:在线程 A 重建缓存的这段时间内,线程 B 仍然在等待锁。
- 线程 A 完成缓存重建,并将新的缓存数据写入 Redis。线程 A 释放锁。
- 线程 B 终于获取了锁。 如果没有 Double Check,线程 B 会 认为 它需要重建缓存,于是它会再次执行数据库查询,覆盖线程 A 刚刚重建的缓存。这会造成不必要的数据库压力和资源浪费,并且线程 A 的努力就白费了。
Double Check 的作用:
Double Check 确保了当线程 B 最终获得锁时,它首先要验证一下:Redis 缓存是否已经被其他线程(例如线程 A)更新。 如果缓存已经被更新,则线程 B 无需再次重建缓存,可以直接返回 Redis 中的新数据,避免重复劳动。
2.6.3 利用逻辑过期解决缓存击穿问题
当用户查询 Redis 时,首先判断是否命中缓存。如果未命中,则直接返回空数据,不查询数据库。如果命中,则取出 value,判断其中的过期时间是否已满足。如果未过期,则直接返回 Redis 中的数据。如果已过期,则开启独立线程,并在返回之前的数据的同时,利用独立线程去重构数据。数据重构完成后,释放互斥锁。

@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 1. 查询店铺信息
Shop shop = getById(id);
Thread.sleep(200);
// 2. 设置逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3. 写入 Redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLocalExpire(Long id) {
// 1. 从 Redis 中查询店铺缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否命中
if (StrUtil.isBlank(shopJson)) {
// 3. 未命中,直接返回空值
return null;
} // 4. 命中,将 json 反序列化为 RedisData 对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 6. 未过期,直接返回
return shop;
} // 7. 已过期,进行缓存重构
// 7.1 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 7.2 判断是否获取成功
if (isLock) {
// 7.3 成功,首先 Double Check: 再次检查 Redis 缓存是否过期
shopJson = stringRedisTemplate.opsForValue().get(key);
// 7.4 判断是否命中
if (StrUtil.isNotBlank(shopJson)) {
// 7.5 命中,判断是否过期
redisData = JSONUtil.toBean(shopJson, RedisData.class);
expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// 7.6 未过期,直接返回店铺信息
return JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
} // 7.7 过期,开启独立线程,实现缓存重构
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
} }); } } // 8. 返回过期店铺信息
return shop;
}
@Override
public Result queryById(Long id) {
// 逻辑过期解决缓存击穿
Shop shop = queryWithLocalExpire(id);
if (shop == null) {
return Result.fail(MessageConstants.SHOP_NOT_FOUND);
}
return Result.ok(shop);
}
2.7 封装 Redis 工具类
基于 StringRedisTemplate
封装一个缓存工具类,满足下列需求:
- 存储对象并设置 TTL:将 Java 对象序列化为 JSON 字符串,存储到 Redis 的 String 类型 Key 中,并设置过期时间 (TTL)。
- 存储对象并设置逻辑过期:将 Java 对象序列化为 JSON 字符串,存储到 Redis 的 String 类型 Key 中,并设置逻辑过期时间,用于解决缓存击穿问题。
- 缓存穿透解决方案:根据 Key 查询缓存,反序列化为指定类型。利用缓存空值的方式解决缓存穿透问题。
- 缓存击穿解决方案:根据 Key 查询缓存,反序列化为指定类型。并
- 利用互斥锁解决缓存击穿问题。
- 利用逻辑过期解决缓存击穿问题。
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicExpire(String key, Object value, Long time, TimeUnit unit) {
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time, unit);
}
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 从 Redis 中查询缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
// 2. 若存在,则直接返回
return JSONUtil.toBean(json, type);
} if (json != null) {
// 若是空值,返回 null return null;
} // 3. 若不存在,根据 id 查询数据库
R r = dbFallback.apply(id);
// 4. 若数据库也不存在,返回错误
if (r == null) {
// 将空值写入 Redis
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
} // 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
// 6. 返回
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, String lockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 从 Redis 中查询缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
// 2. 若存在,则直接返回
return JSONUtil.toBean(json, type);
} if (json != null) {
// 若是空值,返回 null
return null;
}
// 3. 实现缓存重构
// 3.1 获取互斥锁
String lockKey = lockKeyPrefix + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 3.2 判断是否获取成功
if (!isLock) {
// 3.3 失败,则休眠重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, lockKeyPrefix, id, type, dbFallback, time, unit);
}
// 3.4 成功,首先 Double Check: 再次检查 Redis 缓存是否存在
json = stringRedisTemplate.opsForValue().get(key);
// 3.5 判断是否命中
if (StrUtil.isNotBlank(json)) {
// 3.6 命中,直接返回
return JSONUtil.toBean(json, type);
} if (json != null) {
// 若是空值,返回 null
return null;
} // 3.7 缓存不存在,查询数据库
r = dbFallback.apply(id);
// 4. 不存在,则将空值写入 Redis,并返回 null
if (r == null) {
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 5. 写入 Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 6. 释放互斥锁
unlock(lockKey);
}
return r;
}
public <R, ID> R queryWithLocalExpire(
String keyPrefix, String lockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 从 Redis 中查询店铺缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否命中
if (StrUtil.isBlank(json)) {
// 3. 未命中,直接返回空值
return null;
}
// 4. 命中,将 json 反序列化为 RedisData 对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 6. 未过期,直接返回
return r;
} // 7. 已过期,进行缓存重构
// 7.1 获取互斥锁
String lockKey = lockKeyPrefix + id;
boolean isLock = tryLock(lockKey);
// 7.2 判断是否获取成功
if (isLock) {
// 7.3 成功,首先 Double Check: 再次检查 Redis 缓存是否过期
json = stringRedisTemplate.opsForValue().get(key);
// 7.4 判断是否命中
if (StrUtil.isNotBlank(json)) {
// 7.5 命中,判断是否过期
redisData = JSONUtil.toBean(json, RedisData.class);
expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// 7.6 未过期,直接返回店铺信息
return JSONUtil.toBean((JSONObject) redisData.getData(), type);
}
// 7.7 过期,开启独立线程,实现缓存重构
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 重建缓存
setWithLogicExpire(key, r, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
});
}
}
// 8. 返回过期店铺信息
return r;
}