如何保证Redis缓存和数据库的数据一致性

前言

  如果项目业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可,这时架构模型是这样的:

  但随着业务量的增长,项目业务请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。这个阶段通常的做法是,引入缓存来提高读性能,架构模型就变成了这样:

  在实际开发过程中,缓存的使用频率是非常高的,只要使用缓存和数据库存储,就难免会出现双写时数据一致性的问题,就是 Redis 缓存的数据和数据库中保存的数据出现不相同的现象。

image

  如上图所示,大多数人的很多业务操作都是根据这个图来做缓存的,这样能有效减轻数据库压力。但是一旦设计到双写或者数据库和缓存更新等操作,就很容易出现数据一致性的问题。无论是先写数据库,在删除缓存,还是先删除缓存,在写入数据库,都会出现数据一致性的问题。例如:

  • 先删除了redis缓存,但是因为其他什么原因还没来得及写入数据库,另外一个线程就来读取,发现缓存为空,则去数据库读取到之前的数据并写入缓存,此时缓存中为脏数据。
  • 如果先写入了数据库,但是在缓存被删除前,写入数据库的线程因为其他原因被中断了,没有删除掉缓存,就也会出现数据不一致的情况。

  总的来说,写和读在多数情况下都是并发的,不能绝对保证先后顺序,就会很容易出现缓存和数据库数据不一致的情况,那我们又该如何解决呢?

一、谈谈一致性

  首先,我们先来看看有哪几种一致性的情况呢?

  • 强一致性:如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。读请求和写请求会串行化,串到一个内存队列里去,这样会大大增加系统的处理效率,吞吐量也会大大降低。
  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型。一般情况下,高可用只确保最终一致性,不确保强一致性。

二、 情景分析

2.1 针对读场景

  A请求查询数据,如果命中缓存,那么直接取缓存数据返回即可。如果请求中不存在,数据库中存在,那么直接取数据库数据返回,然后将数据同步到Redis中。不会存在数据不一致的情况。

  在高并发的情况下,A请求和B请求一起访问某条数据,如果缓存中数据存在,直接返回即可,如果不存在,直接取数据库数据返回即可。无论A请求B请求谁先谁后,本质上没有对数据进行修改,数据本身没变,只是从缓存中取还是从数据库中取的问题,因此不会存在数据不一致的情况。

  因此,单独的读场景是不会造成Redis与数据库缓存不一致的情况,因此我们不用关心这种情况。

2.2 针对写场景

  如果该数据在缓存中不存在,那么直接修改数据库中的数据即可,不会存在数据不一致的情况。

  如果该数据在缓存中和数据库中都存在,那么就需要既修改缓存中的数据又修改数据库中的数据。如果写数据库的值与更新到缓存值是一样的,可以马上更新缓存;如果写数据库的值与更新缓存的值不一致,在高并发的场景下,还存在先后关系,这就会导致数据不一致的问题。例如:

  • 当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。
  • 在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况。

三、同步策略

  想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:

  从这4种同步策略中,我们需要作出比较的是:更新缓存与删除缓存哪种方式更合适?应该先操作数据库还是先操作缓存?

3.1 先更新缓存,再更新数据库

  这个方案我们一般不考虑。原因是当数据同步时,更新 Redis 缓存成功,但更新数据库出现异常时,会导致 Redis 缓存数据与数据库数据完全不一致,而且这很难察觉,因为 Redis 缓存中的数据一直都存在。

  只要缓存进行了更新,后续的读请求基本上就不会出现缓存未命中的情况。但在某些业务场景下,更新数据的成本较大,并不是单纯将数据的数据查询出来丢到缓存中即可,而是需要连接很多张表组装对应数据存入缓存中,并且可能存在更新后,该数据并不会被使用到的情况。

3.2 先更新数据库,再更新缓存

  这个方案我们一般也是不考虑。原因是当数据同步时,数据库更新成功,但 Redis 缓存更新失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。

  该方案还存在并发引发的一致性问题,假设同时有两个线程进行数据更新操作,如下图所示:

image

  从上图可以看到,线程1虽然先于线程2发生,但线程2操作数据库和缓存的时间,却要比线程1的时间短,执行时序发生错乱,最终这条数据结果是不符合预期的。如果是写多读少的场景,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

3.3 先删除缓存,后更新数据库

  这种方案只是尽可能保证一致性而已,极端情况下,还是有可能发生数据不一致问题,原因是当数据同步时,如果删除 Redis 缓存失败,更新数据库成功,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。由于缓存被删除,下次查询无法命中缓存,需要在查询后将数据写入缓存,增加查询逻辑。同时在高并发的情况下,同一时间大量请求访问该条数据,第一条查询请求还未完成写入缓存操作时,这种情况,大量查询请求都会打到数据库,加大数据库压力。

  该方案还存在并发引发的一致性问题,假设同时有两个线程进行数据更新操作,如下图所示。当缓存被线程一删除后,如果此时有新的读请求(线程二)发生,由于缓存已经被删除,这个读请求(线程二)将会去从数据库查询。如果此时线程一还没有修改完数据库,线程二从数据库读的数据仍然是旧值,同时线程二将读的旧值写入到缓存。线程一完成后,数据库变为新值,而缓存还是旧值。

image

  从上图可见,先删除 Redis 缓存,后更新数据库,当发生读/写并发时,还是存在数据不一致的情况。如何解决呢?最简单的解决办法就是延时双删策略:先淘汰缓存、再写数据库、休眠后再次淘汰缓存。这样做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

public void deleteRedisData(UserInfo userInfo){// 删除Redis中的缓存数据jedis.del(userInfo);// 更新MySQL数据库数据userInfoDao.update(userInfo);try {TimeUnit.SECONDS.sleep(2);} catch(Exception exp){exp.printStackTrace();}// 删除Redis中的缓存数据jedis.del(userInfo);
}

  延时双删就能彻底解决不一致吗?当然不一定来。首先,我们评估的延时时间并不能完全代表实际运行过程中的耗时,运行过程如果因为系统压力过大,我们评估的耗时就是不准确,仍然会导致数据不一致的出现。其次,延时双删虽然在保证事务提交完以后再进行删除缓存,但是如果使用的是MySQL的读写分离的机构,主从同步之间其实也会有时间差。

3.4 先更新数据库,后删除缓存

实际使用中,建议采用这种方案。当然,这种方案其实一样也可能有失败的情况。

  当数据同步时,如果更新数据库成功,而删除 Redis 缓存失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。

image

  该方案还存在并发引发的一致性问题,假设同时有两个线程进行数据更新操作,如下图所示。当数据库的数据被更新后,如果此时缓存还没有被删除,那么缓存中的数据仍然是旧值。如果此时有新的读请求(查询数据)发生,由于缓存中的数据是旧值,这个读请求将会获取到旧值。当缓存刚好失效,这时有请求来读缓存(线程一),未命中缓存,然后到数据库中读取,在要写入缓存时,线程二来修改了数据库,而线程一写入缓存的是旧的数据,导致了数据的不一致。

image

四、解决办法

  当我们在应用中同时使用MySQL和Redis时,如何保证两者的数据一致性呢?下面就来分享几种实用的解决方案。

4.1 双写一致性

  最直接的办法就是在业务代码中同时对MySQL和Redis进行更新。通常我们会先更新MySQL,然后再更新Redis。

// 更新MySQL
userMapper.update(user);
// 更新Redis
redisTemplate.opsForValue().set("user_" + user.getId(), user);

  这种方式最大的问题就是在于网络故障或者程序异常的情况下,可能会导致MySQL和Redis中的数据不一致。因此,我们需要额外的手段来检测和修复数据不一致的情况。

4.2 异步更新(异步通知)

  在更新数据库数据时,同时发送一个异步通知给Redis,让Redis知道数据库数据已经更新,需要更新缓存中的数据。这个过程是异步的,不会阻塞数据库的更新操作。当Redis收到异步通知后,会立即删除缓存中对应的数据,确保缓存中没有旧数据。这样,即使在这个过程中有新的读请求发生,也不会读取到旧数据。等到数据库更新完成后,Redis再次从数据库中读取最新的数据并缓存起来。

// 更新MySQL
userMapper.update(user);
// 发送消息
rabbitTemplate.convertAndSend("updateUser", user.getId());/*** 然后在消息消费者中更新Redis。*/
@RabbitListener(queues = "updateUser")
public void updateUser(String userId) {User user = userMapper.selectById(userId);redisTemplate.opsForValue().set(redisTemplate.opsForValue().set("user_" + user.getId(), user);
}

  这种异步通知的方式,可以确保Redis中的数据与数据库中的数据保持一致,避免出现数据不一致的情况。这种方案可以降低数据不一致的风险,但仍然无法完全避免。因为消息队列本身也可能因为各种原因丢失消息。

4.3 使用Redis的事务支持

  Redis提供了事务(Transaction)支持,可以将一系列的操作作为一个原子操作执行。我们可以利用Redis的事务来实现MySQL和Redis的原子更新。

redisTemplate.execute(new Sessioncallback<Object>(){@0verridepublic Object execute(RedisOperations operations) throws DataAccessException {// 开启事务operations.multi();// 更新MySQLuserMapper.update(user);// 更新Redisoperations.opsForValue().set("user_" + user.getId(),user);// 执行事务operations.exec();return null;}
});

  使用Redis事务可以确保MySQL和Redis的更新在同一事务中执行,避免了中间出现不一致的情况。但需要注意的是,Redis的事务并非严格的ACID事务,可能存在部分成功的情况。

4.4 用 Redisson 实现读锁和写锁

  Redisson 是一个基于 Redis 的分布式 Java 对象存储和缓存框架,它提供了丰富的功能和 API 来操作 Redis 数据库,其中包括了读写锁的支持。读写锁是一种常用的并发控制机制,它允许多个线程同时读取共享资源,但在写操作时互斥,只允许一个线程进行写操作。使用 Redisson 的读写锁方法:

  1. 获取读锁:通过 Redisson 的 RReadWriteLock 对象的 readLock() 方法获取读锁。在获取读锁后,可以并发读取共享资源,不会阻塞其他获取读锁的线程。
  2. 获取写锁:通过 Redisson 的 RReadWriteLock 对象的 writeLock() 方法获取写锁。在获取写锁后,其他获取读锁和写锁的线程将被阻塞,只有当前线程能够进行写操作。
  3. 释放锁:使用完读锁或写锁后,应该及时调用 unlock() 方法释放锁,以便其他线程可以获取锁并进行操作。在 Redisson 中,读锁和写锁都继承自锁对象 RLock,因此可以使用 RLock 的 unlock() 方法来释放锁。

  下面是一个使用 Redisson 读写锁的示例,通过 Redisson 的 RReadWriteLock 对象获取读锁和写锁,并在需要的代码段中进行相应的操作。执行完操作后,使用 unlock() 方法释放锁,最后关闭 Redisson 客户端。

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;public class RedissonReadWriteLockExample {public static void main(String[] args) {// 创建 Redisson 客户端Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);// 获取读写锁RReadWriteLock rwLock = redisson.getReadWriteLock("myLock");RLock readLock = rwLock.readLock();RLock writeLock = rwLock.writeLock();try {// 获取读锁并进行读操作readLock.lock();// 读取共享资源// 获取写锁并进行写操作writeLock.lock();// 写入共享资源} finally {// 释放锁writeLock.unlock();readLock.unlock();}// 关闭 Redisson 客户端redisson.shutdown();}
}

五、结语

  综上所述,我们提供了更全面的MySQL与Redis数据一致性解决方案。根据具体的业务需求和系统环境,选择合适的方案可以提高数据一致性的可靠性。然而,每种方案都有其优缺点和适用场景,需要综合考虑权衡。

  对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

把今天最好的表现当作明天最新的起点…...~

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

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

相关文章

链表 OJ(一)

移除链表元素 题目连接&#xff1a; https://leetcode.cn/problems/remove-linked-list-elements/description/ 使用双指针法&#xff0c;开始时&#xff0c;一个指针指向头节点&#xff0c;另一个指针指向头节点的下一个结点&#xff0c;然后开始遍历链表删除结点。 这里要注…

【SGX系列教程】(五)enclave多线程测试,以及EPC内存测试

文章目录 一. 概述二. 原理分析2.1 多线程在Enclave中的实现流程2.2 多线程和EPC内存分配之间的冲突2.3 解决多线程和EPC内存分配冲突的策略 三. 源码分析3.1 代码结构3.2 源码3.2.1 App文件夹3.2.2 Enclave文件夹3.2.3 Makefile 3.3 总结 四.感谢支持 一. 概述 在Intel SGX环境…

HarmonyOS(43) @BuilderParam标签使用指南

BuilderParam BuilderParam使用举例定义模板定义具体实现BuilderParam初始化 demo源码参考资料 BuilderParam 该标签有的作用有点类似于设计模式中的模板模式&#xff0c;类似于指定一个UI占位符&#xff0c;具体的实现交给具体的Builder&#xff0c;顾名思义&#xff0c;可以…

【算法】排序算法介绍 附带C#和Python实现代码

1. 冒泡排序(Bubble Sort) 2. 选择排序(Selection Sort) 3. 插入排序(Insertion Sort) 4. 归并排序(Merge Sort) 5. 快速排序(Quick Sort) 排序算法是计算机科学中的一个基础而重要的部分,用于将一组数据按照一定的顺序排列。下面介绍几种常见的排序算法,…

可控学习综述:信息检索中的方法、应用和挑战

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

YOLOv10改进 | Conv篇 | 利用YOLO-MS的MSBlock轻量化网络结构(既轻量又长点)

一、本文介绍 本文给大家带来的改进机制是利用YOLO-MS提出的一种针对于实时目标检测的MSBlock模块(其其实不能算是Conv但是其应该是一整个模块)&#xff0c;我们将其用于C2f中组合出一种新的结构&#xff0c;来替换我们网络中的模块可以达到一种轻量化的作用&#xff0c;我将其…

【Python基础】代码如何打包成exe可执行文件

本文收录于 《一起学Python趣味编程》专栏&#xff0c;从零基础开始&#xff0c;分享一些Python编程知识&#xff0c;欢迎关注&#xff0c;谢谢&#xff01; 文章目录 一、前言二、安装PyInstaller三、使用PyInstaller打包四、验证打包是否成功五、总结 一、前言 本文介绍如何…

SFUZZ模糊测试平台全新升级,从标准到实践助力车企安全出海

开源网安模糊测试平台SFuzz全新升级&#xff0c;参照各国相关标准要求进行针对性建设&#xff0c;可为智能网联汽车信息安全测试提供更为强大的工具支持。SFuzz向被测系统输入大量随机数据&#xff0c;模拟各种异常情况&#xff0c;可以发现被测系统内潜在的缺陷和漏洞&#xf…

ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享

ChatGPT高质量prompt技巧分享pdf&#xff0c; ChatGPT提问获取高质量答案的艺术pdf。本书是一本全面的指南&#xff0c;介绍了各种 Prompt 技术的理解和利用&#xff0c;用于从 ChatGPTmiki sharing中生成高质量的答案。我们将探讨如何使用不同的 Prompt 工程技术来实现不同的目…

C语言——结构体

一、定义和使用结构体 1.1概述 前面我们见过的数据类型&#xff0c;比如int,float,char等是在程序中简单的使用&#xff0c;如果我们要根据自己的需求来建立一些复杂的数据&#xff0c;就需要用到结构体。 例如&#xff0c;一个学生的学号&#xff0c;姓名&#xff0c;性别&am…

Python 利用pandas处理CSV文件(DataFrame的基础用法)

前面介绍过通过Python标准库中的CSV模块处理CSV文件&#xff1a; Python 利用CSV模块处理数据 相比CSV模块&#xff0c;pandas的功能更加强大&#xff0c;本文将简单介绍如何通过pandas来处理CSV文件。 文章目录 一、pandas简介二、用法示例2.1 读取CSV文件2.1.1 read_csv参数…

Python 视频的色彩转换

这篇教学会介绍使用OpenCV 的cvtcolor() 方法&#xff0c;将视频的色彩模型从RGB 转换为灰阶、HLS、HSV...等。 因为程式中的OpenCV 会需要使用镜头或GPU&#xff0c;所以请使用本机环境( 参考&#xff1a;使用Python 虚拟环境) 或使用Anaconda Jupyter 进行实作( 参考&#x…

火柴棒图python绘画

使用Python绘制二项分布的概率质量函数&#xff08;PMF&#xff09; 在这篇博客中&#xff0c;我们将探讨如何使用Python中的scipy库和matplotlib库来绘制二项分布的概率质量函数&#xff08;PMF&#xff09;。二项分布是统计学中常见的离散概率分布&#xff0c;描述了在固定次…

MUNIK解读ISO26262--系统架构

功能安全之系统阶段-系统架构 我们来浅析下功能安全系统阶段重要话题——“系统架构” 目录概览&#xff1a; 系统架构的作用系统架构类型系统架构层级的相关安全机制梳理 1.系统架构的作用 架构的思维包括抽象思维、分层思维、结构化思维和演化思维。通过将复杂系统分解…

OZON生活家居用品爆款新品

OZON生活家居用品爆款新品涵盖了多个方面&#xff0c;这些产品不仅满足了消费者对生活品质的追求&#xff0c;也反映了当前市场的热门趋势。以下是一些在OZON平台上备受关注的生活家居用品爆款新品&#xff1a; OZON生活家居用品爆款新品工具&#xff1a;D。DDqbt。COm/74rD T…

昇思25天学习打卡营第17天|基于MobileNetv2的垃圾分类

今天学习的内容是利用视觉图像技术&#xff0c;来实现垃圾分类代码开发的方法。通过读取本地图像数据作为输入&#xff0c;对图像中的垃圾物体进行检测&#xff0c;并且将检测结果图片保存到文件中。 本章节主要包括8部分内容&#xff1a; 1、实验目的 1、了解熟悉垃圾分类应用…

防御笔记第四天(持续更新)

1.状态检测技术 检测数据包是否符合协议的逻辑顺序&#xff1b;检查是否是逻辑上的首包&#xff0c;只有首包才会创建会话表。 状态检测机制可以选择关闭或则开启 [USG6000V1]firewall session link-state tcp ? check Indicate link state check [USG6000V1]firewall ses…

实践致知第12享:如何新建一个Word并设置格式

一、背景需求 小姑电话说&#xff1a;要新建一个Word文档&#xff0c;并将每段的首行设置空2格。 二、解决方案 1、在电脑桌面上空白地方&#xff0c;点击鼠标右键&#xff0c;在下拉的功能框中选择“DOC文档”或“DOCX文档”都可以&#xff0c;如下图所示。 之后&#xff0…

光学传感器图像处理流程(二)

光学传感器图像处理流程&#xff08;二&#xff09; 2.4. 图像增强2.4.1. 彩色合成2.4.2 直方图变换2.4.3. 密度分割2.4.4. 图像间运算2.4.5. 邻域增强2.4.6. 主成分分析2.4.7. 图像融合 2.5. 裁剪与镶嵌2.5.1. 图像裁剪2.5.2. 图像镶嵌 2.6. 遥感信息提取2.6.1. 目视解译2.6.2…

沙龙回顾|MongoDB如何充当企业开发加速器?

数据不仅是企业发展转型的驱动力&#xff0c;也是开发者最棘手的问题。前日&#xff0c;MongoDB携手阿里云、NineData在杭州成功举办了“数据驱动&#xff0c;敏捷前行——MongoDB企业开发加速器”技术沙龙。此次活动吸引了来自各行各业的专业人员&#xff0c;共同探讨MongoDB的…