摘要: 原創出處 http://www.iocoder.cn/TCC-Transaction/transaction-repository/ 「芋道原始碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 TCC-Transaction 1.2.3.3 正式版
???關注**微信公眾號:【芋道原始碼】**有福利:
- RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
- RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
- 您對於原始碼的疑問每條留言都將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢。
- 新的原始碼解析文章實時收到通知。每週更新一篇左右。
- 認真的原始碼交流微信群。
1. 概述
本文分享 事務儲存器。主要涉及如下 Maven 專案:
tcc-transaction-core
:tcc-transaction 底層實現。
在 TCC 的過程中,根據應用記憶體中的事務資訊完成整個事務流程。But 實際業務場景中,將事務資訊只放在應用記憶體中是遠遠不夠可靠的。例如:
- 應用程式異常崩潰,未完成的事務資訊將丟失。
- 應用程式叢集,當提供遠端服務呼叫時,事務資訊需要叢集內共享。
- 發起事務的應用需要重啟部署新版本,因為各種原因,有未完成的事務。
因此,TCC-Transaction 將事務資訊新增到記憶體中的同時,會使用外部儲存進行持久化。目前提供四種外部儲存:
- JdbcTransactionRepository,JDBC 事務儲存器
- RedisTransactionRepository,Redis 事務儲存器
- ZooKeeperTransactionRepository,Zookeeper 事務儲存器
- FileSystemTransactionRepository,File 事務儲存器
本文涉及到的類關係如下圖( 開啟大圖 ):
你行好事會因為得到讚賞而愉悅
同理,開源專案貢獻者會因為 Star 而更加有動力
為 TCC-Transaction 點贊!傳送門
ps:筆者假設你已經閱讀過《tcc-transaction 官方文件 —— 使用指南1.2.x》。
2. 序列化
在《TCC-Transaction 原始碼分析 —— TCC 實現》「4. 事務與參與者」,可以看到 Transaction 是一個比較複雜的物件,內嵌 Participant 陣列,而 Participant 本身也是複雜的物件,內嵌了更多的其他物件,因此,儲存器在持久化 Transaction 時,需要序列化後才能儲存。
org.mengyun.tcctransaction.serializer.ObjectSerializer
,物件序列化介面。實現程式碼如下:
public interface ObjectSerializer<T> {
byte[] serialize(T t);
T deserialize(byte[] bytes);
}
複製程式碼
目前提供 JDK自帶序列化 和 Kyro序列化 兩種實現。
2.1 JDK 序列化實現
org.mengyun.tcctransaction.serializer.JdkSerializationSerializer
,JDK 序列化實現。比較易懂,點選連結直接檢視。
TCC-Transaction 使用的預設的序列化。
2.2 Kyro 序列化實現
org.mengyun.tcctransaction.serializer.KryoTransactionSerializer
,Kyro 序列化實現。比較易懂,點選連結直接檢視。
2.3 JSON 序列化實現
JDK 和 Kyro 的序列化實現,肉眼無法直觀具體儲存事務的資訊,你可以通過實現 ObjectSerializer 介面,實現自定義的 JSON 序列化。
3. 儲存器
org.mengyun.tcctransaction.TransactionRepository
,事務儲存器介面。實現程式碼如下:
public interface TransactionRepository {
/**
* 新增事務
*
* @param transaction 事務
* @return 新增數量
*/
int create(Transaction transaction);
/**
* 更新事務
*
* @param transaction 事務
* @return 更新數量
*/
int update(Transaction transaction);
/**
* 刪除事務
*
* @param transaction 事務
* @return 刪除數量
*/
int delete(Transaction transaction);
/**
* 獲取事務
*
* @param xid 事務編號
* @return 事務
*/
Transaction findByXid(TransactionXid xid);
/**
* 獲取超過指定時間的事務集合
*
* @param date 指定時間
* @return 事務集合
*/
List<Transaction> findAllUnmodifiedSince(Date date);
}
複製程式碼
不同的儲存器通過實現該介面,提供事務的增刪改查功能。
3.1 可快取的事務儲存器抽象類
org.mengyun.tcctransaction.repository.CachableTransactionRepository
,可快取的事務儲存器抽象類,實現增刪改查事務時,同時快取事務資訊。在上面類圖,我們也可以看到 TCC-Transaction 自帶的多種儲存器都繼承該抽象類。
CachableTransactionRepository 構造方法實現程式碼如下:
public abstract class CachableTransactionRepository implements TransactionRepository {
/**
* 快取過期時間
*/
private int expireDuration = 120;
/**
* 快取
*/
private Cache<Xid, Transaction> transactionXidCompensableTransactionCache;
public CachableTransactionRepository() {
transactionXidCompensableTransactionCache = CacheBuilder.newBuilder().expireAfterAccess(expireDuration, TimeUnit.SECONDS).maximumSize(1000).build();
}
}
複製程式碼
- 使用 Guava Cache 記憶體快取事務資訊,設定最大快取個數為 1000 個,快取過期時間為最後訪問時間 120 秒。
#create(...)
實現程式碼如下:
@Override
public int create(Transaction transaction) {
int result = doCreate(transaction);
if (result > 0) {
putToCache(transaction);
}
return result;
}
/**
* 新增到快取
*
* @param transaction 事務
*/
protected void putToCache(Transaction transaction) {
transactionXidCompensableTransactionCache.put(transaction.getXid(), transaction);
}
/**
* 新增事務
*
* @param transaction 事務
* @return 新增數量
*/
protected abstract int doCreate(Transaction transaction);
複製程式碼
- 呼叫
#doCreate(...)
方法,新增事務。新增成功後,呼叫#putToCache(...)
方法,新增事務到快取。 #doCreate(...)
為抽象方法,子類實現該方法,提供新增事務功能。
#update(...)
實現程式碼如下:
@Override
public int update(Transaction transaction) {
int result = 0;
try {
result = doUpdate(transaction);
if (result > 0) {
putToCache(transaction);
} else {
throw new OptimisticLockException();
}
} finally {
if (result <= 0) { // 更新失敗,移除快取。下次訪問,從儲存器讀取
removeFromCache(transaction);
}
}
return result;
}
/**
* 移除事務從快取
*
* @param transaction 事務
*/
protected void removeFromCache(Transaction transaction) {
transactionXidCompensableTransactionCache.invalidate(transaction.getXid());
}
/**
* 更新事務
*
* @param transaction 事務
* @return 更新數量
*/
protected abstract int doUpdate(Transaction transaction);
複製程式碼
- 呼叫
#doUpdate(...)
方法,更新事務。- 若更新成功後,呼叫
#putToCache(...)
方法,新增事務到快取。 - 若更新失敗後,丟擲 OptimisticLockException 異常。有兩種情況會導致更新失敗:(1) 該事務已經被提交,被刪除;(2) 樂觀鎖更新時,快取的事務的版本號(
Transaction.version
)和儲存器裡的事務的版本號不同,更新失敗。為什麼?在《TCC-Transaction 原始碼分析 —— 事務恢復》詳細解析。更新失敗,意味著快取已經不不一致,呼叫#removeFromCache(...)
方法,移除事務從快取中。
- 若更新成功後,呼叫
#doUpdate(...)
為抽象方法,子類實現該方法,提供更新事務功能。
#delete(...)
實現程式碼如下:
@Override
public int delete(Transaction transaction) {
int result;
try {
result = doDelete(transaction);
} finally {
removeFromCache(transaction);
}
return result;
}
/**
* 刪除事務
*
* @param transaction 事務
* @return 刪除數量
*/
protected abstract int doDelete(Transaction transaction);
複製程式碼
- 呼叫
#doDelete(...)
方法,刪除事務。 - 呼叫
#removeFromCache(...)
方法,移除事務從快取中。 #doDelete(...)
為抽象方法,子類實現該方法,提供刪除事務功能。
#findByXid(...)
實現程式碼如下:
@Override
public Transaction findByXid(TransactionXid transactionXid) {
Transaction transaction = findFromCache(transactionXid);
if (transaction == null) {
transaction = doFindOne(transactionXid);
if (transaction != null) {
putToCache(transaction);
}
}
return transaction;
}
/**
* 獲得事務從快取中
*
* @param transactionXid 事務編號
* @return 事務
*/
protected Transaction findFromCache(TransactionXid transactionXid) {
return transactionXidCompensableTransactionCache.getIfPresent(transactionXid);
}
/**
* 查詢事務
*
* @param xid 事務編號
* @return 事務
*/
protected abstract Transaction doFindOne(Xid xid);
複製程式碼
- 呼叫
#findFromCache()
方法,優先從快取中獲取事務。 - 呼叫
#doFindOne()
方法,快取中事務不存在,從儲存器中獲取。獲取到後,呼叫#putToCache()
方法,新增事務到快取中。 #doFindOne(...)
為抽象方法,子類實現該方法,提供查詢事務功能。
#findAllUnmodifiedSince(...)
實現程式碼如下:
@Override
public List<Transaction> findAllUnmodifiedSince(Date date) {
List<Transaction> transactions = doFindAllUnmodifiedSince(date);
// 新增到快取
for (Transaction transaction : transactions) {
putToCache(transaction);
}
return transactions;
}
/**
* 獲取超過指定時間的事務集合
*
* @param date 指定時間
* @return 事務集合
*/
protected abstract List<Transaction> doFindAllUnmodifiedSince(Date date);
複製程式碼
- 呼叫
#findAllUnmodifiedSince(...)
方法,從儲存器獲取超過指定時間的事務集合。呼叫#putToCache(...)
方法,迴圈事務集合新增到快取。 #doFindAllUnmodifiedSince(...)
為抽象方法,子類實現該方法,提供獲取超過指定時間的事務集合功能。
3.2 JDBC 事務儲存器
org.mengyun.tcctransaction.repository.JdbcTransactionRepository
,JDBC 事務儲存器,通過 JDBC 驅動,將 Transaction 儲存到 MySQL / Oracle / PostgreSQL / SQLServer 等關聯式資料庫。實現程式碼如下:
public class JdbcTransactionRepository extends CachableTransactionRepository {
/**
* 領域
*/
private String domain;
/**
* 表字尾
*/
private String tbSuffix;
/**
* 資料來源
*/
private DataSource dataSource;
/**
* 序列化
*/
private ObjectSerializer serializer = new JdkSerializationSerializer();
}
複製程式碼
domain
,領域,或者也可以稱為模組名,應用名,用於唯一標識一個資源。例如,Maven 模組xxx-order
,我們可以配置該屬性為ORDER
。tbSuffix
,表字尾。預設儲存表名為TCC_TRANSACTION
,配置表名後,為TCC_TRANSACTION${tbSuffix}
。dataSource
,儲存資料的資料來源。serializer
,序列化。**當資料庫裡已經有資料的情況下,不要更換別的序列化,否則會導致反序列化報錯。**建議:TCC-Transaction 儲存時,新增欄位,記錄序列化的方式。
表結構如下:
CREATE TABLE `TCC_TRANSACTION` (
`TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT,
`DOMAIN` varchar(100) DEFAULT NULL,
`GLOBAL_TX_ID` varbinary(32) NOT NULL,
`BRANCH_QUALIFIER` varbinary(32) NOT NULL,
`CONTENT` varbinary(8000) DEFAULT NULL,
`STATUS` int(11) DEFAULT NULL,
`TRANSACTION_TYPE` int(11) DEFAULT NULL,
`RETRIED_COUNT` int(11) DEFAULT NULL,
`CREATE_TIME` datetime DEFAULT NULL,
`LAST_UPDATE_TIME` datetime DEFAULT NULL,
`VERSION` int(11) DEFAULT NULL,
PRIMARY KEY (`TRANSACTION_ID`),
UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
複製程式碼
TRANSACTION_ID
,僅僅資料庫自增,無實際用途。CONTENT
,Transaction 序列化。
ps:點選連結檢視 JdbcTransactionRepository 程式碼實現,已經新增完整中文註釋。
3.3 Redis 事務儲存器
org.mengyun.tcctransaction.repository.RedisTransactionRepository
,Redis 事務儲存器,將 Transaction 儲存到 Redis。實現程式碼如下:
public class RedisTransactionRepository extends CachableTransactionRepository {
/**
* Jedis Pool
*/
private JedisPool jedisPool;
/**
* key 字首
*/
private String keyPrefix = "TCC:";
/**
* 序列化
*/
private ObjectSerializer serializer = new JdkSerializationSerializer();
}
複製程式碼
keyPrefix
,key 字首。類似 JdbcTransactionRepository 的domain
屬性。
一個事務儲存到 Reids,使用 Redis 的資料結構為 HASHES。
-
key : 使用
keyPrefix
+xid
,實現程式碼如下:/** * 建立事務的 Redis Key * * @param keyPrefix key 字首 * @param xid 事務 * @return Redis Key */ public static byte[] getRedisKey(String keyPrefix, Xid xid) { byte[] prefix = keyPrefix.getBytes(); byte[] globalTransactionId = xid.getGlobalTransactionId(); byte[] branchQualifier = xid.getBranchQualifier(); // 拼接 key byte[] key = new byte[prefix.length + globalTransactionId.length + branchQualifier.length]; System.arraycopy(prefix, 0, key, 0, prefix.length); System.arraycopy(globalTransactionId, 0, key, prefix.length, globalTransactionId.length); System.arraycopy(branchQualifier, 0, key, prefix.length + globalTransactionId.length, branchQualifier.length); return key; } 複製程式碼
-
HASHES 的 key :使用
version
。 -
HASHES 的 value :呼叫
TransactionSerializer#serialize(...)
方法,序列化 Transaction。實現程式碼如下:public static byte[] serialize(ObjectSerializer serializer, Transaction transaction) { Map<String, Object> map = new HashMap<String, Object>(); map.put("GLOBAL_TX_ID", transaction.getXid().getGlobalTransactionId()); map.put("BRANCH_QUALIFIER", transaction.getXid().getBranchQualifier()); map.put("STATUS", transaction.getStatus().getId()); map.put("TRANSACTION_TYPE", transaction.getTransactionType().getId()); map.put("RETRIED_COUNT", transaction.getRetriedCount()); map.put("CREATE_TIME", transaction.getCreateTime()); map.put("LAST_UPDATE_TIME", transaction.getLastUpdateTime()); map.put("VERSION", transaction.getVersion()); // 序列化 map.put("CONTENT", serializer.serialize(transaction)); return serializer.serialize(map); } 複製程式碼
- TODO 為什麼序列化兩次
在實現 #doFindAllUnmodifiedSince(date)
方法,無法像資料庫使用時間條件進行過濾,因此,載入所有事務後在記憶體中過濾。實現程式碼如下:
@Override
protected List<Transaction> doFindAllUnmodifiedSince(Date date) {
// 獲得所有事務
List<Transaction> allTransactions = doFindAll();
// 過濾時間
List<Transaction> allUnmodifiedSince = new ArrayList<Transaction>();
for (Transaction transaction : allTransactions) {
if (transaction.getLastUpdateTime().compareTo(date) < 0) {
allUnmodifiedSince.add(transaction);
}
}
return allUnmodifiedSince;
}
複製程式碼
ps:點選連結檢視 RedisTransactionRepository 程式碼實現,已經新增完整中文註釋。
FROM 《TCC-Transaction 官方文件 —— 使用指南1.2.x》
使用 RedisTransactionRepository 需要配置 Redis 伺服器如下:
appendonly yes
appendfsync always
3.4 Zookeeper 事務儲存器
org.mengyun.tcctransaction.repository.ZooKeeperTransactionRepository
,Zookeeper 事務儲存器,將 Transaction 儲存到 Zookeeper。實現程式碼如下:
public class ZooKeeperTransactionRepository extends CachableTransactionRepository {
/**
* Zookeeper 伺服器地址陣列
*/
private String zkServers;
/**
* Zookeeper 超時時間
*/
private int zkTimeout;
/**
* TCC 儲存 Zookeeper 根目錄
*/
private String zkRootPath = "/tcc";
/**
* Zookeeper 連線
*/
private volatile ZooKeeper zk;
/**
* 序列化
*/
private ObjectSerializer serializer = new JdkSerializationSerializer();
}
複製程式碼
zkRootPath
,儲存 Zookeeper 根目錄,類似 JdbcTransactionRepository 的domain
屬性。
一個事務儲存到 Zookeeper,使用 Zookeeper 的持久資料節點。
-
path:
${zkRootPath}
+/
+${xid}
。實現程式碼如下:// ZooKeeperTransactionRepository.java private String getTxidPath(Xid xid) { return String.format("%s/%s", zkRootPath, xid); } // TransactionXid.java @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("globalTransactionId:").append(UUID.nameUUIDFromBytes(globalTransactionId).toString()); stringBuilder.append(",").append("branchQualifier:").append(UUID.nameUUIDFromBytes(branchQualifier).toString()); return stringBuilder.toString(); } 複製程式碼
-
data:呼叫
TransactionSerializer#serialize(...)
方法,序列化 Transaction。 -
version:使用 Zookeeper 資料節點自帶版本功能。這裡要注意下,Transaction 的版本從 1 開始,而 Zookeeper 資料節點版本從 0 開始。
ps:點選連結檢視 ZooKeeperTransactionRepository 程式碼實現,已經新增完整中文註釋。
另外,在生產上暫時不建議使用 ZooKeeperTransactionRepository,原因有兩點:
- 不支援 Zookeeper 安全認證。
- 使用 Zookeeper 時,未考慮斷網重連等情況。
如果你要使用 Zookeeper 進行事務的儲存,可以考慮使用 Apache Curator 操作 Zookeeper,重寫 ZooKeeperTransactionRepository 部分程式碼。
3.5 File 事務儲存器
org.mengyun.tcctransaction.repository.FileSystemTransactionRepository
,File 事務儲存器,將 Transaction 儲存到檔案系統。
實現上和 ZooKeeperTransactionRepository,區別主要在於不支援樂觀鎖更新。有興趣的同學點選連結檢視,這裡就不擴充開來。
另外,在生產上不建議使用 FileSystemTransactionRepository,因為不支援多節點共享。用分散式儲存掛載檔案另說,當然還是不建議,因為不支援樂觀鎖併發更新。
666. 彩蛋
這篇略( 超 )微( 級 )水更,哈哈哈,為《TCC-Transaction 原始碼分析 —— 事務恢復》做鋪墊啦。
使用 RedisTransactionRepository 和 ZooKeeperTransactionRepository 儲存事務還是 Get 蠻多點的。
胖友,分享一個朋友圈可好?