数据库和缓存的双写一致性问题

1 前言导入

在使用缓存时,大家的做法都是大同小异,都是先读取缓存,如果缓存无数据则直接访问数据库,再将数据写入缓存,如果缓存有数据则直接返回即可。

好的,现在问题来了,当我们需要更新数据时,应该如何处理数据库和缓存的双写一致性问题?网上有两种方案,一是应该先删除缓存再更新数据库,一是应该先更新数据库,再删除缓存。下面我们对这两种方案进行探讨。

事实上,只要我们对缓存设置过期时间,是可以保证最终一致性的。对缓存设置过期时间之后,即使出现最坏情况,数据库写入成功而缓存更新失败,缓存在到达过期时间后后面的读请求自然会访问数据库并回填缓存。因此,我们的接下来的思考不局限于给缓存设置过期时间这个方案。

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

2.1 该方案造成的不一致

有请求A进行写操作,请求B进行读操作,我们假设以下情景:

  1. 请求A写操作并删除缓存
  2. 请求B读操作发现缓存不存在
  3. 请求B去数据库查询旧值
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库

这种情景下出现了不一致的情况,我们接下来读取的数据将会是脏数据。

2.2 解决方案

如果我只是提出了问题,却没有解决方案,那还怎么出来混?这种不一致的解决方案自然是有的,我们可以使用延时双删的策略。

什么是延时双删呢?通俗的说,我们还是像之前一样,先删除缓存再更新数据库,最后我们需要添加一步,在更新数据库之后休眠一段时间并再次删除缓存。我们之前提到过,在一个请求删除缓存再更新数据库这段时间,可能有另外一个请求去数据库查询了旧值并将其写入缓存。我们只需要等待其完成写入缓存操作后将缓存脏数据删除即可。

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

3.1 该方案造成的不一致

有请求A进行写操作,请求B进行读操作,我们假设缓存已失效,并出现以下情景:

  1. 请求B去数据库查询旧值
  2. 请求A将新值写入数据库
  3. 请求A删除缓存
  4. 请求B将旧值写入缓存

很好,再次出现了不一致的问题,与上面方法一样,我们接下来读取的数据将会是脏数据。这样一看,好像这两种方法也没啥区别啊?

3.2 解决方案

错!它们的区别可大着呢!好像我们之前的情景分析没啥问题,但我们再仔细想一想,在步骤一先于步骤二的情况下,步骤三先于步骤四是一件几乎不可能完成的事!为啥?就因为数据库的读操作的耗时是远远小于写操作的,因此,我们上面假设的情景几乎不可能出现。

如果有完美主义者一定要解决这种问题,那又该怎么办?那可就没办法了,要么就给缓存设置过期时间,要么就使用之前的延时双删。

4 其他造成不一致的原因

在上面的假设中,我们太理想化了,从来都没有想过删除缓存可能失败的问题。无论是先删除缓存再更新数据库,还是先更新数据库再删除缓存,只要删除缓存失败,必然出现不一致的情况。

既然发现问题,必然需要解决问题,其实在我看来,既然有可能删除缓存失败,那么我们就提供一个重试机制即可,一旦删除缓存失败,就开启重试功能。 关于重试机制,我这里提供两种方法,仅供大家参考。

4.1 方法一

删除缓存失败后,我们可以将需要删除的 key 发送至消息队列,然后自己消费消息并获取 key ,一直重试删除操作直至成功即可。

存在问题:对业务代码具有一定的侵入性。

4.2 方法二

既然方法一具有侵入性,那我们就想办法让它没有侵入性。在业务代码更新数据库之后,数据库将信息写入 binlog 日志,然后我们另起一段非业务代码,取出 binlog 日志操作数据以及 key,然后尝试删除缓存,若失败,则将需要删除的 key 发送至消息队列,然后自己消费消息并获取 key ,一直重试删除操作直至成功即可(失败后的操作重复了方法一的方法)。

5 总结

我个人更倾向于使用先更新数据库再删除缓存的方案,对于存在可能删除缓存失败的问题,根据情况选择一种重试机制即可。

参考:【原创】分布式之数据库和缓存双写一致性方案解析

发布了113 篇原创文章 · 获赞 206 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/103056185