Seata实战-AT模式分布式事务原理、源码分析

https://blog.csdn.net/hosaos/article/details/89403552 和https://blog.csdn.net/scientificCommunity/article/details/107290752转载

AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图

第一阶段

在这里插入图片描述
核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析

第二阶段

分布式事务操作成功,则TC通知RM异步删除undolog
在这里插入图片描述
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
在这里插入图片描述
设计思路介绍完毕,下面开始发车盘源码

源码分析

入口之Seata集成Spring

源码分析,首先得找到切入点,不管是dubbo,mybatis,还是seata,为了让Java开发者更易上手使用,往往和Spring有紧密结合,比如实现spring bean里的InitializingBean接口在bean初始化完成后做一些初始化操作,使用拦截器对方法进行拦截,在拦截方法内做一些实现,Seata也是如此

先从Seata所需的Spring配置文件入手,看下Seata的使用需要哪些配置,看下官方Demo里的dubbo例子

https://github.com/fescar-group/fescar-samples

先看服务提供方dubbo-account-service.xml中的配置
在这里插入图片描述
可以看到有两个普通spring项目里没有的配置

  1. DataSourceProxy使用Seata中的代理数据源对普通数据源做一层代理,指定JdbcTemplate中的数据源为代理数据源
  2. 配置了一个名为GlobalTransactionScanner的bean,第一个构造参数为应用id,第二个参数为事务分组

GlobalTransactionScanner就是入口,看下其实现(去掉了构造方法)
在这里插入图片描述

可以看到分别实现了Spring的3个接口InitializingBeanApplicationContextAwareDisposableBean

 

 

我们通过@GlobalTransactional这个注解开启一个全局事务,而GlobalTransactionScanner.wrapIfNecessary()会为所有方法上加了这个注解的bean注入一个包装了GlobalTransactionalInterceptor实例的advisor,然后返回一个代理对象。GlobalTransactionalInterceptor会在该bean的方法调用前进行拦截,判断是否开启全局事务

 

 

 

@Override
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    //判断全局事务是否启用
    if (disableGlobalTransaction) {
        return bean;
    }
    try {
        synchronized (PROXYED_SET) {
            if (PROXYED_SET.contains(beanName)) {
                return bean;
            }
            interceptor = null;
            //check TCC proxy
            if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {
                //TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC
                interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));
            } else {
                Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);
                Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);
 
                //判断bean里的方法上有没有@GlobalTransactional注解
                if (!existsAnnotation(new Class[]{serviceInterface})
                    && !existsAnnotation(interfacesIfJdk)) {
                    return bean;
                }
 
                if (interceptor == null) {
                    //初始化Interceptor,后面会注入代理对象
                    interceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
                    ConfigurationFactory.getInstance().addConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, (ConfigurationChangeListener) interceptor);
                }
            }
            
            //判断当前bean是否已被aop代理过,比如说方法上加了@Transactional就会被spring代理
            //如果没有被代理,调用父类的模板方法进行代理,advisor通过被重写的
            //getAdvicesAndAdvisorsForBean返回上面的interceptor进行包装
            if (!AopUtils.isAopProxy(bean)) {
                bean = super.wrapIfNecessary(bean, beanName, cacheKey);
            } else {
                AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
 
                //把GlobalTransactionalInterceptor包装成advisor
                Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
                for (Advisor avr : advisor) {
                    advised.addAdvisor(0, avr);
                }
            }
            PROXYED_SET.add(beanName);
            return bean;
        }
    } catch (Exception exx) {
        throw new RuntimeException(exx);
    }
}
 
 
@Override
protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName,          
  TargetSource customTargetSource) throws BeansException {
 
    //返回interceptor[]
    return new Object[]{interceptor};
}
 

关键点在afterPropertiesSet()中
在这里插入图片描述
调用了initClient方法
在这里插入图片描述
里面对TmClient,RmClient进行了初始化(参数就是配置文件bean里配置的applicationId和txServiceGroup),并注册了一个Spring的ShutdownHook

TmClient.init()

先看下再看下TmClient的初始化操作,其最终调用到的是TmRpcClient的init()方法,启动了一个定时器不断进行重连操作
在这里插入图片描述
先调用initVars()初始化了父类中一个消息超时的一个定时调度器,定时请求并将超时消息返回值设为null,不影响流程,代码不贴了

初始化了一个mergeSendExecutorService线程池,会将同一个fescar-server的消息合并发送(减少netty通信次数)

最终进到reconnect方法
在这里插入图片描述
先根据事务的分组名称获取到对应的seata-server的ip地址列表,然后进行重连

getAvailServerList中代码如下
在这里插入图片描述
RegistryFactory.getInstance().lookup(transactionServiceGroup)是针对不同注册中心做了适配的,默认看下File形式的实现
在这里插入图片描述
进到FileRegistryServiceImpl#lookup方法,这里结合File.conf配置来说明
在这里插入图片描述
在这里插入图片描述
1、现根据事务分组(key=vgroup_mapping.事务分组名称)找到分组所属的server集群名称,这里是default
2、然后根据集群名称(key=集群名称.grouplist)找到server对应ip端口地址

梳理下TmClient的初始化流程

  1. 启动ScheduledExecutorService定时执行器,每5秒尝试进行一次重连seata-server
  2. 重连时,先从file.conf中根据分组名称(service_group)找到集群名称(cluster_name)
  3. 再根据集群名称找到fescar-server集群ip端口列表
  4. 从ip列表中选择一个用netty进行连接

RmClient.init()

在这里插入图片描述
1、设置了资源管理器resourceManager
2、设置了消息回调监听器,rmHandler用于接收fescar-server在二阶段发出的提交或者回滚请求

RmClient初始化时用到了Java Spi拓展机制,Seata中对ResourceManagerAbstractRMHandler做了SPI适配,以ResouceManager为例说明

在这里插入图片描述
可以看到初始化DefaultResouceManager时会使用ClassLoader去加载对应Jar下的实现,而默认AT模式使用的实现是数据库,也就是rm-datasource包下的实现,找实现类路径需要定位到/resources/META-INF/扩展接口全路径去找
在这里插入图片描述
这样就找到了对应实现类的全路径

  1. ResourceManager对应实现类全路径 io.seata.rm.datasource.DataSourceManager,该类中指定了了提交和回滚的方法
  2. DefaultRMHandler对应实现类全路径io.seata.rm.RMHandlerAT,该类在二阶段代码分析过程再做细讲,只需先记住是个接收server消息并做对应提交或者回滚操作的回调处理类

init()方法,和TmClient初始化过程基本一致,不再重复贴代码

做个总结:

  1. Spring启动时,初始化了2个客户端TmClient、RmClient
  2. TmClient与Server通过Netty建立连接并发送消息
  3. RmClient与Server通过Netty建立连接,负责接收二阶段提交、回滚消息并在回调器(RmHandler)中做处理

下面具体进行到全局事务的两阶段提交过程中做分析

第一阶段

拦截器中开启事务

在需要加全局事务的方法中,会加上GlobalTransactional注解,注解往往对应着拦截器,Seata中拦截全局事务的拦截器是GlobalTransactionalInterceptor
看下其拦截方法
在这里插入图片描述
判断:

  • 如果方法上有全局事务注解,调用handleGlobalTransaction开启全局事务
  • 如果没有,按普通方法执行,避免性能下降

看下handleGlobalTransaction()方法
在这里插入图片描述
可以看到最终调用的是TransactionalTemplate的execute方法,execute方法如下
在这里插入图片描述
分为几步

  1. 开启全局事务beginTransaction
  2. 执行业务方法
  3. 提交事务commitTransaction(若没抛异常)
  4. 执行completeTransactionAfterThrowing回滚操作(抛异常)

这里首先初始化一个GlobalTransaction实例tx,用于保存后续生成的xid跟事务状态等一些属性。然后对事务的传播属性做了些校验。然后我们进入beginTransaction(txInfo, tx);顾名思义,这里快要到核心了

 
  1. private void beginTransaction(TransactionInfo txInfo, GlobalTransaction tx) throws TransactionalExecutor.ExecutionException {

  2. try {

  3. //执行hook的begin()方法做一些额外处理

  4. triggerBeforeBegin();

  5. tx.begin(txInfo.getTimeOut(), txInfo.getName());

  6. triggerAfterBegin();

  7. } catch (TransactionException txe) {

  8. throw new TransactionalExecutor.ExecutionException(tx, txe,

  9. TransactionalExecutor.Code.BeginFailure);

  10.  
  11. }

  12. }

这里的trigger方法执行我们通过TransactionHookManager.registerHook()注册的一些hook方法,如果我们要在事务开始前后做一些事情,就可以通过这种方式。

 

beginTransaction最终调用到了io.seata.tm.api.DefaultGlobalTransaction#begin(int, java.lang.String)方法,代码如下
在这里插入图片描述

继续到 transactionManager.begin

 
  1. @Override

  2. public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)

  3. throws TransactionException {

  4. GlobalBeginRequest request = new GlobalBeginRequest();

  5. request.setTransactionName(name);

  6. request.setTimeout(timeout);

  7.  
  8. //通知seata-server开启全局事务,并拿到全局事务id(xid)

  9. GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);

  10. if (response.getResultCode() == ResultCode.Failed) {

  11. throw new TmTransactionException(TransactionExceptionCode.BeginFailed, response.getMsg());

  12. }

  13. return response.getXid();

  14. }

最后我们就开启了一个全局事务,那么我们的xid是怎么向下游传递的呢,看看对feign的集成是怎么做的?SeataFeignClient.execute

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
 
        //设置xid
        Request modifiedRequest = getModifyRequest(request);
        //调用下游服务
        return this.delegate.execute(modifiedRequest, options);
    }
 
    private Request getModifyRequest(Request request) {
 
        String xid = RootContext.getXID();
 
        if (StringUtils.isEmpty(xid)) {
            return request;
        }
 
        Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE);
        //设置xid到消息头
        headers.putAll(request.headers());
 
        List<String> seataXid = new ArrayList<>();
        seataXid.add(xid);
        headers.put(RootContext.KEY_XID, seataXid);
 
        return Request.create(request.method(), request.url(), headers, request.body(),
                request.charset());
    }

这里用SeataFeignClient替换了默认的feignClient,把xid放到了requestHeader里。那么下游又是怎么拿的呢?SeataHandlerInterceptor.preHandle()

 
  1. @Override

  2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response,

  3. Object handler) {

  4.  
  5. String xid = RootContext.getXID();

  6.  
  7. //从消息头中获取xid

  8. String rpcXid = request.getHeader(RootContext.KEY_XID);

  9. if (log.isDebugEnabled()) {

  10. log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);

  11. }

  12.  
  13. if (xid == null && rpcXid != null) {

  14. RootContext.bind(rpcXid);

  15. if (log.isDebugEnabled()) {

  16. log.debug("bind {} to RootContext", rpcXid);

  17. }

  18. }

  19. return true;

  20. }

这里SeataHandlerInterceptor实现了HandlerInterceptor,springMVC会在Controller方法调用之前拿到所有注册到容器中的拦截器链去执行其preHandle()方法,具体可参考DispatcherServlet.doDispatch()。

 

  1. 调用transactionManager.begin()方法通过TmRpcClient与server通信并生成一个xid
  2. 将xid绑定到Root上下文中

看到这里,也就明确了一点,全局事务开启时,是由TM来发起的

commitTransaction方法类似,由TM发送事务commit信息给seata-server,略去源码

sql解析与undolog生成

全局事务拦截成功后最终还是执行了业务方法的,但是由于Seata对数据源做了代理,所以sql解析与undolog入库操作是在数据源代理中执行的,箭头处的代理就是Seata对DataSource,Connection,Statement做的代理封装类
在这里插入图片描述
最终对Sql进行解析操作,发生在StatementProxy类中
在这里插入图片描述
交给了ExecuteTemplate执行,跟到ExecuteTemplate中
在这里插入图片描述

流程如下

  1. 先判断是否开启了全局事务,如果没有,不走代理,不解析sql,避免性能下降
  2. 调用SQLVisitorFactory对目标sql进行解析
  3. 针对特定类型sql操作(INSERT,UPDATE,DELETE,SELECT_FOR_UPDATE)等进行特殊解析
  4. 执行sql并返回结果

关键点在于特定类型执行器中的execute方法,先看下类继承图
在这里插入图片描述
挑选InsertExecutor为例说明,其execute方法调用的是父类BaseTransactionalExecutor中的execute方法,看下源码
在这里插入图片描述
将ROOT上下文中的xid绑定到了connectionProxy中,并调用了doExecute方法,看下AbstractDMLBaseExecutor中的doExecute方法
在这里插入图片描述

在这里插入图片描述
executeAutoCommitTrue中先将autoCommit设置为false(因为要对sql进行解析,生成undolog在一个事务中入库,避免提前入库)

再执行到executeAutoCommitFalse中,分为4步

  1. 获取sql执行前镜像beforeImage
  2. 执行sql
  3. 获取sql执行后afterimage
  4. 根据beforeImage,afterImage生成undolog记录并添加到connectionProxy的上下文中

到此为止,红色框中几步已经完成
在这里插入图片描述

分支事务注册与事务提交

业务sql执行以及undolog执行完后会在ConnectionProxy中执行commit操作
看下代码
在这里插入图片描述
1、如果处于全局事务中,则调用processGlobalTransactionCommit()处理全局事务提交
2、如果加了全局锁注解,加全局锁并提交
3、如果没有对应注释,按直接进行事务提交

主要看processGlobalTransactionCommit()方法,也是核心代码
在这里插入图片描述
流程分为如下几步

  1. 注册分支事务register(),并将branchId分支id绑定到上下文中
    在这里插入图片描述
  2. UndoLogManager.flushUndoLogs(this) 如果包含undolog,则将之前绑定到上下文中的undolog进行入库
  3. 提交本地事务
  4. 如果操作失败,report()中通过RM提交第一阶段失败消息,如果成功,report()提交第一阶段成功消息

在这里插入图片描述
undolog入库和普通业务sql的执行用的一个connection,处于一个本地事务中,保证了业务数据变更时,一定会有对应undolog存在

至此,第一阶段中undolog提交与本地事务提交,分支事务注册与汇报也已完成
在这里插入图片描述

 

第二阶段

在前面分析RmClient.init()方法时,提到了Seata会使用SPI拓展机制找到RmClient的回调处理器RMHandlerAT,该类是负责接送二阶段seata-server发给RmClient的提交、回滚消息,并作出提交,回滚操作
在这里插入图片描述

先看下RMHandlerAT继承自AbstractRMHandler,AbstractRMHandler中两个handle方法对应,事务提交、回滚操作

全局事务提交

对应了doBranchCommit(request, response)方法
在这里插入图片描述
调用的是getResourceManager(),上面提到SPI拓展提到的DataSourceManager类
在这里插入图片描述
DataSourceManager中调用了asyncWorker来异步提交,看下AsyncWorker中branchCommit方法
在这里插入图片描述
这边只是往一个ASYNC_COMMIT_BUFFER缓冲List中新增了一个二阶段提交的context

但真正提交在哪呢?答案在AsyncWorker的init()方法中,其init()方法会在DataSourceManager中被调用,内部启动一个定时器不断进行全局事务提交操作
在这里插入图片描述
终于跟到了真正的分支事务提交方法中

在这里插入图片描述
分为几步

  1. 先按resourceId(也就是数据连接)对提交操作进行分组,一个数据库的可以一起操作,提升效率
  2. 根据resourceId找到对应DataSourceProxy,并获取一个普通的数据库连接getPlainConnection(),估计这本身不需要做代理操作,故用了普通的数据库连接
  3. 调用UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn)删除undolog

回过头来看下设计原理图
在这里插入图片描述

全局事务回滚

同样的,从io.seata.rm.AbstractRMHandler#doBranchRollback跟到io.seata.rm.datasource.DataSourceManager#branchRollback中,最终回滚方法调用的是UndoLogManager.undo(dataSourceProxy, xid, branchId);
在这里插入图片描述

具体代码如下
在这里插入图片描述

具体根据Undolog进行反解析操作实现在AbstractUndoExecutor的子类中,

/**
 * Execute on.
 *
 * @param conn the conn
 * @throws SQLException the sql exception
 */
public void executeOn(Connection conn) throws SQLException {

    if (IS_UNDO_DATA_VALIDATION_ENABLE && !dataValidationAndGoOn(conn)) {
        return;
    }

    try {
        String undoSQL = buildUndoSQL();

        PreparedStatement undoPST = conn.prepareStatement(undoSQL);

        TableRecords undoRows = getUndoRows();

        for (Row undoRow : undoRows.getRows()) {
            ArrayList<Field> undoValues = new ArrayList<>();
            Field pkValue = null;
            for (Field field : undoRow.getFields()) {
                if (field.getKeyType() == KeyType.PRIMARY_KEY) {
                    pkValue = field;
                } else {
                    undoValues.add(field);
                }
            }

            undoPrepare(undoPST, undoValues, pkValue);

            undoPST.executeUpdate();
        }

    } catch (Exception ex) {
        if (ex instanceof SQLException) {
            throw (SQLException) ex;
        } else {
            throw new SQLException(ex);
        }

    }

}

 

从上述的buildUndoSQL()更到下面的类

 

 

MySQLUndoInsertExecutor为例子说明
/**
 * DELETE FROM a WHERE pk = ?
 */
private static final String DELETE_SQL_TEMPLATE = "DELETE FROM %s WHERE %s = ?";

/**
 * Undo Inset.
 *
 * @return sql
 */
@Override
protected String buildUndoSQL() {
    TableRecords afterImage = sqlUndoLog.getAfterImage();
    List<Row> afterImageRows = afterImage.getRows();
    if (CollectionUtils.isEmpty(afterImageRows)) {
        throw new ShouldNeverHappenException("Invalid UNDO LOG");
    }
    Row row = afterImageRows.get(0);
    Field pkField = row.primaryKeys().get(0);
    // insert sql undo log after image all field come from table meta, need add escape.
    // see BaseTransactionalExecutor#buildTableRecords
    return String.format(DELETE_SQL_TEMPLATE, sqlUndoLog.getTableName(),
                         ColumnUtils.addEscape(pkField.getName(), JdbcConstants.MYSQL));
}

 

undolog表中存储的

afterImage镜像中读取出主键相关信息 然后

insert语句  会被反向解析成 DELETE FROM表WHERE 主键 = ? 这样的格式,然后

解析完成 ?完成占位符替换成主键id的值。


如果判断undolog存在exists,则删除对应undolog,并一并提交
在这里插入图片描述

这时候 再看下这回滚设计原理图,是不是清晰了很多
在这里插入图片描述

 


 

 

 

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
课程简介: 课程总计41课时,从什么是事务讲起,直到分布式事务解决方案,很的0基础基础与提升系列课程。对于难以理解的知识点,全部用画图+实战的方式讲解。 第一部分:彻底明白事务的四个特性:原子性、一致性、隔离性、持久性,用场景和事例来讲解。 第二部分:实战讲数据库事务的6中并发异常:回滚丢失、覆盖丢失、脏读、幻读、不可重复读、MVCC精讲。 第三部分:彻底搞清楚4种事务隔离级别:READ_UNCOMMITTED 读未提交隔离级别、READ_COMMITTED 读已提交隔离级别、REPEATABLE_READ 可重复度隔离级别、SERIALIZABLE 序列化隔离级别 第四部分:彻底搞清楚MySQL的各种锁:行锁、表锁、共享锁、排它锁、Next-Key锁、间隙锁、X锁、S锁、IS锁、IX锁、死锁、索引与锁、意向锁等。 第五部分:彻底搞清楚Spring事务的7种传播级别的原理和使用:PROPAGATION_REQUIRED、PROPAGATION_SUPPORTS、PROPAGATION_MANDATORY、PROPAGATION_REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER、PROPAGATION_NESTED分布式事务的理论基础:RPC定理、BASE理论、XA协议都是什么,原理是什么,有什么关联关系 第六部分:分布式事务的5种解决方案原理和优缺点:2PC两阶段提交法、3PC三阶段提交法、TCC事务补偿、异步确保策略、最大努力通知策略 第七部分:阿里巴巴分布式事务框架Seata:历经多年双十一,微服务分布式事务框架,用一个Nacos+Spring Cloud+Seta+MySql的微服务项目,实战讲解阿里的分布式事务技术,深入理解和学习Seata的AT模式、TCC模式、SAGA模式。 课程资料: 课程附带配套2个项目源码72页高清PDF课件一份阿里巴巴seata-1.1.0源码一份阿里巴巴seata-server安装包一份

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值