缓存层是提高系统响应速度和扩展性的关键组件。然而,缓存层的引入也带来了数据一致性的挑战。
当数据库中的数据发生变化时,如何确保这些变化能够及时且准确地反映到缓存中,是确保用户体验和系统可靠性的重要问题。
1. 数据一致性
首先,我们需要清楚什么情况符合【数据一致性】
- 缓存中有数据,缓存中的值需要和数据库中的值保持一致;
- 缓存中没有数据,数据库中的值必须是最新值。
不属于这两种情况的,则就是缓存和数据库数据不一致问题了。
根据是否接收写请求,我们可以把缓存分成读写缓存 和只读缓存。
1.1 读写缓存
对于读写缓存来说,如果要对数据进行增删改,就需要在缓存中进行,同时还要根据采取的写回策略,决定是否同步写回到数据库中。
- 同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致;
- 异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据库就没有最新的数据了。
所以,对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略。
不过,需要注意的是,如果采用这种策略,就需要同时更新缓存和数据库。
所以,我们要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性,也就是说,两者要不一起更新,要不都不更新,返回错误信息,进行重试。否则,我们就无法实现同步直写。
当然,在有些场景下,我们对数据一致性的要求可能不是那么高,比如说缓存的是电商商品的非关键属性或者短视频的创建或修改时间等,那么,我们可以使用异步写回策略。
1.2 只读缓存
对于只读缓存来说,当有数据新增时,会直接写入数据库;
当有数据删改时,需要把缓存中的数据标记为无效,当应用后续再访问这些增删改的数据时,因为发生缓存缺失,就会读取数据库,把数据放入缓存中了。
那么在上面的步骤中,"数据不一致"的情况会不会发生呢?
1.2.1 新增操作
如果是新增数据,则直接保存到数据库中,缓存中是没有值的。符合一致性的第2种情况,此时缓存和数据库中数据是一致的。
1.2.2 删改数据
如果发生删改操作,既要更新数据库,也要删除缓存,这两个操作如果无法保证原子性,就会发生数据不一致的情况。
那么到底是先更新数据库,再删除缓存?还是先删除缓存,再更新数据库呢?我们分别来讨论下。
先删除缓存,再更新数据库
假如删除缓存成功,更新数据库失败,那么当应用再次访问数据时,发生缓存缺失,就去访问数据库,而数据库中的值为旧值。
先更新数据库,再删除缓存
如果更新数据库成功,删除缓存失败了,那数据库中的值是最新值,缓存中的值是旧值,这肯定是不一致的。
重试机制
从上面的流程可以看出,无论是哪种情况,都会发生数据不一致的情况,那如何解决呢?就是重试机制。
比如说,当应用删除缓存失败或更新数据库失败时,可以把要删除的缓存值或要更新的数据库值保存到消息队列中,然后从消息队列中重新读取这些值,再次的进行删除或更新。
并发情况
在上面的讨论中,说的是在更新数据库和删除缓存中,有一个失败的情况下,导致的"数据不一致"。
实际上,即使这两个操作都成功,当有大量并发请求时,应用还是有可能读到不一致的数据。
先删除缓存,再更新数据库
比如,线程 A 删除缓存成功,再还没来得及更新数据库时,线程 B 开始读取数据,它发现缓存缺失,然后去数据库读取数据,此时有两个问题:
- 线程 B 读取到了旧值;
- 线程 B 会把读取到的旧值,写入到缓存中,这会导致其他线程从缓存中读到旧值(会一直脏下去,直到缓存过期)
等到线程 B 执行完成后,线程 A 才开始更新数据库,此时数据库数据是最新值,缓存中是旧值。
针对这种情况,我们可以在线程 A 更新数据库后,sleep 一小段时间,再执行一次删除缓存操作。
线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。
这个时间怎么确定呢?可以在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。
当其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。
先更新数据库,再删除缓存
当线程 A 更新数据库后,还没来得及删除缓存,此时线程 B开始读取数据,发生缓存命中,读取到旧数据。
但是,线程 A很快就执行删除缓存,让缓存失效了,后续的查询请求会发生缓存缺失,然后去查询数据库最新值了。
所以,这种情况对业务的影响较小。
但是,有一种极端情况
但是这种情况,理论上会出现,实际上概率特别低。
它需要满足,在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
总结
下面我们来总结下,发生数据不一致的情况有哪些
- 删除缓存或更新数据库某一步失败而导致的数据不一致,可以使用重试机制确保删除或更新操作成功。
- 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。