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