【PmHub后端篇】PmHub中Seata分布式事务保障任务审批状态一致性
在分布式系统中,事务管理是保证数据一致性的关键。本文将深入探讨在PmHub系统中,如何利用Seata分布式事务来保证任务审批状态的一致性。分布式事务在面试中是常见的考点,网上教程多偏理论,而实际项目中的应用更为关键。
1 事务基础概念
- 事务的定义:事务是逻辑上的一组操作,要么都执行,要么都不执行。例如在用户A向用户B转账100元的场景中,从用户A账户扣减100元以及向用户B账户增加100元这两个步骤需同时成功或失败。
- 事务的四大特性(ACID)
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。如银行转账时,扣钱和加钱操作需同时成功或失败。
- 一致性(Consistency):事务执行前后,数据库都必须处于一致的状态。转账后,两个账户的总金额应保持不变。
- 隔离性(Isolation):并发事务之间互不影响,一个事务的中间状态对其他事务不可见。同时进行的转账操作不会互相干扰,彼此看不到中间状态。
- 持久性(Durability):一旦事务提交,其结果是永久性的,即使系统崩溃,事务的结果也不会丢失。转账成功后,系统崩溃重启,账户金额的变动依然存在。
2 本地事务与分布式事务
-
本地事务:在单体应用中的事务属于本地事务,如在springboot中通过
@Transactional
注解实现。单体应用中,一个操作涉及的多张表位于同一个数据库,MySQL中InnoDB引擎支持事务,MyISAM不支持。MySQL 中主要是通过 undo log 和 redo log 来控制事务,undo log 是在事务提交前回滚,保证事务的原子性,redo log 是在事务提交后回滚,保证事务的持久性。
-
分布式事务:在分布式微服务系统中,单体系统拆分成多个微服务,每个微服务可能部署在不同机器且数据库隔离。如PmHub中不同微服务使用不同数据库,此时一个操作可能涉及多个机器、服务和数据库,需保证操作要么全部执行成功,要么全部失败,这就需要分布式事务解决方案。
3 分布式事务解决方案
3.1 XA方案
两阶段提交,通过事务管理器协调多个数据库的事务,适合单块应用里跨多个库的分布式事务,具有强一致性,但效率低,不适合高并发场景。
XA一共分为两阶段:
- 第一阶段(prepare):即所有的参与者RM准备执行事务并锁住需要的资源。参与者ready时,向TM报告已准备就绪。
- 第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都ready后,向所有参与者发送commit命令。
目前主流的数据库基本都支持XA事务,包括mysql、oracle、sqlserver、postgre。
XA 事务由一个或多个资源管理器(RM)、一个事务管理器(TM)和一个应用程序(ApplicationProgram)组成。
这里的RM、TM、AP三个角色是经典的角色划分,会贯穿后续Saga、Tcc等事务模式。
把上面的转账作为例子,一个成功完成的XA事务时序图如下:
3.2 TCC方案
Try-Confirm-Cancel,通过三个阶段管理事务,适合短流程、高并发场景,具有强一致性,但补偿代码复杂,难以维护,适用于与资金相关的支付、交易等场景。
TCC分为3个阶段
- Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
- Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作要求具备幂等设计,Confirm 失败后需要进行重试。
- Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致,要求满足幂等设计。
把上面的转账作为例子,通常会在Try里面冻结金额,但不扣款,Confirm里面扣款,Cancel里面解冻金额,一个成功完成的TCC事务时序图如下:
3.3 SAGA方案
补偿事务,通过本地事务和补偿操作保证最终一致性,当一个事务失败时,反向执行已成功的操作进行补偿,无锁,高性能,参与者可异步执行,但不保证事务的隔离性,适用于业务流程长、参与者包含其它公司或遗留系统服务的场景。
SAGA核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
把上面的转账作为例子,一个成功完成的SAGA事务时序图如下:
3.4 本地消息表
在本地事务中插入消息到消息表,并将消息发送到MQ,接收方在本地事务中处理消息并更新消息状态,如果失败则定时重试,保证最终一致性,适合低并发场景,但严重依赖数据库的消息表,高并发场景扩展性差。
设计核心是将需要分布式处理的任务通过消息的方式来异步确保执行。
大致流程如下:
写本地消息和业务操作放在一个事务里,保证了业务和发消息的原子性,要么他们全都成功,要么全都失败。
容错机制:
- 扣减余额事务失败时,事务直接回滚,无后续步骤
- 轮序生产消息失败, 增加余额事务失败都会进行重试
本地消息表的特点:
- 不支持回滚
- 轮询生产消息难实现,如果定时轮询会延长事务总时长,如果订阅binlog则开发维护困难
适用于可异步执行的业务,且后续操作无需回滚的业务
3.5 可靠消息最终一致性方案
基于MQ实现事务,先发送预备消息,执行本地事务后确认或回滚消息,MQ定时检查预备消息状态并回调确认,适合高并发场景,可靠性高,但处理复杂,系统B的事务失败需要重试或手工处理。
在上述的本地消息表方案中,生产者需要额外创建消息表,还需要对本地消息表进行轮询,业务负担较重。阿里开源的RocketMQ 4.3之后的版本正式支持事务消息,该事务消息本质上是把本地消息表放到RocketMQ上,解决生产端的消息发送与本地事务执行的原子性问题。
事务消息发送及提交:
- 发送消息(half消息)
- 服务端存储消息,并响应消息的写入结果
- 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
- 根据本地事务状态执行Commit或者Rollback(Commit操作发布消息,消息对消费者可见)
正常发送的流程图如下:
补偿流程:
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
- Producer收到回查消息,返回消息对应的本地事务的状态,为Commit或者Rollback
事务消息方案与本地消息表机制非常类似,区别主要在于原先相关的本地表操作替换成了一个反查接口
事务消息特点如下:
- 长事务仅需要分拆成多个任务,并提供一个反查接口,使用简单
- 事务消息的回查没有好的方案,极端情况可能出现数据错误
- 适用于可异步执行的业务,且后续操作无需回滚的业务
3.6 最大努力通知方案
系统A执行完本地事务后发送消息到MQ,最大努力通知服务消费消息并调用系统B,如果系统B执行失败则定时重试,最后失败则放弃,简单易实现,适合对一致性要求不高的场景,但不能保证绝对一致性。
发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
- 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
- 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
前面介绍的的本地消息表和事务消息都属于可靠消息,与这里介绍的最大努力通知有什么不同?
-
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
-
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
解决方案上,最大努力通知需要:
- 提供接口,让接受通知放能够通过接口查询业务处理结果
- 消息队列ACK机制,消息队列按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 ,直到达到通知要求的时间窗口上限。之后不再通知
最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口
4 Seata介绍
4.1 Seata基本概念
- Seata是什么:Seata是阿里开源的分布式事务解决方案,是一个简单可扩展自治的事务框架。
- Seata的使用:官网地址为https://seata.apache.org/zh-cn/,开源地址为https://github.com/apache/incubator-seata。使用时,在需要使用分布式事务的地方加上
@GlobalTransactional
注解即可。 - Seata的模式:Seata支持AT模式、TCC模式、Saga模式。
- AT模式:自动补偿事务,通过代理自动管理事务的提交和回滚,适合简单场景,易于使用,开发成本低,但依赖于数据库支持,不适合复杂业务逻辑,如PmHub中使用AT模式。
- TCC模式:开发者手动实现业务逻辑的Try、Confirm和Cancel三个阶段,确保事务的一致性,提供强一致性,适用于需要严格事务管理的场景,但实现复杂,开发成本高。
- Saga模式:长事务模式,通过一系列的子事务来完成主事务,子事务之间独立运行,如果某个子事务失败,则通过补偿事务进行回滚,无需全局锁,高性能,适用于长事务场景,但需要开发补偿逻辑,可能无法保证强一致性。
4.2 Seata底层逻辑
- 一阶段:Seata拦截“业务SQL”,解析SQL语义,找到要更新的业务数据,在更新前保存成“before image”,执行“业务SQL”更新业务数据,更新后保存成“after image”,生成行锁,保证一阶段操作的原子性。
- 二阶段
-
正常提交场景:“业务SQL”在一阶段已提交至数据库,Seata框架删掉一阶段保存的快照数据和行锁,完成数据清理。
-
异常提交场景:二阶段回滚时,Seata用“before image”还原业务数据,还原前校验脏写,对比“数据库当前业务数据”和“after image”,若一致则还原业务数据,不一致则转人工处理。
-
5 Seata实战
5.1 下载安装Seata
从https://seata.apache.org/zh-cn/unversioned/download/seata-server/下载最新2.0.0版本
5.2 建库建表
在mysql创建pmhub-seata库并导入数据库脚本:
CREATE DATABASE `pmhub-seata` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;USE `pmhub-seata`;-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(`xid` VARCHAR(128) NOT NULL,`transaction_id` BIGINT,`status` TINYINT NOT NULL,`application_id` VARCHAR(32),`transaction_service_group` VARCHAR(32),`transaction_name` VARCHAR(128),`timeout` INT,`begin_time` BIGINT,`application_data` VARCHAR(2000),`gmt_create` DATETIME,`gmt_modified` DATETIME,PRIMARY KEY (`xid`),KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(`branch_id` BIGINT NOT NULL,`xid` VARCHAR(128) NOT NULL,`transaction_id` BIGINT,`resource_group_id` VARCHAR(32),`resource_id` VARCHAR(256),`branch_type` VARCHAR(8),`status` TINYINT,`client_id` VARCHAR(64),`application_data` VARCHAR(2000),`gmt_create` DATETIME(6),`gmt_modified` DATETIME(6),PRIMARY KEY (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key` VARCHAR(128) NOT NULL,`xid` VARCHAR(128),`transaction_id` BIGINT,`branch_id` BIGINT NOT NULL,`resource_id` VARCHAR(256),`table_name` VARCHAR(32),`pk` VARCHAR(36),`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',`gmt_create` DATETIME,`gmt_modified` DATETIME,PRIMARY KEY (`row_key`),KEY `idx_status` (`status`),KEY `idx_branch_id` (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;CREATE TABLE IF NOT EXISTS `distributed_lock`
(`lock_key` CHAR(20) NOT NULL,`lock_value` VARCHAR(20) NOT NULL,`expire` BIGINT,primary key (`lock_key`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);SET FOREIGN_KEY_CHECKS = 1;
5.3 更改配置
修改端口和nacos相关配置
# Copyright 1999-2019 Seata.io Group.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seataseata:config:type: nacosnacos:server-addr: 127.0.0.1:8848namespace:group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUPusername: nacospassword: nacosregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUPnamespace:cluster: defaultusername: nacospassword: nacosstore:mode: dbdb:datasource: druiddb-type: mysqldriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/pmhub-seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueuser: rootpassword: 123456min-conn: 10max-conn: 100global-table: global_tablebranch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000# server:# service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
5.4 启动seata
进入seata的bin目录启动seata,windows系统通过双击seata-server.bat
脚本:
mac系统可通过sh seata-server.sh
命令启动
访问http://localhost:7091/
查看nacos确定seata是否成功启动并注册。
6 PmHub实战1:添加任务事务管理
创建项目任务时,添加或更新审批设置需跨库调用且涉及不同数据库,使用Seata的AT模式,在各自业务数据库中新建
undo_log
回滚日志表,在pmhub-project
配置文件中添加seata配置,在接口添加@GlobalTransactional
注解,涉及pmhub_project_task
、pmhub_project_member
、pmhub_project_log
等表。
- 添加任务的具体流程
6.1 业务库添加undo_log 表
因为这里使用的是 Seata 的 AT 模式,故需要在各自的业务数据库中新建 undo_log 回滚日志表,这里主要是 pmhub-project
库和pmhub-workflow
库,建表语句如下:
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',`xid` varchar(128) NOT NULL COMMENT 'global transaction id',`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` longblob NOT NULL COMMENT 'rollback info',`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',`log_created` datetime(6) NOT NULL COMMENT 'create datetime',`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`),KEY `ix_log_created` (`log_created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AT transaction mode undo table';
6.2 对应服务加上对应的seata依赖
在pmhub-project
添加依赖:
<!--添加分布式事务-->
<dependency><groupId>com.laigeoffer.pmhub-cloud</groupId><artifactId>pmhub-base-seata</artifactId>
</dependency>
6.3 Nacos 配置文件 pmhub-project-dev.yml 添加 seata 配置
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping: default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: AT
6.4 接口添加 @GlobalTransactional 注解
@Override@GlobalTransactional(name = "pmhub-project-addTask",rollbackFor = Exception.class) //seata分布式事务,AT模式public String add(TaskReqVO taskReqVO) {// xid 全局事务id的检查(方便查看)String xid = RootContext.getXID();log.info("---------------开始新建任务: "+"\t"+"xid: "+xid);if (ProjectStatusEnum.PAUSE.getStatus().equals(projectTaskMapper.queryProjectStatus(taskReqVO.getProjectId()))) {throw new ServiceException("归属项目已暂停,无法新增任务");}// 1、添加任务ProjectTask projectTask = new ProjectTask();if (StringUtils.isNotBlank(taskReqVO.getTaskId())) {projectTask.setTaskPid(taskReqVO.getTaskId());}BeanUtils.copyProperties(taskReqVO, projectTask);projectTask.setCreatedBy(SecurityUtils.getUsername());projectTask.setCreatedTime(new Date());projectTask.setUpdatedBy(SecurityUtils.getUsername());projectTask.setUpdatedTime(new Date());projectTaskMapper.insert(projectTask);// 2、添加任务成员insertMember(projectTask.getId(), 1, SecurityUtils.getUserId());// 3、添加日志saveLog("addTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(), "参与了任务", null);// 将执行人加入if (taskReqVO.getUserId() != null && !Objects.equals(taskReqVO.getUserId(), SecurityUtils.getUserId())) {insertMember(projectTask.getId(), 0, taskReqVO.getUserId());// 添加日志saveLog("invitePartakeTask", projectTask.getId(), taskReqVO.getProjectId(), taskReqVO.getTaskName(), "邀请 " + getSysUserList(Collections.singletonList(taskReqVO.getUserId())).get(0).getNickName() + " 参与任务", taskReqVO.getUserId());}// 4、任务指派消息提醒extracted(taskReqVO.getTaskName(), taskReqVO.getUserId(), SecurityUtils.getUsername(), projectTask.getId());// 5、添加或更新审批设置(远程调用 pmhub-workflow 微服务)ApprovalSetDTO approvalSetDTO = new ApprovalSetDTO(projectTask.getId(), ProjectStatusEnum.TASK.getStatusName(),taskReqVO.getApproved(), taskReqVO.getDefinitionId(), taskReqVO.getDeploymentId());R<Boolean> result = wfDeployService.insertOrUpdateApprovalSet(approvalSetDTO, SecurityConstants.INNER);if (Objects.isNull(result) || Objects.isNull(result.getData())|| R.fail().equals(result.getData())) {throw new ServiceException("远程调用审批服务失败");}log.info("---------------结束新建任务: "+"\t"+"xid: "+xid);return projectTask.getId();}
6.5 涉及数据表
pmhub_project_task
CREATE TABLE `pmhub_project_task` (`id` varchar(64) NOT NULL COMMENT '主键id',`created_by` varchar(64) DEFAULT NULL COMMENT '创建人',`created_time` datetime DEFAULT NULL COMMENT '创建时间',`updated_by` varchar(64) DEFAULT NULL COMMENT '更新人',`updated_time` datetime DEFAULT NULL COMMENT '更新时间',`task_name` varchar(100) DEFAULT NULL COMMENT '任务名称',`project_id` varchar(64) DEFAULT NULL COMMENT '项目id',`task_priority` tinyint(1) NOT NULL DEFAULT '0' COMMENT '任务优先级',`user_id` bigint(20) NOT NULL COMMENT '用户id',`project_stage_id` varchar(64) NOT NULL COMMENT '项目阶段id',`description` varchar(500) DEFAULT NULL COMMENT '任务描述',`begin_time` datetime DEFAULT NULL COMMENT '预计开始时间',`end_time` datetime DEFAULT NULL COMMENT '预计结束时间',`close_time` datetime DEFAULT NULL COMMENT '截止时间',`task_pid` varchar(64) DEFAULT NULL COMMENT '任务父节点',`assign_to` varchar(64) DEFAULT NULL COMMENT '指派给谁',`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '任务状态',`execute_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '执行状态',`task_process` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '任务进度',`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',`deleted_time` datetime DEFAULT NULL,`task_flow` varchar(200) DEFAULT NULL COMMENT '所属流程',`task_type_id` varchar(64) DEFAULT NULL COMMENT '任务类型id',PRIMARY KEY (`id`) USING BTREE,KEY `idx` (`id`,`project_id`,`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-任务表';
pmhub_project_member
CREATE TABLE `pmhub_project_member` (`id` varchar(64) NOT NULL COMMENT '主键id',`pt_id` varchar(64) NOT NULL COMMENT '项目或者任务id',`user_id` bigint(20) NOT NULL COMMENT '用户id',`joined_time` datetime DEFAULT NULL COMMENT '加入时间',`created_by` varchar(100) DEFAULT NULL COMMENT '创建人',`created_time` datetime DEFAULT NULL COMMENT '创建时间',`updated_by` varchar(100) DEFAULT NULL COMMENT '更新人',`updated_time` datetime DEFAULT NULL COMMENT '更新时间',`type` varchar(32) NOT NULL COMMENT '类型是项目还是任务 task project',`creator` tinyint(1) DEFAULT '0' COMMENT '是否创建者',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-任务成员';
pmhub_project_log
CREATE TABLE `pmhub_project_log` (`id` varchar(64) NOT NULL COMMENT '主键id',`user_id` bigint(20) NOT NULL COMMENT '操作人id',`type` varchar(16) NOT NULL COMMENT '类型 project 或者 task',`operate_type` varchar(32) NOT NULL COMMENT '操作类型',`content` text COMMENT '操作内容',`remark` varchar(500) DEFAULT NULL COMMENT '备注',`pt_id` varchar(64) NOT NULL COMMENT '项目或者任务id',`to_user_id` bigint(20) DEFAULT NULL,`created_by` varchar(64) DEFAULT NULL,`created_time` datetime DEFAULT NULL,`updated_by` varchar(64) DEFAULT NULL,`updated_time` datetime DEFAULT NULL,`log_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1-动态 2-交付物 3-评论',`file_url` varchar(500) DEFAULT NULL COMMENT '文件地址',`icon` varchar(20) DEFAULT NULL,`project_id` varchar(64) NOT NULL COMMENT '项目id',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-任务日志';
7 PmHub实战2:审批状态新建/更新
涉及数据库pmhub-workflow及表pmhub_wf_approval_set,在服务pmhub-workflow-dev.yml中进行配置新增。
7.1 Nacos服务 pmhub-workflow-dev.yml 配置新增
# 分布式事务配置
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping: default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: AT
7.2 具体代码实现
- 接口:
com.laigeoffer.pmhub.workflow.controller.WfDeployController#insertOrUpdateApprovalSet
/*** 添加&更新审批设置* @param approvalSetDTO* @return*/
@InnerAuth
@PostMapping("/insertOrUpdateApprovalSet")
public R<Boolean> insertOrUpdateApprovalSet(@RequestBody ApprovalSetDTO approvalSetDTO) {return R.ok(deployService.insertOrUpdateApprovalSet(approvalSetDTO.getExtraId(), approvalSetDTO.getType(), approvalSetDTO.getApproved(), approvalSetDTO.getDefinitionId(), approvalSetDTO.getDeploymentId()));
}
- 具体实现:
com.laigeoffer.pmhub.workflow.service.impl.WfDeployServiceImpl#insertOrUpdateApprovalSet
@Overridepublic boolean insertOrUpdateApprovalSet(String extraId, String type, String approved, String definitionId, String deploymentId) {LambdaQueryWrapper<WfApprovalSet> qw = new LambdaQueryWrapper<>();// 分布式任务异常场景模拟,睡10秒
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }qw.eq(WfApprovalSet::getExtraId, extraId).eq(WfApprovalSet::getType, type);WfApprovalSet mas = wfApprovalSetMapper.selectOne(qw);if (mas != null) {mas.setApproved(approved);mas.setDefinitionId(definitionId);mas.setDeploymentId(deploymentId);mas.setUpdatedBy(SecurityUtils.getUsername());mas.setUpdatedTime(new Date());wfApprovalSetMapper.updateById(mas);} else {WfApprovalSet wfApprovalSet = new WfApprovalSet();wfApprovalSet.setExtraId(extraId);wfApprovalSet.setType(type);wfApprovalSet.setApproved(approved);wfApprovalSet.setDefinitionId(definitionId);wfApprovalSet.setDeploymentId(deploymentId);wfApprovalSet.setCreatedBy(SecurityUtils.getUsername());wfApprovalSet.setCreatedTime(new Date());wfApprovalSet.setUpdatedBy(SecurityUtils.getUsername());wfApprovalSet.setUpdatedTime(new Date());wfApprovalSetMapper.insert(wfApprovalSet);}return true;}
7.3 涉及数据表
数据库:pmhub-workflow
数据表:pmhub_wf_approval_set
CREATE TABLE `pmhub_wf_approval_set` (`id` varchar(32) NOT NULL,`type` varchar(32) DEFAULT NULL,`approved` varchar(10) DEFAULT NULL,`deployment_id` varchar(64) DEFAULT NULL,`definition_id` varchar(64) DEFAULT NULL,`created_by` varchar(64) DEFAULT NULL,`created_time` datetime DEFAULT NULL,`updated_by` varchar(64) DEFAULT NULL,`updated_time` datetime DEFAULT NULL,`extra_id` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
8 Seata实战测试验证
8.1 启动相关服务
pmhub-gateway
pmhub-auth
pmhub-system
pmhub-project
pmhub-workflow
8.2 ( 正常情况 )用前端访问Pmhub, 添加任务测试
- 页面正常添加任务,检查对应表数据正常插入。
8.3 查看数据库数据是否正常新增
-
pmhub_project_task
-
pmhub_project_member
-
pmhub_project_log
8.4 ( 异常情况 )超时, 没有@GlobalTransactional回滚
-
修改代码, 让线程睡眠10秒
-
重新添加任务,页面报“接口请求超时”
8.5 查看数据库表的数据情况
数据库中项目相关表创建数据,流程状态表数据异常,产生脏数据。
-
pmhub_project_task
-
pmhub_project_member
-
pmhub_project_log
-
pmhub_wf_approval_set
8.6 超时异常解决,添加@GlobalTransactional
让更新状态接口多睡10秒并打好断点,重新添加任务,接口超时,任务添加失败,事务回滚成功。
- 再次测试就会发现数据并没有新增到数据库了, 分布式事务回滚成功!
- undo_log 中存下了插入表格的所有信息状态, 是为了方便分布式事务回滚的。
- 当任务执行完,undo_log 中数据会被清空,undo_log 只是暂时记录一下回滚信息
- 出现异常回滚玩,记录自然而然也就不见了。
8.7 seata 底层逻辑验证
为了了解 seata 的 AT 模式是如何工作的,我们采用断点的方式来慢慢看看其底层的原理吧。
- 页面新建任务
- 查看全局事务 id
- pmhub_project_task 已经插入一条记录
- 查看 undo_log
此时 undo_log 表插入了 2 条记录,并有回滚信息。 - 查询 rollback_info 字段,并用 json 解析出来
SELECT CONVERT (rollback_info USING utf8) FROM undo_log;
在线解析:https://www.json.cn/jsononline/
所以可以看到,undo_log 中存下了插入表格的所有信息状态, 是为了方便分布式事务回滚的。
当任务执行完,undo_log 中数据会被清空,undo_log 只是暂时记录一下回滚信息,出现异常回滚玩,记录自然而然也就不见了。
9 总结
本文深入探讨PmHub系统中Seata分布式事务,介绍事务基础概念、分布式事务解决方案,展示Seata使用方法、底层逻辑及在PmHub中的实战测试,包括添加任务和审批状态更新,帮助开发者保证数据一致性。
10 参考链接
- PmHub分布式事务Seata保证任务审批状态一致性
- 分布式事务最经典的七种解决方案
- 分布式事务Seata保证审批状态一致性
- 项目仓库(GitHub)
- 项目仓库(码云):(国内访问速度更快)
相关文章:
【PmHub后端篇】PmHub中Seata分布式事务保障任务审批状态一致性
在分布式系统中,事务管理是保证数据一致性的关键。本文将深入探讨在PmHub系统中,如何利用Seata分布式事务来保证任务审批状态的一致性。分布式事务在面试中是常见的考点,网上教程多偏理论,而实际项目中的应用更为关键。 1 事务基…...
2025年长三角高校数模竞赛B题Q1-Q3详细求解与Q4详细分析
B题 空气源热泵供暖的温度预测 问题背景 空气源热泵是一种与中央空调类似的设备,其结构主要由压缩主机、热交换 器以及末端构成,依靠水泵对末端房屋提供热量来实现制热。空气源热泵作为热 惯性负载,调节潜力巨大。工作时通过水循环系统将…...
插槽(Slot)的使用方法
插槽是Vue.js中一个强大的功能,允许你在组件中预留位置,让父组件可以插入自定义内容。以下是插槽的主要使用方法: 基本插槽 <!-- 子组件 ChildComponent.vue --> <template><div><h2>子组件标题</h2><slo…...
Milvus Docker 部署教程
1. 环境准备 确保您的系统已安装: DockerDocker ComposePython (用于运行客户端代码) 2. 项目结构 创建项目目录并设置以下文件: milvus-docker/ ├── docker-compose.yml ├── main.py3. 配置文件 docker-compose.yml version: 3.5services:…...
Vue 3 中 watch 的使用与深入理解
在 Vue 3 的 Composition API 中,watch 是一个非常强大的工具,用于监听响应式数据的变化并做出相应的处理。本文将通过一段实际代码来深入解析 watch 的行为和使用技巧。 🧩 示例代码回顾 import { reactive, watch } from vueconst state …...
嵌入式学习笔记 D21:双向链表的基本操作
双向链表的定义与创建双向链表的插入双向链表的查找双向链表的修改双向链表的删除双向链表的逆序MakeFile工具使用 一、双向链表的定义与创建 1.双向链表的定义: 双向链表是在单链表的每个结点中,再设置一个指向其前一个结点的指针域。 struct DOUNode…...
pciutils-3.5.5-win64工具的使用方法
目录 简介: 方法一:使用设备管理器 方法二:pciutils-3.5.5-win64 简介: window系统下查看PCIe设备信息比较困难 linux版本下,查看PCIE的信息比较容易,可在安装插件后直接使用命令得出。 例如…...
全息美AISEO引领AIGEO新趋势
内容概要 在数字化营销变革的浪潮中,全息美AISEO为企业注入了全新的活力。通过结合先进的技术与创造性的策略,AISEO不仅提升了品牌的可见度,更通过精准的用户定位,实现了信息的高效传播。尤其在当下竞争日益激烈的市场环境中&…...
平滑过滤值策略
该策略是一种基于技术分析的交易策略,主要通过计算一系列指标来判断市场趋势,并根据这些指标生成交易信号。 策略概述 该策略的核心在于利用多个技术指标来分析市场动态,并据此制定交易决策。它结合了价格动量、波动性和趋势跟踪等多种因素,旨在提高交易的准确性和效率。…...
Windows平台OpenManus部署及WebUI远程访问实现
前言:继DeepSeek引发行业震动后,Monica.im团队最新推出的Manus AI 产品正席卷科技圈。这款具备自主思维能力的全能型AI代理,不仅能精准解析复杂指令并直接产出成果,更颠覆了传统人机交互模式。尽管目前仍处于封闭测试阶段…...
极验证Geetest 通过python代理 透传
看了几个破解Geetest 四代滑块的视频和文章,有点望而生畏,github上也有收集极验四代滑块所有图片后经过ai训练做的破解代码,但作者说成功率只有90%。 于是考虑通过python透传的方法,j就是对自动化脚本套个【网页界面】的壳&#…...
JDK 1.8 全解析:从核心特性到企业实战的深度实践
引言 JDK 1.8 作为 Java 生态发展史上的里程碑版本,自 2014 年发布以来,凭借 Lambda 表达式、Stream API、新日期时间 API 三大核心特性,彻底重塑了 Java 编程范式。本文结合 Oracle 官方文档、蚂蚁集团、京东零售等企业级实战案例ÿ…...
OptiStruct实例:汽车声控建模
本章通过一个Tim-BaDy模型,展示利用AemmiecaviyMet工具进行声腔网格划分的过程因为南腔是考康边界的声场,所以在开展腔建机之的N营涉设物建大致封闭的边界模器东有内商院建权为例,首先需要准备自车身所合之的、首先委部)结构的有限元型车内声…...
k8s(12) — 版本控制和滚动更新(金丝雀部署理念)
金丝雀部署简介: 1、基本概念 金丝雀部署是一种软件开发中的渐进式发布策略,其核心思想是通过将新版本应用逐步发布给一小部分用户(即 “金丝雀” 用户),在真实环境中验证功能稳定性和性能表现,再逐步扩大发…...
基于matlab实现AUTOSAR软件开发---答疑6
最近有少朋友在咨询我关于模型生成A2L文件,之后在把elf文件的地址提取进去,生成最终的A2L的操作,这个其实可以看下mathwork的帮助文档,地址如下: https://www.mathworks.com/help/ecoder/ug/asap2-cdf-calibration.htmlhttps://www.mathworks.com/help/ecoder/ug/asap2-c…...
铜墙铁壁 - 服务网格的安全之道 (Istio 实例)
铜墙铁壁 - 服务网格的安全之道 (Istio 实例) 在微服务架构中,服务间的通信是频繁且复杂的。传统的安全模型常常假设内部网络是可信的,这在现代分布式系统和云原生(尤其是零信任)环境中是远远不够的。我们需要解决几个核心安全问题: 通信加密 (Encryption):如何确保服务 …...
计量——检验与代理变量
1.非嵌套模型的检验 1Davidson-Mackinnon test 判断哪个模型好 log(y)β0β1x1β2x2β3x3u log(y)β0β1log(x1)β2log(x2)β3log(x3)u 1.对logÿ…...
【C++】解析C++面向对象三要素:封装、继承与多态实现机制
解析C面向对象三要素:封装、继承与多态实现机制 1. 面向对象设计基石2. 封装:数据守卫者2.1 访问控制实现2.2 封装优势 3. 继承:代码复用艺术3.1 继承的核心作用3.2 继承类型对比3.3 典型应用场景3.4 构造函数与析构函数处理3.4.1 构造顺序控…...
c语言 写一个五子棋
c语言 IsWin判赢 display 画棋盘 判断落子的坐标是否已有棋子 判断落子坐标范围是否超出范围 // 五子棋 #include <stdio.h> #include <stdlib.h>// 画棋盘 void display(char map[][10]) {system("clear");printf(" 0 1 2 3 4 5 6 7 8 9\n&…...
深度解析 IDEA 集成 Continue 插件:提升开发效率的全流程指南
一、插件核心功能与应用场景 Continue 是一款专为 JetBrains IDE 设计的 AI 编程助手插件,基于大语言模型实现以下核心功能: 智能代码生成:支持根据自然语言描述生成完整方法、单元测试或设计模式(如线程安全单例模式࿰…...
Node.js
本文来源 : 腾讯元宝 Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境,采用 事件驱动 和 非阻塞 I/O 模型,专为构建高性能、可扩展的网络应用而设计。以下是其核心特性和应用场景的总结: 1…...
idea经常卡顿解决办法
一:前言 (1)使用idea工具开发过久,出现卡顿,等待响应 二:原因 (1)给idea设置的运行内存过小,需要使用的内存超过设置的内存 (2)插件过多&…...
【python】字典:: a list of dictionaries
No, actions is not a dictionary. It’s a list of dictionaries. Each item in the list is a dictionary with three key-value pairs: “measure” (number), “resource” (string), and “reason” (string). Here’s the structure: actions is a list []Each element…...
高效电脑隐私信息清理实用工具
软件介绍 本文介绍的这款Privacy Eraser,它是一款电脑系统隐私清理工具。 功能介绍 这款工具能够清理电脑里的多种信息,比如最近文件、临时文件、注册表信息,还有浏览器插件以及日志文件等等。 引导提示 注重隐私保护的小伙伴一定要将这款…...
在ubuntu系统中将vue3的打包文件dist 部署nginx 并且配置ssl证书 以https方式访问
在ubuntu系统中将vue3的打包文件dist 部署nginx 并且配置ssl证书 以https方式访问 确保 Nginx 已安装准备 Vue 3 打包文件配置 Nginx编辑 Nginx 配置文件启用配置文件测试 Nginx 配置重新加载 Nginx配置 SSL 证书获取 SSL 证书验证证书自动续期验证部署注意事项 确保 Nginx 已安…...
MH22D3开发高级UI应用,适配arm2d驱动
在资源有限的嵌入系统上,要开发出具有现代风格(圆弧,表盘,滚动,滑动,透明,图层叠加等)的UI应用,需要极高的cpu算力和ram,flash资源的支持。 但是往往鱼和熊掌…...
MongoDB数据库深度解析:架构、特性与应用场景
在现代应用程序开发中,数据存储技术的选择至关重要。在众多的数据库管理系统中,MongoDB以其灵活性和强大的功能迅速崛起,成为NoSQL数据库中的佼佼者。本文将深入解析MongoDB的架构、核心特性、性能优化及其在实际应用中的最佳实践,…...
LeetCode 235. 二叉搜索树的最近公共祖先 LeetCode 701.二叉搜索树中的插入操作 LeetCode 450.删除二叉搜索树中的节点
LeetCode 235. 二叉搜索树的最近公共祖先 思路: 根据二叉搜索树的特性,对 “基于二叉树的最近公共祖先 ” 进行优化,在二叉树寻找最近公共祖先时,需要分别对根节点的两个子树进行遍历来判断两个节点是异侧还是同侧。但是在二叉搜…...
GPU异步执行漏洞攻防实战:从CUDA Stream竞争到安全编程规范
点击 “AladdinEdu,同学们用得起的【H卡】算力平台”,H卡级别算力,按量计费,灵活弹性,顶级配置,学生专属优惠。 引言 在高校实验室的GPU加速计算研究中,多卡并行编程已成为提升深度学习训练效…...
[c语言日寄]数据结构:栈
【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋:这是一个专注于C语言刷题的专栏,精选题目,搭配详细题解、拓展算法。从基础语法到复杂算法,题目涉及的知识点全面覆盖,助力你系统提升。无论你是初学者,还是…...
day21:零基础学嵌入式之数据结构
一、双向链表(doulinklist) 1. 2.创建 struct DouLinkList *CreateDouLinkList() {struct DouLinkList *dl malloc(sizeof(struct DouLinkList));if(NULL dl){fprintf(stderr, "CreateDouLinkLis malloc");return NULL;}dl->head NUL…...
数据结构之图的应用场景及其代码
一,最小生成树 最小生成树(Minimum Spanning Tree, MST)是图论中的经典问题,旨在通过选择无向连通图中的边,使得所有节点连通且总边权最小。 1.1 普里姆(Prim)算法 普里姆算法是一种用于求解…...
python克洛伊婚纱摄影预约管理系统
目录 技术栈介绍具体实现截图系统设计研究方法:设计步骤设计流程核心代码部分展示研究方法详细视频演示试验方案论文大纲源码获取/详细视频演示 技术栈介绍 Django-SpringBoot-php-Node.js-flask 本课题的研究方法和研究步骤基本合理,难度适中…...
GCC 使用说明
参数 -fPIC ppc_85xx-gcc -shared -fPIC liberr.c -o liberr.so -fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code), 则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加…...
配置别名路径 @
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 - craco 1. 路径解析配置(Webpack)-- craco 插件 把 / 解析为 src/ 配置步骤: 1.安装 craco npm i -D craco/craco 2. 项目根目录下创建配置文件 craco.co…...
MYSQL基本命令
目录 1.登录命令2.操作数据库命令2.1查询数据库(show)2.2 创建数据库(create)2.3使用数据库(use) 3.操作表命令3.1增加表3.2查询表3.3修改表(alert)3.4 删除(delete/drop) 1.登录命令 mysql -uroot -p2.操作数据库命令 2.1查询数据库(show) show databases;2.2 创建数据库(c…...
C#语法基础
一、什么是.NET平台 .NET 是由 Microsoft 支持的免费开放源代码应用程序平台。 .NET .NET 是一个安全、可靠且高性能的应用程序平台。C# 是 .NET 的编程语言。它是强类型且类型安全的,并集成了并发和自动内存管理。 C# C# 是一种新式、安全且面向对象的编程语言&…...
深度学习框架对比---Pytorch和TensorFlow
一、计算图与执行模式 1. 图的本质:动态图 vs 静态图 PyTorch(动态图,Eager Execution) 运行机制:代码逐行执行,张量操作立即生效,计算图在运行时动态构建。x torch.tensor(1.0, requires_gra…...
antdv3 Tabs.TabPane 右上角增加一个角标Badge
1、Tabs官方说明 Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js 2、Badge角标官方效果图 Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js 3、Tabs.TabPane要实现的效果 4、代码 <Tabs v-m…...
Python-88:英雄升级奖励
问题描述 在一个游戏中,小W拥有 n 个英雄,每个英雄的初始能力值均为 1。她可以通过升级操作来提升英雄的能力值,最多可以进行 k 次升级。 每次升级操作包含以下步骤: 选择一个英雄选择一个正整数 x将该英雄的能力值 aiai 更新…...
使用uv创建python项目
uv创建项目 uv init -p 3.12 qwen3env # -p 指定python版本 # qwen3env是项目名称 # 可以使用下面的步骤 mkdir qwen3env cd qwen3env uv venv -p3.12 .venv # 基于 Python 3.12 创建名为 .venv 的虚拟环境 uv init第一种方式 第二种方式 内容如下 执行python脚本 uv ru…...
window 显示驱动开发-命令和 DMA 缓冲区简介
命令和 DMA 缓冲区非常相似。 但是,命令缓冲区由用户模式显示驱动程序使用,DMA 缓冲区由显示微型端口驱动程序使用。 命令缓冲区具有以下特征: 它永远不会由 GPU 直接访问。 硬件供应商控制格式。 它从呈现应用程序的专用地址空间中的常规…...
深光-谷歌TV TADA/奈飞Netflix/亚马逊Prime Video/YouTube等测试外包服务
一、谷歌TV TADA测试服务 1.CTS CTS测试是一系列旨在确保设备与Android操作系统兼容性的自动化测试,CTS是所有测试项中测试量最大的一项测试。 2.GTS GTS测试是确保Android设备能够正确集成和运行Google Mobile Services(GMS)的关键步骤&am…...
《教育退费那些事儿:从困境到破局》
《教育退费那些事儿:从困境到破局》 教育退费:不容忽视的热点问题 在当今社会,教育消费已成为家庭支出的重要组成部分。无论是 K12 阶段的学科辅导、艺术特长培训,还是成人的职业技能提升、学历继续教育,家长和学生们…...
AtCoder 第405场初级竞赛 A~E题解
A Is it rated? 【题目链接】 原题链接:A - Is it rated? 【考点】 嵌套判断 【题目大意】 有两个分区,有不同的评分区间,给一个评分 r 和分区 x,判断是否在评分区间中。 【解析】 先判断在属于哪个分区,再判断是否在该分区评分区间中。 【难度】 GESP一级 【…...
登录接口中图片验证码Tesseract-OCR识别Java脚本
项目上移植了研发部的产品,文档不全,项目上验证码功能无法关闭,又要做接口/性能测试,开发不配合(作为测试多么无奈),此方法识别命中率不高,仅作借鉴。 版本JDK11 import io.restass…...
专项智能练习(定义判断)_DA_02
2. 单选题 虚假同感偏差也叫虚假一致性偏差,是指人们常常会高估或夸大自己的信念、判断及行为的普遍性。在认知他人时总喜欢把自己的特性赋予他人身上,假定他人与自己是相同的,而当遇到与此相冲突的信息时,会坚信自己信念和判断的…...
安卓A15系统实现修改锁屏界面默认壁纸功能
最近遇到一个A15系统项目,客户要求修改锁屏界面的默认壁纸,客户提供了一张壁纸图片,但是从A15系统的源代码查看时才知道谷歌已经去掉了相关的代码,已经不支持了,A13和A14系统好像是支持的,A15系统的Wallpap…...
Linux之Yum源与Nginx服务篇
1.Yum源知识理论总结概括 Yum源概述 Yum 源 即软件仓库的标识,里面承载着软件包集合 Yum源组成 包含模块 【OS】、【everything】、【EPOL】、【debuginfo】、【source】、【update-source】 【os】:简称operator system 它内部包含操作系统的核心组件&#x…...
帧差法识别
定义: 视频通过闪过x帧画面来实现,帧差法就是利用两帧之间的差异找出。也就是移动目标识别 帧差法识别步骤: 1、灰度处理:将多通道变成双通道压缩图像数据。 cvtColor(before_frame,before_gray,CV_RGB2GRAY);cvtColor(after_f…...