目录
分布式事务中的tcc模式理论介绍的文章非常多,但是网上找到一个代码实现的demo很难,包括阿里的seata官方示例都没有TCC模式的具体实现。今天我们来看一下微服务环境下使用seata TCC模式解决分布式事务的场景,同时提供一个详细的实现。
本文使用的实验环境跟上篇《springcloud+eureka整合分布式事务中间件seata》类似,都是订单、库存和账户3个微服务,全局事务从订单发起:
springboot:2.1.6.RELEASE
orm框架:jdbc
数据库:mysql
数据库连接池:HikariCP
seata server:1.3.0
springcloud:Greenwich.SR2
注:因为微服务采用跟上篇介绍的一样,所以环境搭建就不再重复写了,大家实验过程中有问题的可以参考上篇文章,或者号内留言。
理论回顾
前面我讲了2篇关于seata的文章,都是使用了seata的AT模式,seata AT模式的依赖的还是依赖单个服务或单个数据源自己的事务控制(分支事务),采用的是wal的思想,提交事务的时候同时记录undolog,如果全局事务成功,则删除undolog,如果失败,则使用undolog的数据回滚分支事务,最后删除undolog。
TCC模式的特点是不再依赖于undolog,采用2阶段提交的方式,第一阶段使用prepare尝试事务提交,第二阶段使用commit或者rollback让事务提交或者回滚。官方的示例图如下:
从示例图可以看到,TM对全局事务进行管理,RM对分支事务进行管理,而TC管理着全局事务和分支事务的状态,RM需要注册到TC。TM发起全局事务后,调用TM(每个分支事务)的prepare进行try操作,成功后TC会调用RM的commit方法,失败后TC会调用分支事务的rollback方法。
使用spring的事务管理进行尝试
我试图使用spring的编程式事务来实现2阶段提交,我们先看一下prepare方法,代码如下:
public boolean decrease(String xid, Long userId, BigDecimal payAmount) {
LOGGER.info("------->尝试扣减账户开始account");
//尝试扣减账户金额,事务不提交
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
accountDao.decrease(userId,payAmount);
//此处不提交事务
transactionStatusMap.put(xid, status);
} catch (Exception e) {
LOGGER.error("decrease parepare failure:", e);
return false;
}
LOGGER.info("------->尝试扣减账户结束account");
return true;
}
在这个方法中事务不提交,等到请求commit方法时,再提交事务,commit方法代码如下:
public boolean commit(String xid){
LOGGER.info("commit, xid:{}", xid);
if (null == transactionStatusMap.get(xid)){
return true;
}
transactionManager.commit(transactionStatusMap.get(xid));
transactionStatusMap.remove(xid);
return true;
}
但是spring是不允许这么做的,第二次http请求到来时,线程跟第一次请求的线程不一样了,所以抛出下面异常:
java.lang.IllegalStateException: No value for key [HikariDataSource (HikariPool-1)] bound to thread [http-nio-8181-exec-2]
at org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(TransactionSynchronizationManager.java:213) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCleanupAfterCompletion(DataSourceTransactionManager.java:367) ~[spring-jdbc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager.java:1007) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:793) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:305) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
这个异常源码如下:
//TransactionSynchronizationManager类
public static Object unbindResource(Object key) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Object value = doUnbindResource(actualKey);
if (value == null) {
throw new IllegalStateException(
"No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
}
return value;
}
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static Object doUnbindResource(Object actualKey) {
Map<Object, Object> map = resources.get();//resources是ThreadLocal变量,所以第二个线程不可能取到第一个线程绑定的值
if (map == null) {
return null;//此处直接返回null
}
Object value = map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
// Transparently suppress a ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
value = null;
}
if (value != null && logger.isTraceEnabled()) {
logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" +
Thread.currentThread().getName() + "]");
}
return value;
}
使用jdbc进行尝试
整个项目的sql语句跟上篇文章中基本一样,只是少了undo_log表:
#########################seata_order库
use database seata_order;
CREATE TABLE `orders` (
`id` mediumint(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`product_id` int(11) DEFAULT NULL,
`COUNT` int(11) DEFAULT NULL COMMENT '数量',
`pay_amount` decimal(10,2) DEFAULT NULL,
`status` varchar(100) DEFAULT NULL,
`add_time` datetime DEFAULT CURRENT_TIMESTAMP,
`last_update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
#########################seata_pay库
use database seata_pay;
DROP TABLE account;
CREATE TABLE `account` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`balance` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度',
`last_update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_pay`.`account` (`id`, `user_id`, `total`, `used`, `balance`) VALUES ('1', '1', '1000', '0', '100');
#########################seata_storage库
use database seata_storage;
CREATE TABLE `storage` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
回顾一下实验环境的架构图:
可以看到,order-server既是一个RM,也是一个TM,因为全局事务从这里发起。
这里重点有几个地方说明一下:
1.全局事务从订单服务发起,OrderServiceImpl类create方法,代码如下:
@GlobalTransactional
public boolean create(Order order) {
String xid = RootContext.getXID();
LOGGER.info("------->交易开始");
BusinessActionContext actionContext = new BusinessActionContext();
actionContext.setXid(xid);
boolean result = orderSaveImpl.saveOrder(actionContext, order);//订单服务prepare
if(!result){
throw new RuntimeException("保存订单失败");
}
//远程方法 扣减库存
LOGGER.info("------->扣减库存开始storage中");
result = storageApi.decrease(actionContext, order.getProductId(), order.getCount());//库存服务prepare
if(!result){
throw new RuntimeException("扣减库存失败");
}
LOGGER.info("------->扣减库存结束storage中");
//远程方法 扣减账户余额
LOGGER.info("------->扣减账户开始account中");
result = accountApi.prepare(actionContext, order.getUserId(),order.getPayAmount());//账户服务prepare
LOGGER.info("------->扣减账户结束account中" + result);
LOGGER.info("------->交易结束");
return true;
}
可以看到,全局事务发起的地方需要加@GlobalTransactional注解,这个事务首先获取了全局事务id,也就是xid,然后分别调了3个服务的prepare方法,只要有一个服务prepare返回失败,则抛出异常。
2.两阶段提交,我们以账户服务为例,接口定义如下:
@FeignClient(value = "account-server")
@LocalTCC
public interface AccountApi {
/**
* 扣减账户余额
* @param actionContext save xid
* @param userId 用户id
* @param money 金额
* @return
*/
@TwoPhaseBusinessAction(name = "accountApi", commitMethod = "commit", rollbackMethod = "rollback")
@RequestMapping("/account/decrease")
boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
/**
* Commit boolean.
*
* @param actionContext save xid
* @return the boolean
*/
@RequestMapping("/account/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
/**
* Rollback boolean.
*
* @param actionContext save xid
* @return the boolean
*/
@RequestMapping("/account/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
整个接口的注解有2个,一个是FeignClient,因为服务间采用Feign进行通信,不多说明。第二个就是@LocalTCC,这个就标注了这是一个TCC的分支事务接口类,里面定义了TCC要求的3个方法。尤其强调的是,prepare方法上面要加注解@TwoPhaseBusinessAction,注解里面要指定提交和回滚的方法。
这个接口作为feign客户端,请求发送到了账户服务,账户服务接到对应的请求后分别进行处理。
3.账户服务中controller接到请求后,调用对应的service进行处理,代码如下:
@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService{
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
private Map<String, Statement> statementMap = new ConcurrentHashMap<>(100);
private Map<String, Connection> connectionMap = new ConcurrentHashMap<>(100);
@Resource
private DataSource hikariDataSource;
@Override
public boolean decrease(String xid, Long userId, BigDecimal payAmount) {
LOGGER.info("commit, xid:{}", xid);
LOGGER.info("------->尝试扣减账户开始account");
//模拟超时异常,全局事务回滚
/*try {
Thread.sleep(30*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
try {
//尝试扣减账户金额,事务不提交
Connection connection = hikariDataSource.getConnection();
connection.setAutoCommit(false);
String sql = "UPDATE account SET balance = balance - ?,used = used + ? where user_id = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setBigDecimal(1, payAmount);
stmt.setBigDecimal(2, payAmount);
stmt.setLong(3, userId);
stmt.executeUpdate();
statementMap.put(xid, stmt);
connectionMap.put(xid, connection);
} catch (Exception e) {
LOGGER.error("decrease parepare failure:", e);
return false;
}
LOGGER.info("------->尝试扣减账户结束account");
return true;
}
public boolean commit(String xid){
LOGGER.info("扣减账户金额, commit, xid:{}", xid);
PreparedStatement statement = (PreparedStatement) statementMap.get(xid);
Connection connection = connectionMap.get(xid);
try {
if (null != connection){
connection.commit();
}
} catch (SQLException e) {
LOGGER.error("扣减账户失败:", e);
return false;
}finally {
try {
statementMap.remove(xid);
connectionMap.remove(xid);
if (null != statement){
statement.close();
}
if (null != connection){
connection.close();
}
} catch (SQLException e) {
LOGGER.error("扣减账户提交事务后关闭连接池失败:", e);
}
}
return true;
}
public boolean rollback(String xid){
LOGGER.info("扣减账户金额, rollback, xid:{}", xid);
PreparedStatement statement = (PreparedStatement) statementMap.get(xid);
Connection connection = connectionMap.get(xid);
try {
connection.rollback();
} catch (SQLException e) {
return false;
}finally {
try {
if (null != statement){
statement.close();
}
if (null != connection){
connection.close();
}
statementMap.remove(xid);
connectionMap.remove(xid);
} catch (SQLException e) {
LOGGER.error("扣减账户回滚事务后关闭连接池失败:", e);
}
}
return true;
}
}
从上面的代码中我们可以看到,我们使用了jdbc进行了事务管理,prepare方法缓存了Statement和Connection,commit和rollback方法进行事务的提交和回滚,然后释放连接。
实验结果
启动eureka server,seata server,然后启动上面3个服务。
运行之前,我们先看一下数据库的数据,seata_order库中orders表没有数据,seata_pay库中account表和seata_storage库中storage表数据如下图:
1.模拟commit事务
服务启动后,我们用postman发一个post请求,请求url:http://localhost:8180/order/create,请求参数:
{
"userId":1,
"productId":1,
"count":1,
"money":1,
"payAmount":50
}
请求成功后,我们查看上面3张表的数据,如下图:
可以看到,数据已经提交成功了。这时我们查看一下order-server日志:
2020-08-23 10:13:00.187 INFO 638408 --- [nio-8180-exec-1] io.seata.tm.TransactionManagerHolder : TransactionManager Singleton io.seata.tm.DefaultTransactionManager@2501a5fc
2020-08-23 10:13:00.207 INFO 638408 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.59.132:8091:41466478607220736]
2020-08-23 10:13:00.212 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->交易开始
2020-08-23 10:13:00.232 INFO 638408 --- [nio-8180-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-08-23 10:13:00.431 INFO 638408 --- [nio-8180-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-08-23 10:13:00.445 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->扣减库存开始storage中
2020-08-23 10:13:00.645 INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:00.899 INFO 638408 --- [nio-8180-exec-1] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-storage-server
2020-08-23 10:13:00.900 INFO 638408 --- [nio-8180-exec-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: storage-server instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=storage-server,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2020-08-23 10:13:00.906 INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2020-08-23 10:13:00.938 INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:00.941 INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client storage-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=storage-server,current list of Servers=[10.192.86.60:8182],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:10.192.86.60:8182; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@50d8d3a7
2020-08-23 10:13:01.165 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->扣减库存结束storage中
2020-08-23 10:13:01.165 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->扣减账户开始account中
2020-08-23 10:13:01.211 INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:01.212 INFO 638408 --- [nio-8180-exec-1] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-account-server
2020-08-23 10:13:01.212 INFO 638408 --- [nio-8180-exec-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: account-server instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=account-server,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2020-08-23 10:13:01.213 INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2020-08-23 10:13:01.214 INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:01.215 INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client account-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=account-server,current list of Servers=[10.192.86.60:8181],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:10.192.86.60:8181; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@7145f1b9
2020-08-23 10:13:01.640 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->扣减账户结束account中true
2020-08-23 10:13:01.641 INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl : ------->交易结束
2020-08-23 10:13:01.648 INFO 638408 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466478707884032,branchType=TCC,resourceId=orderApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580213,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:13:01.651 INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.59.132:8091:41466478607220736 41466478707884032 orderApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580213,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:13:01.652 INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.sample.service.OrderSaveImpl : 保存订单, commit, xid:192.168.59.132:8091:41466478607220736
2020-08-23 10:13:01.657 INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractResourceManager : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466478707884032, resourceId: orderApi
2020-08-23 10:13:01.659 INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.664 INFO 638408 --- [ch_RMROLE_1_2_8] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466479647408128,branchType=TCC,resourceId=storageApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580446,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:13:01.664 INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.59.132:8091:41466478607220736 41466479647408128 storageApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580446,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:13:01.677 INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466479647408128, resourceId: storageApi
2020-08-23 10:13:01.677 INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.680 INFO 638408 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466482671501312,branchType=TCC,resourceId=accountApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321581166,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:13:01.680 INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.59.132:8091:41466478607220736 41466482671501312 accountApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321581166,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:13:01.695 INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractResourceManager : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466482671501312, resourceId: accountApi
2020-08-23 10:13:01.695 INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.702 INFO 638408 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [192.168.59.132:8091:41466478607220736] commit status: Committed
2020-08-23 10:13:01.911 INFO 638408 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:02.215 INFO 638408 --- [erListUpdater-1] c.netflix.config.ChainedDynamicProperty : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
仔细查看这段日志,我们发现有3个2阶段提交成功(PhaseTwo_Committed),resourceId分别是orderApi、storageApi和accountApi,而在库存服务和账户服务也分别有相关日志的打印,如下:
2020-08-23 10:13:01.123 INFO 645052 --- [nio-8182-exec-3] i.s.sample.service.StorageServiceImpl : ------->扣减库存prepare开始
2020-08-23 10:13:01.135 INFO 645052 --- [nio-8182-exec-3] i.s.sample.service.StorageServiceImpl : ------->扣减库存prepare结束
2020-08-23 10:13:01.150 WARN 645052 --- [nio-8182-exec-3] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.59.132:8091:41466478607220736 to null
2020-08-23 10:13:01.672 INFO 645052 --- [nio-8182-exec-4] i.s.sample.service.StorageServiceImpl : 扣减库存金额, commit, xid:192.168.59.132:8091:41466478607220736
2020-08-23 10:13:01.358 INFO 643544 --- [nio-8181-exec-1] i.s.sample.service.AccountServiceImpl : ------->尝试扣减账户开始account
2020-08-23 10:13:01.358 INFO 643544 --- [nio-8181-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-08-23 10:13:01.606 INFO 643544 --- [nio-8181-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-08-23 10:13:01.623 INFO 643544 --- [nio-8181-exec-1] i.s.sample.service.AccountServiceImpl : ------->尝试扣减账户结束account
2020-08-23 10:13:01.638 WARN 643544 --- [nio-8181-exec-1] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.59.132:8091:41466478607220736 to null
2020-08-23 10:13:01.686 INFO 643544 --- [nio-8181-exec-2] i.s.sample.service.AccountServiceImpl : 扣减账户金额, commit, xid:192.168.59.132:8091:41466478607220736
2.模拟rollback事务
修改OrderServiceImpl中create方法,最后一行代码改为如下:
throw new RuntimeException("调用2阶段提交的rollback方法");
//return true;
重启服务后,我们再次模拟发送上面的post请求,参数不变,这时查看order-server的日志如下:
前面的日志都一样,我们看一下回滚日志:
2020-08-23 10:54:54.209 INFO 652376 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477022726766592,branchType=TCC,resourceId=accountApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324094118,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:54:54.212 INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477022726766592 accountApi
2020-08-23 10:54:54.247 INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractResourceManager : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477022726766592, resourceId: accountApi
2020-08-23 10:54:54.249 INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.258 INFO 652376 --- [ch_RMROLE_1_2_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477020038217728,branchType=TCC,resourceId=storageApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324093476,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:54:54.258 INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477020038217728 storageApi
2020-08-23 10:54:54.301 INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477020038217728, resourceId: storageApi
2020-08-23 10:54:54.301 INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.306 INFO 652376 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477018775732224,branchType=TCC,resourceId=orderApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324093164,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:54:54.306 INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477018775732224 orderApi
2020-08-23 10:54:54.307 INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.sample.service.OrderSaveImpl : 保存订单金额, rollback, xid:192.168.59.132:8091:41477018662486016
2020-08-23 10:54:54.316 INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractResourceManager : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477018775732224, resourceId: orderApi
2020-08-23 10:54:54.316 INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.336 INFO 652376 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [192.168.59.132:8091:41477018662486016] rollback status: Rollbacked
2020-08-23 10:54:54.353 ERROR 652376 --- [nio-8180-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 调用2阶段提交的rollback方法] with root cause
java.lang.RuntimeException: 调用2阶段提交的rollback方法
at io.seata.sample.service.OrderServiceImpl.create(OrderServiceImpl.java:65) ~[classes/:na]
可以看出,上面有3个2阶段的事务回滚(PhaseTwo_Rollbacked),resourceId分别是orderApi、storageApi和accountApi,这时我们再看库存服务和账户服务的日志,如下:
2020-08-23 10:54:54.101 INFO 645052 --- [nio-8182-exec-2] i.s.sample.service.StorageServiceImpl : ------->扣减库存prepare开始
2020-08-23 10:54:54.103 INFO 645052 --- [nio-8182-exec-2] i.s.sample.service.StorageServiceImpl : ------->扣减库存prepare结束
2020-08-23 10:54:54.105 WARN 645052 --- [nio-8182-exec-2] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.59.132:8091:41477018662486016 to null
2020-08-23 10:54:54.189 INFO 643544 --- [nio-8181-exec-4] i.s.sample.service.AccountServiceImpl : ------->尝试扣减账户开始account
2020-08-23 10:54:54.191 INFO 643544 --- [nio-8181-exec-4] i.s.sample.service.AccountServiceImpl : ------->尝试扣减账户结束account
2020-08-23 10:54:54.192 WARN 643544 --- [nio-8181-exec-4] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.59.132:8091:41477018662486016 to null
2020-08-23 10:54:54.223 INFO 643544 --- [nio-8181-exec-5] i.s.sample.service.AccountServiceImpl : 扣减账户金额, rollback, xid:192.168.59.132:8091:41477018662486016
这时我们再查看数据库,3张表都没有变。可见事务确实做了回滚。
总结
分布式事务的TCC模式和AT模式的本质区别是一个是2阶段提交,一个是交易补偿。seata框架对AT模式的支持是非常方便的,但是对TCC模式的支持,最大的就是自动触发commit和prepare方法,真正的实现还是需要开发人员自己做。
大家有更好的实现2阶段事务提交的方法,欢迎指点。
源码地址:
https://github.com/jinjunzhu/springcloud-eureka-seata-tcc.git
欢迎关注个人公众号,共同学习,共同成长