Redis的缓存问题(二)缓存更新策略与实践
引子
缓存的好处不言而喻,但是也带来了一系列问题。我们数据是保存在缓存(Redis)与数据库(MySQL)中,在使用缓存的时候,我们要如何保证数据的一致性?当我们对数据库数据进行修改时,而缓存没有及时的更新,那么程序中查询的结果就会有出入!如何解决?自然而然就有了缓存的更新策略。
缓存的更新策略
在企业中常用的更新策略大致有以下 3 种:
内存淘汰:redis本身的事情,当redis内存快满了,它自身会部分数据剔除,但是该操作不可控。
超时剔除:设置一个TTL过期时间(expire命令),到期就删除,但是一致性的强弱取决于TTL时间设置的长短。
主动更新(重):一改数据库,就更新缓存。
低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
实现主动更新的3个方案
方案一,代码最复杂,但是可靠性高;
方案二,将缓存与数据库看作一个整体(服务)来操作,但是这个服务不太好维护,市面上目前也没有这种东西。
方案三,最简单,调用者只操作缓存,缓存持久化到数据库的过程交给一个异步的线程,效率高,但是一致性难以保证!如果出现宕机,数据会丢失,所以可靠性也存在问题!
综上所述:首选方案一!
Cache Aside Pattern实现
更新缓存 与 删除缓存
如何使用 “更新缓存” 那么我每修改一次数据库的数据时,Redis就要更新一次值,数据库更新100次,Redis就要更新100次;
而如果是“删除缓存” 那么当我修改数据库的数据时,Redis中的值就会被直接删除,数据库更新100次,Redis只要更新1次!!!
而且使用“删除缓存”的方案,当没人来查询该数据时,我们也不用将数据库的值写入缓存redis中。
综上所述:必然时选择 “删除缓存” !相当于延迟加载。
先操作数据库还是先操作缓存?
(1)先操作缓存再操作数据库
正常情况下:假设数据库与缓存中的值为10,按下述操作最后缓存与数据库中的值都为20。
异常情况:假设数据库与缓存中的值为10,按下述操作最后缓存的值为10而数据库中的值为20
(2)先操作数据库再操作缓存
正常情况下:假设数据库与缓存中的值为10,按下述操作最后缓存与数据库中的值都为20。
异常情况:假设数据库与缓存中的值为10,并且恰好此时缓存失效了(TTL到了,故为空!),先查缓存,未命中,就查询数据库得到值为10;并且恰好此时在更新数据库前,线程2抢夺走CPU资源,更新数据库值为20,再删除缓存(本就为空),然后线程切换,写入缓存(这里写入的值是之前查询的,不是在更新数据库后的新值!!!故写入缓存的值为10)。
综上所述:
上述两种情况都有可能发生!但是第二种情况发生的可能性非常小,在查询数据库后,我们紧接着是要写缓存,而写缓存一般是微秒级别的,几乎不可能在这一瞬间,数据库更新了,且就算数据库更新,写入数据的速度也比不上redis,所以我们选择第二种方案!选择:先操作数据库再操作缓存 的方式来实现!
代码实现
加入超时时间 queryById()
@Override
public Result queryById(Long id) {
// 1.从redis查询商铺缓存
String key = CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
System.out.println("Redis");
return Result.ok(shop);
}
// 4.不存在,根据id查询数据库
Shop shop = getById(id);
// 5.不存在,返回错误
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 6.存在,写入Redis
// 把shop转换成为JSON形式写入Redis
System.out.println("MySQL");
// 同时添加超时时间
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
程序后台修改数据库数据的时候,会调用下面的这个方法update(),然后将缓存删除!
之后如果在查询该店铺,再会由上面的方法写入 queryById(id)
@Override
@Transactional
public Result update(Shop shop) {
System.out.println("up");
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空!");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
我们可以使用Postman模拟后台访问操作!
send之后,我们可以看看数据库中该数据是否发生变化,在通过可视化工具RESP_app 看看redis中的缓存数据有没有被删除。
完整代码,需要自取
链接:https://pan.baidu.com/s/1Fusza_hlhM6FL2df-WPjXQ
提取码:uvht
--来自百度网盘超级会员V4的分享