一、Seata简介
2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),蚂蚁金服后在Fescar 0.4.0 版本中贡献了 TCC 模式。后来更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。
Seata三大基本组件:
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
Seata官方调用流程图:
二、Fescar相比XA二阶段优缺点:
优点:
- 基于SQL解析实现了自动补偿,降低业务侵入性。
- 第一阶段就本地事务就提交了 ,二阶段commit是异步操作相对XA两段全部持有资源更高效。
- Fescar提供了两种模式,AT和MT。在AT模式下事务资源可以是任何支持ACID的数据库,在MT模式下事务资源没有限制,可以是缓存,可以是文件,可以是其他的等等。当然这两个模式也可以混用。
- global lock全局锁实现了写隔离与读隔离。
- Undolog日志自动清理
缺点:
- 代码入侵性体现为配置Fescar的数据代理和加个注解,每个业务库都需要一个Undolog表。
- 从调用图中开源看出性能损耗有:一条Update的SQL,获取全局事务xid(TC通讯)、before image(查询)、after image(查询)、insert undo log(Undolog表的blob字段数据量可不小)、before commit(TC通讯,判断锁冲突);为了自动补偿在Undolog表花了不小开销,而且触发概率比较低。
- 二阶段commit也是需要占用系统资源。
- 二阶段回滚需要删除各节点的Undolog才能释放全局锁。
三、实验
本次实验使用的是官方提供的 springcloud-eureka-feign-mybatis-seata工程,模拟远程调用超时异常;通过 localhost:8180/order/create?userId=1&productId=1&count=10&money=100触发流程,order本地创建订单调用,远程storage扣减库存,远程扣减账户余额时候模拟该超时异常。下面展示下异常情况下日志信息:
OrderServerApplication日志展示了事务增强拦截器GlobalTransactionalInterceptor
i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.3.2:8091:2044579200]
i.seata.sample.service.OrderServiceImpl : ------->交易开始
i.seata.sample.service.OrderServiceImpl : ------->扣减账户开始order中
i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.3.2:8091:2044579200,branchId=2044579202,branchType=AT,resourceId=jdbc:mysql://127.0.0.1/seat-order,applicationData=null
io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.3.2:8091:2044579200 2044579202 jdbc:mysql://127.0.0.1/seat-order
i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.3.2:8091:2044579200 branch 2044579202, undo_log deleted with GlobalFinished
io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
i.seata.tm.api.DefaultGlobalTransaction : [192.168.3.2:8091:2044579200] rollback status: Rollbacked
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.RetryableException: Read timed out executing GET http://account-server/account/decrease?userId=1&money=100] with root cause
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_231]
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_231]
at java.net.SocketInputStream.read(SocketInputStream.java:171) ~[na:1.8.0_231]
at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[na:1.8.0_231]
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246) ~[na:1.8.0_231]
at java.io.BufferedInputStream.read1(BufferedInputStream.java:286) ~[na:1.8.0_231]
at java.io.BufferedInputStream.read(BufferedInputStream.java:345) ~[na:1.8.0_231]
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735) ~[na:1.8.0_231]
at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678) ~[na:1.8.0_231]
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1593) ~[na:1.8.0_231]
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498) ~[na:1.8.0_231]
at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:480) ~[na:1.8.0_231]
at feign.Client$Default.convertResponse(Client.java:143) ~[feign-core-10.2.3.jar:na]
at feign.Client$Default.execute(Client.java:68) ~[feign-core-10.2.3.jar:na]
at com.alibaba.cloud.seata.feign.SeataFeignClient.execute(SeataFeignClient.java:57) ~[spring-cloud-alibaba-seata-2.1.0.RELEASE.jar:2.1.0.RELEASE]
at org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer.execute(FeignLoadBalancer.java:93) ~[spring-cloud-openfeign-core-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer.execute(FeignLoadBalancer.java:56) ~[spring-cloud-openfeign-core-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at com.netflix.client.AbstractLoadBalancerAwareClient$1.call(AbstractLoadBalancerAwareClient.java:104) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at com.netflix.loadbalancer.reactive.LoadBalancerCommand$3$1.call(LoadBalancerCommand.java:303) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at com.netflix.loadbalancer.reactive.LoadBalancerCommand$3$1.call(LoadBalancerCommand.java:287) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at rx.internal.util.ScalarSynchronousObservable$3.call(ScalarSynchronousObservable.java:231) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.util.ScalarSynchronousObservable$3.call(ScalarSynchronousObservable.java:228) ~[rxjava-1.3.8.jar:1.3.8]
at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeConcatMap$ConcatMapSubscriber.drain(OnSubscribeConcatMap.java:286) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeConcatMap$ConcatMapSubscriber.onNext(OnSubscribeConcatMap.java:144) ~[rxjava-1.3.8.jar:1.3.8]
at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:185) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:180) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:94) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:42) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) ~[rxjava-1.3.8.jar:1.3.8]
at rx.Observable.subscribe(Observable.java:10423) ~[rxjava-1.3.8.jar:1.3.8]
at rx.Observable.subscribe(Observable.java:10390) ~[rxjava-1.3.8.jar:1.3.8]
at rx.observables.BlockingObservable.blockForSingle(BlockingObservable.java:443) ~[rxjava-1.3.8.jar:1.3.8]
at rx.observables.BlockingObservable.single(BlockingObservable.java:340) ~[rxjava-1.3.8.jar:1.3.8]
at com.netflix.client.AbstractLoadBalancerAwareClient.executeWithLoadBalancer(AbstractLoadBalancerAwareClient.java:112) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
at org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(LoadBalancerFeignClient.java:83) ~[spring-cloud-openfeign-core-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at com.alibaba.cloud.seata.feign.SeataLoadBalancerFeignClient.execute(SeataLoadBalancerFeignClient.java:56) ~[spring-cloud-alibaba-seata-2.1.0.RELEASE.jar:2.1.0.RELEASE]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:108) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy111.decrease(Unknown Source) ~[na:na]
at io.seata.sample.service.OrderServiceImpl.create(OrderServiceImpl.java:50) ~[classes/:na]
at io.seata.sample.service.OrderServiceImpl$$FastClassBySpringCGLIB$$3d2d368a.invoke() ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at io.seata.spring.annotation.GlobalTransactionalInterceptor$1.execute(GlobalTransactionalInterceptor.java:109) ~[seata-all-1.2.0.jar:1.2.0]
at io.seata.tm.api.TransactionalTemplate.execute(TransactionalTemplate.java:104) ~[seata-all-1.2.0.jar:1.2.0]
at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalTransaction(GlobalTransactionalInterceptor.java:106) ~[seata-all-1.2.0.jar:1.2.0]
at io.seata.spring.annotation.GlobalTransactionalInterceptor.invoke(GlobalTransactionalInterceptor.java:83) ~[seata-all-1.2.0.jar:1.2.0]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at io.seata.sample.service.OrderServiceImpl$$EnhancerBySpringCGLIB$$9c1f4d2e.create() ~[classes/:na]
at io.seata.sample.controller.OrderController.create(OrderController.java:29) ~[classes/:na]
........省略异常
StorageServerApplication日志展示事务分支Branch Rollbacked
i.s.sample.service.StorageServiceImpl : ------->扣减库存开始
i.s.sample.service.StorageServiceImpl : ------->扣减库存结束
c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.3.2:8091:2044579200 to null
i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.3.2:8091:2044579200,branchId=2044579204,branchType=AT,resourceId=jdbc:mysql://127.0.0.1/seat-storage,applicationData=null
io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.3.2:8091:2044579200 2044579204 jdbc:mysql://127.0.0.1/seat-storage
i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.3.2:8091:2044579200 branch 2044579204, undo_log deleted with GlobalFinished : Branch Rollbacked result: PhaseTwo_Rollbacked
AccountServerApplication日志出现sql exception
i.s.sample.service.AccountServiceImpl : ------->扣减账户开始account中
i.s.r.d.exec.AbstractDMLBaseExecutor : execute executeAutoCommitTrue error:io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]
java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]
...省略一些重要异常堆栈信息
2020-05-30 23:31:56.652 WARN 5960 --- [nio-8181-exec-2] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.3.2:8091:2044579200 to null
2020-05-30 23:31:56.654 ERROR 5960 --- [nio-8181-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.jdbc.UncategorizedSQLException:
### Error updating database. Cause: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]
### The error may exist in file [E:\document\GitHub\seata-samples-master\springcloud-eureka-feign-mybatis-seata\account-server\target\classes\mapper\AccountMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: UPDATE account SET residue = residue - ?,used = used + ? where user_id = ?;
### Cause: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]
; uncategorized SQLException; SQL state [null]; error code [0]; io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]; nested exception is java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[192.168.3.2:8091:2044579200] ]] with root cause
四、分布式事务公共模块
1、创建工程common_fescar,引入依赖
0.4.2
com.alibaba.fescar
fescar-tm
${fescar.version}
com.alibaba.fescar
fescar-spring
${fescar.version}
2、将fescar配置文件
拷贝到resources工程下
3、资源提供者每个线程绑定一个XID
public class FescarRMRequestFilter extends OncePerRequestFilter {
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( FescarRMRequestFilter.class);
/**
* 给每次线程请求绑定一个XID
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String currentXID = request.getHeader( FescarAutoConfiguration.FESCAR_XID);
if(!StringUtils.isEmpty(currentXID)){
RootContext.bind(currentXID);
LOGGER.info("当前线程绑定的XID :" + currentXID);
}
try{
filterChain.doFilter(request, response);
} finally {
String unbindXID = RootContext.unbind();
if(unbindXID != null){
LOGGER.info("当前线程从指定XID中解绑 XID :" + unbindXID);
if(!currentXID.equals(unbindXID)){
LOGGER.info("当前线程的XID发生变更");
}
}
if(currentXID != null){
LOGGER.info("当前线程的XID发生变更");
}
}
}
}
4、RestInterceptor过滤器,每次请求都将XID转发到其他微服务
public class FescarRestInterceptor implements RequestInterceptor, ClientHttpRequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
requestTemplate.header( FescarAutoConfiguration.FESCAR_XID, xid);
}
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
HttpHeaders headers = request.getHeaders();
headers.put( FescarAutoConfiguration.FESCAR_XID, Collections.singletonList(xid));
}
return execution.execute(request, body);
}
}
5、创建FescarAutoConfiguration类
/**
* * 创建数据源
* * 定义全局事务管理器扫描对象
* * 给所有RestTemplate添加头信息防止微服务之间调用问题
*/
@Configuration
public class FescarAutoConfiguration {
public static final String FESCAR_XID = "fescarXID";
/***
* 创建代理数据库
* @param environment
* @return
*/
@Bean
public DataSource dataSource(Environment environment){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
try {
dataSource.setDriver(DriverManager.getDriver(environment.getProperty("spring.datasource.url")));
} catch (SQLException e) {
throw new RuntimeException("can't recognize dataSource Driver");
}
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return new DataSourceProxy(dataSource);
}
/***
* 全局事务扫描器
* 用来解析带有@GlobalTransactional注解的方法,然后采用AOP的机制控制事务
* @param environment
* @return
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment){
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("fescar.group.name");
if(applicationName == null){
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
}else{
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
/***
* 每次微服务和微服务之间相互调用
* 要想控制全局事务,每次TM都会请求TC生成一个XID,每次执行下一个事务,也就是调用其他微服务的时候都需要将该XID传递过去
* 所以我们可以每次请求的时候,都获取头中的XID,并将XID传递到下一个微服务
* @param restTemplates
* @return
*/
@ConditionalOnBean({RestTemplate.class})
@Bean
public Object addFescarInterceptor(Collection restTemplates){
restTemplates.stream()
.forEach(restTemplate -> {
List interceptors = restTemplate.getInterceptors();
if(interceptors != null){
interceptors.add(fescarRestInterceptor());
}
});
return new Object();
}
@Bean
public FescarRMRequestFilter fescarRMRequestFilter(){
return new FescarRMRequestFilter();
}
@Bean
public FescarRestInterceptor fescarRestInterceptor(){
return new FescarRestInterceptor();
}
}