<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>

          逸仙電商Seata企業(yè)級落地實(shí)踐

          共 8735字,需瀏覽 18分鐘

           ·

          2021-04-23 22:19


          作者 | 張嘉偉(GitHub ID:l81893521)
          就職于逸仙電商交易中心;Seata Committer,加入 Seata 社區(qū)已有一年半,見證了從 Fescar 到 Seata 的變更,GA 等。

          你可能沒有聽說過逸仙電商,但是你的女朋友不可能沒有聽說過它。逸仙電商旗下有完美日記、小奧汀、完子心選等品牌。完美日記作為國貨美妝界的黑馬用了不到三年時間,達(dá)到了行業(yè)龍頭企業(yè)通常需要十年以上才能達(dá)到的營收規(guī)模。2020 年正式登陸紐約證券交易所,成為第一家在美國上市的“國貨美妝品牌”。在快速增長的業(yè)務(wù)下,系統(tǒng)流量增長速度越來越快,服務(wù)數(shù)量不斷增多,調(diào)用鏈路錯綜復(fù)雜,數(shù)據(jù)不一致的問題日漸顯現(xiàn),為了降低人力成本和系統(tǒng)資源,我們選擇了 Seata。

          本文將會以逸仙電商的業(yè)務(wù)作為背景, 先介紹一下 Seata 的原理, 并給大家進(jìn)行線上演示, 由淺入深去介紹這款中間件, 以便讀者更加容易去理解 Seata 這個中間件。

          1. 問題背景


          在微服務(wù)的架構(gòu)下,數(shù)據(jù)不一致的產(chǎn)生原因。

          2. 業(yè)務(wù)介紹


          挑選了逸仙電商一些比較簡單易懂的業(yè)務(wù)作為開展背景。

          3. 原理分析


          Seata的實(shí)現(xiàn)原理和故障解決以及部署方案。

          4. Demo演示


          如何在線體驗這款中間件,無需整合和下載任何代碼。


          數(shù)據(jù)不一致的原因




          在微服務(wù)的環(huán)境下,由于調(diào)用鏈路跨越多個應(yīng)用,甚至跨越多個數(shù)據(jù)源,數(shù)據(jù)的一致性在普通情況下難以保證,導(dǎo)致數(shù)據(jù)不一致的原因非常多,這里列舉了三個最常見的原因:

          1、業(yè)務(wù)異常一個服務(wù)鏈路調(diào)用中,如果調(diào)用的過程出現(xiàn)業(yè)務(wù)異常,產(chǎn)生異常的應(yīng)用獨(dú)立回滾,非異常的應(yīng)用數(shù)據(jù)已經(jīng)持久化到數(shù)據(jù)庫。
          2、網(wǎng)絡(luò)異常調(diào)用的過程中,由于網(wǎng)絡(luò)不穩(wěn)定,導(dǎo)致鏈路中斷,部分應(yīng)用業(yè)務(wù)執(zhí)行完成,部分應(yīng)用業(yè)務(wù)未被執(zhí)行。
          3、服務(wù)不可用若服務(wù)不可用,無法被正常調(diào)用,也會導(dǎo)致問題的產(chǎn)生。


          這里挑選了逸仙電商業(yè)務(wù)體系里面一個非常通俗容易理解的調(diào)用方式,并且去掉了多余復(fù)雜的鏈路,方便在閱讀過程中更加關(guān)注重點(diǎn)。

          在以往如果出現(xiàn)數(shù)據(jù)不一致的問題,相信大多數(shù)的解決方案是這樣的:

          • 人工補(bǔ)償數(shù)據(jù)
          • 定時任務(wù)檢查和補(bǔ)償數(shù)據(jù)

          但是這兩種方式的缺點(diǎn)也是顯然意見的,一種是浪費(fèi)大量的人力成本和時間,另外一種是浪費(fèi)大量的系統(tǒng)資源去檢查數(shù)據(jù)是否一致和額外的人力成本。

          接下來我會根據(jù)逸仙在生產(chǎn)上穩(wěn)定運(yùn)行將近一年總結(jié)的經(jīng)驗并且盡可能簡單的去描述 Seata 是如何保證數(shù)據(jù)一致的。


          原理




          在接觸一項新技術(shù)之前,我們應(yīng)該先從宏觀的角度去理解它大概包含些什么。在 Seata 中,它大概分為以下三個角色:

          • 黃色,Transaction Manager(TM),client 端
          • 藍(lán)色,Resource Manager(RM),client 端
          • 綠色,Transaction Coordinator(TC),server 端

          你可以根據(jù)顏色,名字,縮寫甚至客戶端/服務(wù)端去區(qū)分這三者的關(guān)系,同時簡單去理解它們每一個自身的職責(zé)大概是要干些什么事情,后面的講解我也會保持一樣的顏色和名字來區(qū)分它們。


          Seata 其中只一個核心是數(shù)據(jù)源代理,意味著在你執(zhí)行一句 Sql 語句時,Seata 會幫你在執(zhí)行之前和之后做一些額外的操作,從而保證數(shù)據(jù)的一致性,并且盡可能做到無感知,讓你使用起來感覺非常方便和神奇。這里首先要去理解兩個知識點(diǎn)。

          • 前置鏡像(Before Image):保存數(shù)據(jù)變更前的樣子
          • 后置鏡像(After Image):保存數(shù)據(jù)變更后的樣子
          • Undo Log:保存鏡像

          有時候新項目接入的時候,有同事會問,為什么事務(wù)不生效,如果你也遇到過同樣的問題,那首先要檢查一下自己的數(shù)據(jù)源是否已經(jīng)代理成功。

          當(dāng)執(zhí)行一句 Sql 時,Seata 會嘗試去獲取這條/批數(shù)據(jù)變更前的內(nèi)容,并保存到前置鏡像中(Insert語句沒有前置鏡像),然后執(zhí)行業(yè)務(wù) Sql,執(zhí)行完后會嘗試去獲取這條/批數(shù)據(jù)變更后的內(nèi)容,并保存到后置鏡像中(Delete 語句沒有后置鏡像),之后會進(jìn)行分支事務(wù)注冊,TC 在收到分支事務(wù)注冊請求時,會持久化這些分支事務(wù)信息和根據(jù)操作數(shù)據(jù)的主鍵為維度作為全局鎖并持久化,可選持久化方式有:

          • file
          • db
          • redis

          在收到 TC 返回的分支注冊成功響應(yīng)后,會把鏡像持久化到應(yīng)用所在的數(shù)據(jù)源的 Undo Log 表中,最后提交本地事務(wù)。

          以上所有操作都會保證在同一個本地事務(wù)中,保證業(yè)務(wù)操作和 Undo Log 操作的原子性。


          一階段




          理解了單個應(yīng)用的處理流程,再從一個完全的調(diào)用鏈路,去看Seata的處理過程,相信理解起來會簡單很多。

          1、首先一個使用了 @GlobalTransactional 的接口被調(diào)用,Seata 會對其進(jìn)行攔截,攔截的角色我們稱之為 TM,這個時候會訪問 TC 開啟一個新的全局事務(wù),TC 收到請求后會生成 XID 和全局事務(wù)信息并持久化,然后返回 XID。
          2、在每一層的調(diào)用鏈路中,XID都必須往下傳遞,然后每一層都經(jīng)過之前說過的處理邏輯,直到執(zhí)行完成/異常拋出。

          直到目前,一階段已經(jīng)執(zhí)行完成。

          另外一個需要注意的問題是,如果發(fā)現(xiàn)事務(wù)不生效,需要檢查XID是否成功往下傳遞。


          二階段提交




          如果在整個調(diào)用鏈路的過程,沒有發(fā)生任何異常,那么二階段提交的過程是非常簡單而且非常的高效,只有兩步:

          • TC 清理全局事務(wù)對應(yīng)的信息
          • RM 清理對應(yīng) Undo Log 信息


          二階段回滾




          若調(diào)用過程中出現(xiàn)異常,會自動觸發(fā)反向回滾:

          反向回滾表示,如果調(diào)用鏈路順序為 A -> B -> C,那么回滾順序為 C -> B -> A。

          例:A=Insert,B=Update,如果回滾時不按照反向的順序進(jìn)行回滾,則有可能出現(xiàn)回滾時先把 A 刪除了,再更新 A,引發(fā)錯誤。

          在回滾的過程中有可能會遇到一種非常極端的情況,回滾到對應(yīng)的模塊時,找不到對應(yīng)的 Undo Log,這種情況主要發(fā)生在:

          • 分支事務(wù)注冊成功,但是由于網(wǎng)絡(luò)原因收不到成功的響應(yīng),Undo Log 未被持久化;
          • 同時全局事務(wù)超時(超時時間可自由配置)觸發(fā)回滾。

          這時候 RM 會持久化一個特殊的 Undo Log,狀態(tài)為 GlobalFinished。由于這個全局事務(wù)已經(jīng)回滾,需要防止網(wǎng)絡(luò)恢復(fù)時,未持久化 Undo Log 的應(yīng)用收到了分支注冊成功的響應(yīng)和持久化 Undo Log,并提交本地最終引發(fā)的數(shù)據(jù)不一致。


          讀已提交



          由于在一階段的時候,數(shù)據(jù)已經(jīng)保存到數(shù)據(jù)庫并提交,所以 Seata 默認(rèn)的隔離級別為讀未提交,如果需要把隔離級別提升至讀已提交則需要使用 @GlobalLock 標(biāo)簽并且在查詢語句上加上 for update:

          @GlobalLock@Transactionalpublic PayMoneyDto detail(ProcessOnEventRequestDto processOnEventRequestDto) {    return baseMapper.detail(processOnEventRequestDto.getProcessInfoDto().getBusinessKey())}@Mapperpublic interface PayMoneyMapper extends BaseMapper<PayMoney> {        @Select("select id, name, amount, account, has_repayment, pay_amount from pay_money m where m.business_key = #{businessKey} for update")    PayMoneyDto detail(@Param("businessKey") String businessKey);}

          這個時 候Seata 會對添加了 for update 的查詢語句進(jìn)行代理:


          如果一個全局事務(wù) 1 正在操作,并且未進(jìn)行二階段提交/回滾的時候,全局鎖是被全局事務(wù)1鎖持有的,同時另外一個全局事務(wù) 2 嘗試去查詢相同的數(shù)據(jù),由于查詢語句被代理,Seata 會嘗試去獲取這條數(shù)據(jù)的全局鎖,直到獲取成功/失敗(重試次數(shù)達(dá)到配置值)為止。


          問題



          在生產(chǎn)上運(yùn)行接近 1 年時間,總體來說遇到的問題不算多,解決起來也比較容易,比如以下這個問題:


          經(jīng)過排查發(fā)現(xiàn),由于 Seata 會使用 jdbc 標(biāo)準(zhǔn)接口嘗試獲取業(yè)務(wù)操作所對應(yīng)的表結(jié)構(gòu),由于表結(jié)構(gòu)改動頻率較少,并且考慮到表結(jié)構(gòu)變更后應(yīng)用會進(jìn)行重啟,所以會對表結(jié)構(gòu)進(jìn)行緩存,如果表結(jié)構(gòu)改動后不對應(yīng)用進(jìn)行重啟,有可能引發(fā)構(gòu)建鏡像時出現(xiàn) NullPointerException。下面貼出關(guān)鍵代碼:

          @Overridepublic TableMeta getTableMeta(final Connection connection, final String tableName, String resourceId) {    if (StringUtils.isNullOrEmpty(tableName)) {        throw new IllegalArgumentException("TableMeta cannot be fetched without tableName");    }    TableMeta tmeta;    final String key = getCacheKey(connection, tableName, resourceId);    //錯誤關(guān)鍵處,嘗試從緩存獲取表結(jié)構(gòu)    tmeta = TABLE_META_CACHE.get(key, mappingFunction -> {        try {            return fetchSchema(connection, tableName);        } catch (SQLException e) {            LOGGER.error("get table meta of the table `{}` error: {}", tableName, e.getMessage(), e);            return null;        }    });    if (tmeta == null) {        throw new ShouldNeverHappenException(String.format("[xid:%s]get table meta failed," +                                                           " please check whether the table `%s` exists.", RootContext.getXID(), tableName));    }    return tmeta;}

          修改表結(jié)構(gòu),需要對應(yīng)用進(jìn)行重啟,即可解決此問題,非常簡單。

          第二個遇到的問題就是在生產(chǎn)運(yùn)行一段時間后,發(fā)現(xiàn) branch_table 和 lock_table 存在數(shù)據(jù)殘留,并且根據(jù) xid 查詢 global_table 沒有對應(yīng)的數(shù)據(jù),導(dǎo)致后續(xù)操作相同的數(shù)據(jù)行會出現(xiàn)獲取全局鎖失敗,并且會每隔一段時間小量出現(xiàn)。這個異常隱藏的比較深,而且在開發(fā)環(huán)境和測試環(huán)境無法復(fù)現(xiàn),通過跟蹤源碼和總結(jié)原因發(fā)現(xiàn),是由于開啟了 Mysql 主從,導(dǎo)致提交/回滾時,Seata 通過 xid 查詢分支事務(wù)時,數(shù)據(jù)未同步到從庫,導(dǎo)致遺漏了一部分分支事務(wù)數(shù)據(jù)。

          源碼部分

          @Overridepublic GlobalStatus commit(String xid) throws TransactionException {    //根據(jù)xid查詢信息,如果開啟主從,會有可能導(dǎo)致查詢信息不完整    GlobalSession globalSession = SessionHolder.findGlobalSession(xid);    if (globalSession == null) {        return GlobalStatus.Finished;    }    globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());    // just lock changeStatus    boolean shouldCommit = SessionHolder.lockAndExecute(globalSession, () -> {        // Highlight: Firstly, close the session, then no more branch can be registered.        globalSession.closeAndClean();        if (globalSession.getStatus() == GlobalStatus.Begin) {            if (globalSession.canBeCommittedAsync()) {                globalSession.asyncCommit();                return false;            } else {                globalSession.changeStatus(GlobalStatus.Committing);                return true;            }        }        return false;    });    if (shouldCommit) {        boolean success = doGlobalCommit(globalSession, false);        //If successful and all remaining branches can be committed asynchronously, do async commit.        if (success && globalSession.hasBranch() && globalSession.canBeCommittedAsync()) {            globalSession.asyncCommit();            return GlobalStatus.Committed;        } else {            return globalSession.getStatus();        }    } else {        return globalSession.getStatus() == GlobalStatus.AsyncCommitting ? GlobalStatus.Committed : globalSession.getStatus();    }}

          @Overridepublic GlobalStatus rollback(String xid) throws TransactionException {    //根據(jù)xid查詢信息,如果開啟主從,會有可能導(dǎo)致查詢信息不完整    GlobalSession globalSession = SessionHolder.findGlobalSession(xid);    if (globalSession == null) {        return GlobalStatus.Finished;    }    globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());    // just lock changeStatus    boolean shouldRollBack = SessionHolder.lockAndExecute(globalSession, () -> {        globalSession.close(); // Highlight: Firstly, close the session, then no more branch can be registered.        if (globalSession.getStatus() == GlobalStatus.Begin) {            globalSession.changeStatus(GlobalStatus.Rollbacking);            return true;        }        return false;    });    if (!shouldRollBack) {        return globalSession.getStatus();    }    doGlobalRollback(globalSession, false);    return globalSession.getStatus();}

          相信此問題會在支持 Raft 之后得到完美的解決。
          pr: https://github.com/seata/seata/pull/3086
          有興趣的朋友也可以嘗試去 review 一下代碼。


          部署-高可用




          Seata 和其他中間件的高可用部署方式差別不大,如圖片所示,確保應(yīng)用服務(wù)和 TC 訪問相同的注冊中心和配置中心,同時只需要啟動多臺 TC,并將 store.mode 改為 db 模式即可完成高可用部署,并選擇合適的注冊中心和配置中心即可,目前支持的配置中心有:

          • nacos
          • consul
          • etcd3
          • eureka
          • redis
          • sofa
          • zookeeper

          可選的配置中心有:

          • nacos
          • etcd3
          • consul
          • apollo
          • zk


          部署-單節(jié)點(diǎn)多應(yīng)用




          當(dāng)然也有更加靈活的部署方式,通過 vgoup-mapping(事務(wù)集群),可以做到單節(jié)點(diǎn)多應(yīng)用的隔離,比如 A 應(yīng)用和 B 應(yīng)用訪問 A-Group 的兩個 TC,C 應(yīng)用和 D 應(yīng)用訪問 B-Group 的兩個 TC,E應(yīng)用和F應(yīng)用訪問 C-Group 的兩個 TC。


          部署-異地容災(zāi)





          通過 vgoup-mapping 也可以做到異地容災(zāi),當(dāng)原有集群出現(xiàn)不可用時,可以通過變更配置立刻轉(zhuǎn)移到備用的集群上。此處以 Nacos 作為注冊中心舉例,TC 配置方式如下:

          # 廣州機(jī)房registry {  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa  type = "nacos"  loadBalance = "RandomLoadBalance"  loadBalanceVirtualNodes = 10  nacos {    application = "seata-server"    serverAddr = "127.0.0.1:8848"    group = "SEATA_GROUP"    namespace = ""    cluster = "Guangzhou"    username = ""    password = ""  }}

          # 上海機(jī)房registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "Shanghai" username = "" password = "" }}


          Demo



          最后通過訪問阿里云知行動手首頁,即可在線快速體驗各種各樣的中間件:

          https://start.aliyun.com

          Seata 直達(dá)傳送門,無需下載代碼,在線編譯和部署:

          https://start.aliyun.com/handson/isnEO76f/distributedtransaction

          推薦閱讀:

          Kafka原理篇:圖解kakfa架構(gòu)原理

          架構(gòu)設(shè)計方法論

          從面試角度一文學(xué)完 Kafka

          數(shù)據(jù)庫跟緩存的雙寫一致性


          關(guān)互聯(lián)網(wǎng)全棧架構(gòu)

          瀏覽 32
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  狠狠狠的操 | 午夜影院操| 色婷婷五月天 | 日韩无码黄色电影网站 | 国产精品福利视频导航 |