在本系列的第一篇文章中,我們研究了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的博客。