分布式事务(六)Seata 事务传播

前言

上篇博文中写道,Seata是将全局事务划分为若干个分支事务来解决分布式事务,分支事务(branchID)和全局事务(XID)一一关联,才能实现事务贯穿全局。这篇博文主要分析各分支事务之间的传播过程以及常见微服务框架中如何实现事务的传播。在分析源码的章节中,主要内容都在代码注释中体现。

事务上下文

Seata提供了一个LowLevel-Api—— RootContext 事务的根上下文对象,它起到的作用是:在应用程序运行时维护XID。所以分析XID传递先从RootContext入手。

应用开启一个全局事务后,RootContext会自动绑定当前事务的XID,事务结束后也会自动解绑XID。所以在应用运行的过程中可以直接调用 RootContext.getXID()方法获取全局事务的唯一标识。

分析io.seata.core.context.RootContext源码


    /**
     * 默认实现是基于 ThreadLocal 的 , XID 保存在当前线程上下文中
     */
    private static ContextCore CONTEXT_HOLDER = ContextCoreLoader.load();
    


    /**
     * 获取当前进程的XID
     *
     * @return the xid
     */
    @Nullable
    public static String getXID() {
        return (String) CONTEXT_HOLDER.get(KEY_XID);
    }

    /**
     * 将XID和当前进程绑定
     *
     * @param xid the xid
     */
    public static void bind(@Nonnull String xid) {
        if (StringUtils.isBlank(xid)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("xid is blank, switch to unbind operation!");
            }
            unbind();
        } else {
            MDC.put(MDC_KEY_XID, xid);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("bind {}", xid);
            }
            CONTEXT_HOLDER.put(KEY_XID, xid);
        }
    }

    /**
     * 将XID和当前进程解除绑定
     *
     * @return the previous xid or null
     */
    @Nullable
    public static String unbind() {
        String xid = (String) CONTEXT_HOLDER.remove(KEY_XID);
        if (xid != null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("unbind {} ", xid);
            }
            MDC.remove(MDC_KEY_XID);
        }
        return xid;
    }

事务传播

Seata全局事务的传播根本上就是XID的传播,只要获取到XID就可以知道当前程序被哪一个全局事务管理。

在同一服务内部传播

上节中RootContext注释中曾提到,RootContext的实现是ThredLocal,XID的绑定就是绑定在当前线程的上下文对象中。所以在服务内部只需要依靠当前线程即可完成事务在服务内部的传播,无需额外处理。

在不同服务中跨服务传播

根据在同一服务内部传播的机制可以推论出,只要能够实现XID的传递,即可实现事务的传播。在跨服务远程调用中带上XID参数成功传递到下游服务中,下游服务会将XID绑定到它自己的RootContext中,这样也就实现了每一个事物分支(branch)和全局事务(XID)的绑定关系。

常见微服务中XID传递实现过程分析

SpringCloudAlibaba 实现XID传递

springCloud集成seata时,需要引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

在这个包中已经实现了三种常见服务调用方式传递XID

up-1c4f1874a986b820dc7988fb2a4733f07d3.png

Feign传递XID

package com.alibaba.cloud.seata.feign;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import feign.Client;
import feign.Request;
import feign.Response;
import io.seata.core.context.RootContext;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.util.StringUtils;

/**
 * 实现通过实现feign.Client接口,完成Feign调用传递XID
 * @author xiaojing
 */
public class SeataFeignClient implements Client {

	private final Client delegate;

	private final BeanFactory beanFactory;

	private static final int MAP_SIZE = 16;

	SeataFeignClient(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
		this.delegate = new Client.Default(null, null);
	}

	SeataFeignClient(BeanFactory beanFactory, Client delegate) {
		this.delegate = delegate;
		this.beanFactory = beanFactory;
	}

	@Override
	public Response execute(Request request, Request.Options options) throws IOException {

		Request modifiedRequest = getModifyRequest(request);
		return this.delegate.execute(modifiedRequest, options);
	}

	private Request getModifyRequest(Request request) {
		// 获取当前进程中的XID
		String xid = RootContext.getXID();
		
		if (StringUtils.isEmpty(xid)) {
			return request;
		}

		Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE);
		headers.putAll(request.headers());
		
		// 将本地的XID添加到调用请求参数中
		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());
	}

}

RestTemplate传递XID

package com.alibaba.cloud.seata.rest;

import java.io.IOException;

import io.seata.core.context.RootContext;

import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.util.StringUtils;

/**
 * RestTemplate传递XID
 * @author xiaojing
 */
public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor {

	@Override
	public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
			ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
		HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
		// 获取与当前进程绑定的XID
		String xid = RootContext.getXID();

		if (!StringUtils.isEmpty(xid)) {
			// 将XID加入到请求参数中
			requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
		}
		return clientHttpRequestExecution.execute(requestWrapper, bytes);
	}

}

Seata处理Cloud以HTTP调用形式传递的XID

package io.seata.integration.http;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;


/**
 * 处理HTTP调用中的XID传递
 *
 * @author wangxb
 */
public class TransactionPropagationInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionPropagationInterceptor.class);

    /**
     * 前置处理
     * @param request
     * @param response
     * @param handler
     * @return
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 当前进程绑定的XID
        String xid = RootContext.getXID();
        // 远程调用中HTTP Header中的XID
        String rpcXid = request.getHeader(RootContext.KEY_XID);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid in RootContext[{}] xid in HttpContext[{}]", xid, rpcXid);
        }
        if (StringUtils.isBlank(xid) && StringUtils.isNotBlank(rpcXid)) {
            // 本地线程没有绑定XID,且上游调用方传递了XID,则将本地线程与XID绑定
            RootContext.bind(rpcXid);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("bind[{}] to RootContext", rpcXid);
            }
        }

        return true;
    }

    /**
     * 后置处理
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 远程调用结束前,判断是否启用的全局事务,判断依据是 当前线程是否绑定了XID
        // CONTEXT_HOLDER.get(KEY_XID) != null
        if (RootContext.inGlobalTransaction()) {
            // 如果绑定了XID,将此XID和当前线程接触绑定
            XidResource.cleanXid(request.getHeader(RootContext.KEY_XID));
        }
    }

}
package io.seata.integration.http;

import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Xid handler.
 *
 * @author wangxb
 */
public class XidResource {

    private static final Logger LOGGER = LoggerFactory.getLogger(XidResource.class);


    public static void cleanXid(String rpcXid) {
        String xid = RootContext.getXID();
        if (StringUtils.isNotBlank(xid)) {
            // 有绑定XID,执行解绑
            String unbindXid = RootContext.unbind();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("unbind[{}] from RootContext", unbindXid);
            }
            if (!StringUtils.equalsIgnoreCase(rpcXid, unbindXid)) {
                LOGGER.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
                if (StringUtils.isNotBlank(unbindXid)) {
                    // 若解绑的XID和实际需要解绑的XID不符,重新绑定(恢复上一步的解绑操作)
                    RootContext.bind(unbindXid);
                    LOGGER.warn("bind [{}] back to RootContext", unbindXid);
                }
            }
        }
    }
}

Dubbo中XID实现传递

通过实现com.alibaba.dubbo.rpc.Filter接口,完成XID在服务调用是传播 。具体分析在代码注释中

package io.seata.integration.dubbo.alibaba;

import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
import io.seata.core.constants.DubboConstants;
import io.seata.core.model.BranchType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 通过实现com.alibaba.dubbo.rpc.Filter接口,完成XID在服务调用是传播
 *
 * @author sharajava
 */
@Activate(group = {DubboConstants.PROVIDER, DubboConstants.CONSUMER}, order = 100)
public class AlibabaDubboTransactionPropagationFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(AlibabaDubboTransactionPropagationFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 必须使用org.apache.dubbo.rpc.RpcContext
        if (!DubboConstants.ALIBABADUBBO) {
            return invoker.invoke(invocation);
        }
        // 获取当前线程中的XID
        String xid = RootContext.getXID();
        // 分支事务模式 AT、TCC等
        BranchType branchType = RootContext.getBranchType();

        // 获取 RPC 调用传递过来的 XID
        String rpcXid = getRpcXid();
        String rpcBranchType = RpcContext.getContext().getAttachment(RootContext.KEY_BRANCH_TYPE);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid in RootContext[{}] xid in RpcContext[{}]", xid, rpcXid);
        }
        boolean bind = false;
        if (xid != null) {
            // 存在本地的XID, 说明这是一个全局事务中的上游服务。将XID置入RPC上下文的参数中
            RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
            RpcContext.getContext().setAttachment(RootContext.KEY_BRANCH_TYPE, branchType.name());
        } else {
            // 本地XID不存在
            if (rpcXid != null) {
                // 同时RPC上下文对象中存在XID, 说明这是一个下游服务。
                // 将上游传递过来的XID绑定到当前线程中
                RootContext.bind(rpcXid);
                // 对不同事务模式做出响应处理
                if (StringUtils.equals(BranchType.TCC.name(), rpcBranchType)) {
                    RootContext.bindBranchType(BranchType.TCC);
                }
                bind = true;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("bind xid [{}] branchType [{}] to RootContext", rpcXid, rpcBranchType);
                }
            }
        }
        try {
            // 执行实际业务方法
            return invoker.invoke(invocation);
        } finally {
            if (bind) {
                BranchType previousBranchType = RootContext.getBranchType();
                // 最后 解除绑定
                String unbindXid = RootContext.unbind();
                if (BranchType.TCC == previousBranchType) {
                    RootContext.unbindBranchType();
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("unbind xid [{}] branchType [{}] from RootContext", unbindXid, previousBranchType);
                }
                if (!rpcXid.equalsIgnoreCase(unbindXid)) {
                    // 调用过程中有新的上下文对象开启,不能清楚
                    LOGGER.warn("xid in change during RPC from {} to {},branchType from {} to {}", rpcXid, unbindXid,
                            rpcBranchType != null ? rpcBranchType : "AT", previousBranchType);
                    if (unbindXid != null) {
                        // 绑定新的XID
                        RootContext.bind(unbindXid);
                        LOGGER.warn("bind xid [{}] back to RootContext", unbindXid);
                        if (BranchType.TCC == previousBranchType) {
                            RootContext.bindBranchType(BranchType.TCC);
                            LOGGER.warn("bind branchType [{}] back to RootContext", previousBranchType);
                        }
                    }
                }
            }
            RpcContext.getContext().removeAttachment(RootContext.KEY_XID);
            RpcContext.getContext().removeAttachment(RootContext.KEY_BRANCH_TYPE);
            RpcContext.getServerContext().removeAttachment(RootContext.KEY_XID);
            RpcContext.getServerContext().removeAttachment(RootContext.KEY_BRANCH_TYPE);
        }
    }

    /**
     * get rpc xid
     * @return
     */
    private String getRpcXid() {
        String rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID);
        if (rpcXid == null) {
            rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID.toLowerCase());
        }
        return rpcXid;
    }

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值