当前位置: 首页 > news >正文

基于SpringBoot实现MySQL与Redis的数据最终一致性

问题场景

在并发场景下,MySQL和Redis之间的数据不一致性可能成为一个突出问题。这种不一致性可能由网络延迟、并发写入冲突以及异常情况处理等因素引起,导致MySQL和Redis中的数据在某些时间点不同步或出现不一致的情况。数据一致性问题的级别可以分为三种:

  • 强一致性:写入何值,读出何值,但在实现中,性能较差。
  • 弱一致性:写入新数据后,承诺在某个时间级别(分、秒、毫秒)后,达到数据一致。
  • 最终一致性:写入新数据后,承诺在规定时间内达到数据一致。

解决方案

强一致性: 强一致性解决方案在高并发场景下实现过于苛刻,本案例暂不讨论。

弱一致性: 一致性的解决方案可以使用“先写MySQL,再删除Redis”策略,这种方案在极限条件下有不一致的可能性,但结合需求和技术实现可以综合评判。弱一致性的应用场景如:社交平台点赞功能,用户可以实时看到点赞的更新,尽管MySQL和Redis可能存在短暂的数据不一致。

最终一致性: 采用“先写MySQL,通过MySQL的Binlog特性,异步写入Redis”。这种方案一般适用于库存、金融等业务场景,但是需要建立相关失败重试、告警、补偿机制,以及容灾措施。

在本案例中,弱一致性采用 Cache Aside 方案,最终一致性采用阿里巴巴开源组件 canal 实现。

Cache Aside

  1. 该方案在读取数据库时,首先从缓存中查询数据库:
    • 如果缓存中存在数据,则直接返回给应用程序。
    • 如果缓存中不存在数据,则从数据库中读取数据,并将数据存储到缓存中,然后返回给应用程序。
  1. 写入数据时,先更数据库的数据,当数据库更新成功后,再删除缓存中的数据。

Cache Aside注意事项
  • 缓存失效:缓存中的数据可能会过期或失效,需要考虑设置合适的缓存过期时间,或使用合适的缓存失效策略(如LRU)来管理缓存中的数据。
  • 缓存穿透:当请求查询一个不存在的数据时,会导致缓存层无法命中,从而直接访问数据库。为了避免缓存穿透问题,可以使用空值缓存或布隆过滤器等技术来减轻数据库的负载。

综上所述,Cache Aside方案适用于读取频率较高、对数据实时性要求不高的场景,通过合理地使用缓存来提高系统性能和扩展性,并通过维护数据的一致性来避免数据不一致的问题。

Cache Aside demo

基于Cache Aside实现点赞功能。

实体类信息

public class Like {private String postId;private int likeCount;// 构造函数、getter和setter方法
}

逻辑层

@Service
public class LikeService {private final LikeRepository likeRepository;private final RedisUtils redisUtils;public LikeService(LikeRepository likeRepository, RedisUtils redisUtils) {this.likeRepository = likeRepository;this.redisUtils = redisUtils;}public Like getLikeInfo(String postId) {String cacheKey = "like:" + postId;// 从缓存中获取点赞信息Like like = (Like) redisUtils.get(cacheKey);// 如果缓存中不存在,则从持久层(数据库)获取if (like == null) {like = likeRepository.findByPostId(postId);// 如果数据库中存在数据,则保存到缓存中if (like != null) {redisUtils.set(cacheKey, like);}}// 如果点赞信息为空,则初始化为0if (like == null) {like = new Like(postId, 0);}return like;}public void addLike(String postId) {String cacheKey = "like:" + postId;// 在持久层(数据库)新增点赞信息Like like = likeRepository.findByPostId(postId);if (like == null) {like = new Like(postId, 1);} else {like.setLikeCount(like.getLikeCount() + 1);}likeRepository.save(like);// 更新缓存中的数据redisUtils.set(cacheKey, like);}
}

canal

引用canal官方说明:

canal [kə’næl] ,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

前置知识:MySQL主从复制原理
  • MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
  • MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal工作原理
  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)
环境搭建

需要的开发环境:

  • MySQL
  • Redis
  • Canal

特别说明:canal只支持JDK 8和JDK 11,如果您在本地物理机安装,请切换JDK默认版本。笔者更建议您使用Docker安装开发环境,由于canal安装后需要修改的配置较多,可以通过Docker-Compose安装。

那么,麻烦ChatGPT写一个Docker-Compose文件吧:

  • version请按本地安装的Docker-Compose版本定义。
  • Docker-Compose安装请自行查询。
version: '2.4'services:mysql:image: mysql:8.0container_name: mysqlrestart: falseenvironment:MYSQL_ROOT_PASSWORD: rootports:- "33060:3306"volumes:- ./mysql-data:/var/lib/mysqlcanal:image: canal/canal-server:v1.1.5container_name: canalrestart: falseports:- "11111:11111"- "11112:11112"depends_on:- mysqlenvironment:- canal.destinations=example- canal.instance.mysql.slaveId=1234- canal.instance.master.address=mysql:3306- canal.instance.dbUsername=root- canal.instance.dbPassword=root- canal.instance.connectionCharset=UTF-8- canal.instance.tsdb.enable=false- canal.instance.gtidon=false- canal.instance.filter.regex=.*- canal.instance.filter.black.regex=mysql\.slave_.*redis:image: redis:latestrestart: alwaysports:- 6379:6379volumes:- ./redis_data:/data

将文件命名为:docker-compose.yml,开始安装。

docker-compose up -d

本案例使用balance余额表来演示,数据库表设计如下:

CREATE TABLE `balance` (`id` varchar(50) NOT NULL COMMENT '主键',`account` varchar(50) NOT NULL COMMENT '账户',`amount` decimal(10,2) NOT NULL COMMENT '金额',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 
COMMENT='余额表';
开发环境
  • JDK 17
  • SpringBoot 3.1.2
  • MyBatis-Plus 3.5.3.1
  • druid
  • lettuce

开发环境根据您的实际需要选择即可。

环境启动后,进入编码阶段。

/*** @author: liu_pc* Date:        2023/8/25* Description: 余额信息变更Redis变成处理类* Version:     1.0*/
@Component
public class BalanceRedisProcessorService implements EntryHandler<Balance>, Runnable {private final Logger logger = LoggerFactory.getLogger(BalanceRedisProcessorService.class);private final RedisUtils redisUtils;private final CanalConfig canalConfig;private final Executor executor;@Value("${canal.server.open}")private boolean open;@Autowiredpublic BalanceRedisProcessorService(RedisUtils redisUtils,CanalConfig canalConfig,@Qualifier("ownThreadPoolExecutor") Executor executor) {this.redisUtils = redisUtils;this.canalConfig = canalConfig;this.executor = executor;}@PostConstructpublic void init() {Map<String, String> mainMdcContext = Maps.newHashMap();mainMdcContext.put("canal-thread", "balance-redis-processor-service");MDC.setContextMap(mainMdcContext);executor.execute(this);logger.info("MySQL-Balance数据自动同步到Redis:线程已经启动");}@Overridepublic void run() {CanalConnector canalConnector = canalConfig.canalConnector();canalConnector.connect();// 回滚到未进行ack的地方canalConnector.rollback();try {while (open) {// 获取数据 每次获取一百条改变数据Message message = canalConnector.getWithoutAck(100);//获取这条消息的idlong batchId = message.getId();int size = message.getEntries().size();if (batchId == -1 || size == 0) {Thread.sleep(1000);continue;}// 处理数据for (CanalEntry.Entry entry : message.getEntries()) {if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());CanalEntry.EventType eventType = rowChange.getEventType();if (eventType == CanalEntry.EventType.UPDATE || eventType == CanalEntry.EventType.INSERT || eventType == CanalEntry.EventType.DELETE) {for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {List<CanalEntry.Column> columns = rowData.getAfterColumnsList();String tableName = entry.getHeader().getTableName();// 判断是否是 Balance 表的 amount 字段变更if ("balance".equals(tableName)) {StringBuilder redisKey = new StringBuilder("balance:");for (CanalEntry.Column column : columns) {logger.info("Balance changed in 'balance' dataInfo: {}", column);if ("id".equals(column.getName())) {String changeId = column.getValue();logger.info("当前变更id为:{}", changeId);redisKey.append(changeId);}if ("amount".equals(column.getName())) {String changeValue = column.getValue();logger.info(changeValue);redisUtils.set(redisKey.toString(), changeValue);}}}}}}}// 确认消费完成这条消息canalConnector.ack(message.getId());logger.info("消费成功");}} catch (Exception e) {logger.warn("canal-消费失败");} finally {// 关闭连接canalConnector.disconnect();}}
}
测试

使用接口调用或者手动改库的方式,制造数据变更,查看日志打印情况:

Redis数据:

完成。

我已将canal实现数据同步代码开源,请自行下载领取,笔者不介意您宝贵的Star,如果能帮到您,十分荣幸。

mdc_logback

同时,如果您对笔者其他文章感兴趣,可以扫一扫关注笔者的公众号:种颗代码技术树

公众号文章更新更及时,以及一些程序员周边相关更新。

感谢您阅读到这里,不胜感激。

相关文章:

基于SpringBoot实现MySQL与Redis的数据最终一致性

问题场景 在并发场景下&#xff0c;MySQL和Redis之间的数据不一致性可能成为一个突出问题。这种不一致性可能由网络延迟、并发写入冲突以及异常情况处理等因素引起&#xff0c;导致MySQL和Redis中的数据在某些时间点不同步或出现不一致的情况。数据一致性问题的级别可以分为三…...

mysql与oracle数据库备份

mysql 1在执行mysql数据备份前&#xff0c;可先执行命令查看磁盘容量&#xff1a; # df -h Filesystem Size Used Avail Use% Mounted on /dev/mapper/VolGroup-lv_root 50G 46G 1.6G 97% / tmpfs 1.9G 92K 1.9G 1% /dev/shm /dev/sda1 485M 39M 421M 9% /boot…...

UE4 材质学习笔记

CheapContrast与CheapContrast_RGB都是提升对比度的&#xff0c;一个是一维输入&#xff0c;一个是三维输入&#xff0c;让亮的地方更亮&#xff0c;暗的地方更暗&#xff0c;不像power虽然也是提升对比度&#xff0c;但是使用过后的结果都是变暗或者最多不变&#xff08;值为1…...

TiDB 源码编译之 TiProxy 篇

作者&#xff1a; ShawnYan 原文来源&#xff1a; https://tidb.net/blog/3d57f54d TiProxy 简介 TiProxy 是一个基于 Apache 2.0 协议开源的、轻量级的 TiDB 数据库代理&#xff0c;基于 Go 语言编写&#xff0c;支持 MySQL 协议。 TiProxy 支持负载均衡&#xff0c;接收来…...

利用驱动漏洞

sbyt3/IObitUnlocker.Wrapper (github.com)...

开始MySQL之路——MySQL约束概述详解

MySQL约束 create table [if not exists] 表名(字段名1 类型[(宽度)] [约束条件] [comment 字段说明],字段名2 类型[(宽度)] [约束条件] [comment 字段说明],字段名3 类型[(宽度)] [约束条件] [comment 字段说明] )[表的一些设置]; 概念 约束英文&#xff1a;constraint 约束实…...

CMake基础和命令介绍

CMake是一个跨平台的构建工具&#xff0c;它可以生成各种不同平台上的构建文件&#xff0c;例如Makefile或Visual Studio项目文件。以下是一些常用的CMake命令&#xff1a; 1. cmake_minimum_required&#xff1a;指定需要的最小CMake版本。 2. project&#xff1a;定义项目名…...

【matlab利用shp文件制作mask白化文件】

matlab白化文件 mask文件的作用matlab制作mask文件mask结果 mask文件的作用 地理信息绘图中的 “mask” 通常指的是遮罩或掩膜&#xff0c;用于在地图或图像上隐藏、高亮或标记特定区域。 数据可视化&#xff1a; 地理信息绘图 mask 可以用于突出显示特定地理区域&#xff0c;使…...

【LLM】解析pdf文档生成摘要

文章目录 一、整体思路二、代码三、小结Reference 一、整体思路 非常简单的一个v1版本 利用langchain和pdfminer切分pdf文档为k块&#xff0c;设置overlap等参数先利用prompt1对每个chunk文本块进行摘要生成&#xff0c;然后利用prompt2对多个摘要进行连贯组合/增删模型可以使…...

方案:AI边缘计算智慧工地解决方案

一、方案背景 在工程项目管理中&#xff0c;工程施工现场涉及面广&#xff0c;多种元素交叉&#xff0c;状况较为复杂&#xff0c;如人员出入、机械运行、物料运输等。特别是传统的现场管理模式依赖于管理人员的现场巡查。当发现安全风险时&#xff0c;需要提前报告&#xff0…...

【Python】【数据结构和算法】查找最大或最小的N个元素

除了直接排序&#xff0c;还可以利用heaq模块的nlargest()和nsmallest()方法&#xff0c;例如&#xff1a; >>> nums [3, 5, 2, 4, 1] >>> smallest heapq.nsmallest(3, nums) >>> print(smallest) [1, 2, 3] >>> largest heapq.nlarg…...

C++day1(笔记整理)

一、Xmind整理&#xff1a; 二、上课笔记整理&#xff1a; 1.第一个c程序&#xff1a;hello world #include <iostream> //#:预处理标识符 //<iostream>:输入输出流类所在的头文件 //istream:输入流类 //ostream:输出流类using namespace std; //std&#x…...

关于chromedriver.exe一系列问题的解决办法

最新 chromedriver.exe下载地址&#xff1a;https://googlechromelabs.github.io/chrome-for-testing/#stable 下载最新版本的 chromedriver.exe 将其解压在 python.exe 同目录下&#xff0c;以及Chrome 的路径下 例如&#xff1a; C:\Program Files\Google\Chrome\Applicati…...

css-选择器、常见样式、标签分类

CSS CSS简介 层叠样式表(英文全称&#xff1a;Cascading Style Sheets)是一种用来表现HTML&#xff08;标准通用标记语言的一个应用&#xff09;或XML&#xff08;标准通用标记语言的一个子集&#xff09;等文件样式的计算机语言。CSS不仅可以静态地修饰网页&#xff0c;还可…...

三星申请新商标:未来将应用于智能戒指,作为XR头显延伸设备

三星最近向英国知识产权局提交了名为“Samsung Curio”的新商标&#xff0c;这预示着三星正积极扩展可穿戴设备生态。该商标被分类为“Class 9”&#xff0c;这表明它有可能被用于未来的智能戒指。 据报道&#xff0c;三星计划将智能戒指作为XR头显设备的延伸&#xff0c;与苹果…...

0201hdfs集群部署-hadoop-大数据学习

文章目录 1 前言2 集群规划3 hadoop安装包上传与安装3.1 上传解压 4 hadoop配置5 从节点同步和环境变量配置6 创建用户7 集群启动8 问题集8.1 Invalid URI for NameNode address (check fs.defaultFS): file:/// has no authority. 结语 1 前言 下面我们配置下单namenode节点h…...

DevOps中的持续测试优势和工具

持续测试 DevOps中的持续测试是一种软件测试类型&#xff0c;它涉及在软件开发生命周期的每个阶段测试软件。持续测试的目标是通过早期测试和经常测试来评估持续交付过程的每一步的软件质量。 DevOps中的持续测试流程涉及开发人员、DevOps、QA和操作系统等利益相关者。 持续…...

函数-C语言(初阶)

目录 一、什么是函数 二、函数的分类 2.1 库函数 2.2 自定义函数 三、函数的参数 3.1 实际参数&#xff08;实参&#xff09; 3.2 形式参数&#xff08;形参&#xff09; 四、函数的调用 4.1 传值调用 4.2 传址调用 五、函数的嵌套调用和链式访问 5.1 嵌套调用 5.2 链式访问…...

elementuiplus设置scroll-to-error之后 提示被遮挡的解决方案

项目场景&#xff1a; 普通的头部固定&#xff0c;中间滑动的布局&#xff0c;中间内容有表单&#xff0c;提交校验不通过时滚动到第一个错误项 问题描述 elementuiplus的scroll-to-error设置之后是局部滚动 当头部内容层级高于中间表单的时候&#xff0c;错误会被遮挡。 ---…...

vue中将新添加的div标签自动定位到可视区域内

可以结合使用Vue的ref和scrollIntoView()方法来实现 <template><div><button click"addDiv">添加新的<div>标签</button><div ref"container" class"container"><div v-for"&#xff08;item,inde…...

Vue3笔记——(尚硅谷张天禹Vue笔记)

Vue3 Vue3简介 1.Vue3简介 .2020年9月18日&#xff0c;Vue.js发布3.0版本&#xff0c;代号: One Piece(海贼王)。耗时2年多、2600次提交、30个RFC、600次PR、99位贡献者 . github上的tags地址: https://github.com/vuejs/vue-next/releases/tag/v3.0.0 2.Vue3带来了什么 .性能…...

正则表达式一小时学完

闯关式学习Regex 正则表达式&#xff0c;我感觉挺不错的&#xff0c;记录一下。 遇到不会的题&#xff0c;可以评论交流。 真的很不错 链接 Regex Learn - Step by step, from zero to advanced....

上门服务系统|上门服务小程序如何提升生活质量?

上门服务其实就是本地生活服务的升级&#xff0c;上门服务包含很多行业可以做的。例如&#xff1a;厨师上门、上门家电维修、跑腿等等。如今各类本地化生活服务越来越受大家的喜爱。基于此市场愿景&#xff0c;我们来谈谈上门服务系统功能。 一、上门服务系统功能 1、预约服务…...

系统报错msvcp120.dll丢失的解决方法,常见的三种解决方法

今天为大家讲述关于系统报错msvcp120.dll丢失的解决方法。在这个信息爆炸的时代&#xff0c;我们每个人都可能遇到各种各样的问题&#xff0c;而这些问题往往需要我们去探索、去解决。今天&#xff0c;我将带领大家走进这个神秘的世界&#xff0c;一起寻找解决msvcp120.dll丢失…...

数据库备份工具有哪些

本文主要介绍下数据库备份工具。 数据库备份工具有很多种&#xff0c;以下是一些常见的数据库备份工具&#xff1a; mysqldump&#xff1a;MySQL官方提供的命令行备份工具&#xff0c;适用于MySQL和MariaDB数据库。它可以将数据库导出为SQL文件&#xff0c;方便进行备份和恢复…...

Sentinel流量控制与熔断降级

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 &#xff0c;关注我&#xff0c;不迷路 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术…...

The Connector 周刊#10:你真的知道什么是DevOps文化吗?

AI 探索 用 LLM 构建企业专属的用户助手&#xff1a;很好的 LLM 应用工程实践&#xff0c;主要介绍了 PingCAP 如何使用大型语言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;构建一个搭载企业专属知识库的智能客服机器人。除了采用行业内通行的基于知识库…...

leetcode438. 找到字符串中所有字母异位词(java)

滑动窗口 找到字符串中所有字母异位词滑动窗口数组优化 上期经典 找到字符串中所有字母异位词 难度 - 中等 Leetcode 438 - 找到字符串中所有字母异位词 给定两个字符串 s 和 p&#xff0c;找到 s 中所有 p 的 异位词 的子串&#xff0c;返回这些子串的起始索引。不考虑答案输出…...

【锐捷】OSPF 多区域配置

【实验名称】 配置 OSPF 多区域。 【实验目的】 配置 OSPF 多区域&#xff0c;理解 OSPF 层次型网络的特点。 【背景描述】 本实验拓扑图中有 3 台路由器&#xff0c;路由器在区域 0 和区域 1 中&#xff0c;路由器 B 在区域 0 和区域 30&#xff0c; 路由器 C 在区域 30。 【需…...

Linux常用命令_权限管理命令

文章目录 1. 权限管理命令: chmod2. 其他权限管理命令2.1 权限管理命令: chown2.2 权限管理命令: chgrp2.3 权限管理命令: umask 1. 权限管理命令: chmod {ugoa}中分别为&#xff1a;u-user、g-group、a-all&#xff1b;谁创建文件&#xff0c;谁是所有者&#xff1b;所属组为所…...

wordpress 视频课堂/湖南专业的关键词优化

一、Config配置简介在微服务系统中&#xff0c;服务较多&#xff0c;相同的配置&#xff1a;如数据库信息、缓存、参数等&#xff0c;会出现在不同的服务上&#xff0c;如果一个配置发生变化&#xff0c;需要修改很多的服务配置。spring cloud提供配置中心&#xff0c;来解决这…...

计算机专业有哪些/seo精灵

head元素元素包含了所有的头部标签元素可以添加在头部区域的元素标签为:title,style,meta,link,script,noscript,base定义不同文档的标题。定义了浏览器工具栏的标题。当网页添加到收藏夹时&#xff0c;显示在收藏夹中的标题。显示在搜索引擎结果页面的标题。eg: 我是标题 标签…...

网站的分页效果怎么做/想学销售去哪培训

转载于:https://blog.51cto.com/noo2008/41504...

岳阳seo快速排名/西安网站seo公司

1、安装Chromeyum install https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm2、安装Seleniumpip3 install selenium3、下载ChromeDriver (注意&#xff1a;chromedriver 的大版本&#xff0c;要与Chrome 的大版本一致)wget http://npm.taobao.org/…...

讨论致同国际网站建设情况/企业宣传方式

每一个函数的对象都有一个length&#xff0c;表示该函数期望接收到的参数格式&#xff0c;他与函数的arguments不同&#xff0c;arguments.length表示函数实际接收到的参数格式。 <script type"text/javascript">function add(num1,num2,num3){}alert(add.len…...

做网站图片路径做缓存吗/今日热点新闻视频

上海最近搞活动调休&#xff0c;要搞深度学习&#xff0c;win上还是不方便&#xff0c;准备弄个ubuntu。于是有以下回忆文字。在机器上装了个双系统。花了两天。再也不想玩了。准备用ubuntu来做深度学习的。本文写于2019年11月4日。机器是神舟Z7-KP7D2&#xff0c;i7-7700HQGTX…...