Redis的缓存问题(二)缓存更新策略与实践

引子

缓存的更新策略

实现主动更新的3个方案

Cache Aside Pattern实现

先操作数据库还是先操作缓存?

代码实现

完整代码,需要自取 


引子

缓存的好处不言而喻,但是也带来了一系列问题。我们数据是保存在缓存(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的分享

posted @ 2022-07-07 12:11  金鳞踏雨  阅读(113)  评论(0编辑  收藏  举报  来源