分布式事务框架seata使用总结

分布式事务框架seata使用总结

博主之前在项目通过消息中间件+本地消息表的方式搭建了一套分布式系统来解决事务一致性问题。

后面考虑补偿和工作量的问题,因此引入了阿里开源分布式框架seata。

一、什么是分布式事务?

 随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:

 

分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,

这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、

创建订单减库存事务,银行转账事务等都是分布式事务。

 

我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:

begin transaction;
	//1.本地数据库操作:张三减少金额
	//2.本地数据库操作:李四增加金额
commit transation;

但是在分布式环境下,会变成下边这样:

begin transaction;
	//1.本地数据库操作:张三减少金额
	//2.远程调用:让李四增加金额
commit transation;

可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。

因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。

二、消息中间件+本地消息表+定时任务解决分布式事务一致性问题

如下原理图:

服务A调用服务B,如果服务A只是调用查询的方式,直接可以通过fegin调用来获取

三、分布式事务demo

Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:

  • Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。

简要说说整个全局事务的执行步骤:
1.TM 向 TC 申请开启一个全局事务,TC 创建全局事务后返回全局唯一的 XID,XID 会在全局事务的上下文中传播;
2.RM 向 TC 注册分支事务,该分支事务归属于拥有相同 XID 的全局事务;
3.TM 向 TC 发起全局提交或回滚;
4.TC 调度 XID 下的分支事务完成提交或者回滚。
与 XA 方案有什么不同?
Seata 的事务提交方式跟 XA 协议的两段式提交在总体上来说基本是一致的,那它们之间有什么不同呢?
我们都知道 XA 协议它依赖的是数据库层面来保障事务的一致性,也即是说 XA 的各个分支事务是在数据库层面上驱动的,由于 XA 的各个分支事务需要有 XA 的驱动程序,一方面会导致数据库与 XA 驱动耦合,另一方面它会导致各个分支的事务资源锁定周期长,这也是它没有在互联网公司流行的重要因素。
基于 XA 协议以上的问题,Seata 另辟蹊径,既然在依赖数据库层会导致这么多问题,那我就从应用层做手脚,这还得从 Seata 的 RM 模块说起,前面也说过 RM 的主要作用了,其实 RM 在内部做了对数据库操作的代理层。

 

 

 

 

 

 结合spring boot 2.0 代码如下:

pom依赖:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <version>2.2.0.RELEASE</version>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>


        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.2.0</version>
        </dependency>

handler层加入全局事务注解

@GlobalTransactional(rollbackFor = Exception.class)
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author :huadong.chen
 * @ProjectName:-irc$
 * @Package: com.system.controller$
 * @ClassName: SeataDemoController$
 * @date :Created in 3/1/21 3:54 PM
 * @description:分布式事务--seata
 * @modified By:
 * @version: 1.01
 */
@Api(value = "分布式事务demo", tags = "分布式事务demo")
@RestController
@RequestMapping("/seata")
public class SeataDemoController {

    @Autowired
    private SysOrgDao sysOrgDao;

    @Autowired
    private VisualizationFeign feign;

    /**
     * 分布式事务demo
     *
     * @return
     */
    @ApiOperation(value = "分布式事务demo")
    @PostMapping("/test")
    @GlobalTransactional(rollbackFor = Exception.class)
    public void test(@RequestHeader("token") String token){
        SysOrg sysOrg = sysOrgDao.findByOldOrgId(1001L);
        //调用微服务
        SysOrg test = feign.test();
        System.out.println(test.getOldOrgId()+"----"+test.getOrgName());
        sysOrg.setOrgName("璧山--sys");
        sysOrgDao.save(sysOrg);
        if (true){
            throw new AppException("报错");
        }

    }
}

  

package com.system.config.aop;

import io.seata.core.context.RootContext;
import io.seata.tm.api.GlobalTransaction;
import io.seata.tm.api.GlobalTransactionContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionException;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @author :huadong.chen
 * @ProjectName: 
 * @Package: com.visualization.actionlogaop$
 * @ClassName: GlobalTransactionAop$
 * @date :Created in 3/3/21 11:24 AM
 * @description:全局事务aop,被调用方切面执行,比如A调B服务,B服务异常捕获进行rollback/完成并释放本地锁, 帮助A服务获取本地锁,从而全局锁回滚/提交
 * @modified By:
 * @version: 1.01
 */
@Slf4j
@Aspect
@Component
public class GlobalTransactionAop {

    @Value("${spring.application.name}")
    private String applicationName;

    /**
     * 切入点
     *
     * @param joinPoint
     * @throws TransactionException
     * @throws io.seata.core.exception.TransactionException
     */
    @Before("execution(* com.crunii.system.feign.calld.*.*(..))")
    public void before(JoinPoint joinPoint) throws TransactionException, io.seata.core.exception.TransactionException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        log.info("拦截需要分布式事务的方法========" + method.getName());
        GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
        // 超时时间 , 所在服务
        tx.begin(300000, applicationName);
        log.info("方法:{}创建分布式事务id:{}", method.getName(), tx.getXid());
    }

    /**
     * 异常执行结果
     *
     * @param e
     * @throws TransactionException
     * @throws io.seata.core.exception.TransactionException
     */
    @AfterThrowing(throwing = "e", pointcut = "execution(* com.crunii.system.feign.calld.*.*(..))")
    public void doRecoveryActions(Throwable e) throws TransactionException, io.seata.core.exception.TransactionException {
        log.info("方法执行异常:{}", e.getMessage());
        if (!StringUtils.isBlank(RootContext.getXID())) {
            log.info("分布式事务Id:{}, 回滚当前事务!", RootContext.getXID());
            GlobalTransactionContext.reload(RootContext.getXID()).rollback();
        }
    }

    /**
     * 方法执行结束
     *
     * @param point
     * @param result
     * @throws TransactionException
     * @throws io.seata.core.exception.TransactionException
     */
    @AfterReturning(value = "execution(* com.crunii.system.feign.calld.*.*(..))", returning = "result")
    public void afterReturning(JoinPoint point, Object result) throws TransactionException, io.seata.core.exception.TransactionException {
        log.info("方法执行结束:{}", result);
        //todo 需要根据具体场景修改此返回
        Map map = new HashMap();
        Integer status = 200;
        if (map.size() > 1) {
            status = Integer.parseInt(map.get("code").toString());
        }
        if (status != 200) {
            if (!StringUtils.isBlank(RootContext.getXID())) {
                log.info("分布式事务Id:{}, 回滚当前事务!", RootContext.getXID());
                GlobalTransactionContext.reload(RootContext.getXID()).rollback();
            }
        }
    }
}

  

client配置:file.conf.example,配置数据库连接,我这儿用的是pgsql。

## transaction log store, only used in server side
store {
  ## store mode: file、db
  mode = "db"
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "postgresql"
    driverClassName = "org.postgresql.Driver"
    url = "jdbc:postgresql://******:5432/mysql?currentSchema=system&charSet=utf-8"
    user = "***"
    password = "****"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
  }
}

 

 

posted @ 2022-03-23 15:18  心思慕晨  阅读(993)  评论(0)    收藏  举报