Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
@GlobalTransactional:全局事务注解,添加了以后可实现分布式事务的回滚和提交,用法与spring的@Transactional注解类似,注解参数的作用也基本一致
seata有四种事务模式,分别为AT模式、TCC模式、Saga模式、XA模式,此处只说明AT模式及TCC模式。
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
本文中,我们将重点介绍 Seata AT 模式的使用,如果您对于 AT 模式原理感兴趣,还请阅读对应于本篇文章的开发者指南。
两阶段提交协议的演变:
我们首先抽象一个使用场景,在用户购买行为的时候需要减少库存并减少账户余额,当库存表 stock_tbl
和 account_tbl
在同一个数据库时,我们可以使用关系数据库自身提供的能力非常容易实现事务。但如果这两个表分属于不同的数据源,我们就要使用 Seata 提供的分布式事务能力了。
观察下方的示例代码,
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count, int money) {
jdbcTemplateA.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
jdbcTemplateB.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
}
如果您曾使用过 Spring 框架 @Transactional
注解的话,也可以根据命名类比理解 @GlobalTransactional
的功能。是的,这里只是引入了一个注解就轻松实现了分布式事务能力,使用 AT 模式可以最小程度减少业务改造成本。
同时,需要注意的是,jdbcTemplateA
和 jdbcTemplateB
使用了不同的数据源进行构造,而这两个不同的数据源都需要使用 Seata 提供的 AT 数据源代理类 DataSourceProxy
进行包装。有关数据源代理帮助我们做了什么,请阅读附录中的事务隔离。
TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。
本文中,我们将重点介绍 Seata TCC 模式的使用,如果您对于 TCC 模式原理感兴趣,想要了解 Seata TCC 对于幂等、空回滚、悬挂问题的解决,还请阅读对应于本篇文章的开发者指南。
TCC 完全不依赖底层数据库,能够实现跨数据库、跨应用资源管理,可以提供给业务方更细粒度的控制。
TCC 是一种侵入式的分布式事务解决方案,需要业务系统自行实现 Try,Confirm,Cancel 三个操作,对业务系统有着非常大的入侵性,设计相对复杂。
TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
在两阶段提交协议中,资源管理器(RM, Resource Manager)需要提供“准备”、“提交”和“回滚” 3 个操作;而事务管理器(TM, Transaction Manager)分 2 阶段协调所有资源管理器,在第一阶段询问所有资源管理器“准备”是否成功,如果所有资源均“准备”成功则在第二阶段执行所有资源的“提交”操作,否则在第二阶段执行所有资源的“回滚”操作,保证所有资源的最终状态是一致的,要么全部提交要么全部回滚。
资源管理器有很多实现方式,其中 TCC(Try-Confirm-Cancel)是资源管理器的一种服务化的实现;TCC 是一种比较成熟的分布式事务解决方案,可用于解决跨数据库、跨服务业务操作的数据一致性问题;TCC 其 Try、Confirm、Cancel 3 个方法均由业务编码实现,故 TCC 可以被称为是服务化的资源管理器。
TCC 的 Try 操作作为一阶段,负责资源的检查和预留;Confirm 操作作为二阶段提交操作,执行真正的业务;Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。
区别于在 AT 模式直接使用数据源代理来屏蔽分布式事务细节,业务方需要自行定义 TCC 资源的“准备”、“提交”和“回滚” 。比如在下方的例子中,
public interface TccActionOne {
@TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。在业务接口中核心的注解是 @TwoPhaseBusinessAction
,表示当前方法使用 TCC 模式管理事务提交,并标明了 Try,Confirm,Cancel 三个阶段。name属性,给当前事务注册了一个全局唯一的的 TCC bean name。同时 TCC 模式的三个执行阶段分别是:
@TwoPhaseBusinessAction
所修饰的方法。如示例代码中的 prepare
方法。commitMethod
属性所指向的方法,来执行Confirm 的工作。rollbackMethod
属性所指向的方法,来执行 Cancel 的工作。其次,可以在 TCC 模式下使用 BusinessActionContext
在事务上下文中传递查询参数。如下属性:
xid
全局事务idbranchId
分支事务idactionName
分支资源id,(resource id)actionContext
业务传递的参数,可以通过 @BusinessActionContextParameter
来标注需要传递的参数。在定义好 TCC 接口之后,我们可以像 AT 模式一样,通过 @GlobalTransactional
开启一个分布式事务。
@GlobalTransactional
public String doTransactionCommit(){
tccActionOne.prepare(null,"one");
tccActionTwo.prepare(null,"two");
}
注意,如果 TCC 参与者是本地 bean(非远程RPC服务),本地 TCC bean 还需要在接口定义中添加 @LocalTCC 注解,比如,
@LocalTCC
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "TccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
seata的服务可去官网下载。我使用seata和nacos集成在一起。
1. 解压seata-server-$version.zip,
修改conf/registry.conf
文件
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
2.由于使用nacos
作为注册中心,所以conf
目录下的file.conf
无需理会。然后就可以直接启动bin/seata-server.bat
,可以在nacos
里看到一个名为seata-server
的服务了。
3.由于seata
使用mysql
作为db
高可用数据库,故需要在mysql
创建一个dubbo-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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT 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 = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) 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',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
4.config.txt
文件复制到seata目录
config.txt
service.vgroupMapping.ruoyi-system-group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/ry-seata?useUnicode=true
store.db.user=root
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
5.nacos-config.sh
复制到seata的conf目录,window脚本请参考seata的config目录下的README-zh.md
nacos-config.sh
#!/usr/bin/env bash
# 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.
while getopts ":h:p:g:t:u:w:" opt
do
case $opt in
h)
host=$OPTARG
;;
p)
port=$OPTARG
;;
g)
group=$OPTARG
;;
t)
tenant=$OPTARG
;;
u)
username=$OPTARG
;;
w)
password=$OPTARG
;;
?)
echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] "
exit 1
;;
esac
done
urlencode() {
for ((i=0; i < ${#1}; i++))
do
char="${1:$i:1}"
case $char in
[a-zA-Z0-9.~_-]) printf $char ;;
*) printf '%%%02X' "'$char" ;;
esac
done
}
if [[ -z ${host} ]]; then
host=localhost
fi
if [[ -z ${port} ]]; then
port=8848
fi
if [[ -z ${group} ]]; then
group="SEATA_GROUP"
fi
if [[ -z ${tenant} ]]; then
tenant=""
fi
if [[ -z ${username} ]]; then
username=""
fi
if [[ -z ${password} ]]; then
password=""
fi
nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"
echo "set nacosAddr=$nacosAddr"
echo "set group=$group"
failCount=0
tempLog=$(mktemp -u)
function addConfig() {
curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$(urlencode $1)&group=$group&content=$(urlencode $2)&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null
if [[ -z $(cat "${tempLog}") ]]; then
echo " Please check the cluster status. "
exit 1
fi
if [[ $(cat "${tempLog}") =~ "true" ]]; then
echo "Set $1=$2 successfully "
else
echo "Set $1=$2 failure "
(( failCount++ ))
fi
}
count=0
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
(( count++ ))
key=${line%%=*}
value=${line#*=}
addConfig "${key}" "${value}"
done
echo "========================================================================="
echo " Complete initialization parameters, total-count:$count , failure-count:$failCount "
echo "========================================================================="
if [[ ${failCount} -eq 0 ]]; then
echo " Init nacos config finished, please start seata-server. "
else
echo " init nacos config fail. "
fi
6.执行命令,后面填写nacos
的IP地址,我的是本机所以是127.0.0.1
sh nacos-config.sh 127.0.0.1
代码示例仅展示AT模式。工程分为4个,分别为:接口工程(dubbo-demo-interface)、用户管理工程(dubbo-demo-user-provider)、组织机构管理工程(dubbo-demo-dept-provider)、消费者模块(dubbo-demo-consumer).
用户管理及组织机构管理为服务端,消费者模块远程调用组织机构新增和用户管理新增,使用seata的全局事务,保证数据的一致性。
dubbo的相关代码此处不再详细说明,有兴趣的可参考我的另一篇博客《dubbo的springboot集成》。
1)实体类(Uesr.java):
package com.jc.shop.dubbo.demo.domain;
/**
* 用户表
*/
public class User implements java.io.Serializable{
/**
* 主键ID
*/
private long id;
/**
* 用户名称
*/
private String name;
/**
* 所属部门
*/
private long deptId;
/**
* 岗位
*/
private String post;
private Dept dept;
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getDeptId() {
return deptId;
}
public void setDeptId(long deptId) {
this.deptId = deptId;
}
public String getPost() {
return post;
}
public void setPost(String post) {
this.post = post;
}
}
2)组织机构类(Dept.java)
package com.jc.shop.dubbo.demo.domain;
import java.io.Serializable;
/**
* 部门
*/
public class Dept implements Serializable {
private long id;
private String name;
private long parentId = 0;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getParentId() {
return parentId;
}
public void setParentId(long parentId) {
this.parentId = parentId;
}
}
3)用户接口类:
package com.jc.shop.dubbo.demo.service;
import com.jc.shop.dubbo.demo.domain.User;
/**
* 业务接口
*/
public interface IUserService {
public int insert(User user);
}
4)组织机构接口类:
package com.jc.shop.dubbo.demo.service;
import com.jc.shop.dubbo.demo.domain.Dept;
public interface IDeptService {
public int insert(Dept dept);
}
1)seata框架的maven依赖:
io.seata
seata-spring-boot-starter
1.4.2
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2)执行数据库脚本
执行用户管理和组织机构的数据库脚本,因为是demo,所以将两个工程的表放在一个库中,表分库存也是一样的,因为两个工程的数据源是分开的:
脚本中必须包含表:“undo_log”,如果分库,每个库中,都应该有该表,该表用于记录数据库操作的日志,用于事务回滚和提交。
/*
Navicat Premium Data Transfer
Source Server : 我的笔记本
Source Server Type : MariaDB
Source Server Version : 110202
Source Host : 192.168.31.23:3306
Source Schema : dubbo-demo
Target Server Type : MariaDB
Target Server Version : 110202
File Encoding : 65001
Date: 09/01/2024 16:46:40
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_dept
-- ----------------------------
DROP TABLE IF EXISTS `t_dept`;
CREATE TABLE `t_dept` (
`t_id` bigint(20) NOT NULL AUTO_INCREMENT,
`t_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`t_parent_id` bigint(20) DEFAULT 0,
PRIMARY KEY (`t_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`t_id` bigint(20) NOT NULL AUTO_INCREMENT,
`t_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`t_dept_id` bigint(20) DEFAULT NULL,
`t_post` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`t_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
3)用户管理工程的yml配置(application.yml):
# seata配置
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名。
tx-service-group: test-group
# 开启自动代理,关闭自动代理,适用于TCC模式
enable-auto-data-source-proxy: true
# 服务配置项
service:
# 虚拟组和分组的映射
vgroup-mapping:
test-group: default #此处的test-group需和tx-service-group的值一致
# 分组和 Seata 服务的映射
grouplist:
default: 127.0.0.1:8091 #seata服务的地址
config:
type: nacos
nacos:
group: SEATA_GROUP
server-addr: 127.0.0.1:8848
namespace:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
4)UserServiceImpl.java实现上面定义的IUserService接口类
package com.jc.shop.dubbo.demo.service.impl;
import com.jc.shop.dubbo.demo.domain.User;
import com.jc.shop.dubbo.demo.mapper.UserMapper;
import com.jc.shop.dubbo.demo.service.IUserService;
import io.seata.core.context.RootContext;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@DubboService(version = "1.0.0")
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper mapper;
@Override
public int insert(User user) {
//打印事务ID,在测试时,只要用户新增和组织机构新增的事务ID相同,就可以任务两个原子操作在同一个事务中。
System.out.println("用户新增的事务ID为:"+ RootContext.getXID());
return mapper.insert(user);
}
}
1)maven的依赖同5.2,此处不再赘述。
2)数据库与用户管理使用同一个库,此处不再赘述
3)用户管理工程的yml配置(application.yml):
# seata配置
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: test-group
# 开启自动代理
enable-auto-data-source-proxy: true
# 服务配置项
service:
# 虚拟组和分组的映射
vgroup-mapping:
test-group: default
# 分组和 Seata 服务的映射
grouplist:
default: 127.0.0.1:8091
config:
type: nacos
nacos:
group: SEATA_GROUP
server-addr: 127.0.0.1:8848
namespace:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
4)DeptServiceImpl实现IDeptService接口
package com.jc.shop.dubbo.demo.service.impl;
import com.jc.shop.dubbo.demo.domain.Dept;
import com.jc.shop.dubbo.demo.mapper.DeptMapper;
import com.jc.shop.dubbo.demo.service.IDeptService;
import io.seata.core.context.RootContext;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@DubboService(version = "1.0.0")
public class DeptServiceImpl implements IDeptService {
@Autowired
private DeptMapper mapper;
@Override
public int insert(Dept dept) {
int i = mapper.insert(dept);
System.out.println("部门新增,影响行数:"+i);
System.out.println("组织机构的事务ID为:"+RootContext.getXID());
return i;
}
}
1)maven依赖与5.2一致,此处不再赘述
2)yml文件配置(application.yml)
# seata配置
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: test-group
# 开启自动代理
enable-auto-data-source-proxy: true
# 服务配置项
service:
# 虚拟组和分组的映射
vgroup-mapping:
test-group: default
# 分组和 Seata 服务的映射
grouplist:
default: 127.0.0.1:8091
config:
type: nacos
nacos:
group: SEATA_GROUP
server-addr: 127.0.0.1:8848
namespace:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
2)远程调用+分布式事务的代码示例
package com.jc.shop.dubbo.demo.service.impl;
import com.jc.core.exception.ServiceException;
import com.jc.shop.dubbo.demo.domain.Dept;
import com.jc.shop.dubbo.demo.domain.User;
import com.jc.shop.dubbo.demo.service.IConsumerService;
import com.jc.shop.dubbo.demo.service.IDeptService;
import com.jc.shop.dubbo.demo.service.IUserService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service
public class ConsumerServiceImpl implements IConsumerService {
@DubboReference(version = "1.0.0")
private IDeptService deptService;
@DubboReference(version = "1.0.0")
private IUserService userService;
@Override
@GlobalTransactional(rollbackFor = {ServiceException.class})
public int insertUser(User user) {
System.out.println("消费端:"+ RootContext.getXID());
Dept dept = user.getDept();
int i = deptService.insert(dept);
user.setDeptId(dept.getId());
//此处在组织机构新增成功后,故意抛出异常,用于测试dept数据是否可以正常回滚。
if(i>0){
throw new ServiceException();
}
int j = userService.insert(user);
System.out.println("用户新增,影响行数:"+j);
return j;
}
}
3)定义controller层接口,使用postman等工具进行测试
package com.jc.shop.dubbo.demo.controller;
import com.jc.core.domain.AjaxResult;
import com.jc.shop.dubbo.demo.domain.User;
import com.jc.shop.dubbo.demo.service.IConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private IConsumerService service;
@PostMapping("/insert")
public AjaxResult insert(@RequestBody User user){
int i = service.insertUser(user);
if(i>0) {
return AjaxResult.success("success");
}else{
return AjaxResult.error();
}
}
}
使用postman进行接口调用
经过测试,发现dept表数据回滚成功,数据为空,若去掉抛异常的代码,则组织机构(t_dept)和用户表(t_user)均可以保存成功。