安装 Nacos
官网下载 Nacos,直接启动即可,这里省略
安装 SeataServer 端
seata 需要一个 server (TC) 作为协调者处理 AT、TCC 和 SAGA 事务模型。
官网下载 Seata
下载地址:https://github.com/seata/seata/releases
配置 Seata 使用 Nacos 注册中心
修改 seata/conf/application.yml
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 192.168.1.201:8848
group: DEFAULT_GROUP
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 192.168.1.201:8848
group: DEFAULT_GROUP
namespace:
cluster: default
username:
password:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
设置 nacos 地址 192.168.1.201:8848
启动 seata:seata/bin/steata-server.bat
nacos 上可以看到 seata-server 的注册信息
登录 seata 后台:http://localhost:7091 7091 是管理端口,nacos 上注册的是 8091 是事务的服务端口(这个就比 Nacos 处理的要好,Nacos 就一个端口)
订单服务和库存服务数据库初始化
CREATE TABLE `t_order` (
`uri` varchar(50) NOT NULL,
`name` varchar(50) DEFAULT NULL COMMENT '订单名称',
`commodity_code` varchar(50) DEFAULT NULL COMMENT '产品编码',
`count` bigint(20) default 0 comment '订单数量',
`money` bigint(20) default 0 comment '金额',
PRIMARY KEY (`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='订单';
CREATE TABLE `t_storage` (
`uri` varchar(50) NOT NULL,
`commodity_code` varchar(50) DEFAULT NULL COMMENT '产品编码',
`count` bigint(20) default 0 comment '库存数量',
PRIMARY KEY (`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='库存';
INSERT INTO `t_storage`(`uri`, `commodity_code`, `count`) VALUES ('product01', 'product01', 1000);
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录
DROP TABLE `undo_log`;
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
配置订单服务
pom.xml 关键配置: 增加 seata 依赖配置,这里使用的是 spring cloud alibaba seata 组件,这个组件包含了 seata 所有依赖和基于 springcloud 的 seata 注册自动化配置。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
application.yaml 相关 seata 配置 (数据库等其他配置省略):
seata:
registry:
# 使用nacos作为注册中心
type: nacos
nacos:
server-addr: 192.168.1.201:8848
# Seata服务名(应与seata-server实际注册的服务名一致)
application: seata-server
# Seata分组名(应与seata-server实际注册的分组名一致)
group: DEFAULT_GROUP
# 一般是用来动态调整seata的集群分组,处理冷备、环境隔离等场景
txServiceGroup: my_test_tx_group
service:
vgroupMapping:
my_test_tx_group: default
创建订单的 Controller、Entity、Mapper 和 Service: OrderController:
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
public OrderService iOrderService;
/**
* 创建订单
* @param order 传递的实体
*/
@ResponseBody
@RequestMapping(method=RequestMethod.POST,value="/add")
public ResultVO<Integer> add(@RequestBody Order order) {
log.info("保存和修改:{}", order);
iOrderService.createOrder(order);
ResultVO result = ResultVO.success("OK");
return result;
}
}
OrderEntity:
@TableName("t_order")
public class Order {
private static final long serialVersionUID = 1L;
@TableId(value="uri", type = IdType.ASSIGN_ID)
protected String uri;
/**
* 订单名称
*/
@TableField("name")
private String name;
/**
* 产品编码
*/
@TableField("commodity_code")
private String commodityCode;
/**
* 订单数量
*/
@TableField("count")
private Long count;
/**
* 金额
*/
@TableField("money")
private Long money;
}
Mapper 和 XML 省略,参考 Mybatispuls 代码生成的结果。
OrderServiceImpl
@Service
@Primary
@Transactional
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private StorageServiceRemote storageServiceRemote;
@Autowired
private OrderMapper orderMapper;
@Override
@GlobalTransactional
public void createOrder(Order order) {
log.info("创建订单:{}", order);
order.setUri(null);
this.save(order);
//扣减库存
log.info("扣减库存:{}", order.getCount());
storageServiceRemote.reduce(order.getCommodityCode(), order.getCount());
//模拟异常
if(Objects.equals("product01", order.getCommodityCode()) && order.getCount() == 12) {
throw new UcloudSystemException("测试seata1异常");
}
}
}
这里的关键是 @GlobalTransactional
,开启全局事务,这个是全局事务的发起者,也就是 TM。
配置库存扣减调用 Feign 服务:
StorageServiceRemote
@FeignClient(name = "service-storage",fallbackFactory = StorageServiceRemoteFallbackFactory.class)
public interface StorageServiceRemote {
@PostMapping("/storage/reduce")
public ResultVO reduce(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count);
}
StorageServiceRemoteFallbackFactory,这里直接抛出异常,保证事务能回滚:
@Slf4j
@Component
public class StorageServiceRemoteFallbackFactory implements FallbackFactory<StorageServiceRemote> {
@Override
public StorageServiceRemote create(Throwable cause) {
log.error("异常:", cause);
throw new UcloudSystemException("测试feign异常");
}
}
service-storage 是库存的服务名
配置库存服务
pom.xml 和 yaml 配置参考 "订单服务" 的配置,只是两个服务的服务名不一样。
配置库存的 Controller、Entity、Mapper 和 Service: StorageController:
@Slf4j
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
public StorageService iStorageService;
@ResponseBody
@PostMapping(value="/reduce")
public ResultVO<Integer> reduce(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count){
log.info("扣减库存:{}, {}", commodityCode, count);
iStorageService.reduce(commodityCode, count);
ResultVO result = ResultVO.success("OK");
return result;
}
}
StorageEntity:
@TableName("t_order")
public class Order {
private static final long serialVersionUID = 1L;
@TableId(value="uri", type = IdType.ASSIGN_ID)
protected String uri;
/**
* 产品编码
*/
@TableField("commodity_code")
private String commodityCode;
/**
* 库存数量
*/
@TableField("count")
private Long count;
}
Mapper 和 XML 省略,参考 Mybatispuls 代码生成的结果。
StorageServiceImpl
@Service
@Primary
@Transactional
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
@Transactional
public void reduce(String commodityCode, Long count) {
Storage storage = this.getOne(new QueryWrapper().eq(true, "commodity_code", commodityCode));
storage.setCount(storage.getCount() - count);
log.info("更新库存信息:{}", storage);
if("product01".equalsIgnoreCase(commodityCode) && count == 11) {
throw new UcloudSystemException("测试回滚异常");
}
this.update(storage, new QueryWrapper().eq(true, "commodity_code", commodityCode));
}
}
Storage 是分支事务,因此这里只要配置 @Transactional 就行了
测试验证
验证调用 Storage 异常事务回滚
调用:http://localhost:6318/order/add
{
"commodityCode": "product01",
"count": 11,
"flag": 0,
"money": 100,
"name": "订单",
}
提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> RM 生成 Storage 快照 undo -> Storage 保存 -> Storage 模拟抛出异常
事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 本地 RM 回滚 -> 删除 Storage 快照 -> Order 本地 RM 回滚
这种其实可以不需要全局分布式事务参与,都是本地事务回滚。
验证调用 Storage 成功后 Order 异常
调用:http://localhost:6318/order/add
{
"commodityCode": "product01",
"count": 12,
"flag": 0,
"money": 100,
"name": "订单",
}
提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> RM 生成 Storage 快照 -> Storage 保存 -> 返回 Order 模拟抛出异常
事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 提交 Commit -> Order 异常后通过 TC 调用 Storage 回滚 undo 日志 -> Storage 回滚并删除快照 -> Order 本地 RM 回滚
数据库的生成过程:
异常发生前数据库信息 库存信息已经发生变化 876
undo 中生成的快照数据:
{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.3.79:8091:8043697401397272602","branchId":8043697401397272603,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"t_storage","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"uri","keyType":"PRIMARY_KEY","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"commodity_code","keyType":"NULL","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":-5,"value":["java.lang.Long",888]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"del_flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"create_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653979471000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"update_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653980897000,0]]}]]}]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"uri","keyType":"PRIMARY_KEY","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"commodity_code","keyType":"NULL","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":-5,"value":["java.lang.Long",876]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"del_flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"create_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653979471000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"update_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653981091000,0]]}]]}]]}}]]}
异常发生后数据库信息: 库存发生回滚 888
undo 快照内容被清空:
分支事务回滚的时候是阻塞状态,全局事务发起者和分支事务的堆栈:
全局事务发起者,回滚阻塞,同步调用 TC:
at io.seata.core.protocol.MessageFuture.get(MessageFuture.java:58)
at io.seata.core.rpc.netty.AbstractNettyRemotingClient.sendSyncRequest(AbstractNettyRemotingClient.java:165)
at io.seata.tm.DefaultTransactionManager.syncCall(DefaultTransactionManager.java:95)
at io.seata.tm.DefaultTransactionManager.rollback(DefaultTransactionManager.java:72)
at io.seata.tm.api.DefaultGlobalTransaction.rollback(DefaultGlobalTransaction.java:163)
at io.seata.tm.api.TransactionalTemplate.rollbackTransaction(TransactionalTemplate.java:165)
at io.seata.tm.api.TransactionalTemplate.completeTransactionAfterThrowing(TransactionalTemplate.java:139)
at io.seata.tm.api.TransactionalTemplate.execute(TransactionalTemplate.java:109)
at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalTransaction(GlobalTransactionalInterceptor.java:147)
at io.seata.spring.annotation.GlobalTransactionalInterceptor.invoke(GlobalTransactionalInterceptor.java:122)
子应用 RM 事务管理,回滚数据:
at io.seata.rm.AbstractRMHandler.doBranchRollback(AbstractRMHandler.java:122)
at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:67)
at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:63)
at io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate(AbstractExceptionHandler.java:116)
at io.seata.rm.AbstractRMHandler.handle(AbstractRMHandler.java:63)
at io.seata.rm.DefaultRMHandler.handle(DefaultRMHandler.java:63)
at io.seata.core.protocol.transaction.BranchRollbackRequest.handle(BranchRollbackRequest.java:35)
at io.seata.rm.AbstractRMHandler.onRequest(AbstractRMHandler.java:150)
at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.handleBranchRollback(RmBranchRollbackProcessor.java:63)
at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.process(RmBranchRollbackProcessor.java:58)
at io.seata.core.rpc.netty.AbstractNettyRemoting.lambda$processMessage$2(AbstractNettyRemoting.java:265)
at io.seata.core.rpc.netty.AbstractNettyRemoting$$Lambda$1055/1686572615.run(Unknown Source)
TCC 分布式事物
前面都是基于 Seata 的 AT 分布式事务模式,有些场景需要使用到 TCC 分布式事务,TCC 分布式事务对代码有一定的侵入性,但是性能更好,能处理复杂业务场景。
TCC 相关测试代码
OrderControler 中添加方法:
/**
* 创建订单TCC
* @param order 传递的实体
*/
@ResponseBody
@RequestMapping(method=RequestMethod.POST,value="/addTcc")
public ResultVO<Integer> addTcc(@RequestBody Order order) {
log.info("保存和修改:{}", order);
iOrderService.createOrderTcc(order);
ResultVO result = ResultVO.success("OK");
return result;
}
OrderServiceImpl 中增加测试事务的方法:注意也是使用 @GlobalTransactional
@GlobalTransactional
public void createOrderTcc(Order order) {
log.info("创建订单TCC:{}", order);
order.setUri(null);
this.save(order);
//扣减库存
log.info("扣减库存TCC:{}", order.getCount());
storageServiceRemote.reduceTcc(order.getCommodityCode(), order.getCount());
//模拟异常
if(Objects.equals("product01", order.getCommodityCode()) && order.getCount() == 12) {
throw new UcloudSystemException("测试seata1异常TCC");
}
}
Feign 添加 reduceTcc 远程方法:
@PostMapping("/storage/reduceTcc")
public void reduceTcc(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count);
StorageControler 库存添加接口:
@ResponseBody
@PostMapping(value="/reduceTcc")
public ResultVO<Integer> reduceTcc(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count){
log.info("扣减库存TCC:{}, {}", commodityCode, count);
iStorageService.reduceTcc(commodityCode, count);
ResultVO result = ResultVO.success("OK");
return result;
}
StorageService 接口增加 TCC 事务:
@LocalTCC
public interface StorageService extends IService<Storage> {
void reduce(String commodityCode, Long count);
/**
* @TwoPhaseBusinessAction 描述⼆阶段提交
* name: 为 tcc⽅法的 bean 名称,需要全局唯⼀,⼀般写⽅法名即可
* commitMethod: Commit⽅法的⽅法名
* rollbackMethod:Rollback⽅法的⽅法名
* @BusinessActionContextParamete 该注解⽤来修饰 Try⽅法的⼊参,
* 被修饰的⼊参可以在 Commit ⽅法和 Rollback ⽅法中通过 BusinessActionContext 获取。
*/
@TwoPhaseBusinessAction(name = "reduceTcc", commitMethod = "reduceTccCommit", rollbackMethod = "reduceTccRollback")
void reduceTcc(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode
,@BusinessActionContextParameter(paramName = "count") Long count);
public boolean reduceTccCommit(BusinessActionContext context);
public boolean reduceTccRollback(BusinessActionContext context);
}
注意:@LocalTCC 需要添加到接口上,对需要走 TCC 的方法配置 @TwoPhaseBusinessAction 两段提交的注解;同时实现相关 commit 和 rollback 接口;BusinessActionContextParameter 注解用于将参数添加到 BusinessActionContext 上下文中,方便 commit 和 rollback 方法能获取到;另外注意获取到的变量都被转换成了字符串,对象变成了 json 字符串,Long 变成了 String。
测试提交
调用:http://localhost:6318/order/addTcc
{
"commodityCode": "product01",
"count": 12,
"flag": 0,
"money": 100,
"name": "订单",
}
Storage 中能看到对应的回滚日志:
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.397 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl : reduceTccCommit事务回滚
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.398 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl : commodityCode=product01, count=12
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.404 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl : 更新库存信息TCC:Storage{uri=product01, commodityCode=product01, count=1000, extend=null, flag=0, delFlag=0, createBy=null, updateBy=null, createDate=Tue May 31 14:44:31 CST 2022, updateDate=Tue May 31 18:00:06 CST 2022, fkDomain=null, fkGroup=null}
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.411 INFO 2468 [rpcDispatch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager : TCC resource rollback result : true, xid: 192.168.3.79:8091:8043697401397272628, branchId: 8043697401397272629, resourceId: reduceTcc
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.411 INFO 2468 [rpcDispatch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> Storage 保存 -> 返回 Order 模拟抛出异常
事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 提交 Commit -> Order 异常后通过 TC 调用 Storage 的 TCCrollBack 方法 -> Order 本地 RM 回滚