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