Redis篇--常见问题篇6--缓存一致性1(Mysql和Redis缓存一致,更新数据库删除缓存策略)

1、概述

在使用Redis作为MySQL的缓存层时,缓存一致性问题是指Redis中的缓存数据与MySQL数据库中的实际数据不一致的情况。这可能会导致读取到过期或错误的数据,从而影响系统的正确性和用户体验。
为了减轻数据库的压力,通常读操作都是先读缓存,缓存没有则读数据库数据在写入缓存;而增/删/改操作介于数据库和缓存之间,由于操作步骤和并发问题,可能产生不一致的现象。

2、缓存一致性问题的表现

  • 脏读:客户端从Redis中读取到的是旧数据或过期数据,而MySQL中的数据已经发生了变化。

3、缓存一致性问题的原因

- 缓存更新不及时:当MySQL中的数据发生变化时,Redis中的缓存没有及时更新或删除,导致客户端读取到过期数据。
- 缓存失效策略不合理:如果缓存的TTL(生存时间)设置不当,可能会导致缓存过早或过晚失效,进而引发一致性问题。
- 并发写入冲突:在高并发场景下,多个客户端同时对同一数据进行写操作,可能导致缓存和数据库之间的数据不一致。

4、解决缓存一致性问题的方法

为了确保Redis和MySQL之间的数据一致性,可以采用以下几种常见的解决方案:

(1)、更新数据库时同步更新缓存(Write Through)

- 原理:

  • 在更新MySQL数据的同时,立即更新Redis中的缓存。这样可以确保缓存中的数据始终与数据库保持一致。

- 优点:

  • 简单易实现,能够保证强一致性。

- 缺点:

  • 写操作的性能会受到影响,因为每次写操作都需要同时更新数据库和缓存。
  • 高并发下Redis写操作结果的不确定性,很可能造成非预期的结果。(删除却能保证结果一致)
  • Redis的写操作可能会造成底层数据结构的改变,造成额外时间开销。如(List的压缩列表转双向列表)。

- 适用场景:

  • 适用于对数据一致性要求较高的场景,尤其是写操作较少的系统。

(2)、更新数据库后删除缓存(Write Behind)(推荐)

- 原理:

  • 在更新MySQL数据后,立即将Redis中对应的缓存键删除。下次读取时,Redis会发现缓存已失效,重新从MySQL中加载最新的数据并更新缓存。

- 优点:

  • 写操作的性能较高,因为只需要更新数据库,不需要立即更新缓存。
  • 避免了缓存不一致问题(高并发场景下更新缓存可能造成缓存结果不确定,但是删除操作结果是确定的)。

- 缺点:

  • 存在短暂的时间窗口,期间可能会读取到旧数据(弱一致性)。
  • 可能会触发缓存击穿,尤其是在高并发场景下。

- 适用场景:

  • 适用于对数据一致性要求不高,但对写性能要求较高的场景。

代码示例:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.concurrent.TimeUnit;@Service
public class ProductService {@Autowired
private ProductRepository productRepository; @Autowired
private RedisTemplate<String, Object> redisTemplate;  @Autowired
private RedissonClient redissonClient;  @Autowiredprivate EntityManager entityManager;  // 数据库// Redis 锁前缀private static final String LOCK_PREFIX = "product:lock:";// 缓存键前缀private static final String CACHE_KEY_PREFIX = "product:cache:";/*** 更新产品信息,并确保缓存一致性* @param productId 产品ID* @param newPrice  新的价格*/@Transactionalpublic void updateProductPrice(Long productId, double newPrice) {// 1. 获取分布式锁,确保同一时间只有一个线程可以更新该产品的价格RLock lock = redissonClient.getLock(LOCK_PREFIX + productId);try {// 尝试获取锁,最多等待5秒,锁的持有时间为10秒if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {// 2. 开始数据库事务,更新产品价格Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));// 更新产品价格product.setPrice(newPrice);productRepository.save(product);// 3. 删除Redis中的缓存,确保下次读取时能够从数据库中获取最新的数据redisTemplate.delete(CACHE_KEY_PREFIX + productId);// 4. 手动刷新实体管理器,确保事务提交后的数据一致性entityManager.flush();} else {throw new RuntimeException("Failed to acquire lock for product " + productId);}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("Interrupted while trying to acquire lock", e);} finally {// 5. 释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();}}}/*** 获取产品信息,优先从缓存中读取,如果缓存不存在则从数据库中读取并更新缓存** @param productId 产品ID* @return 产品信息*/public Product getProductById(Long productId) {// 1. 尝试从 Redis 缓存中获取产品信息String cacheKey = CACHE_KEY_PREFIX + productId;Product cachedProduct = (Product) redisTemplate.opsForValue().get(cacheKey);if (cachedProduct != null) {// 2. 如果缓存存在,直接返回缓存中的数据return cachedProduct;}// 3. 如果缓存不存在,从数据库中获取产品信息Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));// 4. 使用分布式锁,确保只有一个线程能够更新缓存RLock lock = redissonClient.getLock(LOCK_PREFIX + productId);try {// 尝试获取锁,最多等待5秒,锁的持有时间为10秒if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {// 5. 再次检查缓存,防止其他线程已经更新了缓存cachedProduct = (Product) redisTemplate.opsForValue().get(cacheKey);if (cachedProduct == null) {// 6. 如果缓存仍然不存在,将数据库中的数据写入缓存redisTemplate.opsForValue().set(cacheKey, product, 60, TimeUnit.MINUTES);  // 设置缓存过期时间为60分钟}}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("Interrupted while trying to acquire lock", e);} finally {// 7. 释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();}}// 8. 返回产品信息return product;}
}

(3)、为什么先更新数据库,后更新缓存呢?

1、先更新数据库,再更新缓存(Write Behind)

**- 原理:**在写操作时,首先更新MySQL数据库中的数据,然后更新Redis缓存中的数据。这样可以确保数据库中的数据是最新的,即使缓存更新失败,数据库中的数据仍然是正确的。

- 优点:

  • 数据安全:数据库中的数据始终是最新的,确保最终数据的正确性和安全性。
  • 容错性好:如果Redis更新失败或Redis服务不可用,系统仍然可以依赖MySQL中的数据,不会导致数据丢失。
  • 简化回滚逻辑:如果写操作失败,只需回滚数据库中的事务,而不需要同时处理缓存的回滚,降低了复杂性。

- 缺点:

  • 短暂的不一致:在数据库更新成功但缓存尚未更新的时间窗口内,客户端可能会读取到旧的缓存数据。这个时间窗口的长度取决于缓存更新的延迟(通常比较短,可以接受)。
  • 并发写入冲突:在高并发场景下,多个客户端可能同时对同一数据进行写操作,导致缓存更新的竞争问题。可以通过分布式锁等机制解决,但会增加系统复杂度。
  • 写放大问题:每次写操作都需要同时更新数据库和缓存,增加了写操作的开销,尤其是在高并发场景下,可能会对性能产生一定影响。

- 适用场景:

  • 对数据一致性要求较高:如果你的应用对数据一致性要求较高,尤其是不允许读取到过期数据,那么先更新数据库再更新缓存是更好的选择。
  • 容错性要求高:如果你希望即使 Redis 出现故障,系统仍然能够正常运行并依赖数据库中的最新数据,那么这种方案更合适。
2、先更新缓存,再更新数据库(Write Through)

- 原理:在写操作时,首先更新Redis缓存中的数据,然后再更新MySQL数据库中的数据。这样可以确保客户端在写操作完成后立即读取到最新的数据,避免了短暂的不一致问题。

- 优点

  • 避免短暂不一致:客户端在写操作完成后立即可以读取到最新的数据,避免了短暂的不一致问题。
  • 减少缓存击穿:由于缓存已经提前更新,后续的读请求可以直接从Redis中获取最新的数据,减少了缓存击穿的可能性。

- 缺点

  • 数据丢失风险:如果Redis更新成功但MySQL更新失败,可能会导致数据丢失或不一致。此时,Redis中的数据是最新的,但MySQL中的数据仍然是旧的。
  • 复杂的回滚逻辑:如果写操作失败,需要同时回滚Redis和MySQL中的数据,增加了系统的复杂性。特别是当Redis和MySQL之间的事务无法原子化时,可能会导致部分更新成功、部分更新失败的情况。
  • 缓存污染:如果Redis更新成功但MySQL更新失败,Redis中的缓存可能会被污染,导致后续读取到错误的数据。为了解决这个问题,通常需要引入额外的机制(如消息队列、分布式锁等)来确保缓存和数据库的一致性。

- 适用场景

  • 读操作占主导:如果你的应用以读操作为主,写操作较少,先更新缓存可以确保读操作的性能和一致性。
  • 容忍一定的数据丢失风险:如果你的应用可以容忍一定的数据丢失风险,或者有其他机制(如定期同步、备份等)来确保数据的最终一致性,那么这种方案是可以考虑的。
3、最佳实践:结合两者的优势

*先更新数据库,再删除缓存通常是最优的方法,也是最常用的做法。*写操作时,首先更新MySQL数据库中的数据,然后删除Redis中对应的缓存键。下次读取时,Redis会发现缓存已失效,重新从MySQL中加载最新的数据并更新缓存。这种方法既保证了数据库中的数据始终是最新的,又避免了缓存和数据库不一致的问题。

- 优点:

  • 强一致性:数据库中的数据始终是最新的,避免了数据丢失的风险。
  • 简化回滚逻辑:如果写操作失败,只需回滚数据库中的事务,而不需要同时处理缓存的回滚。
  • 减少缓存污染:即使Redis更新失败,也不会导致缓存污染,因为缓存已经被删除。

- 缺点:

  • 短暂的不一致:在数据库更新成功但缓存尚未更新的时间窗口内,客户端可能会读取到旧的缓存数据。(但这个时间通常很短可以接受)
  • 缓存击穿风险:如果大量并发请求同时访问同一个缓存键,可能会导致缓存击穿。但可以通过引入缓存预热、分布式锁等机制来缓解这个问题。

在绝大部分的系统中,数据安全永远才是第一位的,如果以牺牲数据安全为代价来提升系统性能通常都是不可取的。为了保障数据的安全,一般都要将数据保存到数据库中,而不是保存在缓存中(丢失风险大)。缓存最根本的目的是为了提升系统的查询的效率,减轻数据库的查询负担。如果成功更新了缓存,但是在执行更新数据库时服务器突然宕机了。此时缓存中是最新数据,数据库中仍然是旧数据,从数据安全的角度来说就是丢失了数据。所以通常建议一定是先更新数据库,保证数据安全不丢失为第一位。

(4)、其他优化方案

通常我们使用先更新数据库后删除缓存(如上4.2)的方式就足够了。此外还有一些其他优化的方式可以了解下。

1、消息队列MQ

对于一些分布式的场景,可以使用消息队列来解耦MySQL和Redis的写入操作。

在同时操作缓存和数据库时,都无法保证两者都能一次性操作成功,所以我们最好的办法就是重试,这个重试并不是立即重试,因为缓存和数据库可能因为网络或者其它原因停止服务了,立即重试成功率极低,而且重试会占用线程资源,显然不合理,所以我们需要采用异步重试机制。

异步重试我们可以使用消息队列来完成,因为消息队列可以保证消息的可靠性,消息不会丢失,也可以保证正确消费,当且仅当消息消费成功后才会将消息从消息队列中删除。
在这里插入图片描述
说明下:
这种方式需要介入MQ(如RocketMQ、Kafka 2.5+),虽然发布消息到消息队列的速度比直接删除Redis键的速度要慢。但是消息队列可以保证消息的可靠性,提供了异步重试机制,保证任务执行成功后才会删除任务。如果我们把删除Redis键的任务交给消息队列就可以确保成功,避免了Redis直接删除键失败的情况。

个人觉得:这种方式安全性比较好,但实现消息队列带来的成本比较大,也更复杂。仅用消息队列去删除Redis键,实际比直接删除更慢,而且Redis删除key失败的情况非常低,通常没有必要这么做。

2、Canal+Binlog同步

Canal是一个基于MySQL Binlog的增量数据同步工具。它通过监听MySQL的Binlog日志,捕获所有的数据变更(如插入、更新、删除)。当数据库发生变更时,canal就可以帮我们拿到具体操作的数据,然后再去根据具体的数据,去删除对应的缓存。
通过这种方式,我们仅需要关注mysql的修改,无需关心缓存的修改。当修改一条mysql的数据时,mysql就会生成一条binlog日志,我们可以通过Canal订阅这种消息,拿到具体修改的数据,之后就可以在更新缓存了。订阅日志目前比较流行的就是阿里开源的Canal。
注意:Canal本身是没有数据处理能力的,我们可以结合Canal +消息队列一起来使用,从而达到实现更新缓存的操作。
原理示意图:
在这里插入图片描述

优点:

  • 自动同步:无需手动编写代码来同步数据,Canal会自动捕获MySQL的变更并同步到Redis。
  • 低延迟:Canal可以实时捕获MySQL的变更,确保Redis和MySQL之间的数据同步延迟较低。
  • 最终一致性:虽然不能保证强一致性,但可以通过Canal的重试机制和幂等性设计来保证最终一致性。

缺点:

  • 依赖MySQL的Binlog:Canal需要MySQL开启 Binlog,并且必须使用ROW格式的Binlog,否则无法捕获详细的变更信息。
  • 单点故障:Canal本身可能存在单点故障,建议使用Canal的集群模式或多实例部署来提高可用性。
标题扩展介绍下Canal:
1、概念

Canal是阿里巴巴开源的一款基于MySQL数据库增量日志解析的工具,它能够实时捕获MySQL的Binlog(二进制日志),并将这些变更事件转发到其他系统(如Kafka、Redis、Elasticsearch等)。
Canal的核心功能是通过模拟MySQL主从复制协议,监听MySQL的Binlog日志,从而实现数据的实时同步。

2、Canal监听MySQL日志的原理

(1)、模拟MySQL主从复制:

  • Canal通过MySQL的主从复制协议与MySQL建立连接。它模拟了一个MySQL从库的行为,向MySQL发送SHOW MASTER STATUS和SHOW SLAVE STATUS等命令,获取当前的Binlog文件名和位置。

(2)、订阅Binlog事件:

  • Canal使用MySQL提供的binlog dump协议,订阅MySQL的Binlog事件。MySQL会将所有的DDL(数据定义语言)和DML(数据操作语言)操作(如INSERT、UPDATE、DELETE)以二进制日志的形式发送给Canal。

(3)、解析Binlog事件:

  • Canal接收到Binlog事件后,会解析这些二进制日志,提取出具体的表结构变化和数据变更信息。Canal支持多种解析格式,包括Row-based、Statement-based和Mixed-based。

(4)、转发变更事件:

  • 解析后的变更事件可以通过Canal的插件机制,转发到其他系统(如Kafka、Redis、Elasticsearch等),或者直接在应用程序中处理。
3、Canal的架构

Canal 的架构主要包括以下几个组件:

  • Canal Server:负责与MySQL建立连接,监听Binlog日志,并将解析后的变更事件转发给下游系统。
  • Canal Client:负责接收Canal Server发送的变更事件,并进行相应的处理。
  • Canal Adapter:用于将Canal解析的变更事件转发到不同的目标系统(如Kafka、Redis、Elasticsearch等)。

个人觉得:这个方法,首先需要mysql启用binlog日志。还需要我们下载和安装Canal,在配置并启动Canal。然后代码端还要集成Canal的实现。可谓是既费时又费劲,如果只是为了实现删除缓存,个人感觉真的没有必要。

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

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

相关文章

探寻快速排序的局限性及其优化策略

一. 快速排序之局限 快速排序的平均时间复杂度为O(nlogn)。其核心步骤是&#xff1a;先从待排序数组中选定一个元素作为基准&#xff08;pivot&#xff09;&#xff0c;通过一趟排序将数组分成两部分&#xff0c;使得左边部分的元素都小于等于基准元素&#xff0c;右边部分的元…

CS!GO

CS&#xff08;computer science&#xff09;计算机科学&#xff0c;说实话&#xff0c;不是找工作面试&#xff0c;这些题谁会背啊&#xff0c;反正我不行&#xff0c;一问三不知。 咱也不管这些&#xff0c;这个系列&#xff0c;可能会时不时的给出一些计网和操作系统相关的东…

python 内存管理

Python中的内存管理涉及包含所有Python对象和数据结构的私有堆。Python内存管理器在内部确保对此私有堆的管理。需要注意的是&#xff0c;Python堆的管理是由解释器本身执行的&#xff0c;并且用户无法控制它。从源码来看&#xff0c;分为以下几层&#xff1a; level 3&#xf…

Matlab个性化绘图第6期—带标记面的三维折线图

带标记面的三维折线图本质上就是多组折线图&#xff1a; Matlab论文插图绘制模板第92期—折线图&#xff08;Plot&#xff09; 或者三维折线图&#xff1a; Matlab论文插图绘制模板第37期—三维折线图(plot3) 不同之处在于带标记面的三维折线图把每一组数据单独放在一个三维平…

C/C++圣诞树

系列文章 序号直达链接1C/C爱心代码2C/C跳动的爱心3C/C李峋同款跳动的爱心代码4C/C满屏飘字表白代码5C/C大雪纷飞代码6C/C烟花代码7C/C黑客帝国同款字母雨8C/C樱花树代码9C/C奥特曼代码10C/C精美圣诞树11C/C俄罗斯方块12C/C贪吃蛇13C/C孤单又灿烂的神-鬼怪14C/C闪烁的爱心15C…

【C++语言】多态

一、多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某种行为&#xff0c;当不同的对象去完成时会产生出不同的状态。 我们可以举一个例子&#xff1a; 比如买票这种行为&#xff0c;当普通人买票时&#xff0c;是全价买票&am…

GitCode 光引计划投稿|MilvusPlus:开启向量数据库新篇章

在人工智能和大数据时代&#xff0c;向量数据库作为处理非结构化数据的核心技术&#xff0c;正变得越来越重要。MilvusPlus&#xff0c;作为「光引计划」的一部分&#xff0c;应运而生&#xff0c;旨在提供一个高性能、易扩展、全功能的向量数据库解决方案。项目背景根植于对现…

Java设计模式 —— 【结构型模式】外观模式详解

文章目录 概述结构案例实现优缺点 概述 外观模式又名门面模式&#xff0c;是一种通过为多个复杂的子系统提供一个一致的接口&#xff0c;而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口&#xff0c;外部应用程序不用关心内部子系统的具体的细节&#xff0c;这…

Gin-vue-admin(1):环境配置和安装

目录 环境配置如果443网络连接问题&#xff0c;需要添加代理服务器 后端运行前端运行 环境配置 git clone https://gitcode.com/gh_mirrors/gi/gin-vue-admin.git到server文件目录下 go mod tidygo mod tidy 是 Go 语言模块系统中的一个命令&#xff0c;用于维护 go.mod 文件…

浅谈算法交易

本文想基于我的简单理解说说什么是算法交易&#xff0c;或者说是量化交易。 原文地址请访问&#xff1a;浅谈算法交易 什么是算法交易&#xff1f; 刚开始接触算法交易的时候&#xff0c;对它的理解&#xff0c;它就是把我平时的交易规则搬进计算机里自动执行。这个理解也没…

《点点之歌》“意外”诞生记

世界是“点点”的&#xff0c;“点点”是世界的。 (笔记模板由python脚本于2024年12月23日 19:28:25创建&#xff0c;本篇笔记适合喜欢诗文的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 …

项目代码第6讲:UpdownController.cs;理解 工艺/工序 流程、机台信息;前端的“历史 警报/工艺 记录”

一、UpdownController.cs 1、前端传入 当用户在下图的“记录查询”中的 两个界面选项 中,点击“导出”功能时,向后端发起请求,请求服务器下载文件的权限 【权限是在Program.cs中检测的,这个控制器里只需要进行“谁在哪个接口下载了文件”的日志记录】 【导出:是用户把…

Docker 技术系列之安装多版本Mysql5.6和Mysql5.7

image 大家好&#xff0c;后面的就不是关于MAC专有的内容&#xff0c;基本是跟Java环境&#xff0c;基础技术方面有关。所以这个教程对于在linux系统还是macOS都是通用的&#xff0c;不用担心。 上一篇&#xff0c;我们安装好对应的Docker之后&#xff0c;感受到了它的便利。接…

机器学习04-为什么Relu函数

机器学习0-为什么Relu函数 文章目录 机器学习0-为什么Relu函数 [toc]1-手搓神经网络步骤总结2-为什么要用Relu函数3-进行L1正则化修改后的代码解释 4-进行L2正则化解释注意事项 5-Relu激活函数多有夸张1-细数Relu函数的5宗罪2-Relu函数5宗罪详述 6-那为什么要用这个Relu函数7-文…

【GO环境安装】mac系统+GoLand使用

文章目录 下载安装包环境配置GoLandGo Modules 下载安装包 地址&#xff1a;GO下载地址 下载好后直接进行安装&#xff1a; 进入terminal&#xff0c;查看是否安装成功&#xff1a; 环境配置 在文稿下面创建工作目录&#xff1a; 在文稿下新建Go_Works文件夹&#xff0c;在…

京东大数据治理探索与实践 | 京东零售技术实践

01背景和方案 在当今的数据驱动时代&#xff0c;数据作为关键生产要素之一&#xff0c;其在商业活动中的战略价值愈加凸显&#xff0c;京东也不例外。 作为国内领先的电商平台&#xff0c;京东在数据基础设施上的投入极为巨大&#xff0c;涵盖数万台服务器、数 EB 级存储、数百…

Day13 苍穹外卖项目 工作台功能实现、Apache POI、导出数据到Excel表格

目录 1.工作台 1.1 需求分析和设计 1.1.1 产品原型 1.1.2 接口设计 1.2 代码导入 1.2.1 Controller层 1.2.2 Service层接口 1.2.3 Service层实现类 1.2.4 Mapper层 1.3 功能测试 1.4 代码提交 2.Apache POI 2.1 介绍 2.2 入门案例 2.2.1 将数据写入Excel文件 2.2.2 读取Excel文…

程控电阻箱应用中需要注意哪些安全事项?

程控电阻箱是一种用于精确控制电路中电流和电压的电子元件&#xff0c;广泛应用于电子实验、测试设备以及精密测量仪器中。在应用程控电阻箱时&#xff0c;为确保安全和设备的正常运行&#xff0c;需要注意以下几个安全事项&#xff1a; 1. 正确连接&#xff1a;确保电阻箱与电…

SQL server学习09-数据库编程(上)

目录 一&#xff0c;了解T-SQL语言 1&#xff0c;常量&#xff08;标量值&#xff09; 2&#xff0c;变量 1&#xff09;局部变量 2&#xff09;全局变量 二&#xff0c;内置函数 1&#xff0c;字符串函数 2&#xff0c;数学函数 3&#xff0c;日期时间函数 4&#x…

springboot中使用gdal将表中的空间数据转shapefile文件

springboot中使用gdal将表中的空间数据转shapefile文件 代码&#xff1a; // 样本导出-将样本表导出为shapefile&#xff0c;复制样本shp文件到临时目录下 sampleDir是文件夹pathpublic void setYbShapeFile(Yb yb, File sampleDir) {// 创建 前时项 和 后时项 文件夹File y…