分布式事務(wù)
首先葛躏,事務(wù)是用來保證一組數(shù)據(jù)操作的完整性和一致性。事務(wù)的四種特性饭庞,Atomicity原子性,consistency一致性熬荆,isolation隔離性舟山,durability持久性。
分布式事務(wù)大體可以分成兩部分卤恳,首先是事務(wù)累盗,以前的分布式是一個(gè)單體性的事務(wù),其次就是分布式突琳,分布式事務(wù)就是將多個(gè)節(jié)點(diǎn)的事務(wù)看成是一個(gè)整體》荆現(xiàn)在有十個(gè)節(jié)點(diǎn),每臺(tái)機(jī)器部署了不同的應(yīng)用本今,有訂單支付影片等等都部署在不同機(jī)器,如果在同一個(gè)事務(wù)里主巍,處理很簡單冠息,但是如果是在不同的事務(wù),不同機(jī)器上孕索,其他的事務(wù)怎么知道其他事務(wù)發(fā)生失敗了逛艰,失敗了又怎么處理。分布式事務(wù)一般由事務(wù)參與者搞旭,資源服務(wù)器散怖,事務(wù)管理器組成菇绵。事務(wù)參與者類比于機(jī)器和服務(wù),資源服務(wù)器就是用來控制比如庫存數(shù)量镇眷,金額等等咬最,最后是事務(wù)管理器,是用來輔助欠动,比如剛剛的例子永乌,一個(gè)事務(wù)出事了,其他的事務(wù)怎么知道具伍,那么這個(gè)事務(wù)管理就會(huì)通知其他事務(wù)翅雏。最常見的分布式事務(wù)就是支付,下訂單等人芽。
分布式實(shí)現(xiàn)一般有兩種望几,兩段式事務(wù)和三段式事務(wù),基于XA的分布式事務(wù)萤厅,基于消息的最終一致性方案橄抹,這里的信息一般是指信息隊(duì)列或者是Redis一致性緩存,還有TCC編程式補(bǔ)償性事務(wù)祈坠。
兩段式事務(wù)和三段式事務(wù)
TC就是事務(wù)管理器害碾,先準(zhǔn)備好數(shù)據(jù)交流,然后雙方開始提交赦拘,提交完成后告訴事務(wù)管理器慌随,現(xiàn)在有兩個(gè),如果只有一個(gè)躺同,那么久提交失敗阁猜,回退即可。但是這種兩段式還是有點(diǎn)問題的蹋艺,都是在事務(wù)管理器要求你干什么就干什么剃袍,比如準(zhǔn)備就緒了,事務(wù)管理器要你提交捎谨,結(jié)果有一臺(tái)機(jī)器出問題民效,你怎么知道這個(gè)問題是在提交前還是提交后出現(xiàn)的,提交后出現(xiàn)的那就不用回滾了涛救,提交前的那就要回滾畏邢。基于上述缺點(diǎn)检吆,出現(xiàn)了三段式事務(wù)舒萎,但是都是基于兩段式,只不過把第一段分成了兩部分蹭沛,第三段和兩段式的第二段一樣臂寝。三段式的第一階段是canCommit階段章鲤,事務(wù)管理器會(huì)給所有事務(wù)參與者發(fā)送canCommit,各個(gè)事務(wù)管理者根據(jù)自己的狀態(tài)查看能否提交咆贬,如果可以則回執(zhí)OK败徊,否者返回fail,并不開啟本地事務(wù)并執(zhí)行素征。如果所有的都正常集嵌,則進(jìn)入第二階段,否則停止御毅;第二階段即是preCommit根欧,事務(wù)協(xié)調(diào)器向所有參與者發(fā)起準(zhǔn)備事務(wù)請(qǐng)求,參與者接受到后端蛆,開啟本地事務(wù)并執(zhí)行凤粗,但是不提交。第三階段則是提交了今豆。
基于XA的分布式事務(wù)
本質(zhì)上講還是一個(gè)兩段式的提交嫌拣。這個(gè)流程和前面的二三段其實(shí)是差不多的,首先詢問準(zhǔn)備好沒有呆躲,準(zhǔn)備好回個(gè)OK异逐,然后提交執(zhí)行。流程差不多插掂,但是調(diào)用方式出現(xiàn)了變化灰瞻,但是不常見,應(yīng)用場(chǎng)景多辅甥,MySQL酝润,DB2這些關(guān)系型數(shù)據(jù)庫絕大多數(shù)都是基于XA來的。
基于消息一致性方案的分布式事務(wù)
這里和前面介紹的幾種方式有所不同璃弄,參與者有兩個(gè)A系統(tǒng)和消息中間件兩個(gè)要销,首先系統(tǒng)A發(fā)送預(yù)備消息,中間件保存預(yù)備消息后返回說我已經(jīng)收到了夏块。接著執(zhí)行本地事務(wù)疏咐,執(zhí)行后把執(zhí)行結(jié)果告訴消息中間件,不一定是成功的脐供,可能成功也可能失敗凳鬓,消息中間件保存消息回調(diào)』济瘢可能會(huì)覺得回調(diào)和保存消息很多余,我看到這個(gè)圖也是這么想的垦梆,因?yàn)閳?zhí)行事務(wù)無非就是成功和失敗匹颤,為什么要回調(diào)保存仅孩?但是別忘了,分布式事務(wù)是多個(gè)事務(wù)之間的關(guān)系印蓖,我這里只是一個(gè)事務(wù)辽慕,將多個(gè)事務(wù)結(jié)合在一起:
如果有一個(gè)B系統(tǒng),那么當(dāng)A系統(tǒng)執(zhí)行完成了赦肃,消息中間件通知了B:系統(tǒng)A執(zhí)行完成了溅蛉,然后B再執(zhí)行。在整個(gè)售票系統(tǒng)中有一個(gè)支付系統(tǒng)他宛,錢到賬了修改訂單狀態(tài)船侧,但是如果錢到賬了修改訂單狀態(tài)失敗了怎么辦?這個(gè)時(shí)候就可以使用消息一致性了厅各,可以在支付寶下訂單支付的時(shí)候暫停線程镜撩,啟動(dòng)另外一個(gè)線程進(jìn)行修改訂單操作。修改成功了才啟用支付寶線程队塘。這種方案是強(qiáng)一致性方案袁梗,同一個(gè)時(shí)刻成功一定成功,失敗一起失敗憔古,不會(huì)存在其他情況遮怜,但是缺點(diǎn)也很明顯,會(huì)存在等待時(shí)期鸿市,會(huì)使得支付寶線程等待锯梁,這樣會(huì)影響性能。
TCC補(bǔ)償性事務(wù)
主要進(jìn)行的三個(gè)操作依次是Try接口灸芳,Confirm接口涝桅,cancel接口,confirm接口和cancel接口只能使用一個(gè)烙样,事務(wù)要么成功要么失敗冯遂。首先啟動(dòng)事務(wù)協(xié)調(diào)器,告訴事務(wù)協(xié)調(diào)器要開始工作了谒获,接著調(diào)用不同的服務(wù)蛤肌,嘗試進(jìn)行操作,比如扣減庫存和創(chuàng)建訂單批狱,結(jié)果A成功了裸准,B失敗了,那么業(yè)務(wù)就會(huì)調(diào)用cancel接口赔硫,成功了調(diào)用從confirm接口炒俱。try,confirm,cancel接口都是在服務(wù)里面實(shí)現(xiàn)权悟,業(yè)務(wù)只是去調(diào)用這些接口砸王,關(guān)心返回結(jié)果,根據(jù)返回結(jié)果確定是調(diào)用confirm還是cancel接口峦阁。cancel接口把try做過的東西全部取消谦铃,confirm確認(rèn)提交,所以也稱為是補(bǔ)償式榔昔。
基于消息一致性的事務(wù)是一種強(qiáng)一致性的事務(wù)驹闰,很大程度上會(huì)造成資源的浪費(fèi),尤其是對(duì)于時(shí)間的浪費(fèi)撒会,上面的例子是兩個(gè)事務(wù)嘹朗,如果發(fā)展到多個(gè)事務(wù),等待的時(shí)間就會(huì)更多了茧彤。但是他的優(yōu)點(diǎn)也很明顯骡显,就是強(qiáng)一致性,缺點(diǎn)也是強(qiáng)一致性曾掂。在實(shí)際工程中經(jīng)常會(huì)有對(duì)接京東支付惫谤,阿里支付等等的場(chǎng)景,假設(shè)使用TCC珠洗,那么問題來了溜歪,錢打進(jìn)去是回不來的,想要調(diào)用cancel接口那只能自己掏腰包许蓖,而消息一致性就沒有這種問題蝴猪。TCC補(bǔ)償性事務(wù)是柔性事務(wù),在try階段要對(duì)資源做預(yù)留膊爪,在確認(rèn)和取消階段釋放資源自阱,confirm沒有什么,cancel做反向操作米酬。相比基于消息一致性來說TCC的時(shí)效性更強(qiáng)沛豌。
主流的分布式框架
全局事務(wù)服務(wù),GTS
螞蟻金服分布式事務(wù)赃额,DTX
開源TCC框架加派,TCC-Transaction
開源TCC框架,ByteTCC
這里使用TCC-Transaction開源框架跳芳。
api就是接口芍锦,core為核心包,類似于guns-core的核心飞盆,server是事務(wù)監(jiān)控工具娄琉,有多少事務(wù)次乓,事務(wù)狀態(tài),spring即是spring的支持车胡,然后dubbo的支持檬输,unit即為測(cè)試,tutorial教程匈棘。簡要測(cè)試一下,打開簡要教程析命,dbscripts里面執(zhí)行SQL語句主卫,會(huì)生成四個(gè)數(shù)據(jù)庫:
第一個(gè)tcc的庫是必須要建立的,只要使用就需要建立鹃愤;下面的cap,ord,red分布模擬了業(yè)務(wù)場(chǎng)景簇搅,cap為資金賬戶,ord訂單软吐,red紅包瘩将。tutorial里面有兩個(gè)例子,一個(gè)是HTTP的例子凹耙,一個(gè)就是整合了dubbo的例子姿现,HTTP的例子沒有什么問題,很簡單肖抱”傅洌看看dubbo的例子,首先先要部署Tomcat:
order模塊的前綴/即可意述。打開web.xml提佣,發(fā)現(xiàn)三個(gè)模塊的web.xml都沒有報(bào)錯(cuò),是由于執(zhí)行順序的問題荤崇,錯(cuò)誤提示{The content of element type "web-app" must match "(icon?,display-name?,description?,distributable?,context-param,filter,filter-mapping,listener,servlet,servlet-mapping,session-config?,mime-mapping,welcome-file-list?,error-page,taglib,resource-env-ref,resource-ref,security-constraint,login-config?,security-role,env-entry,ejb-ref,ejb-local-ref)".- No grammar constraints (DTD or XML schema) detected for the document}拌屏,listener要再filter-mappering和servlet之間,調(diào)整位置就好了术荤,啟動(dòng)跑起來環(huán)境就搭建好了∫形梗現(xiàn)在就是要按照案例里面的代碼把自己的項(xiàng)目改造一下。首先查看一下工程結(jié)構(gòu):
api和之前業(yè)務(wù)里面的api接口沒有什么區(qū)別喜每,注意有一個(gè)接口:
帶有compensable標(biāo)簽务唐,即是需要進(jìn)行事務(wù)處理的接口,RedPacketAccountServiceImpl這個(gè)接口實(shí)現(xiàn)是查詢紅包信息而已带兜,不需要事務(wù)支持枫笛。
當(dāng)前注解的方法就是try方法,就是業(yè)務(wù)方法刚照,注解提到的confirmMethod刑巧,cancelMethod就是TCC,最后一個(gè)參數(shù)是事務(wù)上下文支持,這里使用的是隱式上下文的支持啊楚,可能這里需要用到隱式參數(shù)傳遞吠冤,接下來下面就要實(shí)現(xiàn)兩個(gè)confirm和cancel兩個(gè)方法,而且必須在同一個(gè)類里恭理,因?yàn)樽⒔饨^大多數(shù)是要通過放射和AOP來讀取拯辙,如果不在同一個(gè)類里面,是沒有辦法找到的颜价,類是通過包名加上類名找到的涯保,只返回一個(gè)字符串就能找到是不可能的,所以只有放在同一個(gè)類里面才能找到
record也就是try業(yè)務(wù)周伦,注意catch里面不是捕獲所有的異常夕春,業(yè)務(wù)TCC是根據(jù)異常的有無來判斷業(yè)務(wù)執(zhí)行成功或者失敗,有異常才會(huì)回滾专挪,對(duì)于業(yè)務(wù)返回的結(jié)果不關(guān)心及志。這里的訂單多出兩種狀態(tài),draft草稿和cancel取消寨腔,draft即是try完但是沒有confirm的訂單速侈,confirm完成就是真正支付完成的訂單了。
注意在cancel和confirm里面都需要判斷訂單是空而且訂單狀態(tài)為draft草稿狀態(tài)脆侮。但是這里涉及到一個(gè)服務(wù)冪等性的問題锌畸,即是一個(gè)服務(wù)重復(fù)多次執(zhí)行和一次執(zhí)行的結(jié)果相同,冪等性還不是理解的很好靖避,做完這個(gè)服務(wù)再去看看潭枣。所以TCC的分布式事務(wù)需要注意兩個(gè)部分,1幻捏、分布式事務(wù)里盆犁,不要輕易在業(yè)務(wù)層捕獲所有異常,2篡九、使用TCC-Transaction時(shí)谐岁,confirm和cancel的冪等性需要自己代碼支持。然后部署Tomcat榛臼,運(yùn)行即可伊佃,注意TCC-trancation-order這個(gè)模塊,在部署artificial的時(shí)候路徑一個(gè)斜杠就好了沛善。
測(cè)試完成之后就可以仿造加到這個(gè)項(xiàng)目上了航揉。首先是打包,idea里面maven的package功能打包:
控制臺(tái)用mvn install進(jìn)去金刁,當(dāng)然了帅涂,最簡單的還是直接接idea里maven install進(jìn)去就好了议薪。需要把剛剛的幾個(gè)tcc-transaction打包成jar加進(jìn)原來的工程里面才能使用tcc事務(wù)。(2020.1.29.凌晨1:20)
部署環(huán)境
部署環(huán)境有點(diǎn)麻煩媳友,比springboot麻煩多了斯议,再加上文檔東寫一點(diǎn)西寫一點(diǎn)的,調(diào)試的頭都大了醇锚。install進(jìn)去之后設(shè)置自己的tcc-transaction版本哼御,我的設(shè)置是1.2.11版本,引入包之后出現(xiàn)了些問題:
這個(gè)問題之前也遇到過焊唬,一般就幾種情況艇搀,jdk版本不一致(這個(gè)可能性最高);pom包之間互相引用求晶,但是調(diào)用的版本不一致;環(huán)境不一致衷笋;log4j的包沒有導(dǎo)入;之前的原因是因?yàn)門omcat使用jdk1.7芳杏,可是項(xiàng)目環(huán)境1.8,導(dǎo)致的問題辟宗,這里gteLogger方法上網(wǎng)百度了一下爵赵,com.alibaba.dubbo.common這個(gè)包一直以來的重大更新都沒有去掉這個(gè)方法,LoggerAdapter這個(gè)類下一個(gè)版本將會(huì)淘汰泊脐,但是現(xiàn)在還有空幻,不應(yīng)該出現(xiàn)問題。那可能就是我打包的tcc-transaction的jar有問題了容客,翻了一下tcc-transaction的幾個(gè)項(xiàng)目秕铛,com.alibaba.dubbo這個(gè)以來全都依賴到,但是版本也都一致的缩挑,而我本來項(xiàng)目環(huán)境也沒有用到這個(gè)包,是直接引入alibobo-springboot的dubbo包的,很可能就是springboot的dubbo包的沖突導(dǎo)致渔嚷。上網(wǎng)看了一下好像沒有找到有出過這樣的問題谚鄙,但是有出現(xiàn)成功的例子,他的版本是1.2.4.23芥丧,拿來試了一下紧阔,結(jié)果就好了。應(yīng)該是整合了nutz所導(dǎo)致的续担。在package tcctransaction的時(shí)候可能有些錯(cuò)誤擅耽,需要在server目錄下面建立config/dev目錄,為什么要建立我也不知道赤拒,不創(chuàng)建他就提示錯(cuò)誤秫筏,找不到目錄诱鞠。
接下來就是按照部署文檔把項(xiàng)目部署上去,首先是把tcc-transaction-dubbo和tcc-transaction-spring目錄下兩個(gè)xml的文件拷貝過來这敬。還要讀到springboot容器里面航夺,而主要要進(jìn)行分布式事務(wù)的就是支付模塊了,所以在guns-order加上一個(gè)config類崔涂,用于讀取配置文件:
啟動(dòng)項(xiàng)目阳掐,出現(xiàn)錯(cuò)誤
就說明是讀取成功了,還需要配置一個(gè)數(shù)據(jù)源的支持冷蚂,transaction repository缭保。按照文檔把bean拿過來:
<bean id="transactionRepository"
class="org.mengyun.tcctransaction.spring.repository.SpringJdbcTransactionRepository">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</bean>
BasicDataSource改成自己的數(shù)據(jù)源Alibaba那個(gè),想用其他的也可以蝙茶。dataSource需要單獨(dú)配置艺骂,不能和業(yè)務(wù)里使用的dataSource復(fù)用,即使使用的是同一個(gè)數(shù)據(jù)庫。JOB恢復(fù)也要配置上隆夯,還有一些表空間等等钳恕,配置完成之后還需要?jiǎng)?chuàng)建表,tcc-transaction需要使用自己的一張表來存儲(chǔ)數(shù)據(jù)蹄衷,需要自己創(chuàng)建忧额。
tbSuffix后綴,這個(gè)模塊是訂單模塊愧口,那么后綴就是order睦番,創(chuàng)建的表就是tcc_transaction_order,還需要加上數(shù)據(jù)源配置:
還有一些零零散散的配置加上即可耍属,啟動(dòng)一下沒有問題即可托嚣。這是服務(wù)提供者的配置,消費(fèi)者的配置也是一樣的恬涧,gateway寫上相同的配置即可注益。
在gateway啟用相同配置并且啟動(dòng)的時(shí)候,問題來了溯捆,在啟動(dòng)的時(shí)候出現(xiàn)了問題:
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/zhanggong004/.m2/repository/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/zhanggong004/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
Exception in thread "main" java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation (class org.slf4j.impl.Log4jLoggerFactory loaded from file:/C:/Users/zhanggong004/.m2/repository/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar). If you are using WebLogic you will need to add 'org.slf4j' to prefer-application-packages in WEB-INF/weblogic.xml: org.slf4j.impl.Log4jLoggerFactory
at org.springframework.util.Assert.instanceCheckFailed(Assert.java:655)
at org.springframework.util.Assert.isInstanceOf(Assert.java:555)
at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(LogbackLoggingSystem.java:286)
at org.springframework.boot.logging.logback.LogbackLoggingSystem.beforeInitialize(LogbackLoggingSystem.java:102)
at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationStartingEvent(LoggingApplicationListener.java:220)
at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEvent(LoggingApplicationListener.java:199)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:127)
at org.springframework.boot.context.event.EventPublishingRunListener.starting(EventPublishingRunListener.java:69)
at org.springframework.boot.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:48)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:302)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
at com.example.dubboconsumer.DubboConsumerApplication.main(DubboConsumerApplication.java:13)
因?yàn)樯厦婺莾蓚€(gè)binding一直都有丑搔,我也就沒有在意,覺得應(yīng)該不是那兩玩意的問題提揍,可能是新加入的pom依賴導(dǎo)致的啤月,把依賴去掉,就沒有這個(gè)問題了劳跃,那么剩下就是在依賴?yán)锩嬲艺业降资且肽膫€(gè)依賴導(dǎo)致谎仲,一個(gè)一個(gè)嘗試發(fā)現(xiàn)是tcc-transaction-dubbo,tcc-transaction-core刨仑,tcc-transaction-spring這三個(gè)依賴導(dǎo)致郑诺,就是tcc-transaction三個(gè)項(xiàng)目導(dǎo)致夹姥,看錯(cuò)誤提示應(yīng)該是log4j日志包沖突導(dǎo)致的,由于dubbo辙诞,spring這兩包都依賴了core包辙售,那么把core包的log4j的依賴exclude即可:
我不知道是哪個(gè)包有slf4j,所以全部加上了飞涂,還是沒用旦部。然后仔細(xì)讀了一下錯(cuò)誤提示,大概意思是說有兩個(gè)類重復(fù)了较店,StaticLoggerBinder.class士八,slf4j和loggback都有,但是找錯(cuò)了梁呈,找到了slf4j的婚度,錯(cuò)誤已經(jīng)提示了:
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
自動(dòng)綁定了slf4j的,其實(shí)就是包沖突官卡,那么把logback的包刪掉就好了陕见。然而這個(gè)項(xiàng)目引用了大量的依賴,每一個(gè)依賴都有可能自帶了logback味抖,到這里還是懵懵懂懂的,于是只好查閱了一下源碼灰粮,既然是日志仔涩,那肯定與監(jiān)聽相關(guān),直接找LoggingApplicationListener相關(guān)粘舟,直接找過去:
啟動(dòng)時(shí)會(huì)通過LoggingSystem加載熔脂,查看LoggingSystem,
這個(gè)systems里面有三個(gè)日志類柑肴,默認(rèn)讀取的是第一個(gè)logback霞揉,但是logback和log4j-slf4j都有項(xiàng)目的implement function,即StaticLoggerBinder方法晰骑,結(jié)果項(xiàng)目自動(dòng)綁定了StaticLoggerBinder方法适秩,導(dǎo)致加載logback的時(shí)候找不到,所以報(bào)錯(cuò)硕舆。確認(rèn)一下秽荞,查找一下第一次出錯(cuò)的地方:
果然,類型不匹配之后就斷言出錯(cuò)了抚官。那么現(xiàn)在的問題就只需要把log4j-slj4f包exclude掉即可扬跋。然而各個(gè)包縱橫交錯(cuò),要全部刪掉或者exclude掉很麻煩凌节,使用maven提供的工具钦听,右鍵maven-》show dependencies
紅線的即是版本不一致冗余的包洒试,找到log4j-slf4j的包exclude就好了,還有一個(gè)maven helper也可以朴上。再啟動(dòng)項(xiàng)目:
好了垒棋。測(cè)試一下之前的業(yè)務(wù),發(fā)現(xiàn)又出現(xiàn)新問題了余指,這次是數(shù)據(jù)庫問題:
顯示連接到了tcc.order_t的庫上面捕犬,很明顯,是加載到錯(cuò)誤的數(shù)據(jù)源了酵镜,就是剛剛的配置的數(shù)據(jù)源bean的問題:
顯示id重復(fù)碉碉,已經(jīng)有一個(gè)了,修改id號(hào)淮韭。修改完又有問題垢粮,tcc-transaction倒是找不到了,所以還是得讀源碼靠粪,把tcc-transaction的數(shù)據(jù)源改了蜡吧。其實(shí)有一個(gè)更簡單的方法,使用同一個(gè)數(shù)據(jù)庫即可占键,但是為了方便昔善,還是改讀取的bean吧。bean的讀取方式記得有好幾種畔乙,可以直接讀xml文件再用getbean方法君仆,也可以用WebApplicationContextUnil來獲取。然而我讀了半天牲距,根本沒有讀入的操作返咱,可能是自動(dòng)轉(zhuǎn)配,注意到
這就一目了然了牍鞠,先引進(jìn)了TransactionRepository咖摹,TransactionRepositor再被自動(dòng)轉(zhuǎn)配進(jìn)程序里面。然后改成dataSource_transaction即可难述。
接下來就是編寫事務(wù)程序了萤晴,tcc-transaction由三部分組成,try胁后,confirm硫眯,cancel三部分組成,try就是本身的業(yè)務(wù):
仿造tcc-transaction的訂單狀態(tài)择同,多添加一個(gè)草稿狀態(tài)两入,那么在下訂單的時(shí)候,訂單狀態(tài)設(shè)置成草稿狀態(tài)
在confirm的時(shí)候再改成其他的狀態(tài)
要注意服務(wù)冪等性敲才,服務(wù)的冪等性需要confirm裹纳,cancel自己實(shí)現(xiàn)择葡,所以在確認(rèn)之前需要判斷是不是草稿狀態(tài),其他就都按部就班了剃氧,cancel也是敏储。如果是草稿狀態(tài),那就變成已關(guān)閉或者是未支付狀態(tài)朋鞍,如果剛下好的訂單不是草稿狀態(tài)已添,那就是系統(tǒng)出問題了。測(cè)試一下出現(xiàn)問題了:
java.lang.RuntimeException: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available
提示沒有指定事務(wù)管理器滥酥,之前是不需要指定的更舞,因?yàn)槟J(rèn)transactionManager就是事務(wù)管理器bean的id,不需要指定坎吻,可能是事務(wù)沖突了缆蝉,tcc有一個(gè),springboot本身自帶也有一個(gè)瘦真,那么就指定一個(gè)名字吧
但是還是不行刊头,還是相同的錯(cuò)誤,后來去找了文檔發(fā)現(xiàn)诸尽,和dubbo結(jié)合是不需要@transactional注解的原杂,去掉就沒事了。再測(cè)試就沒有問題了您机。
日志都提示成功了污尉。
到這來差不多就實(shí)現(xiàn)完成了,總的來說往产,tcc包含四個(gè)組成部分,事務(wù)攔截器某宪,事務(wù)管理器仿村,事務(wù)存儲(chǔ)器,事務(wù)處理job
核心就是事務(wù)處理器兴喂,當(dāng)事務(wù)存儲(chǔ)器存儲(chǔ)了數(shù)據(jù)之后蔼囊,事務(wù)管理和事務(wù)處理只和事務(wù)存儲(chǔ)器有關(guān),job對(duì)事務(wù)數(shù)據(jù)進(jìn)行恢復(fù)衣迷。不管是什么業(yè)務(wù)畏鼓,但凡是需要事務(wù),就會(huì)被事務(wù)攔截器攔截到壶谒,處理后給到事務(wù)管理器云矫,事務(wù)管理器存儲(chǔ)數(shù)據(jù),然后JOB會(huì)針對(duì)不同事務(wù)對(duì)事務(wù)存儲(chǔ)器的內(nèi)容做處理汗菜,一個(gè)處理事務(wù)前让禀,一個(gè)處理事務(wù)后挑社。
閱讀源碼
首先看一下事務(wù)存儲(chǔ)器,這個(gè)應(yīng)該是最簡單的了
在core核心包的repository就是存儲(chǔ)器了巡揍,提供了緩存痛阻,文件,jdbc腮敌,zookeeper五種分布式存儲(chǔ)阱当,上面使用的是jdbc方式,但是FileSystem是很明顯不可取的糜工,因?yàn)榉植际绞聞?wù)是分布式組件體系里面的一部分弊添,如果存在本地,那就意味著這個(gè)只能在同一個(gè)機(jī)器里面的啤斗,cache差不多也是本地的意思表箭,Redis可以考慮,zookeeper也不建議钮莲,因?yàn)閠ransaction數(shù)據(jù)變動(dòng)很大的免钻,zookeeper是強(qiáng)一致性的組件,如果頻繁讀取崔拥,那么對(duì)集群壓力很大极舔。所以一般就是jdbc和redis。
然后其次看一下事務(wù)攔截器
首先链瓦,攔截器分成兩種拆魏,Compensable和Resource兩種,Compensable是注解事務(wù)攔截器慈俯,resource是資源攔截器渤刃,資源是事務(wù)里面很重要的東西,在TCC中try就是用來預(yù)留資源的贴膘,比如在處理業(yè)務(wù)的時(shí)候卖子,try不會(huì)把所有問題都解決掉,會(huì)把一部分不能解決的問題的相關(guān)數(shù)據(jù)資源存在庫里面刑峡,加上版本號(hào)或者是狀態(tài)洋闽。地下面的都是事務(wù)參與者了。
compensable有兩個(gè)突梦,CompensableTransactionAspect和CompensableTransactionInterceptor诫舅,CompensableTransactionAspect是一個(gè)aop的切面
在帶有compensable注解的方法上切面下去,接下來就是Around方法了
在切點(diǎn)開始和結(jié)束都要經(jīng)過這個(gè)Compensable的攔截器宫患。這些注解都是spring的注解刊懈。在切點(diǎn)開始結(jié)束都會(huì)調(diào)用 compensableTransactionInterceptor.interceptCompensableMethod(pjp);方法,進(jìn)去看看
首先傳入一個(gè)代理對(duì)象,pjp就是代理的目標(biāo)俏讹,然后用getCompensableMethod方法獲取對(duì)象的名字当宴,這里的pjp可以抽象成很多對(duì)象,只要帶有了compensable的就是可以被攔截泽疆,在本次業(yè)務(wù)可能就是方法了户矢,那么getCompensableMethod就是獲得方法的名字,然后getAnnotation獲得注解殉疼,之前還以為是用反射獲取注解梯浪,因?yàn)閟pring基本都是用反射取得包名什么的。Compensable是事務(wù)對(duì)象瓢娜,里面有一個(gè)事務(wù)傳播級(jí)別挂洛,默認(rèn)是Request,發(fā)現(xiàn)如果有事務(wù)就用已經(jīng)有的事務(wù)眠砾,如果沒有就重啟一個(gè)事務(wù)虏劲。接下來就是compensable.propagation()獲取這個(gè)事務(wù)的傳播級(jí)別了。接下來那句有點(diǎn)看不懂:
TransactionContext transactionContext = FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs());
serialVersionUID是序列化版本號(hào)褒颈,TransactionXid為事務(wù)ID柒巫,attachments存儲(chǔ)子事務(wù),所以這個(gè)方法就是獲取事務(wù)上下文谷丸,也可以說是獲取事務(wù)本身堡掏。第一次知道事務(wù)居然是這樣存儲(chǔ),還以為是全部存儲(chǔ)在一個(gè)數(shù)據(jù)庫里面刨疼。然后就是這句代碼了泉唁,F(xiàn)actoryOf有點(diǎn)像是工廠模式,剩下就是判斷是否是異步提交揩慕。事務(wù)處理都說通過事務(wù)管理器亭畜,而且不同事務(wù)之間是有隔離性的,所以接下來就是判斷是不是已經(jīng)存在一個(gè)事務(wù)
boolean isTransactionActive = transactionManager.isTransactionActive();
這一句就是判斷是否有事務(wù)存在具體實(shí)現(xiàn)也很簡單迎卤,CURRENT就是threadlocal拴鸵,只要保證線程安全就能保證主從事務(wù)的隔離性。接下來這一段很重要止吐,是用來判斷用戶角色的:
MethodType methodType = CompensableMethodUtils.calculateMethodType(propagation, isTransactionActive, transactionContext);
角色有4種:
但實(shí)際上只關(guān)心root和Provider兩個(gè),root是主事務(wù)侨糟,Provider即為事務(wù)參與者碍扔,事務(wù)參與者也叫分支事務(wù),比如在tcc的sample例子秕重,order就是主事務(wù)不同,redPacket就是分支事務(wù)了;我售票系統(tǒng)里面,buyticket就主事務(wù)二拐,里面的判斷和下訂單都是分支事務(wù)服鹅。因?yàn)槊恳粋€(gè)注解了compensable的方法都會(huì)進(jìn)來,所以是需要判斷主從事務(wù)百新。
進(jìn)入root主事務(wù)執(zhí)行的方法
transaction = transactionManager.begin();
既然是主事務(wù)企软,那就直接開啟一個(gè)全新的事務(wù)。首先創(chuàng)建一個(gè)事務(wù)對(duì)象饭望,然后存儲(chǔ)到數(shù)據(jù)庫仗哨,然后把事務(wù)把他放在threadlocal里面,也就是剛剛的CURRENT對(duì)象里面铅辞,這個(gè)過程也叫注冊(cè)厌漂。
returnValue = pjp.proceed();
然后就是執(zhí)行目標(biāo)方法了。如果有異常斟珊,那就rollback苇倡,沒有異常就commit提交,最后別注意要清除囤踩,因?yàn)檫@個(gè)是主事務(wù)旨椒,主事務(wù)都執(zhí)行完了,那么分支事務(wù)肯定也執(zhí)行完了高职,所以要在隊(duì)列中清除事務(wù)钩乍。到這里root要執(zhí)行的步驟都執(zhí)行完了,總的來說就那么幾件事:開啟全局事務(wù)怔锌,持久化全局事務(wù)寥粹,注冊(cè)全局事務(wù),判斷應(yīng)該是confirm還是cancel埃元,清除事務(wù)涝涤。注意commit只是調(diào)用自己本身的confirm,不調(diào)用子事務(wù)的岛杀。
分支事務(wù)也是一樣:
這個(gè)比較簡單阔拳,判斷是try,confirm還是cancel类嗤,分別執(zhí)行不同的事情糊肠。propagationExitBegin其實(shí)就是修改事務(wù)狀態(tài)。
然后就是Resource資源攔截器了遗锣,Resource的資源攔截器的 ResourceCoordinatorAspect也是和compensable一樣
直接看切面后執(zhí)行的程序货裹,首先從事務(wù)管理器獲取當(dāng)前事務(wù),注意精偿,兩個(gè)intercept之間是不能直接傳遞參數(shù)的弧圆,這里的intercept也就是compensable和Resource這兩個(gè)赋兵,所以只能從事務(wù)管理器獲取事務(wù)對(duì)象。confirm和cancel都不執(zhí)行搔预,只是執(zhí)行try的數(shù)據(jù)霹期,到目前為止,兩個(gè)處理器都沒有執(zhí)行cancel和confirm的方法拯田,前面compensable里面的confirm和cancel只是改變事務(wù)的狀態(tài)而已历造,正在執(zhí)行我們自定義的confirm和cancel還沒有執(zhí)行。進(jìn)入到方法
這確認(rèn)了事務(wù)是try狀態(tài)之后要執(zhí)行的方法勿锅,傳入的自然是方法對(duì)象了帕膜,然后獲取注解,前面有一模一樣的調(diào)用:
獲取compensable對(duì)象溢十,接著下面就是獲取方法名垮刹,而在Java里面獲取對(duì)應(yīng)的方法唯一的方法是全限定名,也就是類名+包名+方法名张弛,注意在compensable注解里面只是配置了一個(gè)方法名字荒典,所以這兩個(gè)confirm和cancel方法只能是和try同一個(gè)類下面,否則找不到包名和類名吞鸭。
然后是獲取事務(wù)和事務(wù)的編號(hào)
判斷是否有一個(gè)全局的事務(wù)上下文對(duì)象了寺董,沒有創(chuàng)建一個(gè)新的
接著獲取目標(biāo)對(duì)象的class類,使用反射機(jī)制刻剥,其實(shí)就是一些實(shí)現(xiàn)類遮咖,比如一些業(yè)務(wù)里面的Impl類
接下來就是要準(zhǔn)備執(zhí)行了,前面兩句是保存confirm和cancel執(zhí)行的上下文造虏,大概就是要給某個(gè)人發(fā)消息御吞,首先要告訴那個(gè)人的電話號(hào)碼吧。Participant也是一個(gè)事務(wù)漓藕,上面也提到過另一個(gè)類型的事務(wù)陶珠,差不多,但是這里要傳入cancel和confirm上下文以及事務(wù)的編號(hào)享钞,很明顯不是自己執(zhí)行用的揍诽,自己執(zhí)行直接就執(zhí)行了,為什么要收集這么多信息栗竖,而且也不需要xid暑脆,當(dāng)前事務(wù)就能獲取xid的。
果不其然狐肢,下面就把所有的信息都給了事務(wù)管理器:
總起來添吗,主要就是處理try階段的事情,并把所有資源封裝处坪,包括了confirm和cancel的上下文信息根资,分支事務(wù)的信息,提交給事務(wù)管理器同窘。enlistParticipant不用看了玄帕,就是把資源寫進(jìn)數(shù)據(jù)庫里面,所以這個(gè)攔截器也沒干啥想邦,就是把資源放數(shù)據(jù)庫里面裤纹,更新狀態(tài)。到這里基本上流程一半完成了丧没,原來的流程是這樣:
CompensableTransactionInterceptor -> ResourceCoordinatorInterceptor -> 事務(wù)參與者 -> ResourceCoordinatorInterceptor -> CompensableTransactionInterceptor鹰椒,又繞回來是因?yàn)閮蓚€(gè)攔截器都使用了Around,現(xiàn)在還差最后一個(gè)CompensableTransactionInterceptor沒有走呕童,回來看一下Compensable
進(jìn)來攔截器的時(shí)候略過了rollback和commit漆际,前面的流程和進(jìn)來攔截器的是一樣的,直接看看rollback
獲取當(dāng)前事務(wù)夺饲,改變事務(wù)狀態(tài)為cancel奸汇,更新事務(wù)狀態(tài),接下來就是rollback了往声,cancel方法害得分異步和同步擂找,但是無論是哪個(gè)都會(huì)執(zhí)行rollbackTransaction,直接看rollbackTransaction
進(jìn)去rollback看看
找到所有的子事務(wù)浩销,進(jìn)行rollback操作
在這個(gè)方法就執(zhí)行了我們的cancel或者是commit方法贯涎,所有這兩句話是真的去執(zhí)行了兩個(gè)預(yù)設(shè)的方法。總的來說慢洋,CompensableTransactionInterceptor(組織了事務(wù)上下文塘雳,注冊(cè)初始化事務(wù)) -> ResourceCoordinatorInterceptor(組織事務(wù)參與者等資源) -> 事務(wù)參與者 -> ResourceCoordinatorInterceptor(這里什么也沒做,因?yàn)镽esource只是在try階段做了東西) -> CompensableTransactionInterceptor(執(zhí)行cancel和confirm)
Dubbo Moniter
維護(hù)自然少不了監(jiān)控
跑去Apache官網(wǎng)下載dubbo-admin且警。然后打包admin粉捻,注意不同環(huán)境需要修改properties文件:
然后加入Tomcat里的webapps,啟動(dòng)Tomcat即可斑芜。
這樣就意味著啟動(dòng)成功了肩刃。登錄密碼就是在剛剛的dubbo.properties里面可以配置。
鏈路監(jiān)控
如果有很多次的請(qǐng)求下訂單服務(wù)杏头,請(qǐng)求的服務(wù)器又有很多臺(tái)盈包,如果出現(xiàn)問題,就需要排查情況醇王,這個(gè)時(shí)候就需要監(jiān)控整條鏈路呢燥。比較常用的工具就是Zipkin,當(dāng)然寓娩,當(dāng)前這個(gè)項(xiàng)目也只有一個(gè)服務(wù)器叛氨,所以這個(gè)鏈路監(jiān)控也就做過樣子而已呼渣。
Zipkin只關(guān)心框框里面的內(nèi)容,首先由一個(gè)Collector收集器寞埠,是用來收集所有的調(diào)用情況屁置,收集好之后就放到Storge里面;然后需要一個(gè)API仁连,也就是一個(gè)獲取Storge里面調(diào)用信息的API蓝角,接著就是展示在UI界面上。
TraceID:每一次請(qǐng)求全局唯一饭冬;ParentID:父請(qǐng)求編號(hào)使鹅;Cilent Start:表示客戶端發(fā)起的請(qǐng)求;Server Receive:表示服務(wù)端收到請(qǐng)求的操作昌抠;Server Send:收到之后返回患朱;Client Receive:客戶端收到響應(yīng)。
接下來就是安裝zipkin啟動(dòng)即可炊苫,這玩意編譯時(shí)間有點(diǎn)長麦乞,編譯好后java -jar zipkin.jar運(yùn)行即可。
環(huán)境搭建
阿里云服務(wù)器上安裝MySQL步驟就很簡單了劝评,居然還是忘記了MySQL新創(chuàng)建的root用戶是不允許外部訪問姐直。只需要把mysql.user表里面的root用戶host改成%即可。