一文读懂关于Redis的缓存更新策略
业务场景:
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
1、主动更新
- Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务维护一致性,调用者调用该服务,无需关心缓存一致性问题
- Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存的数据持久化到数据库,保证最终一致。
PS:一般使用方案1,由缓存的调用者,在更新数据库的同时更新缓存
2、Cache Aside Pattern
由缓存的调用者,在更新数据库的同时更新缓存
操作缓存和数据库时有三个问题需要考虑:
- 删除缓存还是更新缓存?
- 如何保证缓存与数据库的操作的同时成功或失败?
- 先操作缓存还是先操作数据库?
1、删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多 :x:
如果更新数据库的次数多,而读取的次数较少,则每次更新时,都会增加无效的更新缓存操作
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 :heavy_check_mark:
2、如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
3、先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存
1、先删除缓存,再操作数据库
不存在线程安全问题场景
- 线程1收到请求后,先删除缓存,然后更新数据库值为20
- 线程2收到请求后,先查询缓存,并没有命中,则查询数据库的值为20,再写入缓存
存在线程安全问题场景
- 线程1收到请求后,先删除缓存
- 线程2收到查询请求,查询缓存发现没有命中,则去查询数据库的值为10
- 线程2将数据库的值写入缓存
- 线程1更新数据库的值为20,此时导致数据不一致问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2、先操作数据库,再删除缓存:heavy_check_mark:
不存在线程安全问题场景
- 线程2收到请求后更新数据库值为20
- 线程2更新完成后,删除缓存
- 线程1收到请求后,查询缓存,未命中,则查询数据库
- 线程1查询到数据后,写入缓存
存在线程安全问题场景
- 线程1收到查询请求后,查询缓存,未名中,则去查询数据库值为10
- 线程2收到更新请求,更新数据库值为20
- 线程2更新数据库后,删除缓存
- 线程1写入缓存值为10,此时缓存写入的是旧数据
3、总结
缓存更新策略的最佳实践方案:
- 低一致性需求:使用Redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
4、实战
1、查询设置超时时间
```java @Override public Result queryById(Long id) { //从redis查询缓存 String key = RedisConstants.CACHE_SHOP_KEY + id; String shopInfo = stringRedisTemplate.opsForValue().get(key); //判断是否存在 if (StrUtil.isNotBlank(shopInfo)){ //存在则返回 Shop shop = JSONUtil.toBean(shopInfo, Shop.class); return Result.ok(shop); }
//不存在,则查询数据库
Shop shop = getById(id);
//不存在则返回错误
if (null == shop){
return Result.fail("店铺不存在");
}
//写入缓存 设置超时时间为30min
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//数据库存在,则返回
return Result.ok(shop);
} ```
2、更新时删除缓存
```java @Override @Transactional public Result update(Shop shop) { Long id = shop.getId(); if (null == id){ return Result.fail("店铺id不能为空"); } //1.更新数据库 updateById(shop); //2.删除缓存 stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
return Result.ok();
} ```
更多内容请关注:小熊学Java