1. 首页>
  2. 腾讯云代理

双写又不一致…我该怎么办?

腾讯云 2020年09月15日 浏览22

    腾讯云代理 腾讯云新闻 腾讯云代理 腾讯云直播申请 游戏上云

摘要:

作者  马艺超,腾讯课堂开发工程师,主要负责腾讯课堂的后台相关业务开发


缓存由于其高并发和高性能的特性,十分适合现在很多的场景,因此也已经在各种项目中被广泛使用,但随之而来的问题就是,只要用缓存,就可能会涉及到缓存与数据库双存储双写,只要是双写,就一定会有数据一致性的问题。


那么问题来了,你如何解决一致性问题?

一、先更新数据库再更新缓存


同时有请求A和请求B进行更新操作,那么会出现:


1. 线程A更新了数据库


2. 线程B更新了数据库


3. 线程B更新了缓存


4. 线程A更新了缓存


这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。
这就导致了脏数据。


如果是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。


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


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


一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。


串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。


Cache Aside Pattern


最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。


1. 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。


2. 更新的时候,先更新数据库,然后再删除缓存。


为什么是删除缓存,而不是更新缓存?

原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。

比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?

举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。

比较复杂的数据不一致问题分析


数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了...

只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,那么很少的情况下会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况

异步更新

业务层只读缓存,不读取数据库,有异步的更新服务将数据库里的增量更新的缓存中,并且不设置缓存系统的过期时间,最终全量数据回被保存再缓存中,而且异步更新服务需要将更新缓存的操作串行化,比如使用消息队列,这样就可以避免出现并发更新缓存操作的顺序问题。再设计异步服务时要充分保证异步服务的可用性,要有完善的告警,否则可能出现缓存数据和数据库不一致的问题。具体流程图如下。

image
在更新缓存的过程中对于同一个缓存的多个更新操作是没有意义的,可以加一层过滤,如果缓存更新请求消息出队列的时候发现队列中还有对这块缓存的更新请求消息,就该丢弃这次的请求消息,避免频繁更新,如果更行缓存操作失败,需要重试,重试的时候需要考虑该操作是否是最新的(判断消息队列中是否有更新同一块缓存的数据,如果有就丢弃这条),避免新操作把就操作覆盖。   


一、先更新缓存再更新数据库


结果类似与先更新数据库再更新缓存导致了脏数据。


该场景下的脏数据可以使用消息队列将更新数据库的消息串行化等方式加以避免。当然也需要考虑业务场景。

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

该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

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

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

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

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

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

上述情况就会导致不一致的情形出现。
而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决呢?采用延时双删策略:

1. 先淘汰缓存

2. 再写数据库(这两步和原来一样)

3. 休眠1秒,再次淘汰缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?

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

如果你用了mysql的读写分离架构怎么办?

在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

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

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

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

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

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

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

上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

以上的各种策略还是得视业务场景而定,尽量做到用简单的实现方法满足自己的业务场景。


相关文章