分布式事务的解决方案(Seata)(技术篇2-7-2)

129 阅读6分钟

Seata原理概述

image.png image.png

Seata的AT模式

服务框架采用dubbo+ZK的组合 image.png

Seata的配置

  • file.conf配置:使用file配置 ,也可以使用zk, nacos等注册中心。
  • file中使用db模式进行存储

db {
  ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
  datasource = "druid"
  ## mysql/oracle/postgresql/h2/oceanbase etc.
  dbType = "mysql"
  driverClassName = "com.mysql.jdbc.Driver"
  #url = "jdbc:mysql://127.0.0.1:3306/seata_tcc"
  url = "jdbc:mysql://127.0.0.1:3306/seata"
  user = "root"
  password = "root123456"
  minConn = 5
  maxConn = 30
  globalTable = "global_table"
  branchTable = "branch_table"
  lockTable = "lock_table"
  queryLimit = 100
  maxWait = 5000
}

<!--第一部分:mybatis配置-->
    <!--配置读取Propertity文件的位置-->
    <context:property-placeholder location="classpath:config/jdbc.properties"></context:property-placeholder>

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName_account}"></property>
        <property name="url" value="${url_account}"></property>
        <property name="username" value="${username_account}"></property>
        <property name="password" value="${password_account}>"></property>
    </bean>

    <!--配置SqlSessionFactoryBean-->
    <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--        <property name="dataSource" ref="dataSource"></property>-->
        <!--注入Seata提供的数据源代理对象-->
        <property name="dataSource" ref="dataSourceProxy"></property>
        <!--当你设置这个 ,那么在Mybatis的Mapper文件里面就可以直接写对应的类名 而不用写全路径名了-->
        <property name="typeAliasesPackage" value="com.itheima.domain"></property>
    </bean>

    <!--配置mybatis的扫描器-->
    <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.itheima.dao"></property>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"></property>
    </bean>

    <!--第二部分:dubbo配置-->
    <!--配置dubbo的服务名称 -->
    <dubbo:application name="seata-account" ></dubbo:application>
    <!--配置注册中心的地址-->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry>
    <!--配置服务的协议和端口-->
    <dubbo:protocol name="dubbo" port="20881" host="127.0.0.1"></dubbo:protocol>


    <!--第三部分:seata配置-->
    <!--seata的全局配置,配置全局事务管理的扫描器-->
    <bean id="globalTransactionScanner" class="io.seata.spring.annotation.GlobalTransactionScanner">
        <!--注入服务的名称-->
        <constructor-arg value="seata-account"></constructor-arg>
        <!--注入事务的服务组名称(该名称的声明是在TC事务协调器的配置中)
        Seata-server 的file.conf的配置-->
        <constructor-arg value="my_test_tx_group"></constructor-arg>
    </bean>

    <bean id="dataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
        <!--注入原始数据源-->
        <constructor-arg ref="dataSource"></constructor-arg>
    </bean>

    <!--在早期的fescar版本中,无须此配置,但是Seata版本此配置不能省略,配置的是undo_log操作的解析器
     seata中事务是要真正提交,此处是解析undo_log文件-->
    <bean id="undoLogParser" class="io.seata.rm.datasource.undo.parser.JacksonUndoLogParser"></bean>

seata代码使用

@Service  //dubbo的service中,加入超时时间,禁用重试
public class OrderInfoServiceImpl implements OrderInfoService {

    @Autowired
    private OrderInfoDao orderInfoDao;

    @Reference(check = false)
    private ItemService itemService;

    /****
     * 创建订单
     * 调用ItemService修改库存(调用AccountService修改余额)
     *
     * @param  usernumber:购买商品的用户
     * @param  id:购买的商品ID
     * @param  count:要减的数量
     */
    @GlobalTransactional(name="seata-itheima-tx")
    @Override
    public int create(String usernumber, String id, Integer count){
        //从数据库查询商品信息
        Item item = new Item();
        item.setId(id);
        item.setNum(count);
        item.setPrice(100L);
        item.setTitle("华为荣耀4");

        //创建订单
        OrderInfo orderInfo = new OrderInfo();
        String orderId = UUID.randomUUID().toString().replace("-","").toUpperCase();
        orderInfo.setId(orderId);
        orderInfo.setMoney(item.getPrice()*count);
        orderInfo.setCreatetime(new Date());
        orderInfo.setUsernumber(usernumber);
        int acount = orderInfoDao.add(orderInfo);

        System.out.println("添加订单受影响行数:"+acount);

        //制造异常
//        System.out.println("订单添加成功后,出现异常。。。。");
//        int q=10/0;

        //调用ItemService(远程调用)
        Account account = new Account();
        account.setUsernumber(usernumber);
        account.setMoney(item.getPrice()*count); //花掉的钱
        itemService.update(item,account);

        //制造异常
        System.out.println("开始报错了。。。。");
       // int q=10/0;
        return  acount;
    }


}
  • 关于undoLog的配置:Seata的事物是真提交而非假提交,当提交完成之后如果需要撤销则根据undoLog中的数据进行撤销操作。
  • 关于事务组的配置:
    • 实际上是Seata的事物协调器。 image.png

Seata的AT模式的三种异常

image.png

  • AT也是二阶段提交,但是不是刚性事物,也不是柔性事务。

  • 问题1: Seata为什么可以进行回滚操作?
    答:Seata的undoLog中会生成json数据,会记录afterImage操作之前的数据,和beforeImage之后的数据,如果正常执行则此json数据就删除。
    如果需要回滚,则会拿undo_log中之后的数据与数据库的数据进行对比,如果两者一致则说明没有生成脏数据,则Seata会拿之前的数据和数据库中的数据做对比,然后生成逆向的操作sql,执行此操作sql将异常的数据进行回滚删除。

  • 问题2:为什么问题1中提到在回滚数据的时候需要先将操作之后的数据与数据库现有数据作比对呢?
    答:因为数据库事务提交之后是可以被其他线程消费的,这样可能导致产生脏数据或者其他异常数据,需要要进行比对,确保数据没有被其他线程消费,然后在进行修改。

  • Seata相当于自动生成了数据对冲的概念。那如果生成逆向sql呢?需要看源码。

  • Seata操作数据库的时候会进行锁表,如果A服务执行完成了但是全局事务没有锁定,则其他的事物是无法对当前表进行操作的,只有全局事务走完则此表撤销锁,然后可以被其他线程正常执行。

  • AT模式的核心:逆向生成Sql,对于Mysql,Oracle支持比较好,其他的数据库是否支持需要查资料核对了。

  • Seata原理链接:

{
    "@class":"io.seata.rm.datasource.undo.BranchUndoLog",
    "xid":"192.168.181.2:8091:4386660905323926065",
    "branchId":4386660905323926071,
    "sqlUndoLogs":[
        "java.util.ArrayList",
        [
            {
                "@class":"io.seata.rm.datasource.undo.SQLUndoLog",
                "sqlType":"INSERT",
                "tableName":"t_order",
                "beforeImage":{
                    "@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
                    "tableName":"t_order",
                    "rows":[
                        "java.util.ArrayList",
                        [

                        ]
                    ]
                },
                "afterImage":{
                    "@class":"io.seata.rm.datasource.sql.struct.TableRecords",
                    "tableName":"t_order",
                    "rows":[
                        "java.util.ArrayList",
                        [
                            {
                                "@class":"io.seata.rm.datasource.sql.struct.Row",
                                "fields":[
                                    "java.util.ArrayList",
                                    [
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"id",
                                            "keyType":"PRIMARY_KEY",
                                            "type":4,
                                            "value":31
                                        },
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"order_no",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"63098e74e93b49bba77f1957e8fdab39"
                                        },
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"user_id",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"1"
                                        },
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"commodity_code",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"C201901140001"
                                        },
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"count",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":50
                                        },
                                        {
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"amount",
                                            "keyType":"NULL",
                                            "type":8,
                                            "value":100
                                        }
                                    ]
                                ]
                            }
                        ]
                    ]
                }
            }
        ]
    ]
}

  

分布式事务实战-seata的TCC模式。

TCC模式详述:

  • 既然有了二阶段提交,那为什么还要提供TCC模式呢? 答:因为二阶段提交不能指定事务成功或者失败后需要做的事情。或者说比较难实现。例如下图中出现的场景。

image.png

TCC与XA之间的比较

image.png

案例

image.png

代码

  • 对外暴露服务business:
@Override
@GlobalTransactional(name = "seata-tcc-tx",rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public String orderPay(String orderId){
    String xid = RootContext.getXID();
    System.out.println(xid);
    //1.根据订单id查询订单
    Order order = orderService.findById(orderId);
    //2.判断是否有此订单
    if(order != null && order.getStatus().intValue() == OrderStatusEnum.NOT_PAY.getCode()) {
        //3.更新订单状态为支付中
        orderService.prepareUpdateState(null, orderId);
        //4.预扣减余额(冻结账户资金)
        accountService.preparePayment(null,order.getUserId(),order.getTotalAmount());
        //5.预扣减库存(锁定库存)
        inventoryService.prepareDecrease(null,order.getProductId(), order.getCount());
    }else {
        return "订单不存在";
    }
    return "success";
}
  • OrderService中具体TCC方法的配置
@LocalTCC
public interface OrderService {

    /**
     * 创建订单并且进行扣除账户余额支付,并进行库存扣减操作
     *
     * @return string string
     */
    @TwoPhaseBusinessAction(name = "orderPayTccTryPhase",commitMethod = "commit",rollbackMethod = "rollback")
    boolean prepareUpdateState(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "orderId") String orderId);

    /**
     * 修改订单状态为支付完成
     * @param businessActionContext
     */
    public boolean commit(BusinessActionContext businessActionContext);

    /**
     * 修改订单状态为支付失败
     * @param businessActionContext
     */
    public boolean rollback(BusinessActionContext businessActionContext);


    /**
     * 根据id查询订单
     * @param id
     * @return
     */
    Order findById(String id);
}
  • 订单生成之后执行【各个prepare的操作】
  • Seata支持zk, nacos 等配置中心。官方目前并没有限定。
  • Seata的TCC模式要求:事务是prepare中进行处理,commit,rollback阶段默认都是成功的,如果是异常,需要分情况处理:
    • 网络异常:如果是commit阶段网络异常,undo_log中会存在未执行完成的操作日志,且commit方法会不断的重复执行直到网络恢复。
    • 代码异常:绝对不允许出现;否则commit方法一直重复执行,直到修复问题后服务重启,读取undo_log日志然后正确执行commit.

源码分析:

- @TwoPhaseBusinessAction

在程序加载时,解析配置文件中提供的 GlobalTransactionScanner 配置,该类是 AbstractAutoProxyCreator 的 子类,里面重写了 wrapIfNecessary 方法。该方法里面调用了 TCCBeanParserUtils 类中的静态方法

isTccAutoProxy ,而此方法内部执行了解析注解的操作方法 parserRemotingServiceInfo ,此时它会把具有 @TwoPhaseBusinessAction 注解的方法解析出来,并创建成 TccResource 对象

image.png

- ResourceManager对事务的管理

public interface ResourceManagerInbound {
    BranchStatus branchCommit(BranchType var1, String var2, long var3, String var5, String var6) throws TransactionException;

    BranchStatus branchRollback(BranchType var1, String var2, long var3, String var5, String var6) throws TransactionException;
}
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
    TCCResource tccResource = (TCCResource)this.tccResourceCache.get(resourceId);
    if (tccResource == null) {
        throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
    } else {
        Object targetTCCBean = tccResource.getTargetBean();
        Method commitMethod = tccResource.getCommitMethod();
        if (targetTCCBean != null && commitMethod != null) {
            try {
                boolean result = false;
                BusinessActionContext businessActionContext = this.getBusinessActionContext(xid, branchId, resourceId, applicationData);
                Object ret = commitMethod.invoke(targetTCCBean, businessActionContext);
                LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", new Object[]{ret, xid, branchId, resourceId});
                if (ret != null) {
                    if (ret instanceof TwoPhaseResult) {
                        result = ((TwoPhaseResult)ret).isSuccess();
                    } else {
                        result = (Boolean)ret;
                    }
                }

                return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
            } catch (Throwable var13) {
                String msg = String.format("commit TCC resource error, resourceId: %s, xid: %s.", resourceId, xid);
                LOGGER.error(msg, var13);
                throw new FrameworkException(var13, msg);
            }
        } else {
            throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s", resourceId));
        }
    }
}
  • 事务管理
    • seata的tcc模式事务控制也是基于Spring的AOP实现的,它里面也是利用了 MethodInterceptor 方法拦截器来进行 的增强,它里面提供了一个名称为 TccActionInterceptor 的类,该类实现了 MethodInterceptor 接口,并重写了invoke 方法。代码如下:
protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, BusinessActionContext actionContext) {
   String actionName = actionContext.getActionName();
   String xid = actionContext.getXid();
   Map<String, Object> context = this.fetchActionRequestContext(method, arguments);
   context.put("action-start-time", System.currentTimeMillis());
   this.initBusinessContext(context, method, businessAction);
   this.initFrameworkContext(context);
   actionContext.setActionContext(context);
   Map<String, Object> applicationContext = new HashMap(4);
   applicationContext.put("actionContext", context);
   String applicationContextStr = JSON.toJSONString(applicationContext);

   try {
       Long branchId = DefaultResourceManager.get().branchRegister(BranchType.TCC, actionName, (String)null, xid, applicationContextStr, (String)null);
       return String.valueOf(branchId);
   } catch (Throwable var12) {
       String msg = String.format("TCC branch Register error, xid: %s", xid);
       LOGGER.error(msg, var12);
       throw new FrameworkException(var12, msg);
   }
}

附录

  • 技术支持: @yp