本文的主题是商铺缓存。主要包括:添加商铺缓存到 redis,实现缓存和数据库的一致,
redis 缓存面临的三个问题的解决:缓存穿透,缓存雪崩,缓存击穿
实现效果:
1. 添加商户缓存
需求分析:根据 id 查询商铺,若 redis 中有商铺缓存,直接返回。否则查询数据库并将商铺信息缓存到 redis 中
代码实现:
① Controller 层,获取url中的请求参数 “id”
// ShopController.java
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
② redis 查询缓存,若缓存未命中,查询数据库,并保存到 redis 中
\\ ShopServiceImpl.java
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从 redis 查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if(shop == null){
return Result.fail("店铺不存在!");
}
// 6.存在,写入 redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
// 7.返回
return Result.ok(shop);
}
}
问题记录:前端商铺显示 NaN
① return Result.ok() ,里面没有返回 shop
② redis 中有缓存记录,但是给店铺添加了过期时间,然后缓存没有清理掉,所以过期不显示。删除缓存刷新即可
2. 缓存与数据库双写一致
缓存更新策略:
① 内存淘汰:一致性差,利用 redis 的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存
② 超时剔除:给缓存数据添加 TTL 时间,到期后自动删除缓存,下次查询时更新缓存
③ 主动更新:编写业务逻辑,在修改数据库的同时,更新缓存
采用主动更新方式:修改数据库并更新缓存
问题:先修改数据库再更新缓存,还是先更新缓存再修改数据库?
根据上面添加商铺缓存的逻辑,若线程查询redis 缓存未命中,即执行 查询数据库并写入 redis 缓存的逻辑
方案一:线程1删除缓存之后,导致缓存失效. 由于操作数据库是涉及IO,不如redis直接操作内存,因此大概率情况下线程1在更新数据库期间会有线程查询缓存. 可以看到,线程2在线程1更新数据库的时候,查询缓存并拿到了数据库中的快照版本,并将过期的值写入缓存。最后的结果是 redis 中 a = 10,而 mysql 中 a = 20,缓存和数据库不一致
方案二:线程1 先更新数据库再删除缓存,之后线程2由于缓存未命中,查询到数据库中更新的值并写入缓存. 结果上,数据库和缓存数据库一致
问题:更新数据库和删除缓存之间有线程请求业务呢?
线程取到过期缓存:在线程 1 更新数据库还未来得及删除缓存前,线程2查询缓存,由于缓存还未删除,线程2拿到旧数据。但之后的线程由于缓存未命中,会查询数据库并更新缓存
多线程操作数据库:这种情况也会发生缓存和数据库不一致。接着左侧线程3缓存未命中去查询数据库的情况,这时有线程更新数据库,则会出现对缓存更新丢失的情况.即线程1 写入缓存的操作覆盖了线程2 写入缓存的操作。这种情况发生的前提是:更新数据库和删除缓存之间有线程查询缓存导致缓存被删除【即左图】,其次是 查询数据库和写入缓存之前有线程查询数据库并写入了缓存。这种情况会发生但是因为有条件限制,所以概率较低
综合来看,方案二虽然也会发生缓存过期以及缓存与数据库不一致的情况,但是出错概率要小一点
因此选择:先更新数据库,再删除缓存
更新方式:利用 postman 修改店铺信息并发送给服务器
代码实现:
// ShopController.java
/**
* 更新商铺信息
* @param shop 商铺数据
* @return 无
*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
// 写入数据库
return shopService.update(shop);
}