事務上下文
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 的應用運行時的傳播方式。
- 服務內部的事務傳播
/**
* 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);
}
}
}
}
- 跨服務調用的事務傳播
跨服務調用場景下的事務傳播,本質上就是要把 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);
}
}