官 網:http://www.txlcn.org/zh-cn/index.html
git 地址:https://github.com/codingapi/tx-lcn
目錄
一、產生背景
由於採用微服務架構,各個模塊相互獨立,導致原先在Spring容器中的通過@Transactional註解實現的本地事務,無法滿足跨服務的分佈式事務處理。
二、LCN框架定位
框架本身並不生產事務,LCN只是本地事務的協調工。TX-LCN定位於一款事務協調性框架,框架其本身並不操作事務,而是基於對事務的協調從而達到事務一致性的效果。
三、LCN事務模式
LCN模式是通過代理Connection的方式實現對本地事務的操作,然後在由TxManager統一協調控制事務。當本地事務提交回滾或者關閉連接時將會執行“假”操作,該代理的連接已由LCN框架管理。
模式特點:
- 該模式對代碼的嵌入性爲低。
- 該模式僅限於本地存在連接對象且可通過連接對象控制事務的模塊。
- 該模式下的事務提交與回滾是由本地事務方控制,對於數據一致性上有較高的保障。
- 該模式缺陷在於代理的連接需要隨事務發起方一起釋放連接,增加了連接佔用的時間。
四、事務控制原理
TX-LCN由兩大模塊組成:TxClient、TxManager。TxClient作爲模塊的依賴框架,提供TX-LCN的標準支持;TxManager作爲分佈式事務的調控方。事務發起方或者參與方都由TxClient端來控制。
原理圖:
五、核心步驟
1、創建事務組
在事務發起方開始執行業務代碼之前先調用TxManager創建事務組對象,然後拿到事務標識GroupId的過程。
2、加入事務組
添加事務組是指事務參與方在執行完業務方法以後,將該模塊的事務信息通知給TxManager的操作。
3、通知事務組
在發起方執行完業務代碼以後,將發起方執行結果狀態通知給TxManager,TxManager將根據事務最終狀態和事務組的信息來通知相應的參與模塊真正的進行提交或回滾事務,並返回結果給事務發起方。
六、數據庫連接佔用情況
LCN模式:
七、使用
(1)引入依賴 tm tc 對應的各種依賴(可封裝,傳遞引用),配置連接tm 信息 及各種參數配置項信息
(2)需要使用的客戶端在其啓動類上 增加開啓分佈式事務註解@EnableDistributedTransaction
(3)需要分佈式事務的方法上 增加分佈式事務註解@LcnTransaction
八、分佈式事務傳遞性
REQUIRED 當前沒有分佈式事務,就創建。當前有分佈式事務,就加入。
SUPPORTS 當前沒有分佈式事務,非分佈式事務運行。當前有分佈式事務,就加入。
九、關鍵源碼分析
LCN事務控制原理是由事務模塊TxClient下的代理連接池與TxManager的協調配合完成的事務協調控制。
(1)數據源切面
TxClient的代理連接池實現了javax.sql.DataSource接口,並重寫了close方法,事務模塊在提交關閉以後TxClient連接池將執行"假關閉"操作,等待TxManager協調完成事務以後再關閉連接。
@Aspect
@Component
@Slf4j
DataSourceAspect{
@Around("execution(* javax.sql.DataSource.getConnection(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
return dtxResourceWeaver.getConnection(() -> (Connection) point.proceed());
}
}
@Component
@Slf4j
public class DTXResourceWeaver {
public Object getConnection(ConnectionCallback connectionCallback) throws Throwable {
DTXLocalContext dtxLocalContext = DTXLocalContext.cur();
if (Objects.nonNull(dtxLocalContext) && dtxLocalContext.isProxy()) {
String transactionType = dtxLocalContext.getTransactionType();
TransactionResourceProxy resourceProxy = txLcnBeanHelper.loadTransactionResourceProxy(transactionType);
Connection connection = resourceProxy.proxyConnection(connectionCallback);
log.debug("proxy a sql connection: {}.", connection);
return connection;
}
return connectionCallback.call();
}
}
(2)各種上下文
TCGlobalContext (全局 分佈式存儲redis groupId 唯一)
TxContext
DTXLocalContext(本地,當前線程級)
(3)LCN事務攔截器 TransactionAspect
/**
* LCN 事務攔截器
* create by lorne on 2018/1/5
*/
@Aspect
@Component
@Slf4j
public class TransactionAspect implements Ordered {
/**
* DTC Aspect (Type of LCN)
*/
@Pointcut("@annotation(com.codingapi.txlcn.tc.annotation.LcnTransaction)")
public void lcnTransactionPointcut() {
}
@Around("lcnTransactionPointcut() && !txcTransactionPointcut()" +
"&& !tccTransactionPointcut() && !txTransactionPointcut()")
public Object runWithLcnTransaction(ProceedingJoinPoint point) throws Throwable {
DTXInfo dtxInfo = DTXInfo.getFromCache(point);
LcnTransaction lcnTransaction = dtxInfo.getBusinessMethod().getAnnotation(LcnTransaction.class);
dtxInfo.setTransactionType(Transactions.LCN);
dtxInfo.setTransactionPropagation(lcnTransaction.propagation());
return dtxLogicWeaver.runTransaction(dtxInfo, point::proceed);
}
}
@Component
@Slf4j
public class DTXLogicWeaver {
public Object runTransaction(DTXInfo dtxInfo, BusinessCallback business) throws Throwable {
if (Objects.isNull(DTXLocalContext.cur())) {
DTXLocalContext.getOrNew();
} else {
return business.call();
}
log.debug("<---- txlcn start ---->");
DTXLocalContext dtxLocalContext = DTXLocalContext.getOrNew();
TxContext txContext;
// ---------- 保證每個模塊在一個DTX下只會有一個TxContext ---------- //
if (globalContext.hasTxContext()) {
// 有事務上下文的獲取父上下文
txContext = globalContext.txContext();
dtxLocalContext.setInGroup(true);
log.debug("txlcn---有事務上下文,獲取父上下文");
log.debug("Unit[{}] used parent's TxContext[{}].", dtxInfo.getUnitId(), txContext.getGroupId());
} else {
// 沒有的開啓本地事務上下文
log.debug("txlcn---沒有事務上下文,開啓本地上下文");
txContext = globalContext.startTx();
}
// 本地事務調用
if (Objects.nonNull(dtxLocalContext.getGroupId())) {
dtxLocalContext.setDestroy(false);
}
dtxLocalContext.setUnitId(dtxInfo.getUnitId());
dtxLocalContext.setGroupId(txContext.getGroupId());
dtxLocalContext.setTransactionType(dtxInfo.getTransactionType());
// 事務參數
TxTransactionInfo info = new TxTransactionInfo();
info.setBusinessCallback(business);
info.setGroupId(txContext.getGroupId());
info.setUnitId(dtxInfo.getUnitId());
info.setPointMethod(dtxInfo.getBusinessMethod());
info.setPropagation(dtxInfo.getTransactionPropagation());
info.setTransactionInfo(dtxInfo.getTransactionInfo());
info.setTransactionType(dtxInfo.getTransactionType());
info.setTransactionStart(txContext.isDtxStart());
//LCN事務處理器
try {
return transactionServiceExecutor.transactionRunning(info);
} finally {
// 線程執行業務完畢清理本地數據
if (dtxLocalContext.isDestroy()) {
// 通知事務執行完畢
synchronized (txContext.getLock()) {
txContext.getLock().notifyAll();
}
// TxContext生命週期是? 和事務組一樣(不與具體模塊相關的)
if (!dtxLocalContext.isInGroup()) {
globalContext.destroyTx();
}
DTXLocalContext.makeNeverAppeared();
TracingContext.tracing().destroy();
}
log.debug("<---- txlcn end ---->");
}
}
}
(4)LCN分佈式事務業務執行器
/**
* LCN分佈式事務業務執行器
* Created by lorne on 2017/6/8.
*/
@Component
@Slf4j
public class DTXServiceExecutor {
public Object transactionRunning(TxTransactionInfo info) throws Throwable {
// 1. 獲取事務類型
String transactionType = info.getTransactionType();
// 2. 獲取事務傳播狀態
DTXPropagationState propagationState = propagationResolver.resolvePropagationState(info);
// 2.1 如果不參與分佈式事務立即終止
if (propagationState.isIgnored()) {
return info.getBusinessCallback().call();
}
// 3. 獲取本地分佈式事務控制器
DTXLocalControl dtxLocalControl = txLcnBeanHelper.loadDTXLocalControl(transactionType, propagationState);
// 4. 織入事務操作
try {
// 4.1 記錄事務類型到事務上下文
Set<String> transactionTypeSet = globalContext.txContext(info.getGroupId()).getTransactionTypes();
transactionTypeSet.add(transactionType);
dtxLocalControl.preBusinessCode(info);
// 4.2 業務執行前
txLogger.txTrace(
info.getGroupId(), info.getUnitId(), "pre business code, unit type: {}", transactionType);
// 4.3 執行業務
Object result = dtxLocalControl.doBusinessCode(info);
// 4.4 業務執行成功
txLogger.txTrace(info.getGroupId(), info.getUnitId(), "business success");
dtxLocalControl.onBusinessCodeSuccess(info, result);
return result;
} catch (TransactionException e) {
txLogger.error(info.getGroupId(), info.getUnitId(), "before business code error");
throw e;
} catch (Throwable e) {
// 4.5 業務執行失敗
txLogger.error(info.getGroupId(), info.getUnitId(), Transactions.TAG_TRANSACTION,
"business code error");
dtxLocalControl.onBusinessCodeError(info, e);
throw e;
} finally {
// 4.6 業務執行完畢
dtxLocalControl.postBusinessCode(info);
}
}
}
(5)本地分佈式事務控制器
(6) LcnStartingTransaction#postBusinessCode()
@Service(value = "control_lcn_starting")
@Slf4j
public class LcnStartingTransaction implements DTXLocalControl {
@Override
public void postBusinessCode(TxTransactionInfo info) {
log.debug("txlcn---notify group begin,@group({})", info.getGroupId());
long t1 = System.currentTimeMillis();
// RPC close DTX group
transactionControlTemplate.notifyGroup(
info.getGroupId(), info.getUnitId(), info.getTransactionType(),
DTXLocalContext.transactionState(globalContext.dtxState(info.getGroupId())));
long t2 = System.currentTimeMillis();
long usedTime = t2 - t1;
log.debug("txlcn---postBusinessCode,used time {} ms,@group({})", usedTime, info.getGroupId());
if (usedTime >= 1000) {
log.debug("txlcn---postBusinessCode,more than 1000 ms,used {} ms,@group({})", usedTime, info.getGroupId());
}
}
}
(7)服務端 package com.codingapi.txlcn.tm.txmsg.transaction.NotifyGroupExecuteService
@Service("rpc_notify-group")
@Slf4j
public class NotifyGroupExecuteService implements RpcExecuteService {
@Override
public Serializable execute(TransactionCmd transactionCmd) throws TxManagerException {
long t1 = System.currentTimeMillis();
try {
DTXContext dtxContext = dtxContextRegistry.get(transactionCmd.getGroupId());
// 解析參數
NotifyGroupParams notifyGroupParams = transactionCmd.getMsg().loadBean(NotifyGroupParams.class);
int commitState = notifyGroupParams.getState();
// 獲取事務狀態(當手動回滾時會先設置狀態)
int transactionState = transactionManager.transactionStateFromFastStorage(transactionCmd.getGroupId());
if (transactionState == 0) {
commitState = 0;
}
if (commitState == 1) {
transactionManager.commit(dtxContext);
} else if (commitState == 0) {
transactionManager.rollback(dtxContext);
}
return commitState;
} catch (TransactionException e) {
throw new TxManagerException(e);
} finally {
transactionManager.close(transactionCmd.getGroupId());
// 系統日誌
txLogger.txTrace(transactionCmd.getGroupId(), "tm", "notify group successfully.");
}
}
}
(8)客戶端 package com.codingapi.txlcn.tc.txmsg.transaction.DefaultNotifiedUnitService
/**
* Description: 默認RPC命令業務
* Date: 2018/12/20
*
* @author ujued
*/
@Slf4j
public class DefaultNotifiedUnitService implements RpcExecuteService {
@Override
public Serializable execute(TransactionCmd transactionCmd) throws TxClientException {
long t1 = System.currentTimeMillis();
try {
NotifyUnitParams notifyUnitParams = transactionCmd.getMsg().loadBean(NotifyUnitParams.class);
// 保證業務線程執行完畢後執行事務清理操作
TxContext txContext = globalContext.txContext(transactionCmd.getGroupId());
if (Objects.nonNull(txContext)) {
synchronized (txContext.getLock()) {
txLogger.txTrace(transactionCmd.getGroupId(), notifyUnitParams.getUnitId(),
"clean transaction cmd waiting for business code finish.");
txContext.getLock().wait();
}
}
// 事務清理操作 後面調用connect的commit 或者 rollback
transactionCleanTemplate.clean(
notifyUnitParams.getGroupId(),
notifyUnitParams.getUnitId(),
notifyUnitParams.getUnitType(),
notifyUnitParams.getState());
return true;
} catch (TransactionClearException | InterruptedException e) {
throw new TxClientException(e);
}
}
}
/**
* notify connection
*
* @param state transactionState
* @return RpcResponseState RpcResponseState
*/
public RpcResponseState notify(int state, String groupId, String unitId) {
try {
if (state == 1) {
connection.commit();
} else {
connection.rollback();
}
return RpcResponseState.success;
} catch (Exception e) {
log.error(e.getLocalizedMessage(), e);
return RpcResponseState.fail;
} finally {
try {
connection.close();
} catch (Exception e1) {
log.error(e1.getLocalizedMessage(), e1);
}
}
}