目录
本项目代码地址(请到此处下载源码)
开始语
一位普通的程序员,慢慢在努力变强!
【Seata】SpringCloud集成Seatav1.6之XA模式👈
【Seata】SpringBoot集成Seata1.6-AT模式👈
简介
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
![](https://i-blog.csdnimg.cn/blog_migrate/fb0628807db75640be7454055d1ed898.png)
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
简单来讲:就是将seata自动控制事务的全局提交和全局回滚,改为手动提交和回滚!
创建数据库表
-- 数据库
CREATE
DATABASE seata_order DEFAULT CHARACTER SET utf8mb4;
-- seata_order.order_tbl definition
CREATE TABLE `order_tbl`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
`money` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8;
-- seata_order.tcc_fence_log TCC事务日志表
CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT 'global id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch id',
`action_name` varchar(64) NOT NULL COMMENT 'action name',
`status` tinyint(4) NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` datetime(3) NOT NULL COMMENT 'create time',
`gmt_modified` datetime(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 数据库
CREATE
DATABASE seata_stock DEFAULT CHARACTER SET utf8mb4;
-- seata_stock.stock_tbl definition
CREATE TABLE `stock_tbl`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- seata_order.tcc_fence_log TCC事务日志表
CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT 'global id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch id',
`action_name` varchar(64) NOT NULL COMMENT 'action name',
`status` tinyint(4) NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` datetime(3) NOT NULL COMMENT 'create time',
`gmt_modified` datetime(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
yml配置文件(包含nacos配置)
![](https://i-blog.csdnimg.cn/blog_migrate/018c0fad4b313c8019f651acc7dc8e12.png)
order-service-config.yaml
-- order-service-config.yaml
spring:
# staet----------------------------mysql服务配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.233.143:3306/seata_order?allowMultiQueries=true
username: root
password: getianyu_ROOT_123
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 10
min-idle: 10
maxActive: 200
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
connectionErrorRetryAttempts: 3
breakAfterAcquireFailure: true
timeBetweenConnectErrorMillis: 300000
asyncInit: true
remove-abandoned: false
remove-abandoned-timeout: 1800
transaction-query-timeout: 6000
filters: stat,wall,log4j2
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
web-stat-filter:
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
url-pattern: "/druid/*"
allow:
deny:
reset-enable: false
login-username: admin
login-password: admin
# end----------------------------mysql服务配置
# staet----------------------------mybatis-plus服务配置
mybatis-plus:
global-config:
db-config:
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
mapper-locations: classpath*:**/repository/xml/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 驼峰,_映射 app_name = appName
map-underscore-to-camel-case: true
# end----------------------------mybatis-plus服务配置
logging:
level:
io.seata: debug
# staet----------------------------seata服务配置
seata:
config:
type: nacos
nacos:
server-addr: 192.168.233.180:8848
namespace: seata-server
group: SEATA_GROUP
username: nacos
password: Getring0817#.
registry:
type: nacos
nacos:
server-addr: 192.168.233.180:8848
namespace: seata-server
application: seata-server
group: SEATA_GROUP
username: nacos
password: Getring0817#.
service:
vgroup-mapping:
default_tx_group: default
disable-global-transaction: false
grouplist:
default: 192.168.233.180:8091
tx-service-group: default_tx_group
# end----------------------------seata服务配置
server:
port: 9091
spring:
application:
name: order-service
profiles:
active: @project.active@
cloud:
nacos:
config:
server-addr: @nacos.addr@
namespace: ${spring.profiles.active}
username: nacos
password: Getring0817#.
refresh-enabled: true
enabled: true
file-extension: yaml
shared-configs:
- data-id: order-service-config.yaml
refresh: true
discovery:
server-addr: @nacos.addr@
namespace: ${spring.profiles.active}
watch:
enabled: true
alibaba:
seata:
tx-service-group: default_tx_group
stock-service-config.yaml
spring:
# staet----------------------------mysql服务配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.233.143:3306/seata_stock?allowMultiQueries=true
username: root
password: getianyu_ROOT_123
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 10
min-idle: 10
maxActive: 200
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
connectionErrorRetryAttempts: 3
breakAfterAcquireFailure: true
timeBetweenConnectErrorMillis: 300000
asyncInit: true
remove-abandoned: false
remove-abandoned-timeout: 1800
transaction-query-timeout: 6000
filters: stat,wall,log4j2
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
web-stat-filter:
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
stat-view-servlet:
url-pattern: "/druid/*"
allow:
deny:
reset-enable: false
login-username: admin
login-password: admin
# end----------------------------mysql服务配置
# staet----------------------------mybatis-plus服务配置
mybatis-plus:
global-config:
db-config:
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
mapper-locations: classpath*:**/repository/xml/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 驼峰,_映射 app_name = appName
map-underscore-to-camel-case: true
# end----------------------------mybatis-plus服务配置
logging:
level:
io.seata: debug
# staet----------------------------seata服务配置
seata:
config:
type: nacos
nacos:
server-addr: 192.168.233.180:8848
namespace: seata-server
group: SEATA_GROUP
username: nacos
password: Getring0817#.
registry:
type: nacos
nacos:
server-addr: 192.168.233.180:8848
namespace: seata-server
application: seata-server
group: SEATA_GROUP
username: nacos
password: Getring0817#.
service:
vgroup-mapping:
default_tx_group: default
disable-global-transaction: false
grouplist:
default: 192.168.233.180:8091
tx-service-group: default_tx_group
# end----------------------------seata服务配置
server:
port: 9092
spring:
application:
name: stock-service
profiles:
active: @project.active@
cloud:
nacos:
config:
server-addr: @nacos.addr@
namespace: ${spring.profiles.active}
username: nacos
password: Getring0817#.
refresh-enabled: true
enabled: true
file-extension: yaml
shared-configs:
- data-id: stock-service-config.yaml
refresh: true
discovery:
server-addr: @nacos.addr@
namespace: ${spring.profiles.active}
watch:
enabled: true
alibaba:
seata:
tx-service-group: default_tx_group
代码编写
发起请求服务orders
/**
* 下单:创建订单、减库存,涉及到两个服务
*
* @param userId
* @param commodityCode
* @param count
* @param tag commit接口如果入参是[成功],不抛出异常,[失败],抛出异常
*/
@Override
@GlobalTransactional
public void placeOrder(String userId, String commodityCode, Integer count,String tag) {
log.info("OrderService XID = {}", RootContext.getXID());
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
orderMoney);
orderDAO.insert(order);
// stock服务未报错,order服务报错
try {
stockFeignClient.deduct(commodityCode, count);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 输入commit接口
if("product-1".equals(commodityCode) && tag.equals("失败")){
throw new RuntimeException("订单业务处理异常...");
}
}
被orders调用的服务stock
service接口层
package com.work.stock.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
/**
* @author 猿仁
* @date 2023-03-11 15:00
*/
@LocalTCC
public interface StockService {
/**
* 减库存
*
* @param commodityCode
* @param count
*/
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "busCommit", rollbackMethod = "busRollback", useTCCFence = true)
void deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
@BusinessActionContextParameter(paramName = "count") int count);
/**
* 提交事务
*
* @param actionContext
* @return
*/
boolean busCommit(BusinessActionContext actionContext);
/**
* 回滚事务
*
* @param actionContext
* @return
*/
boolean busRollback(BusinessActionContext actionContext);
}
serviceimpl实现层
package com.work.stock.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.work.stock.entity.Stock;
import com.work.stock.repository.StockDAO;
import com.work.stock.service.StockService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* demo模板来源于官网,此处进行了改造
*
* @author 猿仁
* @date 2023/3/11 20:18
*/
@Slf4j
@Service
public class StockServiceImpl extends ServiceImpl<StockDAO, Stock> implements StockService {
@Resource
private StockDAO stockDAO;
/**
* 减库存
*
* @param commodityCode
* @param count
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void deduct(String commodityCode, int count) {
log.info("StockService XID = {}", RootContext.getXID());
QueryWrapper<Stock> wrapper = new QueryWrapper<>();
wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
Stock stock = stockDAO.selectOne(wrapper);
stock.setCount(stock.getCount() - count);
stockDAO.updateById(stock);
if (commodityCode.equals("product-2")) {
throw new RuntimeException("异常:模拟业务异常:stock branch exception");
}
}
@Override
public boolean busCommit(BusinessActionContext actionContext) {
log.info("busCommit xid = " + actionContext.getXid() + "提交成功");
return true;
}
@Override
public boolean busRollback(BusinessActionContext actionContext) {
// 编写对应的业务数据进行回滚
String commodityCode = actionContext.getActionContext("commodityCode", String.class);
int count = actionContext.getActionContext("count", Integer.class);
LambdaQueryChainWrapper<Stock> eq = lambdaQuery().eq(Stock::getCommodityCode, commodityCode);
Long count1 = eq.one().getCount();
// 扣了多少数,需要重新添加回去
lambdaUpdate().set(Stock::getCount, count + count1)
.eq(Stock::getCommodityCode, commodityCode)
.update();
log.info("busRollback xid = " + actionContext.getXid() + "回滚成功");
return true;
}
}
结果分析
1. 成功、失败...等一系列问题,都会在tcc_fence_log表中做记录(当前案例为例,那么数据将会在TCC端进行存储,也就是说会存储在seata_stock.tcc_fence_log表中)
2. 最终结果全局事务为成功时,没有出现任何异常,那么就会调用StockServiceImpl.busCommit这个方法
3. 最终结果全局事务为失败时,不管那一方出现了问题,那么就会调用StockServiceImpl.busRollback这个方法回滚当前事务修改的数据(也就是我们所说的自定义逻辑)
4. 最终调用方orders出现异常回滚,stock方出现异常也会进行回滚,从而达到一致性性