缓存与数据库双写一致性几种策略分析

news/2024/12/27 10:28:48/

一、背景

在高并发场景中,为防止大量请求直接访问数据库,缓解数据库压力,常用的方式一般会增加缓存层起到缓冲作用,减少数据库压力。引入缓存,就会涉及到缓存与数据库中数据如何保持一致性问题,本文将对几种缓存与数据库保证数据一致性的使用方式进行分析。为保证高并发性能,以下分析场景不考虑执行的原子性及加锁等强一致性要求的场景,仅追求最终一致性。

二、读取过程



• 读缓存

• 如果缓存里没有值,那就读取数据库的值

• 同时把这个值写进缓存中

三、更新过程

更新操作有多种策略,各有优劣,主要针对此场景进行分析

策略 1:先更新 db,再删除缓存(常用的 Cache-Aside Pattern 旁路缓存)



问题:

1. 如果更新 db 成功,删缓存失败,将导致数据不一致

2. 极端场景,请求 A 读,B 写

1) 此时缓存刚好失效 2)A 查库得到旧值 3)B 更新 DB 成功

4)B 删除缓存 5)A 将查到的旧值更新到缓存中

此场景的发生需要步骤 2)查 db 始终慢于 3)的更新 db,才能导致 4)先于 5)执行,通常 db 的查询是要快于写入的,所以此极端场景的产生过于严格,不易发生

策略 2:先更新 db, 再更新缓存



问题:

1. 并发更新场景下,更新缓存会导致数据不一致

2. 根据读写比,考虑是否有必要频繁同步更新缓存,而且,如果构造缓存中数据过于复杂,或者数据更新频繁,但是读取并不频繁的情况,还会造成不必要的性能损耗

此种方式不推荐

策略 3: 先更新缓存,再更新 db



同上,不推荐

策略 4:先删缓存,再更新 db



先删缓存,虽然解决了策略 1 中,后删缓存如果失败的场景,但也会发生不一致的问题

例如:请求 A 删除缓存,这时请求 B 来查,就会击穿到数据库,B 读取到旧的值后写入缓存,A 正常更新 db, 由于时间差导致数据不一致的情况

策略 5:缓存延时双删



该策略兼容了策略 1 和策略 4, 解决了先删缓存还是后删缓存的问题,如策略 1 中,更新 db 后删缓存失败和策略 4 中的不一致场景,该策略可以将延时时间内(比如延时 10ms)所造成的缓存脏数据,再次删除。但是,如果延时删缓存失败,策略 4 中不一致问题还会发生,同时延时的实现,如创建线程,或者引入 mq 异步,可能会增加系统复杂度问题。

策略 6:变种双删,前置缓存过期时间



该策略针对策略 1 中后删缓存失败的场景,前置一层缓存数据过期时间(具体时间根据自身系统本身评估,如可覆盖 db 读写耗时或一致性容忍度等),更新 db 后就算删缓存失败,在 expire 时间后也能保证缓存中无数据。同时,前置 expire 失败,或者更新 db 失败,都不会影响数据一致。

能够解决策略 4 中的问题:请求 A 删除缓存,这时请求 B 来查,就会击穿到数据库,B 读取到旧的值后写入缓存,A 正常更新 db, 由于时间差导致数据不一致的情况,描述图如下:

本策略中步骤 1 为 expire 缓存,不会发生击穿缓存到数据库的情况,数据将直接返回。除非更极端情况,如下图:

expire 时间没有覆盖住更新 db 的耗时,类似策略 1 中极端场景,此处不赘述



四、总结

对于每种方案策略,各有利弊,但一致性问题始终存在(文章开头排除了原子性和锁),只是发生的几率在一点点慢慢变小了,方案的评估不仅要根据自身系统的业务场景,如读写比、并发量、一致性容忍度,还要考虑系统复杂度,投入产出比等,寻找最合适的方案。


http://www.ppmy.cn/news/46195.html

相关文章

JDK新增史上最无用提案!竟是为了简化Hello World?

前两天JDK 20更新了,很多人表示很失望,但是我万万没想到的是,还有更令人失望的。 OpenJDK最近又增加了一个新提案,JEP 445,这个提案的主要内容是要简化Hello World的写法。并且该新特性即将在Java 21中作为预览功能推出…

4.mybatis-plus-常用注解

1.TableName 描述:表名注解,标识实体类对应的表使用位置:实体类 将数据库中user表更名为mp_user TableName("mp_user") public class User {private Long id;private String name;private Integer age;private String email; }# 或…

Spring MVC Bean加载控制

回顾一下我们一般写的项目包括那些包吧: config目录存入的是配置类,写过的配置类有: ServletContainersInitConfigSpringConfigSpringMvcConfigJdbcConfigMybatisConfig controller目录存放的是SpringMVC的controller类service目录存放的是service接口和实现类dao目…

Nacos系列-Nacos服务注册与发现

服务注册与发现 1.故事背景2.服务注册2.1服务注册原理2.2服务注册实现 3服务发现3.1 服务发现原理3.2 服务发现实现3.3 LoadBalanced注解 总结提升 1.故事背景 上文我们讲到了Nacos的配置中心,介绍了什么是Nacos的配置中心,以及它的相关概念和使用方法。…

动力节点Springsecurity笔记01-05认证入门

1 问题 如何保护我们的程序? 1.1 创建code目录 目的:后面的security工程均在此目录下学习 创建code目录,并使用idea打开 1.2 不使用安全框架的springboot web程序 1.2.1 新建子模块springboot-01-hello [外链图片转存失败,源站可能有防盗…

深入剖析 Qt QMultiMap :原理、应用与技巧

目录标题 引言QMultiMap 的基本用法接口的用途和实际应用场景综合示例展示QMultiMap的所有用法 迭代器:遍历 QMultiMap 中的元素(Iterators: Traversing Elements in QMultiMap )QMultiMap 的高级用法QMultiMap 的优点和局限性优点局限性 QMu…

一起读源码 —— Fastjson 的核心方法及其实现原理

源码介绍 Fastjson 是阿里巴巴开源的一个 Java 工具库,它常常被用来完成 Java 的对象与 JSON 格式的字符串的相互转化。 此文读的源码是撰写此文时 Fastjson 的最新的发布版本,即 1.2.83 下载源码 请前去 github 找到 release 最新版下载后解压&…

JUC源码系列-ReentrantLock独占锁的释放

前言 开始之前先提一句, JAVA的内置锁在退出临界区之后是会自动释放锁的, 但是ReentrantLock这样的显式锁是需要自己显式的释放的, 所以在加锁之后一定不要忘记在finally块中进行显式的锁释放: Lock lock new ReentrantLock(); ... lock.lock(); try {// 更新对象//捕获异常…