Redis的缓存更新策略
一. 概念
1. Redis缓存策略
业务场景需求:
1. 在基本不会更新数据的情况下可以使用内存淘汰机制
2. 在频繁更新数据的情况下可以使用主动更新,并以超时剔除作为兜底方案。
2. 主动更新的3种方法
1. Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存 2. Read/Write Through Pattern:缓存和数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。 -优点:整合的服务保证了数据的一致性 -缺点:维护和开放成本高 3. Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,最终保持一致。 -优点:异步更新缓存数据,效率高。例如缓存多次更新,但是更新到的缓存并没有被使用,多次将数据持久化到数据库就相当于进行了无用的操作,异步更新相当于将前几次的更新合并为一次更新,因而提高了效率。 -缺点:无法保证一致性,维护成本高
目前主流使用的Redis缓存主动更新的方法是Cache Aside Pattern
二. Redis的读写实践
1. Redis的读
如果缓存在Redis中存在,即缓存命中,则直接返回数据
如果Redis中没有对应缓存,则需要直接查询数据库,然后存入Redis,最后把数据返回
通常情况下,我们会为某个缓存设置一个key值,并针对key值设置一个过期时间,如果被查询的数据对应的key过期了,则直接查询数据库,并将查询得到的数据存入Redis,然后重置过期时间,最后将数据返回,伪代码如下:
/** * 根据用户名获取用户详细信息 * @author*/ public User getUserInfo(String userName) { User user = redisCache.getName("user:" + userName); if (user != null) { return user; } // 从数据库中直接搜索 user = selectUserByUserName(userName); // 将数据写入Redis,并设置过期时间 redisCache.set("user:" + userName, user, 30000); // 返回数据 return user; }
2. Redis的写
一致性问题:
在Redis的key值未过期的情况下,用户修改了个人信息,我们此时既要操作数据库数据,也要操作Redis数据。
到底是先更新数据库还是先更新缓存?
从本质上讲,无论是先写数据库还是先写缓存,都是为了保证数据库和缓存的数据一致,也就是我们常说的数据一致性。
操作缓存和数据库时需要考虑的三个问题:
1. 删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存
结论:推荐直接使用「删除」操作。
2. 如何保证缓存与数据库的操作的同时成功或者失败
对于单体系统:将缓存与数据库操作放在一个事务中
对于分布式系统:利用TCC等分布式事务方案
3. 先操作缓存还是先操作数据库?
3-1. 先删除缓存,再操作数据库
这种方式可能存在以下两种异常情况:
1. 删除缓存失败,这时可以通过程序捕获异常,直接返回结果,不再继续更新数据库,所以不会出现数据不一致的问题
2. 删除缓存成功,更新数据库失败。在多线程下可能会出现数据不一致的问题
这时,Redis中存储的旧数据,数据库的值是新数据,导致数据不一致。这时我们可以采用 延时双删 的策略,即更新数据库数据之后,再删除一次缓存。
用伪代码表示就是:
/** * 延时双删 * @author*/ public void update(String key, Object data) { // 首先删除缓存 redisCache.delKey(key); // 更新数据库 db.updateData(data); // 休眠一段时间,时间依据数据的读取耗费的时间而定 Thread.sleep(500); // 再次删除缓存 redisCache.delKey(key); }
3-2. 先操作数据库,再删除缓存
这种方式可能存在以下两种异常情况:
1. 更新数据库失败,这时可以通过程序捕获异常,直接返回结果,不再继续删除缓存,所以不会出现数据不一致的问题
2. 更新数据库成功,删除缓存失败。导致数据库是最新数据,缓存中的是旧数据,数据不一致
这里, 我们有两种方式来解决数据不一致问题:失败重试 和 异步更新。
方式1. 失败重试
如果删除缓存失败,我们可以捕获这个异常,把需要删除的 key 发送到消息队列。自己创建一个消费者消费,尝试再次删除这个 key,直到删除成功为止。
这种方式有个缺点,首先会对业务代码造成入侵,其次引入了消息队列,增加了系统的不确定性。
方式2. 异步更新
因为更新数据库时会往 binlog 中写入日志,所以我们可以启动一个监听 binlog变化的服务(比如使用阿里的 canal开源组件),然后在客户端完成删除 key 的操作。如果删除失败的话,再发送到消息队列。
总之,对于删除缓存失败的情况,我们的做法是不断地重试删除操作,直到成功。无论是重试还是异步删除,都是最终一致性的思想。
如上图所示,两种方案在多线程的情况下都会产生数据不一致的问题
在先操作数据库再删除缓存的情况下,要发生数据不一致的问题,需要在缓存写入之前完成更新数据库和删除缓存的操作,而写入缓存的耗时非常短。因而发生的概率相对于另一种方案更低。所以优先选择先操作数据库,再删除缓存。
结论:推荐直接使用「先更新数据库再删除缓存」操作。
三. 总结
缓存策略的最佳实践是 Cache Aside 模式。分别分为读缓存最佳实践和写缓存最佳实践。
读缓存最佳实践
先读缓存,命中则返回;
未命中则查询数据库,再写到缓存中。
写缓存最佳实践
先更新数据库,再操作缓存;
操作缓存采用直接删除缓存,而不是修改。
----------------------------------------------------------------------------------------------------------
参考博文:
3. Redis缓存更新策略