计算机系统应用教程网站

网站首页 > 技术文章 正文

从理论到实践,深入解析Redis数据一致性问题(一)

btikc 2024-11-30 19:05:47 技术文章 24 ℃ 0 评论

1.数据一致性问题

对于数据库与缓存数据的一致性问题相信对于大部分开发人员来说并不陌生,也经常遇到。实际上,只要是我们使用到缓存,那么必然就会产生数据库与缓存间的数据不一致的问题。

既然是“必然产生数据不一致性问题”,那么我们在设计程序时就需要考虑这些问题:系统是否必须要求数据库与缓存间数据达到完全一致性(即强一致性)?系统是否能够接受一定时间下的数据不一致性,能够接受多少时间下的数据不一致性?

带着上面的问题,我们又可以将“一致性”分为如下两种形式:

1)强一致性

它要求数据库与缓存间数据要达到完全一致性。要达到强一致性,就只能通过加锁将请求变成串行化执行,但这样系统的吞吐量也就大大降低了。很显然,这样并不是设计缓存的初衷,实际上也没有必要要求强一致性。

2)弱一致性

也称为最终一致性,它能接受一定时间内的数据库与缓存间数据存在短暂的不一致性性,只要求最终的数据是一致的即可。这样既提高了系统的并发性与吞吐量,也能够保障了数据最终的一致性。

了解完上述的“一致性”问题,那么在什么情况下会导致数据库与缓存间数据的不一致问题呢?简单来讲,主要就是“并发请求与更新问题”与“更新/删除异常问题”两个问题所导致的,具体场景如下四种情况。

1.1 先更新数据库,再更新缓存

这种场景要求数据更新时,先更新数据库,待数据库更新成功之后,再更新缓存数据,如图1所示:

如图1所示,很显然,这种方式存在如下两个问题:

  • 1)如果数据库先更新成功了,还未对Redis缓存进行更新的间隙期间(即这个间隙期间数据库是更新后的新数据,而Redis缓存是更新前的旧数据)。如果在这个间隙期间有新的数据读取请求过来,则读到的都是Redis缓存的更新前的旧数据。
  • 2)如果数据库先更新成功了,再次更新Redis缓存时,数据更新失败了(即更新异常,这个时候数据库是更新后的新数据,而Redis缓存是更新前的旧数据)。如果这种更新异常情况发生,那么后面的所有数据读取请求读到的都是Redis的更新前的旧数据,这种情况可能要持续到缓存过期失效之后,才能请求到正确的数据。

1.2 先更新缓存,再更新数据库

这种场景要求数据更新时,先更新缓存,待缓存更新成功之后,再更新数据库,如图2所示:

如图2所示,这种方式相对于“先更新数据库,再更新缓存”看似可以解决读取旧数据的问题,但依然存在如下两个问题:

  • 1)如果Redis缓存先更新成功了,还未对数据库进行更新的间隙期间(即这个间隙期间数据库是更新前的旧数据,而Redis缓存是更新后的新数据),这个间隙期间会导致数据的不一致问题出现;
  • 2)如果Redis缓存先更新成功了,再次更新数据库时,数据更新失败了(即更新异常,这个时候数据库是更新前的旧数据,而Redis缓存是更新后的新数据),这同样会导致数据的不一致问题出现。

面对上面两种数据不一致的情况,如果这个时候有数据读取请求过来,那么读取到的都是Redis缓存未生效的新数据。这种胀读Redis缓存未生效的新数据会导致系统出现严重的后果,如遇到关联查询或者关联业务操作都会面临不可预知的一系列错误。

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

这种场景要求数据更新时,先更新数据库,待数据库更新成功之后,再删除缓存数据,如图3所示:

如图3所示,它看似可以解决“并发更新”的问题,但依然会存在下面的两个问题:

  • 1)如果数据库先更新成功了,还未对Redis缓存数据进行删除的间隙期间(即这个间隙期间数据库是更新后的新数据,而Redis缓存是更新前的旧数据)。如果在这个间隙期间有新的数据读取请求过来,则读到的都是Redis缓存的更新前的旧数据。
  • 2)如果数据库先更新成功了,再次删除Redis缓存数据时,数据删除失败了(即更新异常,这个时候数据库是更新后的新数据,而Redis缓存是更新前的旧数据)。如果这种删除异常情况发生,那么后面的所有数据读取请求读到的都是Redis的更新前的旧数据,这种情况可能要持续到缓存过期失效之后,才能请求到正确的数据。

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

这种场景要求数据更新时,先删除缓存数据,待缓存数据删除成功之后,再更新数据库,如图4所示:

如图4所示,相对于“先更新数据库,再删除缓存”,这种方式在没有高并发的情况下,是有可能保持数据一致性的。它解决了如下两个问题:

  • 1)如果Redis缓存先删除成功了,还未对数据库进行更新的间隙期间,此间隙期间的数据读取请求查询不到任何数据,也就不存在数据一致性的问题;
  • 2)如果Redis缓存先删除成功了,再次更新数据库时,数据更新失败了。同样,之后的数据读取请求查询不到任何数据,也就不存在数据一致性的问题。

尽管如此,但如果是处于读写并发的情况下,还是会出现数据不一致的情况,如图5所示:

如图5所示:

  • 1)请求A的数据写请求先执行将Redis缓存数据删除,删除成功后,但由于网络延迟原因,还没有来得及执行写数据库操作;
  • 2)在此间隙期间,请求B的数据读请求开始查询Redis缓存,发现Redis缓存没数据。接下来再继续请求查询数据库,数据库中有原来的旧数据。请求B获取数据库中原来的旧数据,并同时将其更新到Redis缓存中;
  • 3)此时,请求A网络延迟结束,把新数据写入数据库。这时,导致数据库与Redis缓存出现数据不一致问题。

其实,面对上面这些场景所导致的数据不一致性问题,在实际开发中没有十分完美的解决方案,只有根据自己的应用场景找到最适合方案。

一般使用场景简单且并发较低的情况,解决这些数据不一致性问题可以使用“传统的单删(即先删除缓存,再更新数据库)或者延时双删”就可以满足其要求;如果使用场景复杂、对并发要求较高的场景下,则就需要选择“消息队列异步重试、定时任务异步重试、Binlog日志订阅”等方案。

本文只阐述“延时双删”策略,其他方案待下一篇文章继续展开讨论。


2.延时双删方案

在解决上面的单删(即先删除缓存,再更新数据库)所面临的“在读写并发进行时,会产生缓存是旧数据,而数据库是新数据”的数据不一致问题,我们就可以使用延时双删方案来进行解决。

顾名思义,延时双删就是指:在进行数据更新时,先删除Redis缓存中的数据,然后更新数据库的值。在更新完数据库值之后,我们可以让线程先休眠 一小段时间后再进行一次Redis缓存数据删除操作。有了休眠的这段时间,即使有其他线程从数据库中读取到旧的数据并重新更新到Redis缓存中,我们也能够将其再次删除,以保证Redis缓存中会是新的值。

如图6所示:

现在,通过延时双删方案,我们再来分析上面图5的示例:

  • 1)请求A的数据写请求先执行将Redis缓存数据删除,删除成功后,但由于网络延迟原因,还没有来得及执行写数据库操作;
  • 2)在此间隙期间,请求B的数据读请求开始查询Redis缓存,发现Redis缓存没数据。接下来再继续请求查询数据库,数据库中有原来的旧数据。请求B获取数据库中原来的旧数据,并同时将其更新到Redis缓存中;
  • 3)此时,请求A网络延迟结束,把新数据写入数据库,完成数据库写入后进入休眠;
  • 4)休眠一段时间后,请求A再次删除Redis缓存数据,从而保证了数据的一致性。

对于延时双删方案,这个“延时”到底要延时多久才合适呢?

就如上面的例子,如果请求A再次删除Redis缓存数据太快(即在请求B将数据库中的旧值更新到缓存之前,就已经把缓存删除了),那么这次删除就没任何意义,也解决不了一致性问题。所以,这个休眠时间设置是关键所在。

但不幸的是,这确实很难给定一个准确答案,一般设置原则是“大于缓存的读写时间即可,又或者是大于从数据库读取数据+写入缓存的时间和即可”。实际上,这个时间只又在经过不断的压测和实际环境运行,才能够找到一个合理的预估时间,从而尽可能的去降低发生数据不一致性问题的概率。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表