🥩 缓存更新的三个策略
内存淘汰: redis底层的内存淘汰机制,无需我们自己维护,当内存不足时自动淘汰部分数据,下次查询时更新缓存。这种机制的优点是维护成本极低,但是缺点也很明显,由于淘汰数据的不确定性导致很难保证数据的一致性
超时剔除: 向redis中添加缓存数据的时候设置TTL时间,到期后自动删除缓存,下次查询时更新缓存。这种机制维护成本不是很高,但是数据一致性同样无法做到很高的保证,因为设置之后数据的有效期就固定了,但是更新时间不固定,若是数据在超时剔除之前发生更新然后查询,得到的仍是更新之前的数据
主动更新: 使用代码在修改数据库的同时更新缓存。这种策略能够保证很高的数据一致性,但是伴随而来的就是更高的维护成本,要在每一个更改语句后面加上redis缓存更新
具体使用哪种策略取决于该业务对数据一致性的需求:一致性需求不高的话,可以使用内存淘汰策略。一致性需求较高的话,可以使用主动更新加上超时剔除策略,保证了较高的一致性
🥩 主动更新策略的三种方案
代码(Cache Aside Pattern):最直接的一种方案,使用代码在修改数据库的同时更新缓存
服务(Read/Warite Through Pattern):将redis缓存与数据库整合为一个服务,由这个服务来维护数据的一致性,在更新数据库时只需要调用该服务即可,无需关心服务底层的业务逻辑,类似于封装。但是市面上没有现成的服务可以使用,自己封装这么一个服务也很复杂,所以说这种方案可用性很差
写回(Write Behind Caching Pattern):所有数据库的CRUD操作都在redis缓存中完成,由另外一个独立的线程异步的将缓存中的数据持久化到数据库中,以此来保证数据的最终一致。这种方案有个很大的好处,那就是极大地减少了对数据库的操作,如果主线程在另一个线程两次持久化之间对redis中的数据操作多次,数据库中只会执行最后一次操作,而不是也操作多次。但是也有坏处,那就是如果还没等到另一个线程持久化数据库,此时redis缓存发生宕机,缓存大多数在内存中,此时发生宕机就会导致缓存中的数据消失,数据库中的数据就与宕机前redis中的数据不一致
综上所述,虽然Cache Aside Pattern方案是最复杂的一个,但是他也同样是最可靠的一个,于是我们选择它来进行接下来的代码学习
主动更新策略注意项
数据库发生更新的时候直接删除缓存中的该数据,而不是跟着更新缓存,因为如果发生连续修改多次的情况,更新缓存的话更新次数等于数据库的更新次数;如果是删除缓存数据的话就只需要删除一次,下一次查询直接从数据库中查询再写入缓存。
删除缓存数据和数据库操作应该保证原子性,也就是说删除缓存数据操作和数据库操作应该同时成功或者同时失败,那么该如何实现呢?单体式系统中,可以通过将两个操作放在一个事务中来完成;分布式系统中可以利用TCC等分布式事务方案来实现
删除缓存数据操作和数据库操作的先后顺序是什么? 应该是先写数据库再删除缓存,原因是这种方式发生线程安全性问题的可能较小
🥩 主动更新的代码实现
controller层前端交互
/** * 更新商铺信息 * @param shop 商铺数据 * @return 无 */ @PutMapping public Result updateShop(@RequestBody Shop shop) { // 写入数据库 return shopService.update(shop); }
需要server的update方法,创建接口和实现类完成业务逻辑代码编写。主动更新+超时剔除的策略就只有两步,那就是在写缓存的时候设置超时时间,更新数据库之后删除缓存
// 数据库中存在写入redis的时候设置超时时间 stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); /** * 更新商铺信息 * @param shop 商铺信息 * @return 前端返回数据 */ @Override @Transactional public Result update(Shop shop) { if (shop.getId() == null) { return Result.fail("店铺id不能为空"); } // 更新数据库 updateById(shop); // 删除缓存 stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId()); // 返回 return Result.ok(); }