在数据读多写少的情况下作为缓存来使用,恐怕是Redis使用最普遍的场景了。当使用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;
}

一致性问题

但是,在Redis的key值未过期的情况下,用户修改了个人信息,我们此时既要操作数据库数据,也要操作Redis数据。现在我们面临了两种选择:

  1. 先操作Redis的数据,再操作数据库的数据
  2. 先操作数据库的数据,再操作Redis的数据

如论选择哪种方法,最理想的情况下,两个操作要么同时成功,要么同时失败,否则就会出现Redis和数据库数据不一致的情况。

遗憾的是,目前没有什么框架能够保证Redis的数据和数据库的数据的完全一致性。我们只能根据场景和所需要付出的代码来采取一定的措施降低数据不一致出现的概率,在一致性和性能之间取得一个折中。

下面我们来讨论一下关于Redis和数据库质检数据一致性的一些方案。

方案选择

是删除缓存还是更新缓存?

当数据库数据发生变化的时候,Redis的数据也需要进行相应的操作,那么这个「操作」到底是用「更新」还是用「删除」呢?

「更新」的话调用Redis的set方法,新值替换旧值;「删除」直接删除原来的缓存,下次查询的时候重新读取数据库,然后再更新Redis。

结论:推荐直接使用「删除」操作

因为使用「更新」操作的话,你会面临两种选择

  1. 先更新缓存,再更新数据库
  2. 先更新数据库,再更新缓存

第1种不用考虑了,下面讨论一下「先更新数据库,再更新缓存」这种方案。

如果线程1和线程2同时进行更新操作,但是每个线程的执行顺序如上图所示,此时就会导致数据不一致,因此从这个角度上我们推荐直接使用删除缓存的方式。

此外,推荐使用「删除缓存」还有两点原因。

  1. 如果写数据库的场景比读数据场景多,采用这种方案就会导致缓存就被频繁写入,浪费性能;
  2. 如果缓存要经过一系列复杂的计算才能得到,那么每次写入数据库后,都再次计算写入的缓存无疑也是浪费性能的。

明确这个问题之后,摆在我们面前的就只有两个选择了:

  • 先更新数据库,再删除缓存
  • 先删除缓存,再更新数据库

先更新数据库,再删除缓存

这种方式可能存在以下两种异常情况

  1. 更新数据库失败,这时可以通过程序捕获异常,直接返回结果,不再继续删除缓存,所以不会出现数据不一致的问题
  2. 更新数据库成功,删除缓存失败。导致数据库是最新数据,缓存中的是旧数据,数据不一致

第2种情况应该怎么办呢?我们有两种方式:失败重试异步更新

失败重试

如果删除缓存失败,我们可以捕获这个异常,把需要删除的 key 发送到消息队列。自己创建一个消费者消费,尝试再次删除这个 key,直到删除成功为止。

这种方式有个缺点,首先会对业务代码造成入侵,其次引入了消息队列,增加了系统的不确定性。

异步更新缓存

因为更新数据库时会往 binlog 中写入日志,所以我们可以启动一个监听 binlog变化的服务(比如使用阿里的 canal开源组件),然后在客户端完成删除 key 的操作。如果删除失败的话,再发送到消息队列。

总结

总之,对于删除缓存失败的情况,我们的做法是不断地重试删除操作,直到成功。无论是重试还是异步删除,都是最终一致性的思想。

先删除缓存,再更新数据库

这种方式可能存在以下两种异常情况

  1. 删除缓存失败,这时可以通过程序捕获异常,直接返回结果,不再继续更新数据库,所以不会出现数据不一致的问题
  2. 删除缓存成功,更新数据库失败。在多线程下可能会出现数据不一致的问题

这时,Redis中存储的旧数据,数据库的值是新数据,导致数据不一致。这时我们可以采用延时双删的策略,即更新数据库数据之后,再删除一次缓存。

用伪代码表示就是:

/**
* 延时双删
* @author 公众号【蝉沐风】
*/
public void update(String key, Object data) {
// 首先删除缓存
redisCache.delKey(key);
// 更新数据库
db.updateData(data);
// 休眠一段时间,时间依据数据的读取耗费的时间而定
Thread.sleep(500);
// 再次删除缓存
redisCache.delKey(key);
}

最后给读者留下两个思考题:

  1. 为什么先更新缓存,再更新数据库行不通?
  2. 延时双删的方法为什么要休眠一段时间呢?

欢迎大家评论区留言。


推荐阅读

Redis和数据库的数据一致性问题的更多相关文章

  1. 如何保证Redis与数据库的数据一致性

    一般来说,只要你用到了缓存,不管是Redis还是memcache,就可能会涉及到数据库缓存与数据的一致性问题,这里我们以Redis为例. 我们该如何保证Redis与数据库的一致性呢? So easy: ...

  2. Redis 当成数据库在使用和可靠的分布式锁,Redlock 真的可行么?

    怎样做可靠的分布式锁,Redlock 真的可行么? https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html ...

  3. Redis和数据库 数据同步问题

    Redis和数据库同步问题 缓存充当数据库 比如说Session这种访问非常频繁的数据,就适合采用这种方案:当然了,既然没有涉及到数据库,那么也就不会存在一致性问题: 缓存充当数据库热点缓存 读操作 ...

  4. 高并发架构系列:Redis缓存和MySQL数据一致性方案详解

    一.需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景, ...

  5. Redis缓存和MySQL数据一致性方案(转)

    需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景,主要 ...

  6. Redis与DB的数据一致性解决方案(史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  7. Redis 与 数据库处理数据的两种模式

    Redis 是一个高性能的key-value数据库. redis的出现,很大程度补偿了memcached这类key-value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用.它提供了Pyt ...

  8. 快速搭建Redis缓存数据库

    之前一篇随笔——Redis安装及主从配置已经详细的介绍过Redis的安装于配置.本文要讲的是如何在已经安装过Redis的机器上快速的创建出一个新的Redis缓存数据库. 一.环境介绍 1) Linux ...

  9. Redis 与 数据库处理数据的两种模式(转)

    Redis 是一个高性能的key-value数据库. redis的出现,很大程度补偿了memcached这类key-value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用.它提供了Pyt ...

随机推荐

  1. gitlab修改(重置)root用户密码

    gitlab修改(重置)root用户密码 1.使用root权限登录到服务器. 2.使用以下命令启动控制台:  gitlab-rails console production 该命令有时候启动比较慢,需 ...

  2. linux修改默认的SSH远程端口22

    1.编辑sshd_config文件 [root@localhost ~]#  vim /etc/ssh/sshd_config 搜索 #Port 22行,删除开头的 # 字符,然后将其替换为要使用的端 ...

  3. xshell 6 的使用

    1.前言 xshell是用来远程控制云服务器的linux系统的软件,装载window系统里面,可以向发送linux指令, 需要的关键信息:该系统设备的公网ip, 用户名 ,密码 2.软件下载 官网地址 ...

  4. PPT制作图片磨砂玻璃艺术效果

    如果图片损害,点击链接:https://www.toutiao.com/i6488928834799272462/ 选择"插入"选项卡,"图像"功能组,&quo ...

  5. 安装hadoop2.9.2单机版本 jdk1.8 centos7

    安装JDK1.8 查看JDK1.8的安装 https://www.cnblogs.com/TJ21/p/13715749.html 安装hadoop 上传hadoop 下载hadoop     地址h ...

  6. [爱偷懒的程序员系列]-Section 1. “懒”是一切需求的根源

    一直认为"懒"推进了科技的发展,因为"懒"而促生了各种各样的需求.科技的进步加速了各种信息的交互频率,站在台面上说是因为业务需要提高效率,成本需要降低,服务需要 ...

  7. 达索CATIA许可证(License)管理使用和优化

    现下主流的V6版本CATIA,是由达索公司提供授权的浮动型License,其客户端通过企业内网从许可证服务器获得许可证,最少要有一个服务器端DS License Server提供一定数量的Licens ...

  8. html,js 获取父窗口元素

    $("#父窗口元素ID",window.parent.document) window.parent.document.getElementById("父窗口元素ID&q ...

  9. 【刷题-LeetCode】201 Bitwise AND of Numbers Range

    Bitwise AND of Numbers Range Given a range [m, n] where 0 <= m <= n <= 2147483647, return t ...

  10. C++11之future(二)

    如果有两个线程,其中一个线程想要获取另一个线程的返回值,该怎么办? 于是接下来要谈的package_task就是为了解决这个问题而诞生的. // ConsoleApplication5.cpp : 定 ...