缓存一致性问题

Posted 真菌的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存一致性问题相关的知识,希望对你有一定的参考价值。

缓存一致性问题

缓存具有高性能的特性,所以缓存技术已在互联网行业广泛应用。大家对于读取缓存的基本逻辑已达成了共识,如下图:


但是,对于更新缓存的说明,就众说纷纭了。

从理论上说,给缓存定合理的过期时间,可以保证数据最终一致。这种情况下,如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。接下来讨论的思路是不依赖于给缓存设置过期时间这个方案的。

先列一下三种更细策略:

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

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

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

接下来分别说下三种策略肯能出现的问题

一. 先更新数据库,再更新缓存

情况一

A请求和B请求同时进行更新操作:

  1. 线程A更新了数据库;

  2. 线程B更新了数据库;

  3. 线程B更新了缓存;

  4. 线程A更新了缓存;

这种情况下,出现了脏数据,简单的网络波动就会造成这种问题。

情况二

除线程角度外,还考虑到业务场景角度:

  1. 数据库的写远远大于读的话,这种方案会使数据还没被读取到,缓存就一直频繁地被刷新;

  2. 如果写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合;

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

A请求进行更新操作,B请求同时进行查询操作:

  1. 请求A进行写操作,删除缓存;

  2. 请求B查询发现缓存不存在;

  3. 请求B去数据库查询得到旧值;

  4. 请求B将旧值写入缓存;

  5. 请求A将新值写入数据库;

上面的方案,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

遇到这种情况,如何解决?

可以采用延时双删策略

伪代码如下:

public void write(String key,Object data){

redis.delKey(key);

db.updateData(data);

Thread.sleep(1000);

redis.delKey(key);

}

主要流程如下:

  1. 先淘汰缓存;

  2. 更新数据库;

  3. 休眠1秒;

  4. 再次淘汰缓存;

这样操作的话,可以将1秒内产生的缓存脏数据删除。

中间休眠多久?

应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

之后采用了读写分离机构应该怎么办?

  1. 请求A进行写操作,删除缓存;

  2. 请求A将数据写入数据库了;

  3. 请求B查询缓存发现,缓存没有值;

  4. 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值;

  5. 请求B将旧值写入缓存;

  6. 数据库完成主从同步,从库变为新值;

上述情况,主要还是由于数据不一致导致的。还是使用双删延时策略。只是,休眠时间修改为在主从同步的延时时间基础上,加几百ms。

吞吐量降低?

可以将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间再返回。这么做,加大吞吐量。

第二次删除,如果删除失败怎么办?

请求A进行更新操作,另一个请求B进行查询操作

  1. 请求A进行写操作,删除缓存;

  2. 请求B查询发现缓存不存在;

  3. 请求B去数据库查询得到旧值;

  4. 请求B将旧值写入缓存;

  5. 请求A将新值写入数据库;

  6. 请求A试图去删除请求B写入对缓存值,结果失败了;

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

国外提出了一个缓存更新套路,名为《Cache-Aside pattern》。参考链接:https://coolshell.cn/articles/17416.html

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中;

  • 命中:应用程序从cache中取数据,取到后返回;

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

这样并发就没有问题了?

  1. 缓存刚好失效;

  2. 请求A查询数据库,得一个旧值;

  3. 请求B将新值写入数据库;

  4. 请求B删除缓存;

  5. 请求A将查到的旧值写入缓存;

以上状况确实会产生脏数据。但是,有个必要条件,就是步骤3的写数据库操作比步骤2的读数据库操作耗时更短,才有可能使得步骤4先于步骤5。数据库的读操作的速度远快于写操作的,因此步骤3耗时比步骤2更短,这一情形很难出现。

缓存删除失败有影响吗?

有。一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。

可以参考的解决办法

  • 方案一


流程如下:

  1. 更新数据库数据;

  2. 缓存因为种种问题删除失败;

  3. 将需要删除的key发送至消息队列;

  4. 自己消费消息,获得需要删除的key;

  5. 继续重试删除操作,直到成功;

    这样实现,缺点也是有的,会对业务线代码造成大量的侵入。

  • 方案二

  1. 更新数据库数据;

  2. 数据库会将操作信息写入binlog日志当中;

  3. 订阅程序提取出所需要的数据以及key;

  4. 另起一段非业务代码,获得该信息;

  5. 尝试删除缓存操作,发现删除失败;

  6. 将这些信息发送至消息队列;

  7. 重新从消息队列中获得该数据,重试操作;


以上是关于缓存一致性问题的主要内容,如果未能解决你的问题,请参考以下文章

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题

如何缓存片段视图

从 Apollo 缓存中读取特定类型的所有片段

如果我编写一段代码,其中每个线程修改数组的完全不同部分,那会保持缓存一致性吗?

phalcon: 缓存片段,文件缓存,memcache缓存

如何使控制台中的视图缓存片段过期?