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:讀提交

  1. 事務開始(1 tx.Start);
  2. 事務協調器在內部管理事務請求(2 IgniteInternalTx);
  3. 應用寫入鍵K1和K2(3 tx.putAll(K1-V1, K2-V2));
  4. 事務協調器將K1寫入本地事務映射(4 Put(K1));
  5. 事務協調器向存儲K1的主節點發起一個鎖請求(5 lock(K1));
  6. 主節點在內部管理事務請求(6 IgniteInternalTx);
  7. 主節點向事務協調者發送一個已經準備好的確認(7 ACK);
  8. 對於K2重複如圖1的4-7步驟;
  9. 發起事務提交請求(12 tx.commit);
  10. K1和K2寫入相應的主節點(13 Write(K1)和13 Write(K2));
  11. 主節點確認事務提交(14 ACK);

下一步,看一下可重複讀和序列化的消息流,如圖2所示:

圖2:可重複讀和序列化

  1. 事務開始(1 tx.Start);
  2. 事務協調器在內部管理事務請求(2 IgniteInternalTx);
  3. 應用讀取鍵K1和K2(3 tx.getAll(K1-V1, K2-V2));
  4. 事務協調器開始鍵K1的讀請求處理(4 Get(K1));
  5. 事務協調器向存儲K1的主節點發起一個鎖請求(5 lock(K1));
  6. 主節點在內部管理事務請求(6 IgniteInternalTx);
  7. 主節點向事務協調者發送一個已經準備好的確認(7 ACK)並且返回K1的值;
  8. 對於K2重複如圖2的4-7步驟;
  9. 應用寫入K1和K2(12 tx.putAll(K1-V2, K2-V2));
  10. 事務協調器將K1的更新寫入本地事務映射(13 Put(K1));
  11. 事務協調器將K2的更新寫入本地事務映射(14 Put(K2));
  12. 發起事務提交請求(15 tx.commit);
  13. K1和K2寫入相應的主節點(16 Write(K1)和16 Write(K2));
  14. 主節點確認事務提交(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所示:

圖3:序列化

  1. 事務開始(1 tx.Start);
  2. 事務協調器在內部管理事務請求(2 IgniteInternalTx);
  3. 應用寫入鍵K1(3 tx.put(K1-V1));
  4. 事務協調器將K1寫入本地事務映射(4 Put(K1));
  5. 應用寫入鍵K2(5 tx.put(K2-V2));
  6. 事務協調器將K2寫入本地事務映射(6 Put(K2));
  7. 發起事務提交請求(7 tx.commit);
  8. 事務協調器向存儲K1和K2的主節點發起鎖請求(8 lock(K1, TV1) and 8 lock(K2, TV1));
  9. 主節點在內部管理事務請求(9 IgniteInternalTx);
  10. 主節點向事務協調者發送一個已經準備好的確認(10 ACK);
  11. K1和K2寫入相應的主節點(11 Write(K1)和11 Write(K2));
  12. 如果沒有數據衝突(即K1和K2沒有被其他的應用更新),主節點確認事務提交(12 ACK)。

最後,看一下可重複讀和讀提交的消息流,如圖4所示:

圖4:可重複讀和讀提交

  1. 事務開始(1 tx.Start);
  2. 事務協調器在內部管理事務請求(2 IgniteInternalTx);
  3. 應用寫入鍵K1(3 tx.put(K1-V1));
  4. 事務協調器將K1寫入本地事務映射(4 Put(K1));
  5. 應用寫入鍵K2(5 tx.put(K2-V2));
  6. 事務協調器將K2寫入本地事務映射(6 Put(K2));
  7. 發起事務提交請求(7 tx.commit);
  8. 事務協調器向存儲K1和K2的主節點發起鎖請求(8 lock(K1, TV1) and 8 lock(K2, TV1));
  9. 主節點向事務協調者發送一個已經準備好的確認(9 ACK);
  10. K1和K2寫入相應的主節點(10 Write(K1)和10 Write(K2));
  11. 主節點在內部管理事務請求(11 IgniteInternalTx);
  12. 主節點確認事務提交(12 ACK)。

總結

在本文中,研究了Ignite支持的主要的鎖模型和隔離級別,我們看到,有很大的靈活性和選擇空間,本系列的後面文章中,會研究故障轉移和恢復。

本文譯自GridGain技術佈道師Akmal B. Chaudhri的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章