读写分离场景下的缓存一致性问题

前言

对于一些并发较高,且读多写少的场景,为了提高性能,降低DB压力,大家肯定会想到缓存,这也是我们最常见的提高并发能力的手段;还有就是DB的读写分离,将一些对实时性没有这么高的请求,分流到从库,降低主库的压力。 对于大部分的业务场景来说,使用缓存就足够了(可能在业务发展初期,缓存都没有太大必要)。但是随着业务的发展,请求在经过缓存过滤了一层之后,DB主库负载还是很高,那么可能会再上读写分离,来分流请求。那么问题来了,这时候如果你的缓存是使用懒加载的模式,那么就很有可能会遇到缓存一致性的问题。

DB缓存一致性

在正常的场景下,DB缓存如何保证一致性? 一般有两种模式,Cache Aside 和 Cache Through, 我们基本上都是使用Cache Aside模式,即我们的程序直接和DB以及缓存打交道。

Cache Through(Read/Write Through)的方式一般是我们应用程序只和缓存进行交互,缓存再去通过各种方式去持久化数据到硬盘,这种方式正常场景下我们一般不会使用。它更多的是在类似于操作系统或缓存中间件中实现。

先更新缓存还是DB?

首先不管是更新缓存还是失效缓存,肯定是先更新DB再处理缓存,否则没办法保证数据一致性。

场景1

只要DB更新异常,那么缓存中就会保留错误的数据。虽然在更新DB失败时,可以回滚缓存,但是很麻烦,回滚缓存也可能失败。

场景2 并发场景下,会将旧数据写入缓存,而由于缓存操作要比DB操作快很多,一并出现并发,这种情况会频繁出现。

场景2 一般可以使用双删,但根本上也就是将缓存操作置后,保证缓存操作在DB操作之后来解决问题。

缓存失效 + 懒加载

一般场景下,我们会倾向于使用失效缓存的方式,和懒加载配合使用,代码复杂性低,且在大部分异常场景下,也可以保证数据最终一致性。

和先更新缓存再更新DB不同的是,假如我们删除缓存失败,那么事务回滚,DB和缓存的数据还是可以保持一致。

但是这种方式和上面一样,在并发场景下,一样也有小概率出现不一致的问题:

这个问题出现的概率极小,因为在删除缓存和提交事务之间,这个时间窗口是极短的。但是在读写分离的场景下,异常的时间窗口会变得很长,因为主从同步延迟的时间窗口远比上面这种场景要久得多,出现问题的概率也会更大:

它的异常窗口约等于主从同步的延迟

对于个问题,业界也有解决方案,我们也可以和双删一样,在更新完成后开个线程延迟一段时间再次删除缓存,或者投递到消息队列删除,本质上都是延迟删除,保证即使数据不一致,在过一小段时间后,我们也能保证数据的最终一致性。

但是在读写分离场景下(特别是跨机房的主从同步),上层调用方读到数据错误的概率太大了,甚至不需要并发场景,一个单线程的简单的先写后读的操作,几乎必然会读到旧数据。而且异常数据的存在时间取决于缓存过期时间

更好的解决办法?

这时候我会倾向于不再使用失效缓存的方式,而是先更新DB -> 再更新缓存的模式。

5秒的过期时间只是一个栗子,应该根据实际场景设置得比主从同步时间长一些

我们来看几种更新异常情况:

  1. DB更新异常,此时DB和缓存都不会更新。
  2. DB更新正常,缓存更新失败,DB回滚,缓存未更新。
  3. DB更新正常,由于网络问题,缓存更新超时,但是实际上更新成功了,但是超时异常导致DB回滚,此时DB数据是旧的,缓存数据是新的。
  4. 多个线程并发更新,先更新DB的线程却后更新了缓存,导致DB数据是新数据,缓存数据为旧数据。

对于1、2这种异常情况,由于事务的存在,数据一致性没有问题。

而3、4的异常情况,由于我们将缓存设置了一个极短的过期时间,错误的缓存数据也会快速失效,而由下次请求将正确的数据加载进缓存中。 这种方式在绝大部分的场景下 ,调用方读到的数据都是正确的数据,即使错误,也可以在短时间内纠正,从而保证数据的最终一致性。

这种方式不能说更好,只能说在部分场景更合适,而且它并不适用于那种缓存数据十分复杂,需要大量聚合操作的构成的场景。

结论

无论使用哪种方式,我们都只能保证数据的最终一致性,我们为了获得并发下的高性能,那么就必然会损失部分数据的一致性。这就像一个跷跷板,压下这头,另一头就会翘起来。而我们要做的,则是根据实际的业务场景,进行权衡,选取最合适的方案。

相关推荐
DemonAvenger2 分钟前
MySQL索引原理深度解析与优化策略实战
数据库·mysql·性能优化
CodeSheep17 分钟前
Stack Overflow,轰然倒下了!
前端·后端·程序员
GoGeekBaird25 分钟前
GoHumanLoopHub开源上线,开启Agent人际协作新方式
人工智能·后端·github
水痕0143 分钟前
gin结合minio来做文件存储
java·eureka·gin
Victor3561 小时前
Redis(8)如何安装Redis?
后端
寒士obj1 小时前
Spring事物
java·spring
Victor3561 小时前
Redis(9)如何启动和停止Redis服务?
后端
柯南二号2 小时前
【Java后端】Spring Boot 集成 MyBatis-Plus 全攻略
java·spring boot·mybatis
程序员爱钓鱼3 小时前
Go语言实战案例-创建模型并自动迁移
后端·google·go