Redis学习之缓存实现及缓存更新
什么是缓存?
缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。
为什么需要缓存?
提前准备好数据,便于更快地读写。
缓存是把双刃剑,要权衡利弊。
优点:降低后端负载
提高读写效率,降低响应时间
缺点:数据一致性成本 代码维护成本 运维成本
实现
关键流程
1.暂无缓存,从数据库读,然后设置(更新)缓存
2.已有缓存,直接读缓存
代码示例
/**
* 根据id查询商铺信息
* @param id 商铺id
* @return 商铺详情数据
*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
return shopService.queryById(id);
}
ShopServiceImpl
@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);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return Result.fail("店铺不存在!");
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
if (shop == null){
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
// 5.不存在返回错误
return Result.fail("店铺不存在!");
}
// 6.存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7.返回
return Result.ok(shop);
}
缓存更新
几种常用策略,一般会选择主动更新+超时提出兜底。
由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等
主动更新缓存的几种方式
1.Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。(以数据库的数据为准,业务层自己来控制缓存的写入)
2.Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理.(使用现成的数据写入服务,让服务来维护数据库和缓存的一致性,我门只用写数据即可)
3.Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致。(先更新缓存,通过异步线程定期将缓存的数据持久化(同步道数据库中))
操作缓存和数据库时有三个问题需要考虑: 1.删除缓存还是更新缓存? 更新缓存:每次更新数据库都更新缓存,无效写操作较多 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
2.多线程情况下,如何保证缓存与数据库的操作的同时成功或失败? 1.单体系统:将缓存与数据库操作放在一个事务 2.分布式系统:利用TCC等分布式事务方案
3.先操作缓存还是先操作数据库?
下图为两种方式在多线程下可能出现的问题
两种方式都不能百分百保证一致性,但【先操作数据库,再删除缓存】,出现问题的几率低,可以配合延时双删保证缓存一定被删除。
实现
核心思路如下:
修改ShopController中的业务逻辑,满足下面的需求:
根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
根据id修改店铺时,先修改数据库,再删除缓存
修改重点代码1:修改ShopServiceImpl的queryById方法
设置redis缓存时添加过期时间
修改重点代码2
代码分析:通过之前的淘汰,我们确定了采用删除策略,来解决双写问题,当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从mysql中加载最新的数据,从而避免数据库和缓存不一致的问题
@Transactional
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.输出缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}