黑马点评02:商户查询缓存
商户查询缓存流程

缓存更新策略
先更新数据库,再删除缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired private StringRedisTemplate redisTemplate; // 查询单个店铺(带缓存) @Override public Result queryById(Long id) { String key = CACHE_SHOP_KEY + id; // 1. 从缓存查询 String shopJson = redisTemplate.opsForValue().get(key); // 2. 缓存命中 if (StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } // 3. 缓存未命中,查数据库 Shop shop = getById(id); if (shop == null) { return Result.fail("店铺不存在"); } // 4. 写入缓存(设置过期时间) redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); } // 更新店铺(先更新数据库,再删除缓存) @Override @Transactional public Result update(Shop shop) { Long id = shop.getId(); if (id == null) { return Result.fail("店铺ID不能为空"); } // 1. 更新数据库 updateById(shop); // 2. 删除缓存 String key = CACHE_SHOP_KEY + id; redisTemplate.delete(key); return Result.ok(); }
}
|
先更新数据库再删除缓存,即使有短暂不一致,也不会产生脏数据;而先删缓存再更新数据库,在并发下会产生难以消除的脏数据。
缓存穿透
核心思路:查询不到数据时,缓存一个 null 或特殊标记,设置较短的过期时间。

redis:"" (空字符串),则结束;null (不存在),则查数据库
缓存雪崩
设置随机过期时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Sevice public class ShopService {
private static final long BASE_TTL = 3600; // 基础1小时 private static final Random RANDOM = new Random(); public void saveShopToCache(Shop shop) { String key = "shop:" + shop.getId(); // 1小时 + 随机0~300秒,避免同时过期 long randomOffset = RANDOM.nextInt(300); // 0-300秒 long ttl = BASE_TTL + randomOffset; stringRedisTemplate.opsForValue().set( key, JSONUtil.toJsonStr(shop), ttl, TimeUnit.SECONDS ); }
}
|
缓存击穿
逻辑过期(永不过期 + 异步更新)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
|
@Data public class RedisData<T> { private T data; private LocalDateTime expireTime; // 逻辑过期时间 }
@Service @Slf4j public class ShopService {
@Autowired private StringRedisTemplate redisTemplate; @Autowired private ExecutorService executorService; // 线程池 public Shop getShopById(Long id) { String key = "shop:" + id; // 1. 查缓存 String json = redisTemplate.opsForValue().get(key); if (StrUtil.isBlank(json)) { // 缓存不存在(首次加载),查数据库 return loadShopFromDB(id); } // 2. 反序列化 RedisData<Shop> redisData = JSONUtil.toBean(json, RedisData.class); Shop shop = redisData.getData(); LocalDateTime expireTime = redisData.getExpireTime(); // 3. 判断是否逻辑过期 if (expireTime.isAfter(LocalDateTime.now())) { // 未过期,直接返回 return shop; } // 4. 已过期,尝试获取锁去异步更新 String lockKey = "lock:shop:" + id; RLock lock = redissonClient.getLock(lockKey); // 尝试获取锁(非阻塞) boolean isLock = lock.tryLock(); if (isLock) { try { // 异步更新缓存(不阻塞当前请求) executorService.submit(() -> { try { Shop newShop = shopMapper.selectById(id); RedisData<Shop> newRedisData = new RedisData<>(); newRedisData.setData(newShop); newRedisData.setExpireTime(LocalDateTime.now().plusMinutes(30)); redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newRedisData)); } finally { lock.unlock(); } }); } catch (Exception e) { log.error("异步更新失败", e); } } // 5. 返回旧数据(即使过期了) return shop; } private Shop loadShopFromDB(Long id) { Shop shop = shopMapper.selectById(id); if (shop != null) { // 写入缓存,逻辑过期时间 30 分钟 RedisData<Shop> redisData = new RedisData<>(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusMinutes(30)); redisTemplate.opsForValue().set("shop:" + id, JSONUtil.toJsonStr(redisData)); } return shop; }
}
|