一种分布式事务解决方案记录

目录

背景

问题

思路

方案

后记


背景

       最近一直在忙一个能源类项目的物料管理平台,前一段算是正式上线了。 这个系统本身的业务不算复杂,难点在于新系统是对已经运行了5年的旧系统重构,并且还要将旧系统的存量数据都承接过来,旧系统的核心业务数据已经上亿,很大一部分表的数据都是千万,百万级别,这对于新系统的挑战很大。 之所以要重构,也是因为数据量太大,旧系统有一些核心功能无法再正常使用,像一些查询、物料提报等功能都不能很好的运行,加上现在都在说大模型以及智能化,我们的新平台也从这个角度有一些新的能力提供。 

       考虑到数据量的级别,对于常见的查询等功能,在满足现有功能要求的前提下,肯定是不能像旧系统一样完全靠Mysql的引擎去查询,所以我们引入的Elasticsearch作为查询引擎。这就延伸出Mysql中的数据和Elasticsearch中的数据一致性的挑战,从功能层面,就是有物料新增或者编辑的时候,要同步的查询或者更新ES中的数据,保证数据的一致性。 比较直观的一种方式就是通过同步程序,消费binlog保证两边数据一致,这种方式很多,例如这个这篇博文就讲的很好。但是我们的场景又有不同,ES中的索引并不是完全对应某一张表,索引中的字段来源于Mysql中一张主表和多张表的融合,不能单纯通过同步写入,所以入ES或者更新ES的动作是和对Mysql的操作耦合在一块的。 

       这就有分布式事务的问题,如何解决呢?

       实际写代码的时候,给具体的方法加上事务注解,将所有Mysql的操作都放到方法的最前面,当所有Sql操作完了,最后再调用操作ES的代码,这样如果Mysql操作有异常,就回滚,此时还未写入ES;如果ES写入或者更新有异常,就会整体回退,这样就能保证数据的一致性了。 

问题

       前几天业务上有一批物料启用操作,物料启用会将状态是限制的物料启用,同时将使用了这个物料的分配关系也启用。 这个功能开发和测试的时候,考虑的批量导入的数据量不会超过1000,所以代码层面都是直接将这一批数据拿过来,批量去做查询和更新。但是业务上这次操作的时候,直接导入了1w+的记录,这就造成当时执行这个异步导入任务的节点出现CPU 700%的,并且很多服务出现502的情况。

       经过紧急排查发现是直接拿1w+的code编码查询ES的时候出的问题,这对ES的压力和当前节点都有很大的压力,基本上把当前节点的CPU以及内存都吃完,当前节点基本卡死。后来对代码做了优化,改成超过500条,就分配次查询和处理,该问题解决。 

       但是后来业务上发现一些很奇怪的数据,这些数据在ES中存在,但是在Mysql中不存在。 这就很诡异了,正常来说,如果物料提报能写入ES,就说明mysql的执行操作肯定都正常,因为都在一个事务内,ES中的数据也是查询Mysql中得到的,不应该出现Mysql没有ES有的情况,除非有人手工删除Mysql数据,但是经过排查确认不存在这样的行为。 后来又对比发现这一批异常数据的时间都是前面导入数据时,服务出现502的那个时间段出现的。 

       经过排查和思考,感觉只能是Mysql相关操作都正常执行,但是最后事务提交失败了才会出现这样的情况。这就又回到分布式事务上来了!!但是为什么SQL都执行成功了,事务最后commit会失败呢? 

思路

       尝试在本地写了一个简单的逻辑代码,如下:

    @Transactional(rollbackFor = Exception.class)public void testTransactional(){logger.info("新增配置数据");SysCommPara sysCommPara = new SysCommPara();sysCommPara.setpKey("EMOTION_FEAR_TT");sysCommPara.setpValue("哈哈");sysCommPara.setpRemark("哈哈");sysCommParaMapper.insert(sysCommPara);logger.info("更新机器人信息!");BotInfo botInfo = new BotInfo();botInfo.setBotId(5651401290434658952L);botInfo.setBotName("测试机器人");botInfo.setTenantId(10001L);botInfoMapper.updateByPrimaryKey(botInfo);System.out.println("虽然报异常,但是还是可以正常执行导这里的");logger.info("新增配置数据");SysCommPara sysCommPara1 = new SysCommPara();sysCommPara1.setpKey("EMOTION_FEAR_TT_tt");sysCommPara1.setpValue("哈哈");sysCommPara1.setpRemark("哈哈");sysCommParaMapper.insert(sysCommPara1);System.out.println("ok,function end~");//下面这段模拟ES操作for (int i=0;i<1000;i++){System.out.println("hahah");}System.out.println("OVER");}

       在这段代码中,我尝试模拟以下几种场景,查看错误异常以及数据一致性

(1)Mysql的某个更新直接失败

       这是比较常见也比较容易理解的场景,系统会直接报SQL异常,并且做回退,这种没什么问题。

(2)更新操作对应表本身被锁

       这种其实和(1)是一样的,也是正常回退,并且报的是有事务未提交,无法更新。也就是说,会在对应Mapper这一行代码直接报异常,走正常的事务回滚。 

(3)SQL都可以正常执行成功,在执行到for循环这个地方时,断开数据库连接

       这种是不常见的,也是上面提到出问题的主要原因。 正常来说是不会出现这种情况的,这种异常信息如下:

om.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Communications link failure during commit(). Transaction resolution unknown.at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:1.8.0_101]at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[?:1.8.0_101]at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:1.8.0_101]at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[?:1.8.0_101]at com.mysql.jdbc.Util.handleNewInstance(Util.java:404) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.Util.getInstance(Util.java:387) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:917) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:896) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:885) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1618) ~[mysql-connector-java-5.1.39.jar:5.1.39]at com.alibaba.druid.pool.DruidPooledConnection.commit(DruidPooledConnection.java:748) ~[druid-1.1.14.jar:1.1.14]
org.springframework.dao.DataAccessResourceFailureException: JDBC commit; Communications link failure during commit(). Transaction resolution unknown.; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Communications link failure during commit(). Transaction resolution unknown.at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:81)at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79)at org.springframework.jdbc.support.JdbcTransactionManager.translateException(JdbcTransactionManager.java:184)at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:336)at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743)at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)at com.jingu.ai.robot.tool.SyncUploadOrderToCrmService$$EnhancerBySpringCGLIB$$b590bc55.testTransactional(<generated>)

       可以看到在commit操作的时候失败了,到数据库中去看,发现数据是没提交的,也就是说Mysql自己做数据回滚。 但是ES其实是已经操作成功了。 

注:从错误日志可以看到,这个异常是在上层调用的那个方法地方捕获的。

       什么场景下会出现这种情况?简单整理,有以下三种场景:

① 当前节点fullgc了,也就是有高并发,或者高IO,系统资源不足了
② 网络出问题,到mysql不通了
③ Mysql负载压力太大,commit提交的命令没发送过去,或者发过去了,没执行,失败了

方案

       针对这个问题,我们可以考虑在上层方法这里加一个try catch,在catch中讲写入ES的数据再删除掉做回退,从而保证数据的一致性!

try{//物料信息入库po = transactionService.saveWzbmRecord(reqBo, validationRspBO);
}
catch (Exception e)
{/*** 事务失败有几种情况:* 1、某个表被锁,这种情况,在内部方法执行到对这个表操作的时候,就往下走不动* 2、指定到最后,sql都正常执行了,但是事务失败了,这个会在上层调用这个方法的地方抛异常* 故,在上层这里做容错,避免出现数据不一致的情况,这种情况一般比较少见,主要的场景包括:* 1、当前节点fullgc了,也就是有高并发,或者高IO,系统资源不足了* 2、网络出问题,到mysql不通了* 3、Mysql负载压力太大,commit提交的命令没发送过去,或者发过去了,没执行,失败了*/logger.error("保存或者新增物料失败,异常出错,回退ES数据,失败信息为:{}",e.getMessage());//回退ES中数据List<Long> codeIdList = new ArrayList<>();codeIdList.add(po.getCodeId());syncDataService.deleteMaterialToEs(codeIdList,true, reqBo.getAddType() == 2 || reqBo.getAddType() == 3);rspBO.setCode(CONST.FAILED_CODE);rspBO.setMessage("操作失败,请联系运维核查数据库状态及应用负载情况,或者提交重试!");return rspBO;
}

后记

① 没有调用commit的Mysql操作,或者调用commit失败,到底是谁操作回退的呢? 这是Mysql自身的机制,如果在调用commit之前,mysql挂了,那么数据是会自动回滚;如果最后没有调用commit,事务也会自动回滚。

② 之所以记录这个问题,主要还是在于对事物的理解以及对框架处理事务的机制了解不够清晰,我最开始想的是直接将ES的操作放到事物外面,当Mysql操作成功事务提交了,再去处理ES,这就不就刚好解决问题,但是这又回到原点了,因为ES出错的可能性更大。所以还是要放到一块;但是在什么地方捕获这个commit的失败异常呢? 所以才有了上面的尝试,最终发现是在调用加了事务的方法那一层去捕获,这样就可以在catch中对数据做回退,从而解决问题。

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

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

相关文章

期权懂|个股期权交割操作流程是什么样的?

期权小懂每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 个股期权交割操作流程是什么样的&#xff1f; 一、行权申报&#xff1a; 期权买方在行权日通过其经纪商提交行权指令&#xff0c;表明其决定行使期权权利。 二、行权匹配&#xf…

智能仓储:入库业务流程介绍

01 入库流程 入库业务流程&#xff0c;常见过程是这样的&#xff1a; 创建PO单 > 创建到货清单 > 核对货物 > 入库质检 > 货物贴标签 > 上架 > 库存同步 1、创建PO单 po单指的是的采购订单&#xff0c;比如采购了一车货品&#xff0c;这车的货品可以理解…

MySQL并发控制(一):幻读

假设有如下表结构&#xff1a; CREATE TABLE t(id int(11) NOT NULL,c int(11) DEFAULT NULL,d int(11) DEFAULT NULL,PRIMARY KEY (id),KEY c (c) ) ENGINEInnoDB;insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25); 问&#xff1a;如果执行…

Ubuntu22.04中mysql8 rpm安装

1、安装依赖 sudo apt update sudo apt -y dist-upgrade sudo apt -y install vim net-tools wget gcc make cmake lrzsz sudo apt -y install libmecab2 libjson-perl 2、下载rpm文件 https://dev.mysql.com/downloads/mysql/ https://cdn.mysql.com//Downloads/MySQL-8.0/m…

Intel 性能分析“全家桶” For HPC(一)

本系列是对于HPC应用性能分析涉及的主要方法论及Intel主流工具分享。理解这些方法论将有助于对性能分析结果的理解。同时方法论也可以推广到其他的硬件平台的分析上。除此之外后面也将介绍如何用Vtune, Advisor以及ITAC进行性能分析&#xff0c;以及在性能分析过程中这三种性能…

Qwen1.8B大模型微调流程

提示&#xff1a;本篇笔记是在微调大模型为法律相关模型的教程下记录的&#xff0c;参考的讲解视频在B站上&#xff0c;一搜微调大模型为法律大模型就有很多视频。 文章目录 1. 数据集1.1 数据下载1.2 数据格式转换 2. 模型训练2.1 安装依赖2.2 模型训练 3. 模型推理3.1 LoRA模…

第十六章 使用 iSCSI 服务部署网络存储

1. iSCSI 技术介绍 硬盘是计算机硬件设备中重要的组成部分之一&#xff0c;硬盘存储设备读写速度的快慢也会对服务器的整体性能造成影响。硬盘存储结构、RAID 磁盘阵列技术以及LVM 技术等都是用于存储设备的技术&#xff0c;尽管这些技术有软件层面和硬件层面之分&#xff0c…

【js面试题】JavaScript 中箭头函数与普通函数的深度剖析

在 JavaScript 编程的世界里&#xff0c;函数是极为重要的组成部分。而随着 ES6 的出现&#xff0c;箭头函数成为了 JavaScript 函数家族中的新成员。它与传统的普通函数有着诸多的不同之处&#xff0c;这些差异深刻地影响着我们编写代码的方式以及代码的执行逻辑。本文将对 Ja…

【漫话机器学习系列】Adaboost算法

Adaboost&#xff08;Adaptive Boosting&#xff09;是一种经典的集成学习方法&#xff0c;主要思想是通过将多个弱学习器&#xff08;通常是简单模型&#xff0c;如决策树桩&#xff09;加权组合&#xff0c;来提升整体模型的预测能力。Adaboost 是一种自适应的学习方法&#…

SQL靶场第四关

sql靶场第四关攻略 输入?id1页面正常 输入?id1发现页面也正常 输入?id1"&#xff0c;页面异常&#xff0c;说明存在sql报错注入 在输入?id1" --页面还是报错 1.判断闭合点 我们需要找到闭合点&#xff0c;尝试在双引号后面加个) 输入?id1") --我们发现…

Trunk链路操作题

Trunk链路操作题 论证&#xff1a;

Alogrithm:三色棋

1. 说明 三色旗的问题最早由 E.W.Diikstra 所提出&#xff0c;他所使用的用语为 Dutch Nation Flag&#xff08;Dijkstra 为荷兰人&#xff09;&#xff0c;而多数的作者则使用 Three-Color Flag 来称之。 假设有一条绳子&#xff0c;上面有红、白、蓝三种颜色的旗子&#xff0…

需要排序的子数组

题目描述 给定一个无序数组arr&#xff0c;求出需要排序的最短子数组长度 要求&#xff1a;O(N) 如输入&#xff1a;arr{2,3,7,5,4,6}&#xff0c;返回4&#xff0c;因为只有{7,5,4,6}需要排序。 分析 以{2,3,7,5,4,6,8,9}为例&#xff1a; 前端小于最小波谷&#xff08;3…

Python酷库之旅-第三方库Pandas(154)

目录 一、用法精讲 701、pandas.Timestamp.utcnow方法 701-1、语法 701-2、参数 701-3、功能 701-4、返回值 701-5、说明 701-6、用法 701-6-1、数据准备 701-6-2、代码示例 701-6-3、结果输出 702、pandas.Timestamp.utcoffset方法 702-1、语法 702-2、参数 70…

如何启动神通数据库?神通数据库的启动方式一共有几种?

简单总结&#xff0c;神通数据库启动有三种方式&#xff1a; 1、dba管理工具方式 2、服务方式 &#xff08;1&#xff09;service oscardb_OSRDBd restart &#xff08;2&#xff09;/etc/init.d/oscardb_OSRDBd restart &#xff08;3&#xff09;systemctl start oscardb_OS…

Modbus Poll的使用

最近从串口调试助手接触到了Modbus Poll&#xff0c;一开始用的时候有些生疏&#xff0c;了解之后不得不说真香。 相对于串口调试助手&#xff0c;有些设备厂家会给一些点表和指令码&#xff0c;有些也可以通过modbus协议解析出来&#xff0c;相对来说&#xff0c;使用Modbus …

第四学期-智能数据分析-期末复习题

智能数据分析期末复习&#xff08;2024春&#xff09; 【考试形式】&#xff1a;闭卷&#xff0c;90分钟&#xff0c;笔试 【题型分布】&#xff1a; 单选题10题&#xff0c;每题3分&#xff0c;共计30分 判断题10题&#xff0c;每题2分&#xff0c;共计20分 填空题5题&…

总结的一些MySql面试题

目录 一&#xff1a;基础篇 二&#xff1a;索引原理和SQL优化 三&#xff1a;事务原理 四&#xff1a;缓存策略 一&#xff1a;基础篇 1&#xff1a;定义&#xff1a;按照数据结构来组织、存储和管理数据的仓库&#xff1b;是一个长期存储在计算机内的、有组织的、可共享 的…

C#实现一个HttpClient集成通义千问-开发前准备

集成一个在线大模型&#xff08;如通义千问&#xff09;&#xff0c;来开发一个chat对话类型的ai应用&#xff0c;我需要先了解OpenAI的API文档&#xff0c;请求和返回的参数都是以相关接口文档的标准进行的 相关文档 OpenAI API文档 https://platform.openai.com/docs/api-…

python游戏设计---飞机大战

1.前言 上次做飞机大战游戏有人这么说&#xff1a; 好好好&#xff01;今天必须整一个&#xff0c;今天我们来详细讲解一下&#xff0c;底部找素材文件下载&#xff01;&#xff01;&#xff01; 2.游戏制作 目录如下&#xff1a; 1.导入的包 import pygame import sys imp…