Apache Ignite 事務架構:并發模型和隔離級別

feng2r200 6年前發布 | 30K 次閱讀 并發 軟件架構 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 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所示:

  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所示:

  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所示:

  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的 博客

 

來自:https://my.oschina.net/liyuj/blog/1627248

 

 本文由用戶 feng2r200 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!