Apache Ignite 事務架構:并發模型和隔離級別
在本系列的第一篇文章中,我們研究了2階段提交協議,以及Ignite如何處理各種類型的集群節點,下面是在剩下的文章中要覆蓋的主題:
- 并發模型和隔離級別
- 故障轉移和恢復
- Ignite持久化層中的事務處理(WAL、檢查點及其他);
- 第三方持久化中的事務處理
在本文中,我們會聚焦并發模型和隔離級別。 大多數現代多用戶應用允許并發數據訪問和修改。為了管理此功能,并確保系統從一個一致狀態切換到另一個一致狀態,使用了事務的概念。事務依賴于鎖,它可以在事務開始時(悲觀鎖)獲得,也可以在事務結束提交之前(樂觀鎖)獲得。 Ignite支持兩種并發模型: 悲觀 和 樂觀 ,下面先講悲觀并發模型。
悲觀并發模型
悲觀并發模型的一個例子是兩個銀行賬戶之間的轉賬,需要確保兩個銀行賬戶的借貸狀態正確記錄。這時需要給兩個賬戶加鎖來確保更新全部完成并且余額正確。 在悲觀并發模型中,應用需要在事務開始時鎖定即將要讀、寫或者修改的所有數據。Ignite還支持一組悲觀并發模型的 隔離級別 ,在讀寫數據時提供了靈活性:
- 讀提交
- 可重復讀
- 序列化
在讀提交模型中,鎖是在寫操作對數據進行任何改變之前獲得的,比如 put() 或者 putAll() ,而可重復讀以及序列化模型用于讀寫操作都需要獲得鎖的場景。Ignite還有些內置的功能,使得調試和解決分布式死鎖問題更容易。 下面的代碼示例展示了可重復讀的悲觀事務,因為應用需要對一個特定銀行賬戶進行讀和寫的操作:
try (Transaction tx = Ignition.ignite().transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) {
Account acct = cache.get(acctId);
assert acct != null;
...
// Deposit into account.
acct.update(amount);
// Store updated account in cache.
cache.put(acctId, acct);
tx.commit();
}
本例中,通過**txStart() 和 tx.commit() 方法分別來進行事務的開啟和提交。 txStart() 方法傳遞了PESSIMISTIC和REPEATABLE READ參數,在try塊體中,代碼在 acctId 鍵上執行了一個 cache.get() 操作,之后,一些資金存入賬戶并且緩存使用 cache.put()**進行了更新。 下面的代碼示例展示了讀提交并且帶有死鎖處理的悲觀事務:
try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.PESSIMISTIC, TransactionIsolation.READ_COMMITTED, TX_TIMEOUT, 0)) {
// More code here.
tx.commit();
} catch (CacheException e) {
if (e.getCause() instanceof TransactionTimeoutException &&
e.getCause().getCause() instanceof TransactionDeadlockException)
System.out.println(e.getCause().getCause().getMessage());
}
本例中,代碼展示了如何使用Ignite的 死鎖檢測機制 ,這簡化了可能由應用代碼導致的分布式死鎖的調試。要開啟這個特性,需要開啟一個超時時間非0的Ignite事務(TX_TIMEOUT > 0),還需要捕獲包含死鎖詳細信息的TransactionDeadlockException。 下面再看一下不同隔離級別的消息流,對于讀提交,如圖1所示,在這個隔離模型中,Ignite對于讀操作不會獲得鎖,比如 get() 或者 getAll() ,這對很多場景可能更適合。
- 事務開始( 1 tx.Start );
- 事務協調器在內部管理事務請求( 2 IgniteInternalTx );
- 應用寫入鍵K1和K2( 3 tx.putAll(K1-V1, K2-V2) );
- 事務協調器將K1寫入本地事務映射( 4 Put(K1) );
- 事務協調器向存儲K1的主節點發起一個鎖請求( 5 lock(K1) );
- 主節點在內部管理事務請求( 6 IgniteInternalTx );
- 主節點向事務協調者發送一個已經準備好的確認( 7 ACK );
- 對于K2重復如圖1的4-7步驟;
- 發起事務提交請求( 12 tx.commit );
- K1和K2寫入相應的主節點( 13 Write(K1)和13 Write(K2) );
- 主節點確認事務提交( 14 ACK );
下一步,看一下可重復讀和序列化的消息流,如圖2所示:
- 事務開始( 1 tx.Start );
- 事務協調器在內部管理事務請求( 2 IgniteInternalTx );
- 應用讀取鍵K1和K2( 3 tx.getAll(K1-V1, K2-V2) );
- 事務協調器開始鍵K1的讀請求處理( 4 Get(K1) );
- 事務協調器向存儲K1的主節點發起一個鎖請求( 5 lock(K1) );
- 主節點在內部管理事務請求( 6 IgniteInternalTx );
- 主節點向事務協調者發送一個已經準備好的確認( 7 ACK )并且返回K1的值;
- 對于K2重復如圖2的4-7步驟;
- 應用寫入K1和K2( 12 tx.putAll(K1-V2, K2-V2) );
- 事務協調器將K1的更新寫入本地事務映射( 13 Put(K1) );
- 事務協調器將K2的更新寫入本地事務映射( 14 Put(K2) );
- 發起事務提交請求( 15 tx.commit );
- K1和K2寫入相應的主節點( 16 Write(K1)和16 Write(K2) );
- 主節點確認事務提交( 17 ACK );
總結一下,在悲觀模型中,在事務完成之前鎖一直持有,并且鎖會阻止其他事務對數據的訪問。 下一步看一下樂觀并發模型。
樂觀并發模型
樂觀并發模型的一個例子是計算機輔助設計(CAD),這里一個設計師工作于整個設計的一部分,通常會將設計從中央倉庫中檢出到本地工作區,然后進行部分更新之后將成果檢入中央倉庫,因為設計師只負責整個設計的一部分,所以不可能與其他部分的更新產生沖突。 與悲觀并發模型相反,樂觀并發模型延遲了鎖的獲取,這樣更適合于資源爭用較少的應用,比如上面描述的CAD的例子。Ignite還支持一些樂觀并發模型的 隔離級別 ,這提供了讀寫數據方面的靈活性:
- 讀提交
- 可重復讀
- 序列化( 無死鎖 )
回顧一下前文中關于2階段提交中各個階段的討論,當使用樂觀并發模型時,在準備階段,鎖是在主節點獲取的。在使用序列化模式時,如果通過事務請求的數據已經改變,在準備階段事務會失敗。這時,開發者需要編程控制應用的行為,即是否需要重啟事務。而其他的兩個模式,可重復讀和讀提交,不會檢查數據是否改變。雖然這會帶來性能方面的好處,但是沒有了數據的原子性保證,因此,這兩個模式在生產中很少用到。 下面的代碼示例展示了序列化的樂觀事務,因為應用需要對一個特定銀行賬戶進行讀和寫的操作:
while (true) {
try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC, TransactionIsolation.SERIALIZABLE)) {
Account acct = cache.get(acctId);
assert acct != null;
...
// Deposit into account.
acct.update(amount);
// Store updated account in cache.
cache.put(acctId, acct);
tx.commit();
// Transaction succeeded. Exiting the loop.
break;
} catch (TransactionOptimisticException e) {
// Transaction has failed. Retry.
}
}
本例中,在外側有個while循環,判斷事務是否失敗,它可以重試。下一步,有**txStart() 和 tx.commit()**方法,分別用于事務的開始和提交。**txStart() 方法傳遞了OPTIMISTIC和SERIALIZABLE參數,在try塊體中,代碼先在acctId鍵上執行了 cache.get() 操作,之后,一些資金存入賬戶并且緩存使用 cache.put()**進行了更新。如果事務成功,代碼會從循環中中斷,如果事務不成功,會拋出異常然后事務重試。對于樂觀的序列化事務,訪問鍵的順序不受限制,因為Ignite為了 避免死鎖 ,事務鎖是通過一個額外的檢查并行地獲得的。 下面看一下不同隔離級別下的消息流,先從序列化開始,如圖3所示:
- 事務開始( 1 tx.Start );
- 事務協調器在內部管理事務請求( 2 IgniteInternalTx );
- 應用寫入鍵K1( 3 tx.put(K1-V1) );
- 事務協調器將K1寫入本地事務映射( 4 Put(K1) );
- 應用寫入鍵K2( 5 tx.put(K2-V2) );
- 事務協調器將K2寫入本地事務映射( 6 Put(K2) );
- 發起事務提交請求( 7 tx.commit );
- 事務協調器向存儲K1和K2的主節點發起鎖請求( 8 lock(K1, TV1) and 8 lock(K2, TV1) );
- 主節點在內部管理事務請求( 9 IgniteInternalTx );
- 主節點向事務協調者發送一個已經準備好的確認( 10 ACK );
- K1和K2寫入相應的主節點( 11 Write(K1)和11 Write(K2) );
- 如果沒有數據沖突(即K1和K2沒有被其他的應用更新),主節點確認事務提交( 12 ACK )。
最后,看一下可重復讀和讀提交的消息流,如圖4所示:
- 事務開始( 1 tx.Start );
- 事務協調器在內部管理事務請求( 2 IgniteInternalTx );
- 應用寫入鍵K1( 3 tx.put(K1-V1) );
- 事務協調器將K1寫入本地事務映射( 4 Put(K1) );
- 應用寫入鍵K2( 5 tx.put(K2-V2) );
- 事務協調器將K2寫入本地事務映射( 6 Put(K2) );
- 發起事務提交請求( 7 tx.commit );
- 事務協調器向存儲K1和K2的主節點發起鎖請求( 8 lock(K1, TV1) and 8 lock(K2, TV1) );
- 主節點向事務協調者發送一個已經準備好的確認( 9 ACK );
- K1和K2寫入相應的主節點( 10 Write(K1)和10 Write(K2) );
- 主節點在內部管理事務請求( 11 IgniteInternalTx );
- 主節點確認事務提交( 12 ACK )。
總結
在本文中,研究了Ignite支持的主要的鎖模型和隔離級別,我們看到,有很大的靈活性和選擇空間,本系列的后面文章中,會研究故障轉移和恢復。
本文譯自GridGain技術布道師Akmal B. Chaudhri的 博客 。
來自:https://my.oschina.net/liyuj/blog/1627248