基于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

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

list(介绍与实现)

目录 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 list的构造 1.2.2 list iterator的使用 1.2.3 list capacity 1.2.4 list element access 1.2.5 list modififiers 1.2.6 list的迭代器失效 2. list的模拟实现 2.1 模拟实现list 2.2 list的反向迭代器 1.…

代码随想录第32天|122.买卖股票的最佳时机 II,55. 跳跃游戏 ,45. 跳跃游戏 II

122.买卖股票的最佳时机 II 122. 买卖股票的最佳时机 II 思路比较简单 class Solution {public int maxProfit(int[] prices) {int res0,sum0;for(int i0;i<prices.length-1;i){if(prices[i1]-prices[i]>0){sumprices[i1]-prices[i];}ressum>res?sum:res;}return …

笔记本重装win7旗舰版原版操作系统

正常开机的电脑&#xff1a;直接重装&#xff08;最简单&#xff0c;最快&#xff09;、Ghost重装、U盘重装、光盘重装、硬盘安装 不能正常开机的电脑&#xff1a;U盘重装、光盘重装、硬盘安装 注意&#xff1a; 1.windows7原版系统是不带任何驱动程序的&#xff0c;请事先准…

w ndows7旗舰版怎么重装系统,windows7旗舰版iso怎么安装

现在安装系统要求操作简单、方便。硬盘安装方式就是最简单、最方便的系统安装方法。保证电脑能进入系统的前提下&#xff0c;本地硬盘安装windows7旗舰版iso系统&#xff0c;能够让你快速体验新的windows7旗舰版iso系统。接下来详细讲解下安装windows7旗舰版iso系统的操作过程&…

w ndows7旗舰版网卡驱动,Ghost windows7 64位系统旗舰版网卡驱动工具推荐下载

雨林木风Ghost windows7 64位系统旗舰版网卡驱动工具可以使系统能够快速的链接网络&#xff0c;提供丰富的驱动文件&#xff0c;雨林木风Ghost windows7 64位系统旗舰版网卡驱动工具是很多的用户遇到问题都可以解决的一种工具&#xff0c;所以今天我们就来看看吧。 深度技术Gho…

windows7旗舰版安装过程

一&#xff0c;首先我提供一个windows7旗舰版<带激活工具>的下载地址: 7600_x86_zh-cn_Ultimate_DVD.iso 4 s5 T: O7 L. P* g如果连接错误就复制下面地址到迅雷thunder://QUFodHRwOi8vZG93bjIuZ2hvc3QyLmNuLzc2MDBfeDg2X3poLWNuX1VsdGltYXRlX0RWRC5pc29aWg泗水,泗水论坛,…

windows7系统之家旗舰版下载

肯定有很多朋友都想要了解windows7系统之家这款系统吧?毕竟这款系统可是非常值得一试的,如果大家想要使用这款系统的话,大家就需要事先了解它了,所以下面就给大家带来windows7系统之家下载吧。 ●系统之家ghost win7 x64 旗舰版v1610作品简述: 《系统之家ghost win7 x64 旗…

服务器 字体文件太大,网页的字体文件过大

今天写好了一个网页&#xff0c;开心的放到了云服务器&#xff0c;查看效果&#xff0c;结果发现忘记把字体文件拷过去。但是当字体文件拷过去后&#xff0c;再次访问网页时&#xff0c;网页加载变得十分缓慢&#xff0c;几分钟后才会将字体加载完成。 当时那个郁闷啊&#xff…

高等数学上册 第十章 重积分 第十一章 曲线积分与曲面积分 知识点总结

重积分 二重积分计算法&#xff1a; 直角坐标下&#xff1a;化为二次积分 { 如果图形是 X Y 型&#xff0c;则都可以&#xff0c;但要考虑哪个计算不定积分方便 如果图形既不是 X 也不是 Y 型&#xff0c;则要拆分 极坐标下&#xff1a; ∬ f ( x , y ) d x d y ∬ f ( ρ cos…

DevOps系列文章 之 Python基础

Python 基础篇&#xff08;一&#xff09; print( )函数、input( )函数 用 Python 来打印 Hello, world 你会惊奇地发现Python是如此的简洁与优雅。 # 打印Hello, world print(Hello, world) Hello, world# input()函数&#xff1a;接受一个标准输入数据&#xff0c;返回为s…

九龙证券|主力资金 矿业龙头尾盘净买超亿元

家用电器职业取得主力大手笔抢筹。 今天主力资金净流出260.84亿元&#xff0c;其间创业板净流出93.77亿元&#xff0c;沪深300成份股净流出40.64亿元。 申万一级职业中&#xff0c;今天有25个职业上涨&#xff0c;家用电器涨幅居首&#xff0c;达3.31%&#xff1b;其后煤炭、建…

九龙证券|又3个涨停,退市风险急升!

*ST新海退市危险急剧上升&#xff01; 到4月14日&#xff0c;*ST新海收盘价接连14个买卖日低于1元/股。按照退市新规&#xff0c;若*ST新海在接下来6个买卖日收盘价继续低于1元/股&#xff0c;将触及买卖类强制退市景象而终止上市&#xff0c;公司股票将不进入退市整理期。 面…

股票查询小程序_以龙虎榜数据为例

功能需求 1.程序启动后&#xff0c;给用户提供查询接口&#xff0c;允许用户重复查股票行情信息(用到循环&#xff09; 2.允许用户通过模糊查询股票名&#xff0c;比如输入“生物”&#xff0c;就把所有股票名称中包含“生物”的所有股票的信息打印出来 3.允许按收盘价,涨跌…

【龙虎榜小红牛分析系统G6.2】发布时间2021年03月14日

----更新功能如下 龙虎榜小红牛分析系统G6.20 #龙虎榜小红牛系统 #官方微信公众号&#xff1a;gxzfp888A.优化了数据库写入前先判断是否存在当天数据&#xff0c;避免因多次点入&#xff0c;重复写入数据问题&#xff0c;即一键下载龙虎和涨停数据。 B. 优化了再做&#xff0c…

上海亚商投顾:沪指窄幅震荡 ChatGPT概念股全线下挫

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 三大指数早盘小幅冲高&#xff0c;随后又震荡走低&#xff0c;午后一度集体翻绿&#xff0c;临近尾盘有所回升。Ch…

python 通达信板块_[python]沪深龙虎榜数据导入通达信的自选板块,并标注于K线图上...

将沪深龙虎榜数据导入通达信的自选板块,并标注于K线图上 原理:python读取前一次处理完的计算5日后涨跌幅输出的csv文件 文件名前加"[paint]" 安照通达信的画图文件和板块文件格式,输出文件 用通达信的导入功能,导入画图文件和板块文件即可 事前数据截图: 处理后…

[python]数据整理,将取得的众多的沪深龙虎榜数据整一整

将昨日取得的众多的沪深龙虎榜数据整一整 提取文件夹内所有抓取下来的沪深龙虎榜数据&#xff0c;整理出沪深两市&#xff08;含中小创&#xff09;涨幅榜股票及前5大买入卖出资金净值&#xff0c;保存到csv文件 再手动使用数据透视表进行统计 原始数据&#xff1a; 整理后数据…

攻防演练期间一次对某企业的渗透测试

免责声明 由于传播、利用本文章说黑客所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者说黑客不为此承担任何责任&#xff0c;一旦造成后果请自行承担&#xff01; 前言 某次攻防演练中&#xff0c;主办方只提供了目标…

应用TortoiseSVN的SubWCRev管理VisualStudio C#项目编译版本号

1、拷贝Porperties目录下的文件AssemblyInfo.cs生成副本AssemblyInfo.template.cs, 作为版本管理的模板文件。 2、修改模板文件中的想要管理的版本号信息 // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.5.0.$WCREV$")]//0.9.5…

c#之反射详解

总目录 文章目录 总目录一、反射是什么&#xff1f;1、C#编译运行过程2、反射与元数据3、反射的优缺点 二、反射的使用1、反射相关的类和命名空间1、System.Type类的应用2、System.Activator类的应用3、System.Reflection.Assembly类的应用4、System.Reflection.Module类的应用…