基于Seata实现分布式事务

2,723 阅读7分钟

一、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(<generated>) ~[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(<generated>) ~[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,引入依赖

<properties>
    <fescar.version>0.4.2</fescar.version>
</properties>
<dependencies>
    <dependency>
        <groupId>com.alibaba.fescar</groupId>
        <artifactId>fescar-tm</artifactId>
        <version>${fescar.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.fescar</groupId>
        <artifactId>fescar-spring</artifactId>
        <version>${fescar.version}</version>
    </dependency>
</dependencies>

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<RestTemplate> restTemplates){
        restTemplates.stream()
                .forEach(restTemplate -> {
                    List<ClientHttpRequestInterceptor> 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();
    }
}

6、记得将涉及到分布式事务的每个数据库都新建一个Undolog表

五、参考资料

  1. 官方实验Demo集合
  2. 本次博客实验所用Demo
  3. 分布式事务框架Fescar在SpringCloud环境下的应用实践