如何解决缓存和数据库的数据不一致问题

数据不一致问题是操作数据库和操作缓存值的过程中,其中一个操作失败的情况。实际上,即使这两个操作第一次执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据。

如何更新缓存

更新缓存的步骤就两步,更新缓存和更新数据库。但是这两步会引申多种情况。

  1. 先更新数据库还是先更新缓存?更新缓存时先删除还是直接更新?
  2. 假设第一步成功了,第二步失败了怎么办?
  3. 假设 2 个线程同时更新同一个数据,A 线程先完成第一步,B 线程先完成第二步,此时该怎么办?
先更新缓存,再更新数据库

对于这个组合可能会出现一下两种情况:

  1. 假设第 2 步数据库更新失败了,要求回滚缓存的更新,这时该怎么办呢?我们知道 Redis 不支持事务回滚,除非我们采用手工回滚的方式,先保存原有数据,然后再将缓存更新回原来的数据,这种解决方案不考虑。
  2. 线程A先将缓存的数据更新为 1,然后在更新数据库前,线程B将缓存的数据更新为 2,由于网络等原因,线程B先一步把数据库更新为 2,然后线程A将数据库的数据更新为 1,此时,就出现了缓存和数据库中数据不一致的现象,如下图所示。

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

使用这种组合,即使数据库更新失败了也不需要回滚缓存。这种组合会出现两种问题。

假设某个商品数量是10个,线程A要更新商品的数量为20,所以它会删除缓存中的内容。这时,另一个线程 B 要读取这个商品的数量,它查询缓存发现未命中后,会从数据库中读取到商品数量为10,并且写入到缓存中,然后线程 A 继续更改数据库,将商品的数量更新为20。

为了解决一致性问题,可以让线程A给Key加锁,这种处理方法可能会使大量的读请求卡在锁中。

先更新数据库,再更新缓存
  1. 数据库更新成功,更新缓存失败怎么办?

数据库更新成功后,不会因为缓存是否成功而回滚。此时一般会采取重试机制来补偿,但是重试机制如果存在延时还是会出现数据不一致的问题,不好处理。

  1. 两个线程同时更新同一个数据,线程A先完成第一步,线程B先完成了第二步怎么办?

线程A 先将数据库的数据更新为 1,然后在更新缓存前,线程B 将数据库的数据更新为 2,然后把缓存更新为 2,然后 线程A更新缓存为 1。
此时,数据库中的数据是 2,而缓存中的数据却是 1,缓存和数据库中的数据不一致。

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

假如某个商品数据在缓存中不存在,线程 A 读取数据时从数据库中查询到商品数量为10,在未写入缓存中时另一个线程 B 更新数据。它更新数据库中的商品数量为20,并且清空缓存。这时线程 A 把从数据库中读到的商品数量10 的数据写入到缓存中。

最终,该商品数量在缓存中是 10(旧值),在数据库中是 20(新值),缓存和数据库数据不一致。

从理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。

因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现线程 B 已经更新了数据库并且删除了缓存,线程 A 才更新完缓存的情况。

而一旦线程 A 早于线程 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。

所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

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

在先删除缓存,更新数据库后,线程A更新完数据库值以后,让他睡眠一小段时间,再进行一次缓存删除操作。

加上睡眠时间,是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。线程A 睡眠的时间,就需要大于线程B读取数据再写入缓存的时间。这个时间是不好确定的,需要根据业务来估算。

其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。

没有一个组合是完美的,它们都有可能读到旧数据的可能,只不过概率不同,我建议更新缓存时,先更新数据库再删除缓存。

任何一种方案都不是完美的,但如果为了解决极小出现的概率要花好几倍的代价去解决(比如订阅 binlog 日志),从技术上来讲是得不偿失的,所以需要同业务方去协调一个适用的方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/472549.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于requests框架实现接口自动化测试项目实战

requests库是一个常用的用于http请求的模块,它使用python语言编写,在当下python系列的接口自动化中应用广泛,本文将带领大家深入学习这个库,Python环境的安装就不在这里赘述了,我们直接开干。 01、requests的安装 wi…

信息安全技术基础知识

一、考点分布 信息安全基础(※※)信息加密解密技术(※※※)密钥管理技术(※※)访问控制及数字签名技术(※※※)信息安全的保障体系 二、信息安全基础 信息安全包括5个基本要素&#…

Python三级考试笔记

Python三级考试笔记【源源老师】 三级标准 一、 理解编码、数制的基本概念,并且会应用。 1. 能够进行二进制、十进制以及十六进制之间的转换; 2. 理解Python中的数制转换函数。 二、 掌握一维数据的表示和读写方法,能够编写程序处理一维数据…

蓝桥杯电子类单片机提升三——NE555

目录 单片机资源数据包_2023 一、NE555和定时器工作模式 1.NE555的介绍 2.定时器的计数模式 二、NE555频率读取代码的实现 1.定时器0初始化 2.通过读取TH0和TL0来读取频率 3.通过中断读取频率 三、完整代码演示 通过读取TH0和TL0来读取频率 main.c 通过中断读取频…

Docker 第十四章 : Docker 三剑客之 Machine

第十四章 : Docker 三剑客之 Machine 本章知识点: Docker Machine 是 Docker 三剑客之一,它是一个工具,允许用户在本地或远程机器上创建 Docker 主机。它简化了 Docker 环境的设置,特别是在不同的操作系统和云平台上。通过 Docker Machine,用户可以轻松地在虚拟机或物理…

Leetcode - 周赛384

目录 一,3033. 修改矩阵 二,3035. 回文字符串的最大数量 三,3036. 匹配模式数组的子数组数目 II 一,3033. 修改矩阵 这道题直接暴力求解,先算出每一列的最大值,再将所有为-1的区域替换成该列的最大值&am…

qt-C++笔记之打印所有发生的事件

qt-C笔记之打印所有发生的事件 code review! 文章目录 qt-C笔记之打印所有发生的事件1.ChatGPT问答使用 QApplication 的 notify 方法使用 QObject 的 event 方法 2.使用 QObject 的 event 方法3.使用 QApplication 的 notify 方法 1.ChatGPT问答 在Qt C中,若要打…

树莓派5 EEPROM引导加载程序恢复镜像

树莓派5不能正常启动,可以通过电源led灯的闪码来判断错误发生的大致情形。 LED警告闪码 如果树莓派由于某种原因无法启动,或者不得不关闭,在许多情况下,LED会闪烁特定的次数来指示发生了什么。LED会闪烁几次长闪烁,然…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 2月17日,星期六

每天一分钟,知晓天下事! 2024年2月17日 星期六 农历正月初八 1、 中疾控:我国自主研发的猴痘mRNA疫苗即将进入临床试验。 2、 2024年度总票房破100亿元,其中春节档已突破70亿元。 3、 国产大飞机首次国外亮相,C919已抵…

自动化测试-RIDE编写自动化脚本

自动化脚本软件测试的必修内容,是自动化测试的核心,脚本的逻辑严谨性、可维护性非常重要,优秀的自动化脚本需要能兼顾用例的正确有效性和自动化测试的效率,本篇文章将介绍如何用RIDE写自动化脚本。我们将深入探讨RIDE的具体用法&a…

Python hash函数

在Python编程中,hash()函数是一个重要的内置函数,用于计算对象的哈希值。哈希值是一种由固定长度的字符串表示的数据摘要,通常用于在散列表中快速查找、比较对象或数据完整性验证等场景。本文将深入探讨Python中的hash()函数,包括…

The method toList() is undefined for the type Stream

The method toList() is undefined for the type Stream &#xff08;JDK16&#xff09; default List<T> toList() { return (List<T>) Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray()))); }