分布式事務處理方案大 PK!
說好了寫 TienChin 項目的,最近這個分布式事務算是一個支線任務吧,今天是最后一篇,松哥再來一個短篇和小伙伴們總結(jié)一下分布式事務。
首先先說一個大原則:分布式事務能不用就不要用,畢竟這個用起來還是有一些麻煩的。當然,不用和不會用可是兩碼事。
1. 分布式事務基礎理論
學習分布式事務,有一些基礎理論需要我們先來了解下。
1.1 本地事務
本地事務是指將多條語句作為一個整體進行操作的功能,通過數(shù)據(jù)庫事務可以確保該事務范圍內(nèi)的所有操作都可以全部成功或者全部失敗,如果事務失敗,那么效果就和沒有執(zhí)行這些SQL一樣,不會對數(shù)據(jù)庫數(shù)據(jù)有任何改動。也就是事務具有原子性,一個事務中的一系列操作要么全部成功,要么全部失敗。一般來說,事務具有 4 個屬性:
- Atomic:原子性,將一個事務中的所有 SQL 作為原子工作單元執(zhí)行,要么全部執(zhí)行,要么全部不執(zhí)行;
- Consistent:一致性,事務完成后,所有數(shù)據(jù)的狀態(tài)都是一致的,以銀行轉(zhuǎn)帳為例,如果 A 賬戶減去了 100,則 B 賬戶則必定加上了 100;
- Isolation:隔離性,如果有多個事務并發(fā)執(zhí)行,每個事務作出的修改必須與其他事務隔離;
- Duration:持久性,即事務完成后,對數(shù)據(jù)庫數(shù)據(jù)的修改被持久化存儲。
這四個屬性通常稱為 ACID 特性。
這塊松哥之前專門錄過相關(guān)的視頻,這里就不再贅述了。
- https://www.bilibili.com/video/BV1Eq4y1R7Ds
1.2 分布式事務
當我們的項目上了微服務之后,分布式事務就是一個比較常見的問題了,我們也會遇到很多相關(guān)的場景。
就拿我們前兩天講的商品下單的分布式事務的案例來說,像下面這樣,一共有五個服務,架構(gòu)如下圖:
- eureka:這是服務注冊中心。
- account:這是賬戶服務,可以查詢/修改用戶的賬戶信息(主要是賬戶余額)。
- order:這是訂單服務,可以下訂單。
- storage:這是一個倉儲服務,可以查詢/修改商品的庫存數(shù)量。
- bussiness:這是業(yè)務,用戶下單操作將在這里完成。
當用戶想要下單的時候,調(diào)用了 bussiness 中的接口,bussiness 中的接口又調(diào)用了它自己的 service,在 service 中,通過 feign 調(diào)用 storage 中的接口去扣庫存,然后再通過 feign 調(diào)用 order 中的接口去創(chuàng)建訂單(order 在創(chuàng)建訂單的時候,不僅會創(chuàng)建訂單,還會扣除用戶賬戶的余額)。
這三個操作,我們希望他們能夠同時成功或者同時失敗。然而如上圖所示,三個微服務都有自己的 DB,這是三個完全不同的 DB,相當于三個不同的本地事務,按照傳統(tǒng)的本地事務規(guī)則,我們顯然是無法實現(xiàn)三個操作同時成功或者同時失敗的。
想要實現(xiàn) storage、order 以及 account 中的操作同時成功或者同時失敗,就得考慮分布式事務了。
最后,我們再來看看分布式事務的概念:分布式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于的不同節(jié)點之上,數(shù)據(jù)庫的操作執(zhí)行成功與否,不僅取決于本地 DB 的執(zhí)行結(jié)果,也取決于第三方系統(tǒng)的執(zhí)行結(jié)果。而分布式事務就保證這些操作要么全部成功,要么全部失敗。本質(zhì)上,分布式事務就是為了保證不同數(shù)據(jù)庫的數(shù)據(jù)一致性。
1.3 CAP
CAP 定理(CAP theorem),有時候又被稱作布魯爾定理(Brewer's theorem),它指出對于一個分布式計算系統(tǒng)來說,不可能同時滿足以下三點:
一致性(Consistency):在分布式系統(tǒng)中的所有數(shù)據(jù)備份,在同一時刻是否具備同樣的值。(等同于所有節(jié)點訪問同一份最新的數(shù)據(jù)副本)。
可用性(Availability):在集群中一部分節(jié)點故障后,集群整體是否還能響應客戶端的讀寫請求。(對數(shù)據(jù)更新具備高可用性)。
分區(qū)容錯性(Partition tolerance):這個我覺得可能對有的小伙伴來說有點難以理解,我就簡單說一下,先來說分區(qū):因為我們是分布式系統(tǒng),分布式系統(tǒng)中不同的微服務位于不同的網(wǎng)絡節(jié)點上,當發(fā)生網(wǎng)絡故障或者節(jié)點故障的時候,不同的服務之間就無法通信了,也就是說發(fā)生了分區(qū);再來看分區(qū)容錯性:這是說,當我們的系統(tǒng)中出現(xiàn)分區(qū)的時候,系統(tǒng)還要能運行,不能罷工!一般來說,在一個分布式系統(tǒng)中,分區(qū)發(fā)生的概率還是比較大的,不會發(fā)生分區(qū)的系統(tǒng),那就不是分布式系統(tǒng)了,而是單體應用了。
CAP 原則的精髓就是要么 AP,要么 CP,要么 AC,但是不存在 CAP。因為在分布式系統(tǒng)內(nèi),P 是必然的發(fā)生的,不選 P,一旦發(fā)生分區(qū),整個分布式系統(tǒng)就完全無法使用了,這樣的系統(tǒng)就太脆弱了。所以對于分布式系統(tǒng),我們只能能考慮當發(fā)生分區(qū)錯誤時,如何選擇一致性和可用性(選擇一致性,意味著服務在某段時間內(nèi)不可用,選擇了可用性,意味著服務雖然一直可用但是返回的數(shù)據(jù)卻不一致)。
而根據(jù)一致性和可用性的選擇不同,開源的分布式系統(tǒng)往往又被分為 CP 系統(tǒng)和 AP 系統(tǒng)。
當一套系統(tǒng)在發(fā)生分區(qū)故障后,客戶端的任何請求都被卡死或者超時,但是系統(tǒng)的每個節(jié)點總是會返回一致的數(shù)據(jù),則這套系統(tǒng)就是 CP 系統(tǒng),經(jīng)典的比如 Zookeeper。
如果一套系統(tǒng)發(fā)生分區(qū)故障后,客戶端依然可以訪問系統(tǒng),但是獲取的數(shù)據(jù)有的是新的數(shù)據(jù),有的還是老數(shù)據(jù),那么這套系統(tǒng)就是 AP 系統(tǒng),經(jīng)典的比如 Eureka。
1.4 BASE
因為無法同時滿足 CAP,所以又有了 BASE 理論,BASE 理論指的是:
- 基本可用 Basically Available:分布式系統(tǒng)在出現(xiàn)故障的時候,允許損失部分可用性,即保證核心可用。
- 軟狀態(tài) Soft State:允許系統(tǒng)存在中間狀態(tài),而該中間狀態(tài)不會影響系統(tǒng)整體可用性。
- 終一致性 Eventual Consistency:系統(tǒng)中的所有數(shù)據(jù)副本經(jīng)過一定時間后,最終能夠達到一致的狀態(tài)。
BASE 理論的核心思想是即便無法做到強一致性,但應該采用適合的方式保證最終一致性。
BASE 理論本質(zhì)上是對 CAP 理論的延伸,是對 CAP 中 AP 方案的一個補充。
1.5 剛?cè)岵?/h3>
事務有剛性事務和柔性事務之分。
剛性事務(如單數(shù)據(jù)庫中的本地事務)完全遵循 ACID 規(guī)范,即數(shù)據(jù)庫事務正確執(zhí)行的四個基本要素:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔離性(Isolation)
- 持久性(Durability)
柔性事務,主要就是只分布式事務了,柔性事務為了滿足可用性、性能與降級服務的需要,降低一致性(Consistency)與隔離性(Isolation)的要求,遵守 BASE 理論:
- 基本業(yè)務可用性(Basic Availability)
- 柔性狀態(tài)(Soft state)
- 最終一致性(Eventual consistency)
當然,柔性事務也部分遵循 ACID 規(guī)范:
- 原子性:嚴格遵循
- 一致性:事務完成后的一致性嚴格遵循;事務中的一致性可適當放寬
- 隔離性:并行事務間不可影響;事務中間結(jié)果可見性允許安全放寬
- 持久性:嚴格遵循
柔性事務有不同的分類,不過基本上都可以看作是分布式事務的解決方案:
- 兩階段型:分布式事務二階段提交,對應技術(shù)上的 XA、JTA/JTS,這是分布式環(huán)境下事務處理的典型模式。
- 補償型:我們之前文章介紹的 TCC,就算是一種補償型事務,在 Try 成功的情況下,如果事務要回滾,Cancel 將作為一個補償機制,回滾 Try 操作;TCC 各操作事務本地化,且盡早提交(沒有兩階段約束);當全局事務要求回滾時,通過另一個本地事務實現(xiàn)“補償”行為。TCC 是將資源層的二階段提交協(xié)議轉(zhuǎn)換到業(yè)務層,成為業(yè)務模型中的一部分。
- 異步確保型:將一些有同步?jīng)_突的事務操作變?yōu)楫惒讲僮鳎苊鈱?shù)據(jù)庫事務的爭用,如消息事務機制。
- 最大努力通知型:通過通知服務器(消息通知)進行,允許失敗,有補充機制。
2. 分布式事務實踐
2.1 XA
先來說說 XA。
XA 是一種典型的兩階段提交(2PC,Two-phase commit protocol),而兩階段提交是一種強一致性設計,在兩階段提交中,一般會引入一個事務協(xié)調(diào)者的角色來協(xié)調(diào)管理各個事務參與者,例如我們之前文章中使用的 seata-server 其實是就是一個事務協(xié)調(diào)者。所謂的兩階段分別指的是準備和提交兩個階段。
XA 規(guī)范 是 X/Open 組織定義的分布式事務處理(DTP,Distributed Transaction Processing)標準。
XA 規(guī)范描述了全局的事務管理器與局部的資源管理器之間的接口。XA規(guī)范的目的是允許多個資源(如數(shù)據(jù)庫,應用服務器,消息隊列等)在同一事務中訪問,這樣可以使 ACID 屬性跨越應用程序而保持有效。
XA 規(guī)范使用兩階段提交來保證所有資源同時提交或回滾任何特定的事務。
XA 規(guī)范在上世紀 90 年代初就被提出。目前,幾乎所有主流的數(shù)據(jù)庫如 MySQL、Oracle、MSSQL 等都對 XA 規(guī)范提供了支持。
XA 事務的基礎是兩階段提交協(xié)議。需要有一個事務協(xié)調(diào)者來保證所有的事務參與者都完成了準備工作(第一階段)。如果協(xié)調(diào)者收到所有參與者都準備好的消息,就會通知所有的事務都可以提交了(第二階段)。MySQL 在這個 XA 事務中扮演的是參與者的角色,而不是協(xié)調(diào)者(事務管理器)。
MySQL 的 XA 事務分為內(nèi)部 XA 和外部 XA。外部 XA 可以參與到外部的分布式事務中,需要應用層介入作為協(xié)調(diào)者;內(nèi)部 XA 事務用于同一實例下跨多引擎事務,由 Binlog 作為協(xié)調(diào)者,比如在一個存儲引擎提交時,需要將提交信息寫入二進制日志,這就是一個分布式內(nèi)部 XA 事務,只不過二進制日志的參與者是 MySQL 本身。MySQL 在 XA 事務中扮演的是一個參與者的角色,而不是協(xié)調(diào)者。
XA 事務的特點是:
- 簡單易理解,開發(fā)較容易。
- 對資源進行了長時間的鎖定,并發(fā)度低。
2.2 3PC
3PC 主要是為了彌補 2PC 的不足而產(chǎn)生的,2PC 有哪些不足呢?
- 同步阻塞:2PC 在執(zhí)行過程中,所有參與節(jié)點(也就是一個分支事務)都是事務阻塞型的,當參與者占有公共資源時,其他第三方節(jié)點訪問公共資源不得不處于阻塞狀態(tài),也就是在 2PC 執(zhí)行的過程中,資源是被鎖住的。
- 單點故障:在 2PC 中,事務協(xié)調(diào)者扮演了舉足輕重的作用,由于事務協(xié)調(diào)者的重要性,一旦事務協(xié)調(diào)者發(fā)生故障,事務的參與者就會一直阻塞下去。尤其是在第二階段,如果協(xié)調(diào)者發(fā)生故障,那么所有的參與者還都處于鎖定事務資源的狀態(tài)中,而無法繼續(xù)完成事務操作。還有一個問題,就是當事務協(xié)調(diào)者發(fā)出 commit 指令之前,如果宕機了,此時雖然可以重新選舉一個新的協(xié)調(diào)者出來,但是還是無法解決因為事務協(xié)調(diào)者宕機導致的事務參與者處于阻塞狀態(tài)的問題。
3PC 則嘗試解決 2PC 的這些問題。3PC 主要是把 2PC 中的第一階段再次一分為二,這樣 3PC 就有 CanCommit、PreCommit 以及 DoCommit 三個不同的階段。不過 3PC 并不能解決 2PC 的所有問題,3PC 主要解決了單點故障問題,并且減少了阻塞。一旦事務參與者(分支事務)無法及時收到來自事務協(xié)調(diào)者的信息,那么分支事務會默認執(zhí)行 commit,而不會一直持有事務資源并處于阻塞狀態(tài),不過這種機制也帶來了新的問題,假設事務協(xié)調(diào)者發(fā)送了 abort 指令給各個分支事務,然而由于網(wǎng)絡問題導致分支事務沒有及時接收到該指令,那么分支事務在等待超時之后執(zhí)行了 commit 操作,這樣就和其他接到 abort 命令并執(zhí)行回滾的分支事務之間存在數(shù)據(jù)不一致的情況。
我們來看看 3PC 的流程:
- CanCommit 階段:這個階段所做的事很簡單,就是事務協(xié)調(diào)者詢問各個分支事務,你是否有能力完成此次事務?如果都返回 yes,則進入第二階段;有一個返回 no 或等待響應超時,則中斷事務,并向所有分支事務發(fā)送 abort 請求。
- PreCommit 階段:此時事務協(xié)調(diào)者會向所有的分支事務發(fā)送 PreCommit 請求,分支事務收到后開始執(zhí)行事務操作,并將 Undo 和 Redo 信息記錄到事務日志中。分支執(zhí)行完事務操作后(此時屬于未提交事務的狀態(tài)),就會向事務協(xié)調(diào)者反饋“Ack”表示我已經(jīng)準備好提交了,并等待協(xié)調(diào)者的下一步指令。
- DoCommit 階段:在階段二中如果所有的分支事務節(jié)點都可以進行 PreCommit 提交,那么事務協(xié)調(diào)者就會從“預提交狀態(tài)”轉(zhuǎn)變?yōu)椤疤峤粻顟B(tài)”,然后向所有的分支事務節(jié)點發(fā)送"doCommit"請求,分支事務節(jié)點在收到提交請求后就會各自執(zhí)行事務提交操作,并向協(xié)調(diào)者節(jié)點反饋“Ack”消息,協(xié)調(diào)者收到所有參與者的 Ack 消息后完成事務。
相反,如果有一個分支事務節(jié)點未完成 PreCommit 的反饋或者反饋超時,那么協(xié)調(diào)者都會向所有的參與者節(jié)點發(fā)送 abort 請求,從而中斷事務。
2.3 TCC
關(guān)于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年發(fā)表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。
TCC 模式主要有如下一些優(yōu)缺點:
優(yōu)點:
- 性能提升:通過具體業(yè)務來實現(xiàn)控制資源鎖的粒度變小,不會鎖定整個資源。
- 數(shù)據(jù)最終一致性:基于 Confirm 和 Cancel 的冪等性,保證事務最終完成確認或者取消,保證數(shù)據(jù)的一致性。
- 可靠性:解決了 XA 協(xié)議的協(xié)調(diào)者單點故障問題,由主業(yè)務方發(fā)起并控制整個業(yè)務活動,業(yè)務活動管理器也變成多點,引入集群。
缺點:
- 對微服務的侵入性強,微服務的每個事務都必須實現(xiàn) try,confirm,cancel 等 3 個方法,開發(fā)成本高,今后維護改造的成本也高。
- 為了達到事務的一致性要求,try,confirm、cancel 接口必須實現(xiàn)等冪性操作,這在一定程度上增加了開發(fā)工作量。
TCC 主要是兩個階段,步驟如下:
- Try 階段(一階段):嘗試執(zhí)行,完成所有業(yè)務檢查(一致性), 預留必須業(yè)務資源(準隔離性)。
- Confirm 階段(二階段):確認執(zhí)行真正執(zhí)行業(yè)務,不作任何業(yè)務檢查,只使用 Try 階段預留的業(yè)務資源,Confirm 操作滿足需要滿足冪等性,Confirm 執(zhí)行失敗后需要進行重試。
- Cancel 階段:取消執(zhí)行,釋放 Try 階段預留的業(yè)務資源,Cancel 操作也需要滿足冪等性。Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致。
在我們之前的文章中,松哥也給大家舉了 TCC 的例子了,這里就不再贅述了。
2.4 SAGA
SAGA 最初出現(xiàn)在 1987 年 Hector Garcaa-Molrna & Kenneth Salem 發(fā)表的論文 SAGAS 里。這篇論文的核心思想是將長事務拆分為多個短事務,由 Saga 事務協(xié)調(diào)器協(xié)調(diào),如果每個短事務都成功提交完成,那么全局事務就正常完成,如果某個步驟失敗,則根據(jù)相反順序一次調(diào)用補償操作。
Saga 事務的特點是:
- 并發(fā)度高,不用像 XA 事務那樣長期鎖定資源。
- 需要定義正常操作以及補償操作(回滾),開發(fā)量工作量比 XA 大。
- 一致性較弱,對于轉(zhuǎn)賬,可能發(fā)生 A 用戶已扣款,最后轉(zhuǎn)賬又失敗的情況
SAGA 適用的場景較多,適用于長事務或者對中間結(jié)果不敏感的業(yè)務場景。
2.5 本地消息表
本地消息表這個方案最初是 ebay 架構(gòu)師 Dan Pritchett 在 2008 年發(fā)表給 ACM 的文章中提出。
顧名思義,本地消息表就是會有一張存放本地消息的表,一般都是放在數(shù)據(jù)庫中,然后在執(zhí)行業(yè)務的時候?qū)I(yè)務的執(zhí)行和將消息放入消息表中的操作放在同一個事務中,這樣就能保證消息放入本地表以及業(yè)務肯定是一起執(zhí)行成功的。
當一個操作執(zhí)行成功之后,再去執(zhí)行下一個操作,如果下一個操作調(diào)用成功了好說,消息表的消息狀態(tài)可以直接改為已成功;如果下一個任務調(diào)用失敗也沒關(guān)系,會有后臺任務定時去讀取本地消息表,篩選出還未成功的消息再調(diào)用對應的服務(重試),服務更新成功了再變更消息的狀態(tài)。
重試就得保證對應服務的方法是冪等的,而且一般重試會有最大次數(shù),超過最大次數(shù)可以記錄下報警讓人工處理。
根據(jù)上面的描述,小伙伴們其實可以看到,本地消息表其實實現(xiàn)的是最終一致性,容忍了數(shù)據(jù)暫時不一致的情況。
本地消息表的特點:
- 長事務僅需要分拆成多個任務,使用簡單。
- 生產(chǎn)者需要額外的創(chuàng)建消息表。
- 每個本地消息表都需要進行輪詢(如果有失敗的要重試)。
- 消費者的邏輯如果無法通過重試成功,那么還需要更多的機制,來回滾操作。
根據(jù)本地消息表的特點我們可以發(fā)現(xiàn),本地消息表適用于可異步執(zhí)行且后續(xù)操作無需回滾的業(yè)務。
2.6 消息事務
這種方案的核心思路,其實就是通過消息中間件來將全局事務轉(zhuǎn)為本地事務,通過消息中間件來確保各個分支事務最終都能調(diào)用成功。
不過后來發(fā)現(xiàn)利用 Alibaba 的 RocketMQ(4.3之后)可以更好的實現(xiàn)分布式事務。
RocketMQ 是一種最終一致性的分布式事務,就是說它保證的是消息最終一致性,而不是像 2PC、3PC、TCC 那樣強一致分布式事務,在 RocketMQ 中有一種消息叫做 Half Message,Half Message 是指暫不能被 Consumer 消費的消息,雖然 Producer 已經(jīng)把消息成功發(fā)送到了 Broker 端,但此消息被標記為暫不能投遞狀態(tài),處于該種狀態(tài)下的消息稱為半消息,此時需要 Producer 對消息進行二次確認后,Consumer 才能去消費它。
RocketMQ 就是基于 Half Message 來實現(xiàn)的分布式事務,舉一個轉(zhuǎn)賬的例子:
- A 服務先發(fā)送個 Half Message 給 Brock 端,消息中攜帶 B 服務即將要 +100 元的信息。
- 當 A 服務知道 Half Message 發(fā)送成功后,那么開始本地事務。
- 執(zhí)行本地事務(會有三種情況1、執(zhí)行成功;2、執(zhí)行失敗;3、網(wǎng)絡等原因?qū)е聸]有響應) 3.1 如果本地事務成功,那么 A 向 Broker 服務器發(fā)送 Commit,這樣 B 服務就可以消費該 message。3.2 如果本地事務失敗,那么 A 向 Broker 服務器發(fā)送 Rollback,那么就會直接刪除上面這條半消息。3.3 如果由于網(wǎng)絡或者生產(chǎn)者應用重啟等原因。導致 A 一直沒有對 Half Message 進行二次確認,此時 Broker 服務器會定時掃描長期處于半消息的消息,會主動詢問 A 端該消息的最終狀態(tài)(Commit 或者 Rollback),這個操作也就是所謂的消息回查。
可能有小伙伴會說,那要是 B 最終執(zhí)行失敗怎么辦?對于這種情況,我們幾乎可以斷定就是代碼有問題所以才引起異常,因為消費端 RocketMQ 有重試機制,如果不是代碼問題一般重試幾次就能成功。
如果是代碼的原因引起多次重試失敗后,也沒有關(guān)系,將該異常記錄下來,由人工處理,人工兜底處理后,就可以讓事務達到最終的一致性。
2.7 最大努力通知
發(fā)起通知方通過一定的機制最大努力將業(yè)務處理結(jié)果通知到接收方。具體包括:
- 有一定的消息重試機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對消息進行重試。
- 消息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費消息后要再次消費,此時可由接收方主動向通知方查詢消息信息來滿足需求。
在前面兩個小節(jié)介紹的的本地消息表和事務消息都屬于可靠消息,這與我們這里介紹的最大努力通知有什么不同?
- 可靠消息一致性:消息發(fā)起方需要保證將消息發(fā)出去,并且將消息發(fā)到接收方,消息的可靠性關(guān)鍵由發(fā)起方來保證。
- 最大努力通知:消息發(fā)起方盡最大努力將業(yè)務處理結(jié)果通知給接收方,但是可能消息接收不到,此時需要接收方主動調(diào)用發(fā)起方的接口查詢業(yè)務處理結(jié)果,此時消息的可靠性關(guān)鍵在接收方。
僅此而已。
在具體的解決方案上,最大努力通知需要消息發(fā)起方提供接口,讓被通知方能夠通過接口查詢業(yè)務處理結(jié)果。
最大努力通知適用于業(yè)務通知類型,最常見的場景就是支付回調(diào),支付服務收到第三方服務支付成功通知后,先更新自己庫中訂單支付狀態(tài),然后同步通知訂單服務支付成功。如果此次同步通知失敗,會通過異步腳步不斷重試地調(diào)用訂單服務的接口。
最大努力通知更多是業(yè)務上的設計,在基礎設施層,可以直接使用二階段消息,或者事務消息、本地消息表等來實現(xiàn)。
3. 小結(jié)
好啦,學習分布式事務解決方案,最大的感受就是:沒有銀彈!
參考資料:
https://help.aliyun.com/document_detail/132895.html
https://cloud.tencent.com/developer/article/1860632
https://zh.m.wikipedia.org/zh-hans/CAP%E5%AE%9A%E7%90%86
https://zhuanlan.zhihu.com/p/35616811