Golang基于DTM的分布式事务SAGA实战-CSDN博客
源代码:https://github.com/Ssummer520/dtm-gin
代码在宿主机运行 docker network:bridge
docker安装,安装成功后可以访问http://localhost:36789/ 打开dtm事务web-ui
docker run -itd --name dtm -p 36789:36789 -p 36790:36790 yedf/dtm:latest
部署mysql
基于docker部署
docker run -d \
--name mysql-latest \
-e MYSQL_ROOT_PASSWORD=sa123456 \
-e MYSQL_USER=sa \
-e MYSQL_PASSWORD=sa123456 \
-e MYSQL_DATABASE=test\
-p 3306:3306 \
-v db_data:/var/lib/mysql \
mysql:latest
dtm子事务屏障
异常与子事务屏障 | DTM开源项目文档
准备 RM 数据表
子事务依赖屏障表
子事务屏障技术依赖本地数据库中创建子事务屏障相关的表(barrier),在源代码的示例中默认的数据库和表分别为(部署依赖的数据库)
库:dtm_barrier
表:barrier(子事务屏障依赖表)
我们创建上面的库表,如果需要自定义库表名称
create database if not exists dtm_barrier
/*!40100 DEFAULT CHARACTER SET utf8mb4 */
;
drop table if exists dtm_barrier.barrier;
create table if not exists dtm_barrier.barrier(
id bigint(22) PRIMARY KEY AUTO_INCREMENT,
trans_type varchar(45) default '',
gid varchar(128) default '',
branch_id varchar(128) default '',
op varchar(45) default '',
barrier_id varchar(45) default '',
reason varchar(45) default '' comment 'the branch type who insert this record',
create_time datetime DEFAULT now(),
update_time datetime DEFAULT now(),
key(create_time),
key(update_time),
UNIQUE key(gid, branch_id, op, barrier_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
转账微服务依赖库表
库:busi
表:user_account
CREATE DATABASE if not exists dtm_busi
/*!40100 DEFAULT CHARACTER SET utf8mb4 */
;
drop table if exists dtm_busi.user_account;
create table if not exists dtm_busi.user_account(
id int(11) PRIMARY KEY AUTO_INCREMENT,
user_id int(11) UNIQUE,
balance DECIMAL(10, 2) not null default '0',
trading_balance DECIMAL(10, 2) not null default '0',
create_time datetime DEFAULT now(),
update_time datetime DEFAULT now(),
key(create_time),
key(update_time)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
insert into dtm_busi.user_account (user_id, balance)
values (1, 10000),
(2, 10000) on DUPLICATE KEY
UPDATE balance =
values (balance);
ddl和数据
/*
Navicat Premium Dump SQL
Source Server : localhost_3306
Source Server Type : MySQL
Source Server Version : 80039 (8.0.39)
Source Host : localhost:3306
Source Schema : test
Target Server Type : MySQL
Target Server Version : 80039 (8.0.39)
File Encoding : 65001
Date: 17/08/2024 10:44:02
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user_account
-- ----------------------------
DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`balance` decimal(10,2) NOT NULL DEFAULT '0.00',
`trading_balance` decimal(10,2) NOT NULL DEFAULT '0.00',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of user_account
-- ----------------------------
BEGIN;
INSERT INTO `user_account` (`id`, `user_id`, `balance`, `trading_balance`, `create_time`, `update_time`) VALUES (1, 1000, 11051.00, 0.00, '2024-08-16 09:19:44', '2024-08-16 13:52:11');
INSERT INTO `user_account` (`id`, `user_id`, `balance`, `trading_balance`, `create_time`, `update_time`) VALUES (2, 1001, 11051.00, 0.00, '2024-08-16 09:20:13', '2024-08-16 13:52:11');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
允许空回滚
如图我们模拟丢包的问题,让try阶段没有进行依赖于 gin框架引入timeout中间件进行控制
func timeoutMiddleware(duration time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), duration)
defer cancel()
c.Request = c.Request.WithContext(ctx)
done := make(chan struct{})
go func() {
c.Next()
close(done)
}()
select {
case <-ctx.Done():
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "request timed out"})
c.Abort()
case <-done:
}
}
}
我们在代码里面sleep3秒中http自定义配置会引起超时timeout
结果:
dtm一阶段timeout
rm1 因我们sleep之后直接进行了return
rm2 try之后触发cancel
数据库金额未发生变化 只看到第二条数据update_time发生的变更
防悬挂控制
我们在上面基础上取掉return 通过timeout中间件触发timeout
dtm触发timeout
rm1触发timeout之后紧接着触发cancel
rm2正常进行tcc流程rm1失败立即回滚
结果
幂等
幂等我们在dtm多次传入相同的全局事务id 操作只有一次会成功