問題分析與解決方案)
1. 問題現(xiàn)象與背景分析最近在重構(gòu)一個(gè)訂單處理系統(tǒng)時(shí)遇到了一個(gè)詭異的問題。系統(tǒng)采用SpringBootMyBatis-Plus技術(shù)棧其中有個(gè)批量保存訂單明細(xì)的功能為了提高性能我將其改造成了異步處理。核心代碼如下Transactional public void processOrder(OrderDTO order) { // 主訂單入庫 orderMapper.insert(order); // 異步處理訂單明細(xì) CompletableFuture.runAsync(() - { ListOrderItem items convertToItems(order); orderItemService.saveBatch(items, 1000); // 批量插入 }, executor); }理論上主訂單和明細(xì)應(yīng)該要么全部成功要么全部失敗。但實(shí)際運(yùn)行中卻發(fā)現(xiàn)主訂單記錄正常入庫但明細(xì)數(shù)據(jù)經(jīng)常丟失。更奇怪的是在開發(fā)環(huán)境調(diào)試時(shí)這個(gè)問題并非100%復(fù)現(xiàn)大約有30%的概率會(huì)出現(xiàn)。2. 事務(wù)傳播機(jī)制與線程邊界2.1 Spring事務(wù)的基本原理Spring的事務(wù)管理是基于ThreadLocal實(shí)現(xiàn)的。當(dāng)我們使用Transactional注解時(shí)Spring會(huì)在方法調(diào)用前通過AOP創(chuàng)建一個(gè)Connection對象將該Connection綁定到當(dāng)前線程的ThreadLocal中方法內(nèi)所有數(shù)據(jù)庫操作都使用這個(gè)Connection方法結(jié)束后根據(jù)執(zhí)行情況提交或回滾事務(wù)關(guān)鍵點(diǎn)在于事務(wù)上下文是與線程綁定的。當(dāng)我們在異步線程中執(zhí)行數(shù)據(jù)庫操作時(shí)會(huì)使用新的Connection與原線程的事務(wù)完全隔離。2.2 saveBatch的內(nèi)部實(shí)現(xiàn)MyBatis-Plus的saveBatch方法看似簡單但內(nèi)部有多個(gè)關(guān)鍵步驟// MyBatis-Plus 3.5.1 源碼片段 public boolean saveBatch(CollectionT entityList, int batchSize) { String sqlStatement getSqlStatement(SqlMethod.INSERT_ONE); return executeBatch(entityList, batchSize, (sqlSession, entity) - { sqlSession.insert(sqlStatement, entity); }); }實(shí)際上它會(huì)自動(dòng)判斷是否開啟事務(wù)通過TransactionSynchronizationManager.isSynchronizationActive()如果沒有事務(wù)會(huì)為每個(gè)batch創(chuàng)建獨(dú)立的事務(wù)每個(gè)batch提交后立即提交事務(wù)這就解釋了為什么我們的明細(xì)數(shù)據(jù)會(huì)丟失異步線程中的saveBatch操作與原方法的事務(wù)無關(guān)一旦異步線程執(zhí)行失敗主事務(wù)不會(huì)回滾。3. 問題復(fù)現(xiàn)與根因定位3.1 最小化復(fù)現(xiàn)代碼為了徹底理解問題我構(gòu)建了一個(gè)最小復(fù)現(xiàn)案例SpringBootTest public class TransactionTest { Autowired private TestService testService; Test public void testAsyncBatch() { testService.mainMethod(); // 等待異步操作完成 Thread.sleep(3000); } } Service class TestService { Transactional public void mainMethod() { // 主線程插入 mainMapper.insert(new MainEntity()); CompletableFuture.runAsync(() - { // 模擬批量插入 ListSubEntity list generateData(100); subMapper.saveBatch(list); }); } }通過這個(gè)測試案例可以穩(wěn)定復(fù)現(xiàn)主表成功、子表失敗的情況。3.2 關(guān)鍵問題診斷使用調(diào)試模式跟蹤執(zhí)行過程發(fā)現(xiàn)了幾個(gè)關(guān)鍵現(xiàn)象主線程和異步線程使用的是不同的Connection對象異步線程中的saveBatch每次都會(huì)自動(dòng)提交如果異步操作拋出異常主事務(wù)不會(huì)回滾在MySQL的general_log中可以看到多個(gè)獨(dú)立的事務(wù)4. 解決方案設(shè)計(jì)與實(shí)現(xiàn)4.1 方案一使用編程式事務(wù)不推薦最直觀的解決方案是在異步線程中手動(dòng)管理事務(wù)CompletableFuture.runAsync(() - { TransactionTemplate transactionTemplate new TransactionTemplate(transactionManager); transactionTemplate.execute(status - { return orderItemService.saveBatch(items); }); }, executor);這種方案的缺點(diǎn)是代碼侵入性強(qiáng)需要手動(dòng)處理事務(wù)傳播行為與主事務(wù)仍然是分離的4.2 方案二使用TransactionSynchronizationManager推薦更優(yōu)雅的方案是利用Spring的事務(wù)同步機(jī)制Transactional public void processOrder(OrderDTO order) { orderMapper.insert(order); // 注冊事務(wù)同步 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { // 在主事務(wù)提交后執(zhí)行 orderItemService.saveBatch(convertToItems(order)); } } ); }這個(gè)方案的優(yōu)點(diǎn)是保證主事務(wù)提交后才執(zhí)行批量操作仍然保持異步執(zhí)行的優(yōu)勢代碼結(jié)構(gòu)清晰4.3 方案三使用事件監(jiān)聽機(jī)制分布式場景適用對于更復(fù)雜的系統(tǒng)可以考慮使用Spring的事件機(jī)制Transactional public void processOrder(OrderDTO order) { orderMapper.insert(order); applicationEventPublisher.publishEvent(new OrderCreatedEvent(order)); } Async TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void handleOrderCreatedEvent(OrderCreatedEvent event) { orderItemService.saveBatch(convertToItems(event.getOrder())); }這種方案的擴(kuò)展性更好適合未來可能需要的分布式事務(wù)場景。5. 生產(chǎn)環(huán)境驗(yàn)證與性能對比5.1 性能測試數(shù)據(jù)我們對三種方案進(jìn)行了壓測1000次調(diào)用批量插入100條記錄方案平均耗時(shí)(ms)成功率備注原始方案120070%數(shù)據(jù)不一致編程式事務(wù)1500100%性能較差事務(wù)同步1250100%推薦事件監(jiān)聽1300100%擴(kuò)展性好5.2 事務(wù)監(jiān)控配置為了確保方案可靠性我們配置了事務(wù)監(jiān)控# application.yml spring: datasource: hikari: pool-name: HikariCP register-mbeans: true jmx: enabled: true management: endpoints: web: exposure: include: health,info,metrics,prometheus metrics: export: prometheus: enabled: true通過Prometheus Grafana監(jiān)控事務(wù)相關(guān)指標(biāo)spring_transactions_activespring_transactions_committedspring_transactions_rollback6. 擴(kuò)展思考與最佳實(shí)踐6.1 MyBatis-Plus版本選擇經(jīng)過測試發(fā)現(xiàn)不同版本的MyBatis-Plus對批量操作的支持有差異3.4.x批量操作性能一般事務(wù)控制不夠靈活3.5.x優(yōu)化了批量插入邏輯推薦使用4.xAPI有較大變化需要評估遷移成本當(dāng)前推薦使用3.5.3版本與SpringBoot 2.7.x兼容性最好。6.2 批量操作優(yōu)化建議合理設(shè)置batchSize通常500-2000之間性能最佳考慮使用rewriteBatchedStatementstrueMySQL對于超大批量建議分片處理// 分片處理示例 ListListOrderItem partitions Lists.partition(items, 1000); partitions.forEach(partition - { orderItemService.saveBatch(partition); });6.3 事務(wù)設(shè)計(jì)原則保持事務(wù)短小精悍避免在事務(wù)中進(jìn)行遠(yuǎn)程調(diào)用異步操作要明確事務(wù)邊界對于關(guān)鍵業(yè)務(wù)添加補(bǔ)償機(jī)制7. 常見問題排查指南7.1 問題現(xiàn)象數(shù)據(jù)部分丟失排查步驟檢查是否跨線程操作查看數(shù)據(jù)庫連接池配置檢查Transactional注解位置查看MyBatis-Plus版本7.2 問題現(xiàn)象性能突然下降可能原因批量大小設(shè)置不合理沒有啟用批處理優(yōu)化事務(wù)隔離級別過高解決方案-- MySQL批處理優(yōu)化 SET GLOBAL max_allowed_packet256M; SET GLOBAL net_buffer_length1M;7.3 問題現(xiàn)象死鎖處理方法分析死鎖日志調(diào)整批量處理順序考慮使用樂觀鎖Version private Integer version;8. 個(gè)人實(shí)踐心得在實(shí)際項(xiàng)目中處理這個(gè)問題時(shí)我總結(jié)了幾個(gè)關(guān)鍵經(jīng)驗(yàn)不要輕信自動(dòng)提交很多開發(fā)者以為MyBatis-Plus的saveBatch會(huì)自動(dòng)參與當(dāng)前事務(wù)這是常見的誤解。實(shí)際上它的行為取決于具體場景。線程切換是事務(wù)的隱形殺手在微服務(wù)架構(gòu)中線程切換經(jīng)常發(fā)生如Feign調(diào)用、異步處理等要特別注意事務(wù)上下文是否延續(xù)。測試要包含失敗場景僅測試成功路徑是不夠的必須模擬各種異常情況特別是網(wǎng)絡(luò)抖動(dòng)、超時(shí)等邊界條件。監(jiān)控是最后防線無論設(shè)計(jì)多么完善生產(chǎn)環(huán)境總會(huì)出現(xiàn)意外。完善的事務(wù)監(jiān)控可以快速定位問題。文檔要注明限制在團(tuán)隊(duì)內(nèi)部文檔中我特別標(biāo)注了哪些方法必須在事務(wù)內(nèi)調(diào)用哪些可以異步處理避免了其他同事踩坑。這個(gè)案例讓我深刻認(rèn)識到框架的便利性有時(shí)會(huì)掩蓋底層復(fù)雜性。作為開發(fā)者我們需要在享受便利的同時(shí)保持對底層原理的好奇心和理解深度。特別是在并發(fā)和事務(wù)這種核心領(lǐng)域一點(diǎn)點(diǎn)的疏忽就可能導(dǎo)致嚴(yán)重的數(shù)據(jù)不一致問題。