Seata分佈式事務的事務傳播之源碼分析

事務上下文

Seata 的事務上下文由 RootContext 來管理。
應用開啓一個全局事務後,RootContext 會自動綁定該事務的 XID,事務結束(提交或回滾完成),RootContext 會自動解綁 XID。

/**
     * 事務的全局開啓方法
     * @param timeout Given timeout in MILLISECONDS.
     * @param name    Given name.
     * @throws TransactionException
     */
    @Override
    public void begin(int timeout, String name) throws TransactionException {
        if (role != GlobalTransactionRole.Launcher) {
            check();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Ignore Begin(): just involved in global transaction [{}]", xid);
            }
            return;
        }
        if (xid != null) {
            throw new IllegalStateException();
        }
        if (RootContext.getXID() != null) {
            throw new IllegalStateException();
        }
        //開啓事務,服務端返回全局事務的唯一標識,然後進行綁定事務,這個xid會傳遞到後面的所有分支事務
        xid = transactionManager.begin(null, null, name, timeout);
        status = GlobalStatus.Begin;
        // 綁定 XID
        RootContext.bind(xid);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Begin new global transaction [{}]", xid);
        }

    }

服務端的接收到事務的處理開啓請求的處理方法

    /**
     * 事務開啓
     * @param applicationId           ID of the application who begins this transaction.
     * @param transactionServiceGroup ID of the transaction service group.
     * @param name                    Give a name to the global transaction.
     * @param timeout                 Timeout of the global transaction.
     * @return
     * @throws TransactionException
     */
    @Override
    public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
        throws TransactionException {
        //創建一個全局的會話,也就是創建一個全局的XID
        GlobalSession session = GlobalSession.createGlobalSession(applicationId, transactionServiceGroup, name,
            timeout);
        session.addSessionLifecycleListener(SessionHolder.getRootSessionManager());

        session.begin();

        //transaction start event
        //事務開啓事件
        eventBus.post(new GlobalTransactionEvent(session.getTransactionId(), GlobalTransactionEvent.ROLE_TC,
            session.getTransactionName(), session.getBeginTime(), null, session.getStatus()));

        LOGGER.info("Successfully begin global transaction xid = {}", session.getXid());
        //返回XID給客戶端
        return session.getXid();
    }
    /**
     * 進行發送請求,獲取XID
     * @param applicationId           ID of the application who begins this transaction.
     * @param transactionServiceGroup ID of the transaction service group.
     * @param name                    Give a name to the global transaction.
     * @param timeout                 Timeout of the global transaction.
     * @return
     * @throws TransactionException
     */
    @Override
    public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
        throws TransactionException {
        GlobalBeginRequest request = new GlobalBeginRequest();
        request.setTransactionName(name);
        request.setTimeout(timeout);
        //向服務端TC發送請求,獲取到XID
        GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
        if (response.getResultCode() == ResultCode.Failed) {
            throw new TmTransactionException(TransactionExceptionCode.BeginFailed, response.getMsg());
        }
        //返回XID,提交和回滾的時候會根據這個xid查到會話
        return response.getXid();
    }

事務傳播

Seata 全局事務的傳播機制就是指事務上下文的傳播,從根本上,就是 XID 的應用運行時的傳播方式。

  1. 服務內部的事務傳播
/**
 * The type Thread local context core.
 * 默認的,RootContext 的實現是基於 ThreadLocal 的,即 XID 綁定在當前線程上下文中。
 * ThreadLocalContextCore 內部封裝了一個ThreadLocal線程內部對象
 */
@LoadLevel(name = "ThreadLocalContextCore", order = Integer.MIN_VALUE)
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);
    }

    @Override
    public Map<String, String> entries() {
        return threadLocal.get();
    }
}

所以說服務內部的 XID 傳播通常是天然的通過同一個線程的調用鏈路串連起來的。默認不做任何處理,事務的上下文就是傳播下去的。
如果希望掛起事務上下文,則需要通過 RootContext 提供的 API 來實現:

// 掛起(暫停)
String xid = RootContext.unbind();
// TODO: 運行在全局事務外的業務邏輯
// 恢復全局事務上下文
RootContext.bind(xid);

比如Spring cloud alibaba seata的服務內部的調用代碼,主要是本地的請求則

/**
 * 本地事務,進行處理事務的上下文功能,這個攔截器主要是處理事務的上下文相關的傳遞功能的
 */
public class SeataHandlerInterceptor implements HandlerInterceptor {

	private static final Logger log = LoggerFactory
			.getLogger(SeataHandlerInterceptor.class);

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
			Object handler) {
		//獲取全局事務的唯一標識,這個是XID是從服務器傳過來的
		String xid = RootContext.getXID();
		String rpcXid = request.getHeader(RootContext.KEY_XID);
		if (log.isDebugEnabled()) {
			log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
		}
		if (xid == null && rpcXid != null) {
			//把事務綁定到當前線程當中,這樣在同一個服務內容,他的事務是傳遞的
			RootContext.bind(rpcXid);
			if (log.isDebugEnabled()) {
				log.debug("bind {} to RootContext", rpcXid);
			}
		}
		return true;
	}
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
			Object handler, Exception e) {

		String rpcXid = request.getHeader(RootContext.KEY_XID);
		if (StringUtils.isEmpty(rpcXid)) {
			return;
		}
		//調用完成之後,則掛起事務
		String unbindXid = RootContext.unbind();
		if (log.isDebugEnabled()) {
			log.debug("unbind {} from RootContext", unbindXid);
		}
		if (!rpcXid.equalsIgnoreCase(unbindXid)) {
			log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
			if (unbindXid != null) {
				RootContext.bind(unbindXid);
				log.warn("bind {} back to RootContext", unbindXid);
			}
		}
	}

}
  1. 跨服務調用的事務傳播
    跨服務調用場景下的事務傳播,本質上就是要把 XID 通過服務調用傳遞到服務提供方,並綁定到 RootContext 中去。
    Spring Cloud的跨服務調用主要提供了二種方式進行跨服務調用,分別是Fegin和RestTemplate的方式進行跨服務調用,所以在Spring Cloud alibaba seata當中也提供了這種的事務傳遞方式,下面具體來分析具體的代碼,項目結構如下
    在這裏插入圖片描述
    feign目錄主要是當我們進行跨服務調用方式使用的是feign客戶端的方法時,就會被這裏的自動配置啓動的攔截器給攔截到,rest目錄放的是使用RestTemplate方式調用封裝的XID傳遞的具體邏輯,後臺會展開說這個rest目錄的具體內容,web目錄可以理解成是服務內部的XID傳遞,然後把XID設置到請求頭當中傳遞給另一個微服務,具體的引入Spring Cloud alibaba seata的自動配置類如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.seata.rest.SeataRestTemplateAutoConfiguration,\
com.alibaba.cloud.seata.web.SeataHandlerInterceptorConfiguration,\
com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration,\
com.alibaba.cloud.seata.feign.SeataFeignClientAutoConfiguration,\
com.alibaba.cloud.seata.feign.hystrix.SeataHystrixAutoConfiguration

這些自動配置類,主要是設置不同請求方式的攔截器,拿SeataRestTemplateInterceptor來說

@Configuration
public class SeataRestTemplateAutoConfiguration {

	@Bean
	public SeataRestTemplateInterceptor seataRestTemplateInterceptor() {
		return new SeataRestTemplateInterceptor();
	}

	@Autowired(required = false)
	private Collection<RestTemplate> restTemplates;

	//設置請求的攔截器
	@Autowired
	private SeataRestTemplateInterceptor seataRestTemplateInterceptor;

	@PostConstruct
	public void init() {
		if (this.restTemplates != null) {
			for (RestTemplate restTemplate : restTemplates) {
				List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>(
						restTemplate.getInterceptors());
				//請求之前會先執行這個攔截器
				interceptors.add(this.seataRestTemplateInterceptor);
				restTemplate.setInterceptors(interceptors);
			}
		}
	}

}

具體的攔截器代碼如下:

public class SeataRestTemplateInterceptor implements ClientHttpRequestInterceptor {
	@Override
	public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
			ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
		HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
		//獲取當前線程的全局事務會話ID
		String xid = RootContext.getXID();

		if (!StringUtils.isEmpty(xid)) {
			//設置到請求頭,傳遞到下一個服務當中
			requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
		}
		return clientHttpRequestExecution.execute(requestWrapper, bytes);
	}
}

發佈了25 篇原創文章 · 獲贊 0 · 訪問量 7805
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章