RocketMQ 分布式事務(wù)消息

一、什么是事務(wù)

事務(wù)是將一次執(zhí)行過程中所涉及的所有操作納入到一個不可分割的執(zhí)行單元,組成事務(wù)的所有操作只有在所有操作均能正常執(zhí)行的情況下才能提交,只要其中任一操作執(zhí)行失敗,都將導(dǎo)致整個事務(wù)的回滾。一句話來說,就是保證多個操作要么都做,要么都不做。同時(shí)一旦事務(wù)提交,則其所做的修改會永久保存到數(shù)據(jù)庫。

二、事務(wù)的四個特性(ACID)

  • A:原子性(Atomicity)
    一個事務(wù)(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結(jié)束在中間某個環(huán)節(jié)。事務(wù)在執(zhí)行過程中發(fā)生錯誤,會被回滾(Rollback)到事務(wù)開始前的狀態(tài),就像這個事務(wù)從來沒有執(zhí)行過一樣。
  • C:一致性(Consistency)
    事務(wù)的一致性指的是在一個事務(wù)執(zhí)行之前和執(zhí)行之后數(shù)據(jù)庫都必須處于一致性狀態(tài)。如果事務(wù)成功地完成,那么系統(tǒng)中所有變化將正確地應(yīng)用,系統(tǒng)處于有效狀態(tài)。如果在事務(wù)中出現(xiàn)錯誤,那么系統(tǒng)中的所有變化將自動地回滾,系統(tǒng)返回到原始狀態(tài)。
  • I:隔離性(Isolation)
    指的是在并發(fā)環(huán)境中,當(dāng)不同的事務(wù)同時(shí)操縱相同的數(shù)據(jù)時(shí),每個事務(wù)都有各自的完整數(shù)據(jù)空間。由并發(fā)事務(wù)所做的修改必須與任何其他并發(fā)事務(wù)所做的修改隔離。事務(wù)查看數(shù)據(jù)更新時(shí),數(shù)據(jù)所處的狀態(tài)要么是另一事務(wù)修改它之前的狀態(tài),要么是另一事務(wù)修改它之后的狀態(tài),事務(wù)不會查看到中間狀態(tài)的數(shù)據(jù)。
  • D:持久性(Durability)
    指的是只要事務(wù)成功結(jié)束,它對數(shù)據(jù)庫所做的更新就必須永久保存下來。即使發(fā)生系統(tǒng)崩潰,重新啟動數(shù)據(jù)庫系統(tǒng)后,數(shù)據(jù)庫還能恢復(fù)到事務(wù)成功結(jié)束時(shí)的狀態(tài)。

三、InnoDB 事務(wù)實(shí)現(xiàn)

基于衡量事務(wù)的四個特性,InnoDB 實(shí)現(xiàn)事務(wù)實(shí)際上就是 4 個特性的實(shí)現(xiàn)。

  • 原子性

    • 在 MySQL 中有很多類型的日志,二進(jìn)制日志、查詢?nèi)罩尽㈠e誤日志、慢查詢?nèi)罩镜鹊?。除了這些日志,還提供了兩種事務(wù)日志,redo log 用來保證持久性, undo log 是原子性和隔離性實(shí)現(xiàn)的基礎(chǔ)。
    • 數(shù)據(jù)庫每執(zhí)行一條更新數(shù)據(jù)的 sql 就會生成一條 undo log,比如 insert 一條數(shù)據(jù),就會生出一條 delete 的 undo log。如果事務(wù)執(zhí)行失敗或者調(diào)用 rollback 就可以根據(jù) undo log 做數(shù)據(jù)回滾。
  • 隔離性

    • 隔離性是指,事務(wù)內(nèi)部的操作與其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。嚴(yán)格的隔離性,對應(yīng)了事務(wù)隔離級別中的Serializable (可串行化),但實(shí)際應(yīng)用中出于性能方面的考慮很少會使用可串行化。
    • InnoDB 采用可重復(fù)讀隔離級別,使用 MVCC 和行鎖、間隙鎖實(shí)現(xiàn)隔離性。
  • 持久性

    • InnoDB作為MySQL的存儲引擎,數(shù)據(jù)是存放在磁盤中的,但如果每次讀寫數(shù)據(jù)都需要磁盤IO,效率會很低。為此,InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分?jǐn)?shù)據(jù)頁的映射,作為訪問數(shù)據(jù)庫的緩沖:當(dāng)從數(shù)據(jù)庫讀取數(shù)據(jù)時(shí),會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;當(dāng)向數(shù)據(jù)庫寫入數(shù)據(jù)時(shí),會首先寫入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會定期刷新到磁盤中(這一過程稱為刷臟)。
    • Buffer Pool的使用大大提高了讀寫數(shù)據(jù)的效率,但是也帶了新的問題:如果MySQL宕機(jī),而此時(shí)Buffer Pool中修改的數(shù)據(jù)還沒有刷新到磁盤,就會導(dǎo)致數(shù)據(jù)的丟失,事務(wù)的持久性無法保證。
    • 于是,redo log被引入來解決這個問題:當(dāng)數(shù)據(jù)修改時(shí),除了修改Buffer Pool中的數(shù)據(jù),還會在redo log記錄這次操作;當(dāng)事務(wù)提交時(shí),會調(diào)用fsync接口對redo log進(jìn)行刷盤。如果MySQL宕機(jī),重啟時(shí)可以讀取redo log中的數(shù)據(jù),對數(shù)據(jù)庫進(jìn)行恢復(fù)。redo log采用的是WAL(Write-ahead logging,預(yù)寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數(shù)據(jù)不會因MySQL宕機(jī)而丟失,從而滿足了持久性要求。

    既然redo log也需要在事務(wù)提交時(shí)將日志寫入磁盤,為什么它比直接將Buffer Pool中修改的數(shù)據(jù)寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:
    (1)刷臟是隨機(jī)IO,因?yàn)槊看涡薷牡臄?shù)據(jù)位置隨機(jī),但寫redo log是追加操作,屬于順序IO。
    (2)刷臟是以數(shù)據(jù)頁(Page)為單位的,MySQL默認(rèn)頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。

  • 一致性

    • 一致性是指事務(wù)執(zhí)行結(jié)束后,數(shù)據(jù)庫的完整性約束沒有被破壞,事務(wù)執(zhí)行的前后都是合法的數(shù)據(jù)狀態(tài)。
    • 一致性不僅由數(shù)據(jù)庫本身來保證,同時(shí)業(yè)務(wù)系統(tǒng)也保證數(shù)據(jù)的一致性。

四、分布式事務(wù)的由來

現(xiàn)代軟件架構(gòu)隨著業(yè)務(wù)領(lǐng)域劃分為多個微服務(wù),共同組成了復(fù)雜的軟件系統(tǒng)。而從數(shù)據(jù)庫層面來看,隨著數(shù)據(jù)量的爆發(fā),不得不采用分庫分表的方式,降低數(shù)據(jù)庫的壓力。這樣,就造成多個服務(wù)依賴不同的數(shù)據(jù)庫,那么在同時(shí)操作的時(shí)候,如何保證事務(wù)?這就是分布式事務(wù)。

簡而言之,分布式事務(wù)就是一個大的事務(wù)由不同的子事務(wù)組成,這些小的事務(wù)操作分布在不同的服務(wù)器節(jié)點(diǎn)上面,屬于不同的微服務(wù),分布式事務(wù)需要保證同一事務(wù)下的子事務(wù)要么全部成功,要么全部失敗,即保證數(shù)據(jù)的最終一致性。

五、分布式事務(wù)解決方案

在這篇不想用太大的篇幅說一些概念上的東西,但是要說 RocketMQ 的分布式事務(wù)實(shí)現(xiàn),所以在這里順便提一下當(dāng)前分布式事務(wù)的集中解決方案:

  • 兩階段提交(2PC)

    兩階段提交(2PC) 是 Oracle Tuxedo 系統(tǒng)提出的 XA 分布式事務(wù)協(xié)議的其中一種實(shí)現(xiàn)方式,參考 《分布式事務(wù)之兩階段提交(2PC)》

  • Try-Confirm-Cancle (TCC)

    TCC 是基于嘗試、確認(rèn)、取消來實(shí)現(xiàn)分布式事務(wù)的,想了解更多,參考 《分布式事務(wù)之補(bǔ)償事務(wù)( TCC )》 。

  • 本地消息表

    本地消息表 方案最初是ebay提出的,核心是將需要分布式處理的任務(wù)通過消息日志的方式來異步執(zhí)行。消息日志可以存儲到本地文本、數(shù)據(jù)庫或消息隊(duì)列,再通過業(yè)務(wù)規(guī)則自動或人工發(fā)起重試。人工重試更多的是應(yīng)用于支付場景,通過對賬系統(tǒng)對事后問題的處理。

image.png

除了上述外,還有一些解決方案,比如阿里 SEATA ,SAGA方案和最大努力通知...感興趣同學(xué)們可以自行了解,當(dāng)然還有我們這篇要說的 MQ 事務(wù)。

六、MQ 事務(wù)

RocketMQ 是阿里開源的一款高性能、高吞吐量的分布式消息中間件,基于消息異步方式提供了對分布式事務(wù)的支持,實(shí)現(xiàn)事務(wù)最終一致性。

下面是 RocketMQ 事務(wù)消息的基本流程交互圖:

image.png

如圖其中分為兩個流程:正常事務(wù)消息的發(fā)送及提交、事務(wù)消息的補(bǔ)償流程。

1.事務(wù)消息發(fā)送及提交:

(1) 發(fā)送 half 消息。
(2) 服務(wù)端響應(yīng)消息寫入結(jié)果。
(3) 根據(jù)發(fā)送結(jié)果執(zhí)行本地事務(wù)(如果寫入失敗,此時(shí)half消息對業(yè)務(wù)不可見,本地邏輯不執(zhí)行)。
(4) 根據(jù)本地事務(wù)狀態(tài)執(zhí)行 Commit 或者 Rollback( Commit 操作生成消息索引,消息對消費(fèi)者可見)

流程圖如下:

image.png

2.補(bǔ)償流程:

(1) 對沒有 Commit/Rollback 的事務(wù)消息( pending 狀態(tài)的消息),從服務(wù)端發(fā)起一次“回查”
(2) Producer收到回查消息,檢查回查消息對應(yīng)的本地事務(wù)的狀態(tài)
(3) 根據(jù)本地事務(wù)狀態(tài),重新Commit或者Rollback

其中,補(bǔ)償階段使用定時(shí)器回查方式用于解決消息 Commit 或者 Rollback 發(fā)生超時(shí)或者失敗的情況。

七、RocektMQ 事務(wù)消息的使用

如上,小伙伴們應(yīng)該對 RocketMQ 的事務(wù)消息有了一定的了解,下面看下如何在開發(fā)場景下如何使用。

發(fā)送事務(wù)消息時(shí)和普通的消息區(qū)別是,自己要新建一個 TransactionMQProducer 和對應(yīng)的一個 TransactionListener的實(shí)現(xiàn)。

  • TransactionMQProducer
    具體的配置有 group、 nameServer 地址、執(zhí)行本地事務(wù)的線程池和事務(wù)監(jiān)聽器的實(shí)現(xiàn)。
this.producer = new TransactionMQProducer(config.getGroup());
    this.producer.setNamesrvAddr(config.getNameServer());
    this.producer.setExecutorService(config.getExecutorService());
    this.producer.setTransactionListener(config.getTransactionListener());
  • TransactionListener
    實(shí)現(xiàn) TransactionListener 接口的兩個方法:
    • executeLocalTransaction(Message message, Object o)
      用于執(zhí)行本地事務(wù)的方法。
    • checkLocalTransaction(MessageExt messageExt)
      RocketMQ 回查本地事務(wù)狀態(tài)調(diào)用的方法。

代碼詳見 ?? : https://github.com/wangning1018/rocketmq-transaction-message-demo

歡迎訪問個人博客 獲取更多知識分享。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容