<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          對比 5 種分布式事務方案,還是寵幸了阿里的 Seata(原理 + 實戰(zhàn))

          共 10203字,需瀏覽 21分鐘

           ·

          2020-12-07 21:01

          分布式事務的產生

          我們先看看百度上對于分布式事務的定義:分布式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于不同的分布式系統(tǒng)的不同節(jié)點之上。

          額~ 有點抽象,簡單的畫個圖好理解一下,拿下單減庫存、扣余額來說舉例:

          當系統(tǒng)的體量很小時,單體架構完全可以滿足現(xiàn)有業(yè)務需求,所有的業(yè)務共用一個數據庫,整個下單流程或許只用在一個方法里同一個事務下操作數據庫即可。此時做到所有操作要么全部提交 或 要么全部回滾很容易。

          分庫分表、SOA

          可隨著業(yè)務量的不斷增長,單體架構漸漸扛不住巨大的流量,此時就需要對數據庫、表做 分庫分表處理,將應用 SOA 服務化拆分。也就產生了訂單中心、用戶中心、庫存中心等,由此帶來的問題就是業(yè)務間相互隔離,每個業(yè)務都維護著自己的數據庫,數據的交換只能進行 RPC 調用。

          當用戶再次下單時,需同時對訂單庫 order、庫存庫 storage、用戶庫 account 進行操作,可此時我們只能保證自己本地的數據一致性,無法保證調用其他服務的操作是否成功,所以為了保證整個下單流程的數據一致性,就需要分布式事務介入。

          Seata 優(yōu)勢

          實現(xiàn)分布式事務的方案比較多,常見的比如基于 XA 協(xié)議的 2PC3PC,基于業(yè)務層的 TCC,還有應用消息隊列 + 消息表實現(xiàn)的最終一致性方案,還有今天要說的 Seata 中間件,下邊看看各個方案的優(yōu)缺點。

          2PC

          基于 XA 協(xié)議實現(xiàn)的分布式事務,XA 協(xié)議中分為兩部分:事務管理器和本地資源管理器。其中本地資源管理器往往由數據庫實現(xiàn),比如 Oracle、MYSQL 這些數據庫都實現(xiàn)了 XA 接口,而事務管理器則作為一個全局的調度者。

          兩階段提交(2PC),對業(yè)務侵?很小,它最?的優(yōu)勢就是對使??透明,用戶可以像使?本地事務?樣使?基于 XA 協(xié)議的分布式事務,能夠嚴格保障事務 ACID 特性。

          2PC的缺點也是顯而易見,它是一個強一致性的同步阻塞協(xié)議,事務執(zhí)?過程中需要將所需資源全部鎖定,也就是俗稱的 剛性事務。所以它比較適?于執(zhí)?時間確定的短事務,整體性能比較差。

          一旦事務協(xié)調者宕機或者發(fā)生網絡抖動,會讓參與者一直處于鎖定資源的狀態(tài)或者只有一部分參與者提交成功,導致數據的不一致。因此,在?并發(fā)性能?上的場景中,基于 XA 協(xié)議的分布式事務并不是最佳選擇。

          3PC

          三段提交(3PC)是二階段提交(2PC)的一種改進版本 ,為解決兩階段提交協(xié)議的阻塞問題,上邊提到兩段提交,當協(xié)調者崩潰時,參與者不能做出最后的選擇,就會一直保持阻塞鎖定資源。

          2PC 中只有協(xié)調者有超時機制,3PC 在協(xié)調者和參與者中都引入了超時機制,協(xié)調者出現(xiàn)故障后,參與者就不會一直阻塞。而且在第一階段和第二階段中又插入了一個準備階段(如下圖,看著有點啰嗦),保證了在最后提交階段之前各參與節(jié)點的狀態(tài)是一致的。

          雖然 3PC 用超時機制,解決了協(xié)調者故障后參與者的阻塞問題,但與此同時卻多了一次網絡通信,性能上反而變得更差,也不太推薦。

          TCC

          所謂的 TCC 編程模式,也是兩階段提交的一個變種,不同的是 TCC 為在業(yè)務層編寫代碼實現(xiàn)的兩階段提交。TCC 分別指 TryConfirmCancel ,一個業(yè)務操作要對應的寫這三個方法。

          以下單扣庫存為例,Try 階段去占庫存,Confirm 階段則實際扣庫存,如果庫存扣減失敗 Cancel 階段進行回滾,釋放庫存。

          TCC 不存在資源阻塞的問題,因為每個方法都直接進行事務的提交,一旦出現(xiàn)異常通過則 Cancel 來進行回滾補償,這也就是常說的補償性事務。

          原本一個方法,現(xiàn)在卻需要三個方法來支持,可以看到 TCC 對業(yè)務的侵入性很強,而且這種模式并不能很好地被復用,會導致開發(fā)量激增。還要考慮到網絡波動等原因,為保證請求一定送達都會有重試機制,所以考慮到接口的冪等性。

          消息事務(最終一致性)

          消息事務其實就是基于消息中間件的兩階段提交,將本地事務和發(fā)消息放在同一個事務里,保證本地操作和發(fā)送消息同時成功。下單扣庫存原理圖:

          • 訂單系統(tǒng)向 MQ 發(fā)送一條預備扣減庫存消息,MQ 保存預備消息并返回成功 ACK
          • 接收到預備消息執(zhí)行成功 ACK,訂單系統(tǒng)執(zhí)行本地下單操作,為防止消息發(fā)送成功而本地事務失敗,訂單系統(tǒng)會實現(xiàn) MQ 的回調接口,其內不斷的檢查本地事務是否執(zhí)行成功,如果失敗則 rollback 回滾預備消息;成功則對消息進行最終 commit 提交。
          • 庫存系統(tǒng)消費扣減庫存消息,執(zhí)行本地事務,如果扣減失敗,消息會重新投,一旦超出重試次數,則本地表持久化失敗消息,并啟動定時任務做補償。

          基于消息中間件的兩階段提交方案,通常用在高并發(fā)場景下使用,犧牲數據的強一致性換取性能的大幅提升,不過實現(xiàn)這種方式的成本和復雜度是比較高的,還要看實際業(yè)務情況。

          Seata

          Seata 也是從兩段提交演變而來的一種分布式事務解決方案,提供了 ATTCCSAGAXA 等事務模式,這里重點介紹 AT模式。

          既然 Seata 是兩段提交,那我們看看它在每個階段都做了點啥?下邊我們還以下單扣庫存、扣余額舉例。

          先介紹 Seata 分布式事務的幾種角色:

          • Transaction Coordinator(TC): ?全局事務協(xié)調者,用來協(xié)調全局事務和各個分支事務(不同服務)的狀態(tài), 驅動全局事務和各個分支事務的回滾或提交。

          • Transaction Manager?: ?事務管理者,業(yè)務層中用來開啟/提交/回滾一個整體事務(在調用服務的方法中用注解開啟事務)。

          • Resource Manager(RM): ?資源管理者,一般指業(yè)務數據庫代表了一個分支事務(Branch Transaction),管理分支事務與 TC 進行協(xié)調注冊分支事務并且匯報分支事務的狀態(tài),驅動分支事務的提交或回滾。

          Seata 實現(xiàn)分布式事務,設計了一個關鍵角色 UNDO_LOG (回滾日志記錄表),我們在每個應用分布式事務的業(yè)務庫中創(chuàng)建這張表,這個表的核心作用就是,將業(yè)務數據在更新前后的數據鏡像組織成回滾日志,備份在 UNDO_LOG 表中,以便業(yè)務異常能隨時回滾。

          第一個階段

          比如:下邊我們更新 user 表的 name 字段。

          update?user?set?name?=?'小富最帥'?where?name?=?'程序員內點事'

          首先 Seata 的 JDBC 數據源代理通過對業(yè)務 SQL 解析,提取 SQL 的元數據,也就是得到 SQL 的類型(UPDATE),表(user),條件(where name = '程序員內點事')等相關的信息。

          第一個階段的流程圖

          先查詢數據前鏡像,根據解析得到的條件信息,生成查詢語句,定位一條數據。

          select??name?from?user?where?name?=?'程序員內點事'
          數據前鏡像

          緊接著執(zhí)行業(yè)務 SQL,根據前鏡像數據主鍵查詢出后鏡像數據

          select?name?from?user?where?id?=?1
          數據后鏡像

          把業(yè)務數據在更新前后的數據鏡像組織成回滾日志,將業(yè)務數據的更新和回滾日志在同一個本地事務中提交,分別插入到業(yè)務表和 UNDO_LOG 表中。

          回滾記錄數據格式如下:包括 afterImage 前鏡像、beforeImage 后鏡像、 branchId 分支事務ID、xid 全局事務ID

          {
          ????"branchId":641789253,
          ????"xid":"xid:xxx",
          ????"undoItems":[
          ????????{
          ????????????"afterImage":{
          ????????????????"rows":[
          ????????????????????{
          ????????????????????????"fields":[
          ????????????????????????????{
          ????????????????????????????????"name":"id",
          ????????????????????????????????"type":4,
          ????????????????????????????????"value":1
          ????????????????????????????}
          ????????????????????????]
          ????????????????????}
          ????????????????],
          ????????????????"tableName":"product"
          ????????????},
          ????????????"beforeImage":{
          ????????????????"rows":[
          ????????????????????{
          ????????????????????????"fields":[
          ????????????????????????????{
          ????????????????????????????????"name":"id",
          ????????????????????????????????"type":4,
          ????????????????????????????????"value":1
          ????????????????????????????}
          ????????????????????????]
          ????????????????????}
          ????????????????],
          ????????????????"tableName":"product"
          ????????????},
          ????????????"sqlType":"UPDATE"
          ????????}
          ????]
          }

          這樣就可以保證,任何提交的業(yè)務數據的更新一定有相應的回滾日志。

          在本地事務提交前,各分支事務需向 全局事務協(xié)調者 TC 注冊分支 ( Branch Id) ,為要修改的記錄申請 全局鎖 ,要為這條數據加鎖,利用 SELECT FOR UPDATE 語句。而如果一直拿不到鎖那就需要回滾本地事務。TM 開啟事務后會生成全局唯一的 XID,會在各個調用的服務間進行傳遞。

          有了這樣的機制,本地事務分支(Branch Transaction)便可以在全局事務的第一階段提交,并馬上釋放本地事務鎖定的資源。相比于傳統(tǒng)的 XA 事務在第二階段釋放資源,Seata 降低了鎖范圍提高效率,即使第二階段發(fā)生異常需要回滾,也可以快速 從UNDO_LOG 表中找到對應回滾數據并反解析成 SQL 來達到回滾補償。

          最后本地事務提交,業(yè)務數據的更新和前面生成的 UNDO LOG 數據一并提交,并將本地事務提交的結果上報給全局事務協(xié)調者 TC。

          第二個階段

          第二階段是根據各分支的決議做提交或回滾:

          如果決議是全局提交,此時各分支事務已提交并成功,這時 全局事務協(xié)調者(TC) 會向分支發(fā)送第二階段的請求。收到 TC 的分支提交請求,該請求會被放入一個異步任務隊列中,并馬上返回提交成功結果給 TC。異步隊列中會異步和批量地根據 Branch ID 查找并刪除相應 UNDO LOG 回滾記錄。

          如果決議是全局回滾,過程比全局提交麻煩一點,RM 服務方收到 TC 全局協(xié)調者發(fā)來的回滾請求,通過 XIDBranch ID 找到相應的回滾日志記錄,通過回滾記錄生成反向的更新 SQL 并執(zhí)行,以完成分支的回滾。

          注意:這里刪除回滾日志記錄操作,一定是在本地業(yè)務事務執(zhí)行之后

          上邊說了幾種分布式事務各自的優(yōu)缺點,下邊實踐一下分布式事務中間 Seata 感受一下。

          Seata 實踐

          Seata 是一個需獨立部署的中間件,所以先搭 Seata Server,這里以最新的 seata-server-1.4.0 版本為例,下載地址:https://seata.io/en-us/blog/download.html

          解壓后的文件我們只需要關心 \seata\conf 目錄下的 file.conf 和 ?registry.conf 文件。

          Seata Server

          file.conf

          file.conf 文件用于配置持久化事務日志的模式,目前提供 filedbredis 三種方式。

          file.conf 文件配置

          注意:在選擇 db 方式后,需要在對應數據庫創(chuàng)建 globalTable(持久化全局事務)、branchTable(持久化各提交分支的事務)、 lockTable(持久化各分支鎖定資源事務)三張表。

          --?the?table?to?store?GlobalSession?data
          --?持久化全局事務
          CREATE?TABLE?IF?NOT?EXISTS?`global_table`
          (
          ????`xid`???????????????????????VARCHAR(128)?NOT?NULL,
          ????`transaction_id`????????????BIGINT,
          ????`status`????????????????????TINYINT??????NOT?NULL,
          ????`application_id`????????????VARCHAR(32),
          ????`transaction_service_group`?VARCHAR(32),
          ????`transaction_name`??????????VARCHAR(128),
          ????`timeout`???????????????????INT,
          ????`begin_time`????????????????BIGINT,
          ????`application_data`??????????VARCHAR(2000),
          ????`gmt_create`????????????????DATETIME,
          ????`gmt_modified`??????????????DATETIME,
          ????PRIMARY?KEY?(`xid`),
          ????KEY?`idx_gmt_modified_status`?(`gmt_modified`,?`status`),
          ????KEY?`idx_transaction_id`?(`transaction_id`)
          )?ENGINE?=?InnoDB
          ??DEFAULT?CHARSET?=?utf8;

          --?the?table?to?store?BranchSession?data
          --?持久化各提交分支的事務
          CREATE?TABLE?IF?NOT?EXISTS?`branch_table`
          (
          ????`branch_id`?????????BIGINT???????NOT?NULL,
          ????`xid`???????????????VARCHAR(128)?NOT?NULL,
          ????`transaction_id`????BIGINT,
          ????`resource_group_id`?VARCHAR(32),
          ????`resource_id`???????VARCHAR(256),
          ????`branch_type`???????VARCHAR(8),
          ????`status`????????????TINYINT,
          ????`client_id`?????????VARCHAR(64),
          ????`application_data`??VARCHAR(2000),
          ????`gmt_create`????????DATETIME(6),
          ????`gmt_modified`??????DATETIME(6),
          ????PRIMARY?KEY?(`branch_id`),
          ????KEY?`idx_xid`?(`xid`)
          )?ENGINE?=?InnoDB
          ??DEFAULT?CHARSET?=?utf8;

          --?the?table?to?store?lock?data
          --?持久化每個分支鎖表事務
          CREATE?TABLE?IF?NOT?EXISTS?`lock_table`
          (
          ????`row_key`????????VARCHAR(128)?NOT?NULL,
          ????`xid`????????????VARCHAR(96),
          ????`transaction_id`?BIGINT,
          ????`branch_id`??????BIGINT???????NOT?NULL,
          ????`resource_id`????VARCHAR(256),
          ????`table_name`?????VARCHAR(32),
          ????`pk`?????????????VARCHAR(36),
          ????`gmt_create`?????DATETIME,
          ????`gmt_modified`???DATETIME,
          ????PRIMARY?KEY?(`row_key`),
          ????KEY?`idx_branch_id`?(`branch_id`)
          )?ENGINE?=?InnoDB
          ??DEFAULT?CHARSET?=?utf8;

          registry.conf

          registry.conf 文件設置 注冊中心 和 配置中心:

          目前注冊中心支持 nacoseurekarediszkconsuletcd3sofa 七種,這里我使用的 eureka作為注冊中心 ;配置中心支持 nacosapollozkconsuletcd3 五種方式。

          registry.conf 文件配置

          配置完以后在 \seata\bin 目錄下啟動 seata-server 即可,到這 Seata 的服務端就搭建好了。

          Seata Client

          Seata Server 環(huán)境搭建完,接下來我們新建三個服務 order-server(下單服務)、storage-server(扣減庫存服務)、account-server(賬戶金額服務),分別服務注冊到 eureka

          每個服務的大體核心配置如下:

          spring:
          ????application:
          ????????name:?storage-server
          ????cloud:
          ????????alibaba:
          ????????????seata:
          ????????????????tx-service-group:?my_test_tx_group
          ????datasource:
          ????????driver-class-name:?com.mysql.jdbc.Driver
          ????????url:?jdbc:mysql://47.93.6.1:3306/seat-storage
          ????????username:?root
          ????????password:?root

          #?eureka?注冊中心
          eureka:
          ????client:
          ????????serviceUrl:
          ????????????defaultZone:?http://$
          {eureka.instance.hostname}:8761/eureka/
          ????instance:
          ????????hostname:?47.93.6.5
          ????????prefer-ip-address:?true

          業(yè)務大致流程:用戶發(fā)起下單請求,本地 order 訂單服務創(chuàng)建訂單記錄,并通過 RPC 遠程調用 storage 扣減庫存服務和 account 扣賬戶余額服務,只有三個服務同時執(zhí)行成功,才是一個完整的下單流程。如果某個服執(zhí)行失敗,則其他服務全部回滾。

          Seata 對業(yè)務代碼的侵入性非常小,代碼中使用只需用 @GlobalTransactional 注解開啟一個全局事務即可。

          @Override
          @GlobalTransactional(name?=?"create-order",?rollbackFor?=?Exception.class)
          public?void?create(Order?order)?
          {

          ????String?xid?=?RootContext.getXID();

          ????LOGGER.info("------->交易開始");
          ????//本地方法
          ????orderDao.create(order);

          ????//遠程方法?扣減庫存
          ????storageApi.decrease(order.getProductId(),?order.getCount());

          ????//遠程方法?扣減賬戶余額
          ????LOGGER.info("------->扣減賬戶開始order中");
          ????accountApi.decrease(order.getUserId(),?order.getMoney());
          ????LOGGER.info("------->扣減賬戶結束order中");

          ????LOGGER.info("------->交易結束");
          ????LOGGER.info("全局事務 xid:?{}",?xid);
          }

          前邊說過 Seata AT 模式實現(xiàn)分布式事務,必須在相關的業(yè)務庫中創(chuàng)建 undo_log 表來存數據回滾日志,表結構如下:

          --?for?AT?mode?you?must?to?init?this?sql?for?you?business?database.?the?seata?server?not?need?it.
          CREATE?TABLE?IF?NOT?EXISTS?`undo_log`
          (
          ????`id`????????????BIGINT(20)???NOT?NULL?AUTO_INCREMENT?COMMENT?'increment?id',
          ????`branch_id`?????BIGINT(20)???NOT?NULL?COMMENT?'branch?transaction?id',
          ????`xid`???????????VARCHAR(100)?NOT?NULL?COMMENT?'global?transaction?id',
          ????`context`???????VARCHAR(128)?NOT?NULL?COMMENT?'undo_log?context,such?as?serialization',
          ????`rollback_info`?LONGBLOB?????NOT?NULL?COMMENT?'rollback?info',
          ????`log_status`????INT(11)??????NOT?NULL?COMMENT?'0:normal?status,1:defense?status',
          ????`log_created`???DATETIME?????NOT?NULL?COMMENT?'create?datetime',
          ????`log_modified`??DATETIME?????NOT?NULL?COMMENT?'modify?datetime',
          ????PRIMARY?KEY?(`id`),
          ????UNIQUE?KEY?`ux_undo_log`?(`xid`,?`branch_id`)
          )?ENGINE?=?InnoDB
          ??AUTO_INCREMENT?=?1
          ??DEFAULT?CHARSET?=?utf8?COMMENT?='AT?transaction?mode?undo?table';

          到這環(huán)境搭建的工作就完事了,完整案例會在后邊貼出 GitHub 地址,就不在這占用篇幅了。

          測試 Seata

          項目中的服務調用過程如下圖:

          服務調用過程

          啟動各個服務后,我們直接請求下單接口看看效果,只要 order 訂單表創(chuàng)建記錄成功,storage 庫存表 used 字段數量遞增、account 余額表 used 字段數量遞增則表示下單流程成功。

          原始數據

          請求后正向流程是沒問題的,數據和預想的一樣

          下單數據

          而且發(fā)現(xiàn) TM 事務管理者 order-server 服務的控制臺也打印出了兩階段提交的日志

          控制臺兩次提交

          那么再看看如果其中一個服務異常,會不會正常回滾呢?在 account-server 服務中模擬超時異常,看能否實現(xiàn)全局事務回滾。

          全局事務回滾

          發(fā)現(xiàn)數據全沒執(zhí)行成功,說明全局事務回滾也成功了

          那看一下 undo_log 回滾記錄表的變化情況,由于 Seata 刪除回滾日志的速度很快,所以要想在表中看見回滾日志,必須要在某一個服務上打斷點才看的更明顯。

          回滾記錄

          總結

          上邊簡單介紹了 2PC3PCTCCMQSeata 這五種分布式事務解決方案,還詳細的實踐了 Seata 中間件。但不管我們選哪一種方案,在項目中應用都要謹慎再謹慎,除特定的數據強一致性場景外,能不用盡量就不要用,因為無論它們性能如何優(yōu)越,一旦項目套上分布式事務,整體效率會幾倍的下降,在高并發(fā)情況下弊端尤為明顯。


          點個在看支持我吧,轉發(fā)就更好了
          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  五月激情婷婷影音娱乐 | 日本女人在线视频 | 久久久极品视频 | www.成人在线 | 先锋影音亚洲AV每日资源网站 |