失效的7個(gè)坑與底層原理:從JDBC到分布式事務(wù))
1. 事務(wù)失效的七宗罪我踩過(guò)的坑你大概率也會(huì)踩先講個(gè)真實(shí)事故。幾年前我維護(hù)過(guò)一個(gè)電商訂單系統(tǒng)線(xiàn)上出了一個(gè)詭異的bug用戶(hù)支付成功后訂單狀態(tài)正常更新但庫(kù)存卻莫名其妙多扣了一次。代碼看起來(lái)完全沒(méi)問(wèn)題——service方法上明明白白標(biāo)著Transactional數(shù)據(jù)庫(kù)也確認(rèn)是InnoDB。查了兩天才定位到根因同一個(gè)類(lèi)里方法A調(diào)用方法BB的事務(wù)注解根本沒(méi)生效。這就是Spring事務(wù)最經(jīng)典的坑之一自調(diào)用繞過(guò)代理。Transactional依賴(lài)AOP代理生效而Spring的代理機(jī)制決定了一個(gè)類(lèi)內(nèi)部的方法調(diào)用是不會(huì)經(jīng)過(guò)代理對(duì)象的。this.orderService.update()拿到的這個(gè)this根本不是Spring容器里那個(gè)被代理過(guò)的bean而是原始對(duì)象。原始對(duì)象上的注解Spring根本看不見(jiàn)。結(jié)果就是你以為開(kāi)啟了事務(wù)實(shí)際上沒(méi)有。庫(kù)存扣減在無(wú)事務(wù)狀態(tài)下執(zhí)行訂單那邊一旦報(bào)錯(cuò)回滾庫(kù)存早就提交了。1.1 自調(diào)用問(wèn)題最隱蔽的事務(wù)殺手自調(diào)用這個(gè)坑的隱蔽之處在于它不會(huì)報(bào)錯(cuò)不會(huì)警告日志里什么都看不出來(lái)只有數(shù)據(jù)對(duì)不上賬的時(shí)候才會(huì)暴露。Service public class OrderService { Transactional public void createOrder(OrderDTO dto) { // 扣庫(kù)存 stockService.deduct(dto.getSkuId(), dto.getQuantity()); // 插入訂單 orderMapper.insert(buildOrder(dto)); // 這里調(diào)用了自身方法事務(wù)注解失效 this.sendNotification(dto); } Transactional(propagation Propagation.REQUIRES_NEW) public void sendNotification(OrderDTO dto) { // 通知邏輯 } }上面這段代碼里sendNotification的REQUIRES_NEW完全無(wú)效。因?yàn)閠his.sendNotification()走的是原始對(duì)象不是代理。解決辦法有三個(gè)把需要事務(wù)的方法挪到另一個(gè)Service類(lèi)——最推薦職責(zé)也最清晰。注入自身代理Autowired private OrderService self;然后調(diào)用self.sendNotification()。使用AopContext.currentProxy()需要開(kāi)啟EnableAspectJAutoProxy(exposeProxy true)然后((OrderService) AopContext.currentProxy()).sendNotification()。方案2最實(shí)用但要注意循環(huán)依賴(lài)問(wèn)題——Spring 4.3之后字段注入的循環(huán)依賴(lài)是允許的但構(gòu)造器注入就不行。方案3代碼看著別扭但確實(shí)有效。1.2 非public方法注解白寫(xiě)了Transactional用在private或者protected方法上Spring容器啟動(dòng)時(shí)根本不報(bào)錯(cuò)但方法執(zhí)行時(shí)事務(wù)就是沒(méi)開(kāi)。原理不復(fù)雜Spring AOP生成代理的時(shí)候JDK動(dòng)態(tài)代理基于接口只代理public方法CGLIB雖然能代理非public方法但Spring官方明確說(shuō)了Transactional只支持public方法。最騷的是如果你把Transactional放在public方法上、但里面調(diào)用了private方法private方法里的事務(wù)注解同樣無(wú)效——因?yàn)槭聞?wù)邊界已經(jīng)在public方法的代理上確定了內(nèi)層調(diào)用不會(huì)再開(kāi)啟新事務(wù)除非傳播行為是REQUIRES_NEW。我見(jiàn)過(guò)不少項(xiàng)目把Transactional加到private方法上然后一臉疑惑地說(shuō)為什么回滾了沒(méi)生效。排查方法很簡(jiǎn)單看日志里有沒(méi)有TransactionInterceptor的getTransaction記錄沒(méi)有就是代理沒(méi)走。1.3 異常被吞回滾了個(gè)寂寞這是最常見(jiàn)也最無(wú)語(yǔ)的一種。Java的檢查型異常checked exception默認(rèn)不會(huì)觸發(fā)Spring事務(wù)回滾但很多人不知道這一點(diǎn)隨手寫(xiě)了個(gè)throws Exception然后事務(wù)就失效了。Transactional public void transfer(Account from, Account to, BigDecimal amount) throws Exception { accountMapper.decrease(from.getId(), amount); accountMapper.increase(to.getId(), amount); // 業(yè)務(wù)校驗(yàn)不通過(guò)拋了個(gè)業(yè)務(wù)異常 throw new BizException(余額不足); }BizException如果是繼承Exception而不是RuntimeException上面這個(gè)事務(wù)不會(huì)回滾錢(qián)就憑空消失了。Spring的默認(rèn)回滾策略是只回滾RuntimeException和Error。檢查型異常如IOException不會(huì)觸發(fā)回滾因?yàn)镾pring認(rèn)為你既然顯式聲明了這個(gè)異常說(shuō)明你可以處理它事務(wù)繼續(xù)提交是合理的。解決方案兩個(gè)方向業(yè)務(wù)異常統(tǒng)一繼承RuntimeException——大多數(shù)項(xiàng)目的做法最簡(jiǎn)單。在Transactional上顯式指定Transactional(rollbackFor Exception.class)——最穩(wěn)一勞永逸。我個(gè)人的習(xí)慣是兩者都做自定義業(yè)務(wù)異常繼承RuntimeException同時(shí)在Transactional上寫(xiě)rollbackFor Exception.class。雖然看起來(lái)冗余但團(tuán)隊(duì)里總有人會(huì)寫(xiě)檢查型異常。1.4 多線(xiàn)程子線(xiàn)程異常主線(xiàn)程事務(wù)照樣提交Transactional的事務(wù)上下文存在ThreadLocal里子線(xiàn)程拿不到父線(xiàn)程的事務(wù)上下文。Transactional public void batchProcess(ListOrder orders) { orders.parallelStream().forEach(order - { // 這里拋異常父線(xiàn)程事務(wù)不會(huì)感知 processOne(order); }); }如果一定要在多線(xiàn)程里用事務(wù)只能每個(gè)線(xiàn)程自己開(kāi)事務(wù)用TransactionTemplate包住子線(xiàn)程的任務(wù)代碼然后在線(xiàn)程池層面做異常聚合——子線(xiàn)程里有任何失敗主線(xiàn)程如何決定是否整體回滾需要自己實(shí)現(xiàn)協(xié)調(diào)邏輯。這個(gè)問(wèn)題在批量導(dǎo)入、報(bào)表生成、定時(shí)任務(wù)場(chǎng)景里非常常見(jiàn)。我的建議是批量任務(wù)盡量不用Transactional包整段而是每條或每批用TransactionTemplate單獨(dú)開(kāi)事務(wù)配合失敗記錄表做補(bǔ)償。這樣既能保證數(shù)據(jù)一致又不會(huì)因?yàn)橐粋€(gè)線(xiàn)程失敗導(dǎo)致大批量回滾。1.5 數(shù)據(jù)庫(kù)引擎不支持事務(wù)這個(gè)坑現(xiàn)在少了但老項(xiàng)目中還能見(jiàn)到。MySQL的MyISAM引擎根本不支持事務(wù)你注解標(biāo)得再多也沒(méi)用。確認(rèn)方法很簡(jiǎn)單SHOW TABLE STATUS WHERE Name your_table;看Engine字段InnoDB支持事務(wù)MyISAM不支持。另外如果表是MyISAMSpring事務(wù)管理器初始化時(shí)也不報(bào)錯(cuò)執(zhí)行時(shí)DataSourceTransactionManager還是會(huì)調(diào)setAutoCommit(false)但引擎層面不認(rèn)等于白忙。1.6 final類(lèi)或final方法CGLIB也救不了Spring事務(wù)默認(rèn)用AOP代理。如果目標(biāo)類(lèi)配置了JDK動(dòng)態(tài)代理基于接口那沒(méi)問(wèn)題如果走CGLIB基于類(lèi)繼承生成子類(lèi)那么final類(lèi)無(wú)法被繼承final方法無(wú)法被重寫(xiě)代理自然失效。Spring Boot 2.x之后默認(rèn)proxyTargetClasstrue走CGLIBfinal問(wèn)題就浮出水面了。很多人把service類(lèi)寫(xiě)成final為了防繼承結(jié)果事務(wù)全失效。排查方法啟動(dòng)時(shí)看日志里是JdkDynamicAopProxy還是CglibAopProxy再看你的類(lèi)和方法有沒(méi)有final修飾符。1.7 事務(wù)管理器沒(méi)配置或配置錯(cuò)了Spring Boot的DataSourceAutoConfiguration會(huì)自動(dòng)配置DataSourceTransactionManager但你如果引入了多數(shù)據(jù)源或者自定義了DataSource而沒(méi)有配套注冊(cè)PlatformTransactionManager那Transactional就會(huì)失效。另外EnableTransactionManagement注解在Spring Boot里默認(rèn)開(kāi)啟不需要你重復(fù)添加。但如果你用了Spring MVC非Boot項(xiàng)目忘了加EnableTransactionManagement事務(wù)也會(huì)靜默失效。排查清單很簡(jiǎn)單Transactional方法執(zhí)行的線(xiàn)程棧里有沒(méi)有TransactionInterceptor有就是代理生效了沒(méi)有就是代理沒(méi)生效。2. Spring事務(wù)的底層運(yùn)作機(jī)制從JDBC到動(dòng)態(tài)代理很多人用Transactional用了好幾年但完全不知道它背后發(fā)生了什么。面試被問(wèn)Spring事務(wù)是怎么實(shí)現(xiàn)的只會(huì)答AOP兩個(gè)字就再也說(shuō)不出更多了。其實(shí)底層鏈路沒(méi)那么玄拆開(kāi)看就三層JDBC的事務(wù)能力、Spring的事務(wù)抽象、AOP的代理包裝。2.1 第一層JDBC的事務(wù)基石萬(wàn)事萬(wàn)物都離不開(kāi)JDBC。Spring事務(wù)再花哨落到底層就是這三行代碼Connection conn dataSource.getConnection(); conn.setAutoCommit(false); // 關(guān)閉自動(dòng)提交 conn.commit(); // 提交 conn.rollback(); // 回滾setAutoCommit(false)的意思是之后的SQL不會(huì)立即生效必須等commit()才真正寫(xiě)入數(shù)據(jù)庫(kù)MySQL的binlog和redolog層面是另一套機(jī)制。如果中途異常調(diào)用rollback()之前的所有操作全部撤銷(xiāo)。所以Spring事務(wù)的本質(zhì)就是幫你管理Connection在合適的時(shí)機(jī)調(diào)用commit或rollback。就這么簡(jiǎn)單。2.2 第二層PlatformTransactionManager抽象Spring抽象了一個(gè)接口——PlatformTransactionManager核心方法就三個(gè)public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition); void commit(TransactionStatus status); void rollback(TransactionStatus status); }getTransaction獲取或創(chuàng)建事務(wù)。注意這里的措辭——如果你已經(jīng)在事務(wù)里根據(jù)傳播行為決定是復(fù)用當(dāng)前事務(wù)還是掛起后新建。commit提交事務(wù)。但提交前會(huì)檢查T(mén)ransactionStatus里的rollbackOnly標(biāo)記如果之前有人標(biāo)記了只回滾這里會(huì)拋UnexpectedRollbackException。rollback回滾事務(wù)。常用的實(shí)現(xiàn)是DataSourceTransactionManager針對(duì)Hibernate有HibernateTransactionManager針對(duì)JPA有JpaTransactionManager。DataSourceTransactionManager的doBegin方法做了幾件事從DataSource拿一個(gè)Connection。如果當(dāng)前Connection是自動(dòng)提交模式關(guān)閉它并記下來(lái)方便事務(wù)結(jié)束恢復(fù)。設(shè)置隔離級(jí)別如果TransactionDefinition指定了。設(shè)置只讀標(biāo)志readOnlytrue會(huì)走Connection.setReadOnly對(duì)MySQL的InnoDB來(lái)說(shuō)這能優(yōu)化一些查詢(xún)路徑。把這個(gè)Connection綁定到TransactionSynchronizationManager的ThreadLocal里。后面的DAO操作拿到的Connection就是從TransactionSynchronizationManager里取的。這就是同一個(gè)事務(wù)里所有SQL共用同一個(gè)數(shù)據(jù)庫(kù)連接的原理——ThreadLocal綁定。2.3 第三層AOP代理的切入點(diǎn)Spring事務(wù)的AOP基于TransactionInterceptor它實(shí)現(xiàn)了MethodInterceptor攔截所有被Transactional標(biāo)記的方法。調(diào)用鏈?zhǔn)沁@樣的外部調(diào)用orderService.createOrder()實(shí)際調(diào)用的是代理對(duì)象的方法。TransactionInterceptor.invoke()被觸發(fā)先調(diào)用TransactionAspectSupport的createTransactionIfNecessary。根據(jù)Transactional上的配置傳播行為、隔離級(jí)別、超時(shí)等調(diào)用PlatformTransactionManager.getTransaction()。執(zhí)行業(yè)務(wù)方法。方法正常返回提交事務(wù)方法拋異常根據(jù)rollbackFor配置決定是否回滾。清理ThreadLocal中的連接恢復(fù)自動(dòng)提交狀態(tài)。所以Transactional加在哪個(gè)方法上代理就攔哪個(gè)方法——事務(wù)邊界就是方法邊界。方法跑多久事務(wù)就開(kāi)多久。2.4 只讀事務(wù)與UnexpectedRollbackException的真相熱詞里有一條UnexpectedRollbackException: transaction rolled back because it has been marked as rollback-only這是個(gè)非常經(jīng)典的問(wèn)題。場(chǎng)景是這樣的方法A開(kāi)啟事務(wù)調(diào)用方法BB標(biāo)記了rollback-onlytrue比如B捕獲了異常但內(nèi)部回滾邏輯已運(yùn)行。然后A繼續(xù)執(zhí)行正常返回時(shí)調(diào)用commit()此時(shí)Spring發(fā)現(xiàn)事務(wù)被標(biāo)記為rollback-only直接拋UnexpectedRollbackException事務(wù)回滾。換句話(huà)說(shuō)你代碼里沒(méi)拋異常但事務(wù)已經(jīng)注定要回滾了。這經(jīng)常出現(xiàn)在try-catch吃了異常但沒(méi)重新拋出的情況下。排查時(shí)看日志里有沒(méi)有Transaction rolled back because it has been marked as rollback-only的WARN告警跟著調(diào)用棧就能找到是哪個(gè)方法把事務(wù)標(biāo)記了。3. 事務(wù)傳播行為與隔離級(jí)別什么時(shí)候用哪個(gè)別背口訣網(wǎng)上關(guān)于傳播行為的口訣一大堆什么REQUIRED默認(rèn)用REQUIRES_NEW記日志NESTED做嵌套背下來(lái)容易用對(duì)難。我把這幾個(gè)傳播行為逐個(gè)講透配上真實(shí)場(chǎng)景你就能理解了。3.1 傳播行為詳解Spring定義了7種傳播行為大部分項(xiàng)目用到的不超過(guò)4種但理解每一種的原理仍然重要。傳播行為含義典型場(chǎng)景REQUIRED有事務(wù)就復(fù)用沒(méi)有就新建默認(rèn)值99%的場(chǎng)景用它SUPPORTS有事務(wù)就加入沒(méi)有就無(wú)事務(wù)執(zhí)行查詢(xún)方法可選MANDATORY必須在事務(wù)中執(zhí)行否則拋異常強(qiáng)制事務(wù)的方法REQUIRES_NEW掛起當(dāng)前事務(wù)永遠(yuǎn)新建一個(gè)寫(xiě)日志、異步通知NOT_SUPPORTED掛起當(dāng)前事務(wù)以無(wú)事務(wù)方式執(zhí)行導(dǎo)出大數(shù)據(jù)量文件NEVER如果有事務(wù)就拋異常強(qiáng)制無(wú)事務(wù)的執(zhí)行NESTED嵌套事務(wù)基于保存點(diǎn)Savepoint大批量循環(huán)處理單條失敗不影響整體REQUIRED是默認(rèn)值理解起來(lái)很簡(jiǎn)單Spring檢查當(dāng)前ThreadLocal里有沒(méi)有綁定事務(wù)有就加入沒(méi)有就新建一個(gè)。注意加入意味著共用一個(gè)事務(wù)、一個(gè)Connection、一套commit/rollback。內(nèi)層方法拋異常回滾外層也一起回滾。REQUIRES_NEW的核心是掛起。邏輯是從TransactionSynchronizationManager里把當(dāng)前事務(wù)的Connection解綁、掛起然后新建一個(gè)完全獨(dú)立的事務(wù)持有獨(dú)立的Connection。內(nèi)層事務(wù)提交/回滾不影響外層事務(wù)。外層事務(wù)回滾內(nèi)層已經(jīng)提交的數(shù)據(jù)不會(huì)跟著回滾。典型場(chǎng)景就是操作日志業(yè)務(wù)方法開(kāi)啟事務(wù)日志記錄要獨(dú)立提交——不管業(yè)務(wù)成功還是失敗日志都要寫(xiě)進(jìn)去。如果你用REQUIRED業(yè)務(wù)回滾時(shí)日志也跟著回滾那排查問(wèn)題的時(shí)候就看不到錯(cuò)誤日志了。NESTED跟REQUIRES_NEW的區(qū)別要講清楚。REQUIRES_NEW是物理事務(wù)獨(dú)立的Connection、獨(dú)立的commit/rollbackNESTED是邏輯事務(wù)基于Savepoint保存點(diǎn)。內(nèi)層方法失敗回滾到保存點(diǎn)外層可以繼續(xù)執(zhí)行外層最終提交時(shí)一起提交所有保存點(diǎn)之前的操作。但注意NESTED需要底層數(shù)據(jù)庫(kù)支持保存點(diǎn)。MySQL的InnoDB支持但有些數(shù)據(jù)庫(kù)不支持且JpaTransactionManager對(duì)NESTED的支持有限。實(shí)際項(xiàng)目中能用REQUIRES_NEW解決的就別用NESTED少踩坑。3.2 隔離級(jí)別臟讀、不可重復(fù)讀、幻讀Spring事務(wù)隔離級(jí)別有五種對(duì)應(yīng)JDBC的隔離級(jí)別隔離級(jí)別臟讀不可重復(fù)讀幻讀說(shuō)明DEFAULT---使用數(shù)據(jù)庫(kù)默認(rèn)MySQL默認(rèn)REPEATABLE_READREAD_UNCOMMITTED會(huì)會(huì)會(huì)讀未提交不推薦READ_COMMITTED不會(huì)會(huì)會(huì)Oracle默認(rèn)REPEATABLE_READ不會(huì)不會(huì)會(huì)MySQL InnoDB下不會(huì)MySQL默認(rèn)SERIALIZABLE不會(huì)不會(huì)不會(huì)串行化性能最差臟讀讀到別的事務(wù)未提交的數(shù)據(jù)。不可重復(fù)讀同一事務(wù)內(nèi)兩次讀取同一數(shù)據(jù)結(jié)果不一樣因?yàn)閯e的事務(wù)提交了更新?;米x同一事務(wù)內(nèi)兩次查詢(xún)返回的記錄數(shù)不一樣因?yàn)閯e的事務(wù)插入了新記錄。MySQL的InnoDB在REPEATABLE_READ級(jí)別下通過(guò)MVCC多版本并發(fā)控制 Gap Lock間隙鎖實(shí)際上消除了幻讀問(wèn)題。所以MySQL默認(rèn)的REPEATABLE_READ比Oracle的READ_COMMITTED在某些場(chǎng)景下更嚴(yán)格。實(shí)際項(xiàng)目中我見(jiàn)過(guò)很多團(tuán)隊(duì)把隔離級(jí)別統(tǒng)一設(shè)成READ_COMMITTED理由是MySQL的RR有間隙鎖并發(fā)插入容易死鎖。這個(gè)說(shuō)法有一定道理但不是絕對(duì)的。如果你的系統(tǒng)沒(méi)有復(fù)雜的范圍查詢(xún)并發(fā)插入場(chǎng)景RR不會(huì)成為瓶頸如果你確實(shí)遇到死鎖頻繁再降級(jí)也不遲。3.3 事務(wù)超時(shí)與回滾策略Transactional(timeout 5)表示事務(wù)最多執(zhí)行5秒超時(shí)則強(qiáng)制回滾。底層實(shí)現(xiàn)是JDBC的Connection.setNetworkTimeout或者由Spring在每次SQL執(zhí)行前檢查時(shí)間不同實(shí)現(xiàn)有差異。但要注意timeout是從事務(wù)開(kāi)始到最后一次SQL執(zhí)行的時(shí)間不是方法總耗時(shí)。如果你的方法在事務(wù)前做了一堆耗時(shí)操作比如遠(yuǎn)程調(diào)用這部分不計(jì)入事務(wù)超時(shí)。所以不要指望timeout能保護(hù)整體方法執(zhí)行時(shí)間。關(guān)于回滾策略我再?gòu)?qiáng)調(diào)一次// 推薦寫(xiě)法 Transactional(rollbackFor Exception.class) // 或者只回滾特定異常 Transactional(rollbackFor BizException.class, noRollbackFor IllegalStateException.class)noRollbackFor用的場(chǎng)景不多但有一種情況很實(shí)用某些非關(guān)鍵數(shù)據(jù)比如統(tǒng)計(jì)緩存的更新失敗不應(yīng)該讓主事務(wù)回滾可以列在noRollbackFor里。4. 編程式事務(wù)當(dāng)Transactional真的不合適時(shí)Transactional是聲明式事務(wù)用起來(lái)方便但有三個(gè)致命弱點(diǎn)事務(wù)粒度太大整個(gè)方法一個(gè)事務(wù)如果方法里有網(wǎng)絡(luò)調(diào)用或批量循環(huán)事務(wù)會(huì)長(zhǎng)時(shí)間占用數(shù)據(jù)庫(kù)連接。自調(diào)用失效前面已經(jīng)講過(guò)了。無(wú)法精確控制提交時(shí)機(jī)想先做幾步提交再繼續(xù)做Transactional實(shí)現(xiàn)不了。這時(shí)候就該上手編程式事務(wù)了。Spring提供了TransactionTemplate。4.1 TransactionTemplate的用法Service public class OrderService { private final TransactionTemplate transactionTemplate; public OrderService(PlatformTransactionManager transactionManager) { this.transactionTemplate new TransactionTemplate(transactionManager); // 可以設(shè)置默認(rèn)配置 this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); } public void createOrder(OrderDTO dto) { transactionTemplate.executeWithoutResult(status - { stockMapper.deduct(dto.getSkuId(), dto.getQuantity()); orderMapper.insert(buildOrder(dto)); }); } }TransactionTemplate.execute回調(diào)里你可以在任何地方開(kāi)啟事務(wù)塊粒度完全由自己控制。更重要的是它徹底繞開(kāi)了自調(diào)用問(wèn)題——因?yàn)槟闶侵苯诱{(diào)用事務(wù)管理器不走代理。還有一個(gè)場(chǎng)景強(qiáng)烈推薦用TransactionTemplate大批量循環(huán)插入。如果用Transactional包住整個(gè)for循環(huán)幾萬(wàn)條數(shù)據(jù)一個(gè)事務(wù)提交時(shí)數(shù)據(jù)庫(kù)壓力巨大一旦失敗全部回滾代價(jià)太高。改成循環(huán)內(nèi)用TransactionTemplate每100條一個(gè)事務(wù)失敗了只回滾這一批配合日志記錄失敗批次整體效率和可靠性都高很多。4.2 手動(dòng)控制TransactionStatus如果execute回調(diào)不適合你比如需要事務(wù)中間做分支判斷、需要中途提交可以用更底層的PlatformTransactionManagerAutowired private PlatformTransactionManager transactionManager; public void complexBiz() { DefaultTransactionDefinition def new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status transactionManager.getTransaction(def); try { // 業(yè)務(wù)邏輯1 dao.update(...); // 到這里先提交一次 transactionManager.commit(status); // 開(kāi)啟新事務(wù) TransactionStatus status2 transactionManager.getTransaction(def); try { // 業(yè)務(wù)邏輯2 dao.insert(...); transactionManager.commit(status2); } catch (Exception e) { transactionManager.rollback(status2); throw e; } } catch (Exception e) { transactionManager.rollback(status); throw e; } }這段代碼展示了如何手動(dòng)控制多段事務(wù)但我強(qiáng)烈不建議在業(yè)務(wù)代碼里寫(xiě)這種底層代碼——容易出錯(cuò)而且代碼很難讀。99%的場(chǎng)景TransactionTemplate夠用了。4.3 編程式事務(wù)的注意點(diǎn)TransactionTemplate是線(xiàn)程安全的可以做成Bean注入不要每次new一個(gè)。executeWithoutResult和execute的區(qū)別后者可以返回業(yè)務(wù)結(jié)果前者只能執(zhí)行不能返回。需要返回值就用execute。事務(wù)回調(diào)里拋出的異常RuntimeException會(huì)觸發(fā)回滾檢查型異常默認(rèn)不會(huì)。要回滾檢查型異常需要手動(dòng)status.setRollbackOnly()或者包裝成RuntimeException。5. 多數(shù)據(jù)源與分布式事務(wù)跳出單庫(kù)思維的邊界單庫(kù)單事務(wù)的好日子總有到頭的時(shí)候。微服務(wù)一拆、分庫(kù)一搞Transactional就管不住跨庫(kù)的數(shù)據(jù)一致性了。我先講清楚Spring事務(wù)在多數(shù)據(jù)源下的表現(xiàn)再展開(kāi)分布式事務(wù)。5.1 多數(shù)據(jù)源下的事務(wù)各管各的如果你配置了兩個(gè)DataSource比如一個(gè)主庫(kù)一個(gè)從庫(kù)Spring里注冊(cè)了對(duì)應(yīng)的兩個(gè)PlatformTransactionManager同一個(gè)Transactional方法里操作了兩個(gè)庫(kù)——那這兩個(gè)庫(kù)的事務(wù)是互相獨(dú)立的。主庫(kù)提交成功、從庫(kù)提交失敗你沒(méi)有任何辦法通過(guò)單機(jī)事務(wù)解決。Spring Boot里處理多數(shù)據(jù)源的常規(guī)姿勢(shì)是Transactional(transactionManager xxxTransactionManager)指定用哪個(gè)事務(wù)管理器。方法里操作另一個(gè)庫(kù)那份數(shù)據(jù)不在事務(wù)保護(hù)范圍內(nèi)。所以多數(shù)據(jù)源場(chǎng)景下的策略是一個(gè)事務(wù)只操作一種數(shù)據(jù)源??鐢?shù)據(jù)源的業(yè)務(wù)要么拆分方法要么引入分布式事務(wù)方案不要指望Transactional救你。5.2 分布式事務(wù)的主流方案對(duì)比分布式事務(wù)是個(gè)大話(huà)題我只挑主流方案講給個(gè)選型思路。XA協(xié)議兩階段提交2PCXA是最正統(tǒng)的分布式事務(wù)協(xié)議分兩步準(zhǔn)備階段所有參與者執(zhí)行SQL但不提交各自保留Undo/Redo log和提交階段協(xié)調(diào)者發(fā)提交指令各參與者提交。問(wèn)題在于準(zhǔn)備階段會(huì)一直持有數(shù)據(jù)庫(kù)鎖高并發(fā)場(chǎng)景下鎖沖突嚴(yán)重性能非常差。Java里典型的實(shí)現(xiàn)是Atomikos、Bitronix。說(shuō)實(shí)話(huà)互聯(lián)網(wǎng)高并發(fā)場(chǎng)景用XA的很少了更多出現(xiàn)在金融等強(qiáng)一致性要求的內(nèi)部系統(tǒng)。TCCTry-Confirm-CancelTCC把每個(gè)操作拆成三個(gè)階段Try預(yù)留資源、Confirm確認(rèn)執(zhí)行、Cancel取消釋放。優(yōu)點(diǎn)是業(yè)務(wù)層面控制粒度性能比XA好很多缺點(diǎn)是侵入性極強(qiáng)每個(gè)業(yè)務(wù)都要寫(xiě)三套邏輯。典型場(chǎng)景賬戶(hù)轉(zhuǎn)賬。Try階段凍結(jié)轉(zhuǎn)出金額Confirm階段扣款Cancel階段解凍。這個(gè)方案需要深入理解業(yè)務(wù)才能寫(xiě)得對(duì)如果你只是想快速實(shí)現(xiàn)一個(gè)分布式事務(wù)TCC的學(xué)習(xí)成本挺高的。消息事務(wù) 最終一致性這是目前最常用、成本最低的方案。思路很簡(jiǎn)單利用本地事務(wù)把業(yè)務(wù)操作和發(fā)消息綁定在同一次數(shù)據(jù)庫(kù)事務(wù)里通過(guò)消息中間件RocketMQ/RabbitMQ最終把數(shù)據(jù)變更同步給其他服務(wù)。舉個(gè)訂單和積分的例子開(kāi)啟本地事務(wù)插入訂單表同時(shí)插入積分消息表。本地事務(wù)提交后后臺(tái)任務(wù)或MQ的事務(wù)消息機(jī)制把積分消息表里未發(fā)送的記錄發(fā)到MQ。積分服務(wù)從MQ消費(fèi)消息給用戶(hù)加積分。如果積分服務(wù)失敗消息進(jìn)入重試隊(duì)列直到成功。這種方案做不到強(qiáng)一致中間會(huì)有一段數(shù)據(jù)不一致的時(shí)間窗口但絕大多數(shù)業(yè)務(wù)都能接受。核心機(jī)制是本地消息表實(shí)現(xiàn)成本低、不用引入重量級(jí)中間件是目前國(guó)內(nèi)互聯(lián)網(wǎng)公司的首選方案。Seata的AT模式Seata是阿里開(kāi)源的分布式事務(wù)框架AT模式的思路是代理你的SQL記錄Before Image和After Image數(shù)據(jù)快照在全局提交時(shí)自動(dòng)執(zhí)行補(bǔ)償SQL。對(duì)業(yè)務(wù)代碼侵入性很小但性能有一定損耗每行數(shù)據(jù)要記錄前后快照。如果你的系統(tǒng)需要跨服務(wù)事務(wù)、又不想太動(dòng)代碼Seata AT是可以考慮的。5.3 選擇建議場(chǎng)景推薦方案單體應(yīng)用單數(shù)據(jù)庫(kù)什么都不用本地事務(wù)夠用微服務(wù)對(duì)一致性要求不高本地消息表 MQ最終一致微服務(wù)強(qiáng)一致要求高Seata AT 或 TCC金融級(jí)強(qiáng)一致低并發(fā)XA/2PC非核心數(shù)據(jù)可以容忍短暫不一致MQ異步 對(duì)賬補(bǔ)償分布式事務(wù)沒(méi)有銀彈。選型的核心不是哪個(gè)方案高級(jí)而是你的業(yè)務(wù)能容忍多大程度的不一致。6. 事務(wù)排查工具箱從日志到監(jiān)控的一整套姿勢(shì)事務(wù)的問(wèn)題一旦出現(xiàn)往往都是線(xiàn)上的、隱性的、難復(fù)現(xiàn)的。所以一定要有一套預(yù)防和排查的手段而不是等出了問(wèn)題再手忙腳亂。6.1 日志配置把事務(wù)邊界打印出來(lái)Spring的事務(wù)日志默認(rèn)是DEBUG級(jí)別不打開(kāi)根本看不到。在application.yml里配置logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG org.springframework.jdbc.datasource.JdbcTransactionManager: DEBUG打開(kāi)后你會(huì)看到類(lèi)似這樣的日志Obtained JDBC transaction from DataSource [HikariDataSource (HikariPool-1)] Participating in existing transaction Initiating transaction commit Initiating transaction rollback這些日志的排列能幫你快速判斷事務(wù)有沒(méi)有開(kāi)啟、是新建還是復(fù)用、提交還是回滾、有沒(méi)有因?yàn)閞ollback-only被強(qiáng)制回滾。6.2 ThreadLocal狀態(tài)的運(yùn)行時(shí)監(jiān)控如果你懷疑某個(gè)方法執(zhí)行時(shí)事務(wù)狀態(tài)不對(duì)可以在代碼里手動(dòng)檢測(cè)import org.springframework.transaction.support.TransactionSynchronizationManager; boolean isInTransaction TransactionSynchronizationManager.isActualTransactionActive(); System.out.println(當(dāng)前是否在事務(wù)中: isInTransaction);isActualTransactionActive()返回true說(shuō)明當(dāng)前線(xiàn)程確實(shí)有數(shù)據(jù)庫(kù)事務(wù)處于活動(dòng)狀態(tài)。如果外面標(biāo)了Transactional但這里返回false說(shuō)明代理沒(méi)生效——自調(diào)用、final方法、私有方法逐個(gè)排查。還可以查看當(dāng)前事務(wù)名Object name TransactionSynchronizationManager.getCurrentTransactionName();這能幫你快速定位當(dāng)前事務(wù)是哪個(gè)方法開(kāi)啟的。6.3 監(jiān)控事務(wù)耗時(shí)與連接占用事務(wù)另一個(gè)容易忽略的問(wèn)題是連接泄漏和長(zhǎng)事務(wù)。一個(gè)事務(wù)如果執(zhí)行了幾十秒連接就占用了幾十秒連接池很快被打滿(mǎn)。建議做法在DataSource層加慢SQL日志HikariCP有connectionTimeout、validationTimeout配置同時(shí)開(kāi)啟leakDetectionThreshold檢測(cè)連接泄漏。用TransactionSynchronizationManager.registerSynchronization注冊(cè)事務(wù)同步回調(diào)統(tǒng)計(jì)每個(gè)事務(wù)的耗時(shí)。Transactional public void bizMethod() { long start System.currentTimeMillis(); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCompletion(int status) { long cost System.currentTimeMillis() - start; if (cost 1000) { log.warn(事務(wù)耗時(shí)過(guò)長(zhǎng): {}ms, status{}, cost, status); } } }); }這個(gè)方法能精確定位哪個(gè)事務(wù)方法最耗時(shí)但它需要每一個(gè)業(yè)務(wù)方法都加一遍侵入性強(qiáng)。更好的方案是寫(xiě)一個(gè)AOP切面統(tǒng)一攔截Transactional方法做耗時(shí)統(tǒng)計(jì)。6.4 事務(wù)失效排查的標(biāo)準(zhǔn)操作流程如果線(xiàn)上出現(xiàn)了疑似事務(wù)失效的問(wèn)題我的排查順序是這樣的看日志打開(kāi)事務(wù)DEBUG日志確認(rèn)方法執(zhí)行時(shí)有沒(méi)有Obtained JDBC Transaction記錄。沒(méi)有就是代理沒(méi)生效走第2步有就繼續(xù)看提交/回滾記錄走第5步??捶椒ê灻遣皇莗ublic類(lèi)或方法有沒(méi)有final看調(diào)用方是不是同一個(gè)類(lèi)內(nèi)部調(diào)用是不是沒(méi)有被Spring管理的對(duì)象比如new出來(lái)的看注解Transactional有沒(méi)有寫(xiě)對(duì)rollbackFor有沒(méi)有配多數(shù)據(jù)源下transactionManager指對(duì)了沒(méi)有看異常異常有沒(méi)有被try-catch吞掉異常類(lèi)型是不是RuntimeException看數(shù)據(jù)庫(kù)表引擎支不支持事務(wù)連接是不是走到了正確的DataSource這套流程走一遍90%的事務(wù)問(wèn)題都能定位。7. 最后的實(shí)戰(zhàn)建議事務(wù)規(guī)范從我做起寫(xiě)到這里我把這些年跟事務(wù)打交道沉淀下來(lái)的幾條規(guī)矩放在最后算是個(gè)人的實(shí)戰(zhàn)總結(jié)也是新項(xiàng)目啟動(dòng)時(shí)的檢查清單。事務(wù)方法要短小精悍。事務(wù)范圍之內(nèi)不要做遠(yuǎn)程調(diào)用、不要做復(fù)雜的文件IO、不要進(jìn)行大批量循環(huán)。否則連接池遲早被打死。把非數(shù)據(jù)庫(kù)操作移到事務(wù)外或者拆成多個(gè)事務(wù)塊。事務(wù)邊界要盡可能晚開(kāi)啟盡可能早提交。前置校驗(yàn)、參數(shù)組裝這些放事務(wù)外真正需要數(shù)據(jù)庫(kù)原子性的操作才放進(jìn)事務(wù)塊。很多人習(xí)慣方法一進(jìn)來(lái)就開(kāi)事務(wù)其實(shí)很多操作根本不需要事務(wù)保護(hù)。回滾配置要顯式寫(xiě)。Transactional(rollbackFor Exception.class)這行字雖然啰嗦但它能擋住95%的回滾失效問(wèn)題。自定義的業(yè)務(wù)異常統(tǒng)一繼承RuntimeException省得每個(gè)方法都要考慮這個(gè)異常會(huì)不會(huì)回滾。批量處理用TransactionTemplate而不是Transactional。循環(huán)里的事務(wù)一定要用編程式事務(wù)精確控制粒度Transactional包整個(gè)循環(huán)就是把數(shù)據(jù)庫(kù)往死里逼。分布式事務(wù)不要貪多求全。能靠業(yè)務(wù)流程設(shè)計(jì)規(guī)避的就盡量規(guī)避比如通過(guò)本地消息表做最終一致性比引入一套重量級(jí)分布式事務(wù)框架劃算得多。選型之前先想清楚你的數(shù)據(jù)不一致容忍度。事務(wù)這個(gè)東西說(shuō)到底是數(shù)據(jù)庫(kù)連接和錯(cuò)誤處理時(shí)機(jī)的管理藝術(shù)。理解底層機(jī)制比背多少面試題都管用。希望這篇文章里踩過(guò)的坑、總結(jié)的思路能讓你少走幾條彎路。