[微服务的绊脚石--分布式事务] Seata-AT模式深入分析

orderClient.create(userId, commodityCode, orderCount);

}

在同一个包下,还有一个io.seata.spring.annotation.GlobalTransactionScanner,它继承了org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator,在它的initClient()方法中,对TM和RM进行了初始化。

private void initClient() {

// …

//init TM

TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);

// …

//init RM

RMClient.init(applicationId, txServiceGroup);

// …

registerSpringShutdownHook();

}

TM的初始化

TMClient 的init 方法中获取了io.seata.core.rpc.netty.TmNettyRemotingClient的实例,用于处理各种与服务端的消息交互。

// TMClient

public static void init(String applicationId, String transactionServiceGroup, String accessKey, String secretKey) {

TmNettyRemotingClient tmNettyRemotingClient = TmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup, accessKey, secretKey);

tmNettyRemotingClient.init();

}

TmNettyRemotingClient继承了io.seata.core.rpc.netty.AbstractNettyRemotingClient,在AbstractNettyRemotingClient的init方法中,设置了定时任务用于定时重发 RegisterTMRequest(RM 客户端会发送 RegisterRMRequest)请求尝试连接服务端,具体逻辑是: NettyClientChannelManager 中的 channels 中缓存了客户端 channel,如果此时 channels 不存在或者已过期,那么就会尝试连接服务端以重新获取 channel 并将其缓存到 channels 中。

@Override

public void init() {

timerExecutor.scheduleAtFixedRate(new Runnable() {

@Override

public void run() {

clientChannelManager.reconnect(getTransactionServiceGroup());

}

}, SCHEDULE_DELAY_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.MILLISECONDS);

if (NettyClientConfig.isEnableClientBatchSendRequest()) {

mergeSendExecutorService = new ThreadPoolExecutor(MAX_MERGE_SEND_THREAD,

MAX_MERGE_SEND_THREAD,

KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<>(),

new NamedThreadFactory(getThreadPrefix(), MAX_MERGE_SEND_THREAD));

mergeSendExecutorService.submit(new MergedSendRunnable());

}

super.init();

clientBootstrap.start();

}

RM的初始化

RMClient的init方法中,RmNettyRemotingClient.getInstance 处理逻辑与 TM 大致相同;ResourceManager 是 RM 资源管理器,负责分支事务的注册、提交、上报、以及回滚操作,以及全局锁的查询操作,DefaultResourceManager 会持有当前所有的 RM 资源管理器,进行统一调用处理。

TransactionMessageHandler 是 RM 消息处理器,用于负责处理从 TC 发送过来的指令,并对分支进行分支提交、分支回滚,以及 UndoLog 删除操作;最后 init 方法跟 TM 逻辑也大体一致;DefaultRMHandler 封装了 RM 分支事务的一些具体操作逻辑。

public static void init(String applicationId, String transactionServiceGroup) {

RmNettyRemotingClient rmNettyRemotingClient = RmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup);

rmNettyRemotingClient.setResourceManager(DefaultResourceManager.get());

rmNettyRemotingClient.setTransactionMessageHandler(DefaultRMHandler.get());

rmNettyRemotingClient.init();

}

添加拦截器

在GlobalTransactionScanner的wrapIfNecessary方法中,会扫描带有@GlobalTransactional@GlobalLock等注解的方法,并添加对应的拦截器。

  1. 判断是否存在对应的注解

  2. 创建拦截器

  3. 将拦截器添加到目标对象

/**

  • The following will be scanned, and added corresponding interceptor

*/

@Override

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {

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));

ConfigurationCache.addConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,

(ConfigurationChangeListener)interceptor);

} else {

Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);

Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);

//#### <1> ####

if (!existsAnnotation(new Class[]{serviceInterface})

&& !existsAnnotation(interfacesIfJdk)) {

return bean;

}

if (globalTransactionalInterceptor == null) {

//#### <2> ####

globalTransactionalInterceptor = new GlobalTransactionalInterceptor(failureHandlerHook);

ConfigurationCache.addConfigListener(

ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,

(ConfigurationChangeListener)globalTransactionalInterceptor);

}

interceptor = globalTransactionalInterceptor;

}

LOGGER.info(“Bean[{}] with name [{}] would use interceptor [{}]”, bean.getClass().getName(), beanName, interceptor.getClass().getName());

if (!AopUtils.isAopProxy(bean)) {

bean = super.wrapIfNecessary(bean, beanName, cacheKey);

} else {

//#### <3> ####

AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);

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);

}

}

事务处理

以全局事务@GlobalTransactional为例,在io.seata.spring.annotation.GlobalTransactionalInterceptor的invoke方法中,handleGlobalTransaction方法,在该方法中又调用了io.seata.tm.api.TransactionalTemplate的execute方法。

@Override

public Object invoke(final MethodInvocation methodInvocation) throws Throwable {

Class<?> targetClass =

methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;

Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);

if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) {

final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);

final GlobalTransactional globalTransactionalAnnotation =

getAnnotation(method, targetClass, GlobalTransactional.class);

final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class);

boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes);

if (!localDisable) {

if (globalTransactionalAnnotation != null) {

return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);

} else if (globalLockAnnotation != null) {

return handleGlobalLock(methodInvocation, globalLockAnnotation);

}

}

}

return methodInvocation.proceed();

}

Object handleGlobalTransaction(final MethodInvocation methodInvocation,

final GlobalTransactional globalTrxAnno) throws Throwable {

boolean succeed = true;

try {

return transactionalTemplate.execute(new TransactionalExecutor() {

@Override

public Object execute() throws Throwable {

return methodInvocation.proceed();

}

// …

});

}

// …

}

在TransactionalTemplate的execute方法中执行了具体事务处理,比如开启事务、提交、回滚等等。

public Object execute(TransactionalExecutor business) throws Throwable {

// 1. Get transactionInfo

TransactionInfo txInfo = business.getTransactionInfo();

if (txInfo == null) {

throw new ShouldNeverHappenException(“transactionInfo does not exist”);

}

// 1.1 Get current transaction, if not null, the tx role is ‘GlobalTransactionRole.Participant’.

GlobalTransaction tx = GlobalTransactionContext.getCurrent();

// 1.2 Handle the transaction propagation.

Propagation propagation = txInfo.getPropagation();

SuspendedResourcesHolder suspendedResourcesHolder = null;

try {

// …

// 1.3 If null, create new transaction with role ‘GlobalTransactionRole.Launcher’.

if (tx == null) {

tx = GlobalTransactionContext.createNew();

}

// set current tx config to holder

GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo);

try {

// 2. If the tx role is ‘GlobalTransactionRole.Launcher’, send the request of beginTransaction to TC,

// else do nothing. Of course, the hooks will still be triggered.

beginTransaction(txInfo, tx);

Object rs;

try {

// Do Your Business

rs = business.execute();

} catch (Throwable ex) {

// 3. The needed business exception to rollback.

completeTransactionAfterThrowing(txInfo, tx, ex);

throw ex;

}

// 4. everything is fine, commit.

commitTransaction(tx);

return rs;

} finally {

//5. clear

resumeGlobalLockConfig(previousConfig);

triggerAfterCompletion();

cleanUp();

}

} finally {

// If the transaction is suspended, resume it.

if (suspendedResourcesHolder != null) {

tx.resume(suspendedResourcesHolder);

}

}

}

RM为什么不需要@GlobalTransactional?


在上一篇的代码示例中,我们只在TM端的BusinessService 方法上添加了@GlobalTransactional注解,而在下游微服务中并没有添加任何注解,为什么也可以当做全局事务处理呢?

事务上下文

我们先来看看 Seata 的事务上下文,它是由 RootContext 来管理的。

应用开启一个全局事务后,RootContext 会自动绑定该事务的 XID,事务结束(提交或回滚完成),RootContext 会自动解绑 XID。

// 绑定 XID

RootContext.bind(xid);

// 解绑 XID

String xid = RootContext.unbind();

应用可以通过 RootContext 的 API 接口来获取当前运行时的全局事务 XID。

// 获取 XID

String xid = RootContext.getXID();

应用是否运行在一个全局事务的上下文中,就是通过 RootContext 是否绑定 XID 来判定的。

public static boolean inGlobalTransaction() {

return CONTEXT_HOLDER.get(KEY_XID) != null;

}

事务传播

Seata 全局事务的传播机制就是指事务上下文的传播,根本上,就是 XID 的应用运行时的传播方式。

1. 服务内部的事务传播

默认的,RootContext 的实现是基于 ThreadLocal 的,即 XID 绑定在当前线程上下文中。

public class ThreadLocalContextCore implements ContextCore {

private ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<Map<String, String>>() {

@Override

protected Map<String, String> initialValue() {

return new HashMap<String, String>();

}

};

@Override

public String put(String key, String value) {

return threadLocal.get().put(key, value);

}

@Override

public String get(String key) {

return threadLocal.get().get(key);

}

@Override

public String remove(String key) {

return threadLocal.get().remove(key);

}

}

所以服务内部的 XID 传播通常是天然的通过同一个线程的调用链路串连起来的。默认不做任何处理,事务的上下文就是传播下去的。

如果希望挂起事务上下文,则需要通过 RootContext 提供的 API 来实现:

// 挂起(暂停)

String xid = RootContext.unbind();

// TODO: 运行在全局事务外的业务逻辑

// 恢复全局事务上下文

RootContext.bind(xid);

2. 跨服务调用的事务传播

通过上述基本原理,我们可以很容易理解:

跨服务调用场景下的事务传播,本质上就是要把 XID 通过服务调用传递到服务提供方,并绑定到 RootContext 中去。

只要能做到这点,理论上 Seata 可以支持任意的微服务框架。

我们注意到,在Common模块的SeataFilter中,从Http请求Header中获取了全局事务ID XID,并将其设置到了事务上下文RootContext中。

@Override

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest) servletRequest;

String xid = req.getHeader(RootContext.KEY_XID.toLowerCase());

boolean isBind = false;

if (StringUtils.isNotBlank(xid)) {

RootContext.bind(xid);

isBind = true;

}

try {

filterChain.doFilter(servletRequest, servletResponse);

} finally {

if (isBind) {

RootContext.unbind();

}

}

}

而在SeataRestTemplateInterceptor中,先从RootContext获取了XID,然后设置到了Http请求头中,这样下游的RM就能通过SeataFilter获取到XID了。

@Override

public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {

HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);

String xid = RootContext.getXID();

if (StringUtils.isNotEmpty(xid)) {

requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);

}

return clientHttpRequestExecution.execute(requestWrapper, bytes);

}

复制代码

@GlobalLock有什么用?


前面初始化部分的代码中,也出现了对@GlobalLock的处理,那么它的用法跟 @GlobalTransactional 有什么区别呢?

如果是用 @GlobalLock 修饰的业务方法,虽然该方法并非某个全局事务下的分支事务,但是它对数据资源的操作也需要先查询全局锁,如果存在其他 Seata 全局事务正在修改,则该方法也需等待。所以,如果想要 Seata 全局事务执行期间,数据库不会被其他事务修改,则该方法需要强制添加 GlobalLock 注解,来将其纳入 Seata 分布式事务的管理范围。

功能有点类似于 Spring 的 @Transactional 注解,如果你希望开启事务,那么必须添加该注解,如果你没有添加那么事务功能自然不生效,业务可能出 BUGSeata 也一样,如果你希望某个不在全局事务下的 SQL 操作不影响 AT 分布式事务,那么必须添加 GlobalLock 注解。

全局锁的组成和作用

Seata AT 模式的全局锁主要由表名加操作行的主键两个部分组成,通过在服务端保存全局锁的方法保证:

  1. 全局事务之前的写隔离

  2. 全局事务与被 GlobalLock 修饰方法间的写隔离性

全局锁的注册

当客户端在进行一阶段本地事务提交前,会先向服务端注册分支事务,此时会将修改行的表名、主键信息封装成全局锁一并发送到服务端进行保存,如果服务端保存时发现已经存在其他全局事务锁定了这些行主键,则抛出全局锁冲突异常,客户端循环等待并重试。

全局锁的查询

被 @GlobalLock 修饰的方法虽然不在某个全局事务下,但是其在提交事务前也会进行全局锁查询,如果发现全局锁正在被其他全局事务持有,则自身也会循环等待。

全局锁的释放

由于二阶段提交是异步进行的,当服务端向客户端发送 branch commit 请求后,客户端仅仅是将分支提交信息插入内存队列即返回,服务端只要判断这个流程没有异常就会释放全局锁。因此,可以说如果一阶段成功则在二阶段一开始就会释放全局锁,不会锁定到二阶段提交流程结束。

VTh76.png

但是如果一阶段失败二阶段进行回滚,则由于回滚是同步进行的,全局锁直到二阶段回滚完成才会被释放。

为什么ExceptionHandler会导致全局事务失效?


在SpringBoot项目中,我们经常使用@ControllerAdvice来构造ExceptionHandler,用于处理各种异常。有的时候会因此导致全局事务无法回滚,这是为什么呢?

以本文的 ApiExceptionHandler为例,

首先我们来看TM的执行,上文我们提到实际的全局事务处理,比如开启全局事务,提交,回滚,是在TransactionalTemplate的execute方法中实现的,所以为了能够回滚,就必须保证业务处理business.execute()抛出异常

try {

// Do Your Business

rs = business.execute();

} catch (Throwable ex) {

// 3. The needed business exception to rollback.

completeTransactionAfterThrowing(txInfo, tx, ex);

架构学习资料

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

成才会被释放。

为什么ExceptionHandler会导致全局事务失效?


在SpringBoot项目中,我们经常使用@ControllerAdvice来构造ExceptionHandler,用于处理各种异常。有的时候会因此导致全局事务无法回滚,这是为什么呢?

以本文的 ApiExceptionHandler为例,

首先我们来看TM的执行,上文我们提到实际的全局事务处理,比如开启全局事务,提交,回滚,是在TransactionalTemplate的execute方法中实现的,所以为了能够回滚,就必须保证业务处理business.execute()抛出异常

try {

// Do Your Business

rs = business.execute();

} catch (Throwable ex) {

// 3. The needed business exception to rollback.

completeTransactionAfterThrowing(txInfo, tx, ex);

架构学习资料

[外链图片转存中…(img-tw3Gtehd-1714483556541)]

[外链图片转存中…(img-KF6KfCFf-1714483556541)]

[外链图片转存中…(img-ffVO79wE-1714483556542)]

[外链图片转存中…(img-D6UFuYu2-1714483556542)]

[外链图片转存中…(img-x9ZwDWSI-1714483556542)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值