数据库和缓存一致性问题有几种解决方案

来源:9-5 多线程并发与线程安全总结

杜小牧

2019-03-06

数据库和缓存一致性问题有几种解决方案

写回答

2回答

Jimin

2019-03-07

你好,这个问题提的很好,虽然提问的比较简单,但不能简单的回答,具体的方案其实很多,我需要做一下整理,之后再专门回答一下(有可能会写一篇手记)

1
0

Jimin

2019-03-12

这个问题我之前想多了,细看一下就是db和缓存同时操作的一致性问题,本质上就是大家常说的缓存一致性问题。接下来我们具体讨论一下:


缓存中的数据和数据源并非总是一致的,应用程序必须实现一种帮助确保缓存和数据源一致的策略,这种策略不仅能够确保缓存里面的数据是最新的,而且当缓存中的数据不是最新的时候,需要能够监测到并且采取相应的措施。在更新缓存方面存在多种选择方案:对于更新完数据库,可以选择更新缓存或删除缓存;也可以先删除或更新完缓存后,再更新数据库。众多业务的抉择不同,在业界也存在很大的争议。

理论上给缓存设置合理的过期时间,无论采用各种更新策略均是可以保证最终一致性的。这种方案下,对存入缓存的数据设置过期时间,所有的写操作以数据库成功为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,我们主要讨论不设置过期时间场景下的一致性问题。

下面分别讨论四种更新策略:

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

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

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

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


1、先更新数据库,再更新缓存

(1)线程安全不满足: 

同时有请求1和请求2进行更新操作,并发情况下会出现下列竞态条件: 

线程1更新了数据库 

线程2更新了数据库 

线程2更新了缓存 

线程1更新了缓存 

请求1更新缓存应该比请求2更新缓存早才对,但是因为网络等原因,可能出现按上述顺序执行,2却比1更早更新了缓存。导致出现脏数据。 


(2)业务场景不匹配: 

对于写多读少的业务场景下,这种方案就会导致,数据没读到缓存已经被频繁的更新,极其浪费性能。 

有些场景数据库数据并非直接写入缓存,而是经过一系列复杂的计算后再写入缓存。这种场景下,每次更新数据库后再经过复杂计算写入缓存值,是极其浪费性能的。

这两种场景下,删除缓存明显更为适合。 


2、先更新缓存,再更新数据库

几乎没有这样使用的:

(1)数据库更新失败频率较高,大部分业务涉及多表联动回滚不易更新缓存,业务一致性遭到严重破坏;

(2)数据库死锁周期内读取的始终是错误值;

(3)1中提到的两个线程同时更新也同样存在。


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

(1)线程安全不满足:

该方案会导致不一致的原因也是在并发情况下会出现脏数据,现在同时有一个请求1进行更新操作,另一个请求2进行查询操作。那么会出现如下情形:

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

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

请求2去数据库查询得到旧值 

请求2将旧值写入缓存 

请求1将新值写入数据库 

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


(2)主从分离不满足:

在使用DB主从读写分离架构的场景下,使用先删除缓存在更新DB造成数据不一致的原因如下:

还是两个请求,一个请求1进行更新操作,另一个请求2进行查询操作。 

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

请求1将数据写入数据库

请求2查询缓存发现,缓存没有值 

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

请求2将旧值写入缓存 

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

上述情形,就是使用主从情况下数据不一致的原因。


下面我们探讨下如何解决这两个问题

对于(1)和(2)两种场景下的脏数据问题可以采用延时双删策略解决,即采用下面的方式将指定延迟内所造成的脏数据再次删除:


写请求时先删除缓存

更新数据库

sleep(指定延迟)再删除缓存


具体的休眠等待时间需要结合自己的项目的读数据业务耗时/主从同步延迟进行仔细评估。然后写数据的休眠时间则在读数据业务逻辑的耗时/主从同步基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。


不过采用这种同步淘汰策略,吞吐量必然会大大降低,解决方式是将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,可以加大吞吐量。 


异步删除带来的新问题是第二次删除失败必须正确处理,因为第二次删除失败,就会出现如下情形:

还是有两个请求,一个请求1进行更新操作,另一个请求2进行查询操作,假设是单库: 

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

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

请求2去数据库查询得到旧值 

请求2将旧值写入缓存 

请求1将新值写入数据库 

请求1试图去删除请求2写入对缓存值,结果失败了。 

如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。 如何解决参考最后一项:先更新数据库,再删除缓存


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

Cache-Aside Pattern模式提议以及业界主流均是采用先更新数据库再删除缓存的方式,其实这种方式也不能完全解决并发脏数据问题,场景如下:


这会有两个请求,一个请求1做查询操作,一个请求2做更新操作,那么会有如下情形产生:

假设现在缓存刚好失效(例如LRU,我们在讨论的仍是不设置过期时间)

请求1查询数据库,得一个旧值 

请求2将新值写入数据库 

请求2删除缓存 

请求1将查到的旧值写入缓存 

这种场景发生的概率极低,必要的两个条件约束是在缓存刚好失效且写DB比读DB还要慢。


如果追求极端方案,最简单的是给缓存加上合适的过期时间即可;其次采用3中的异步延时删除策略,保证读取之后再进行删除操作。最后一项导致不一致的原因就是步骤3中提到的缓存删除失败,解决方案是提供一套具有保障重试的机制即可。


常规系统里,大家主要是通过以上一些手段来处理,但是都有一个相同点:都是在主线程里同步处理的。还有一种异步处理手段,许多大公司内部使用的异步处理,比如借助databus等组件通过获取数据库insert、update、delete的binlog,转换成实体对象,再更新到缓存里。这种方式因为是异步操作,相对同步处理会稍微慢一点点,对于更新完并不是立即读的场景很适用,也可以针对临时的失败做些重试,并且不会影响主流程的返回。

0
0

Java高并发编程,构建并发知识体系,提升面试成功率

构建完整并发与高并发知识体系,倍增高薪面试成功率!

3923 学习 · 832 问题

查看课程