解決分布式事務(wù),Seata真香!
背景
大家好,今天給大家分享一個(gè)在 2022 年出去面試 Java 幾乎必問的一個(gè)技術(shù),那就是 seata。
什么??你才看了第一句話心里有閃現(xiàn)了無數(shù)個(gè)問號(hào)?因?yàn)闆]聽說過 seata 這個(gè)東西?
沒關(guān)系,為了避免兄弟們出去面試被問到 seata 的時(shí)候,一臉蒙圈,我們今天就把這個(gè)東西給大家講明白。
既然要給大家講什么是 seata,那就得先說一下這個(gè)東西的定位,這東西就是現(xiàn)在很火的 Spring Cloud Alibaba 里的一個(gè)組件,是專門幫助我們解決分布式事務(wù)問題的,也就是說,seata 是一個(gè)分布式事務(wù)框架。
什么是分布式事務(wù)
那可能很多小伙伴很蒙圈了,什么是分布式事務(wù)?好吧,為了保證大家能繼續(xù)看下去,我們先說一下什么是分布式事務(wù)這個(gè)問題。
舉個(gè)最簡(jiǎn)單的例子,假設(shè)現(xiàn)在你負(fù)責(zé)了一個(gè)訂單系統(tǒng),一個(gè)庫(kù)存系統(tǒng),一個(gè)營(yíng)銷系統(tǒng),然后呢,當(dāng)你的訂單系統(tǒng)收到用戶一個(gè)請(qǐng)求要?jiǎng)?chuàng)建訂單的時(shí)候,這個(gè)時(shí)候你得做三件事情。
第一,調(diào)用庫(kù)存系統(tǒng)的接口鎖定庫(kù)存,第二,調(diào)用調(diào)用營(yíng)銷系統(tǒng)的接口鎖定優(yōu)惠券,第三,你訂單系統(tǒng)自己得在 MySQL 里插入一系列訂單的數(shù)據(jù)。
比如下圖 1 所示:
那么現(xiàn)在問題來了,你訂單系統(tǒng)有自己的訂單數(shù)據(jù)庫(kù),可以去插入訂單數(shù)據(jù),那庫(kù)存系統(tǒng)是不是也應(yīng)該有自己的庫(kù)存數(shù)據(jù)庫(kù),去鎖定庫(kù)存數(shù)據(jù)?
營(yíng)銷系統(tǒng)是不是應(yīng)該有自己的營(yíng)銷數(shù)據(jù)庫(kù),去鎖定優(yōu)惠券?當(dāng)然是了!每個(gè)人都有自己的數(shù)據(jù)庫(kù),這一個(gè)都不能少。
如下圖 2 所示:
那現(xiàn)在問題又來了,既然一次創(chuàng)建訂單的請(qǐng)求,要涉及到訂單、庫(kù)存、營(yíng)銷三個(gè)系統(tǒng),分別操作各自自己的三個(gè)數(shù)據(jù)庫(kù),才能完成這次請(qǐng)求。
那是不是可能會(huì)出現(xiàn)這么一種情況,首先呢,你先調(diào)用庫(kù)存系統(tǒng),鎖定了庫(kù)存了,O 了。
接著呢,你又調(diào)用了營(yíng)銷系統(tǒng),鎖定了優(yōu)惠券,也 O 了。最后呢,當(dāng)你訂單系統(tǒng)要往自己的訂單數(shù)據(jù)庫(kù)里插入數(shù)據(jù)的時(shí)候,網(wǎng)絡(luò)抽風(fēng)了,導(dǎo)致你這一次插入訂單數(shù)據(jù)失敗了,直接 exception 異常了,你蒙圈了。
如下圖 3 所示:
那這個(gè)時(shí)候你覺得可能會(huì)產(chǎn)生什么樣的問題呢,其實(shí)很簡(jiǎn)單,這個(gè)時(shí)候你這個(gè)訂單要購(gòu)買的商品庫(kù)存已經(jīng)被鎖定了,你為了下這個(gè)訂單用的優(yōu)惠券,也已經(jīng)被鎖定了。
結(jié)果呢,你的訂單自己本身的數(shù)據(jù)并沒進(jìn)入數(shù)據(jù)庫(kù),然后還返回一個(gè)了異常信息給用戶說,本次下單失敗。
但是你說下單失敗就失敗吧,結(jié)果呢,運(yùn)營(yíng)看庫(kù)存數(shù)據(jù)的時(shí)候可能會(huì)一臉蒙圈,為啥有一些商品庫(kù)存被鎖定了,結(jié)果沒有對(duì)應(yīng)的跟訂單,而且一直沒人付款來購(gòu)買呢??
然后用戶自己也有點(diǎn)發(fā)蒙,因?yàn)橐徊樽约旱膬?yōu)惠券,好不容易攢了幾張券來買東西,結(jié)果現(xiàn)在訂單沒下成,優(yōu)惠券狀態(tài)都搞成已使用了,自己還沒法用這些優(yōu)惠券了。
如下圖 4 所示:
其實(shí)這就是一個(gè)非常經(jīng)典的分布式事務(wù)的問題了,你一個(gè)創(chuàng)建訂單的請(qǐng)求,橫跨了訂單、庫(kù)存、營(yíng)銷三個(gè)系統(tǒng),分別涉及三個(gè)數(shù)據(jù)庫(kù)。
所有很可能會(huì)發(fā)現(xiàn),你的庫(kù)存和營(yíng)銷的數(shù)據(jù)操作都成功了,而且?guī)齑婧蜖I(yíng)銷數(shù)據(jù)庫(kù)里的本地事務(wù)都提交了,結(jié)果訂單插入數(shù)據(jù)庫(kù)失敗了,訂單數(shù)據(jù)庫(kù)里的本地事務(wù)回滾了,但是庫(kù)存和營(yíng)銷數(shù)據(jù)庫(kù)里的本地事務(wù)已經(jīng)提交了,他們是不會(huì)回滾的。
如下圖 5 所示:
什么叫做逆向補(bǔ)償
那既然問題已經(jīng)找到了,我們希望的應(yīng)該是什么效果呢?
我們其實(shí)希望的效果是,如果訂單要是插入數(shù)據(jù)庫(kù)失敗了,訂單數(shù)據(jù)庫(kù)本地事務(wù)回滾了,我們應(yīng)該想辦法去通知一下庫(kù)存系統(tǒng)和營(yíng)銷系統(tǒng),把之前在庫(kù)存數(shù)據(jù)庫(kù)和營(yíng)銷數(shù)據(jù)庫(kù)里已經(jīng)提交的數(shù)據(jù)修改做一個(gè)逆向補(bǔ)償,進(jìn)行恢復(fù)。
什么叫做逆向補(bǔ)償呢?意思就是說,之前庫(kù)存系統(tǒng)如果在數(shù)據(jù)庫(kù)里執(zhí)行的是 insert,那么此時(shí)就應(yīng)該執(zhí)行 delete,把之前插入的數(shù)據(jù)刪除了。
如果之前執(zhí)行的 delete,現(xiàn)在就應(yīng)該執(zhí)行 insert,把刪除的額數(shù)據(jù)重新插入回去,如果之前執(zhí)行的是 udpate 語句,現(xiàn)在就應(yīng)該再次執(zhí)行一個(gè) update 語句,把數(shù)據(jù)恢復(fù)到更新之前的狀態(tài)。
如下圖 6 所示:
互聯(lián)網(wǎng)最流行的分布式事務(wù)組件 seata
那既然我們想要實(shí)現(xiàn)這個(gè)效果,這個(gè)時(shí)候問題就來了,單單依賴我們自己那肯定搞不定這個(gè)問題了,這個(gè)時(shí)候就必須引入 Spring Cloud Alibaba 里的大佬組件,seata。
seata 就是專門幫助我們解決這個(gè)問題的,如果我們要是在系統(tǒng)里引入 seata 框架之后,其實(shí)每個(gè)系統(tǒng)里都會(huì)嵌入 seata,同時(shí)我們還需要去部署一個(gè) seata server。
如下圖 7 所示:
這個(gè)時(shí)候,我們的系統(tǒng)運(yùn)行原理會(huì)變成這樣:訂單系統(tǒng)中的 seata 會(huì)發(fā)送請(qǐng)求給 seata server 去開啟一個(gè)全局事務(wù),然后庫(kù)存系統(tǒng)先運(yùn)行,他在進(jìn)行數(shù)據(jù)庫(kù) crud 的時(shí)候,這些操作都會(huì)被 seata 框架進(jìn)行攔截。
然后 seata 框架會(huì)在一個(gè)本地事務(wù)里,把你的 sql 語句和逆向補(bǔ)償日志,一起插入到你的庫(kù)存數(shù)據(jù)庫(kù)里去,在庫(kù)存數(shù)據(jù)庫(kù)里必須有一個(gè) undo_log 表,存儲(chǔ) seata 的逆向補(bǔ)償日志。
那這個(gè)逆向補(bǔ)償日志是什么呢?簡(jiǎn)單,如果你的 sql 是 insert,那逆向補(bǔ)償日志可以幫助你后續(xù)構(gòu)建 delete 語句來刪除,如果你的 sql 是 update,那逆向補(bǔ)償日志可以記錄你更新之前的舊數(shù)據(jù),他可以幫助你后續(xù)把數(shù)據(jù) update 到老版本的狀態(tài)。
如下圖 8 所示:
你庫(kù)存系統(tǒng)的 sql 語句和他們的補(bǔ)償日志,是在一個(gè)本地事務(wù)里一起提交的,一起成功或者一起失敗,所以但凡你的庫(kù)存系統(tǒng)更新成功了,就一定會(huì)有對(duì)應(yīng)的補(bǔ)償日志也會(huì)在庫(kù)存 數(shù)據(jù)庫(kù)里的,以備不時(shí)之需,營(yíng)銷系統(tǒng)其實(shí)也是相同的運(yùn)行原理。
那么假設(shè)說庫(kù)存系統(tǒng)和營(yíng)銷系統(tǒng),按照這個(gè)思路都執(zhí)行完畢了,到訂單系統(tǒng)了,他結(jié)果撂挑子了,插入訂單數(shù)據(jù)庫(kù)失敗。
當(dāng)然,在插入的時(shí)候其實(shí)也會(huì)有對(duì)應(yīng)的補(bǔ)償日志會(huì)一起提交,但是因?yàn)檫@個(gè)時(shí)候網(wǎng)絡(luò)問題,導(dǎo)致插入訂單和插入補(bǔ)償日志一起失敗了。
所以此時(shí)訂單系統(tǒng)的 seata 就會(huì)上報(bào) seata server 說,大哥,我這兒完?duì)僮恿?,您要不通知?kù)存和營(yíng)銷兩個(gè)兄弟,逆向補(bǔ)償一下吧。
如下圖 9 所示:
接著 seata server 發(fā)現(xiàn)說,這分布式事務(wù)都失敗了,那趕緊的,他會(huì)通知庫(kù)存系統(tǒng)和營(yíng)銷系統(tǒng)里的 seata 框架小兄弟說,兄弟們,趕緊的,把之前插入你們數(shù)據(jù)庫(kù)里的 undo_log 表里的補(bǔ)償日志拿出來,構(gòu)建一下逆向補(bǔ)償 sql。
之前是 insert 你就給我弄個(gè) delete,之前是 delete 你就給我弄個(gè) insert,之前是 update 你還是 update,逆向補(bǔ)償 sql 趕緊跑一把,把數(shù)據(jù)給我恢復(fù)了,前隊(duì)改后隊(duì),跑步前進(jìn),hurry up 起來。
如下圖 10 所示:
總結(jié)
太棒了,到這個(gè)時(shí)候?yàn)橹梗覀兙桶l(fā)現(xiàn) seata 老大的作用了,你訂單、庫(kù)存、營(yíng)銷三個(gè)系統(tǒng)隨便跑,有誰失敗了,seata server 收到你的失敗通知,就會(huì)告訴別的系統(tǒng)用 undo log 日志構(gòu)建補(bǔ)償 sql,把數(shù)據(jù)都給回滾了,完美。