成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

別在 MyBatis-Plus 里瞎用 @Transactional!這鍋背到我差點被踢出項目組

開發 前端
今兒個咱就掰開揉碎了聊,保證讓你看完再也不會在事務管理上栽跟頭,說不定還能反向指導新人避坑,妥妥的升職加薪小技巧??!

兄弟們,咱今天來嘮嘮 MyBatis-Plus 里那個讓人又愛又恨的 @Transactional 注解。先給大家講個真實的 "血淚教訓":上個月我在項目里一頓操作猛如虎,對著 Service 層狂甩 @Transactional 注解,自以為事務管理穩如老狗,結果線上接連爆雷 —— 訂單創建成功但庫存沒扣,用戶退款申請提交了但財務系統沒同步,最慘的是某天凌晨收到運維大哥的奪命連環 call,說數據庫連接池被撐爆了。當時項目經理看我的眼神,恨不得把我連人帶電腦一起踢出項目組...

痛定思痛之后,我抱著 MyBatis-Plus 官方文檔和 Spring 事務源碼啃了三天三夜,終于摸清了這個注解在 MyBatis-Plus 里的各種坑。今兒個咱就掰開揉碎了聊,保證讓你看完再也不會在事務管理上栽跟頭,說不定還能反向指導新人避坑,妥妥的升職加薪小技巧啊!

一、先搞明白:MyBatis-Plus 的事務和 Spring 到底啥關系?

好多小伙伴可能跟我當初一樣犯迷糊:MyBatis-Plus 不是 ORM 框架嗎,為啥要用 Spring 的 @Transactional 注解?這就得從 MyBatis-Plus 的出身說起了 —— 它本質上是 MyBatis 的增強工具,本身并不提供事務管理功能,而是完全依賴 Spring 的事務管理機制。所以咱們聊的 @Transactional,本質上還是 Spring 的注解,只不過在 MyBatis-Plus 的使用場景里,有些細節需要特別注意。

1.1 底層原理:Spring 是怎么玩轉事務的?

這里咱用個接地氣的比喻:Spring 的事務管理就像一場舞臺劇,@Transactional 注解就是導演給演員(方法)貼的標簽,告訴幕后的 AOP 代理(替身演員):"這一段戲需要開啟事務,要是演砸了(拋異常)得回滾??!" 具體來說:

  • 代理模式:Spring 會給加了 @Transactional 的類生成動態代理(JDK 代理或 CGLIB 代理),當調用目標方法時,實際執行的是代理類中的事務增強邏輯。
  • 事務攔截器:org.springframework.transaction.interceptor.TransactionInterceptor 是核心攔截器,它會在方法執行前開啟事務,執行過程中監控異常,執行完畢后根據情況提交或回滾事務。
  • 數據源綁定:通過 ThreadLocal 將數據庫連接與當前線程綁定,確保同一個事務內使用的是同一個數據庫連接。

這里有個關鍵知識點:MyBatis-Plus 的 Mapper 方法在執行時,必須通過 Spring 容器獲取的 Mapper 代理對象調用,才能保證處于 Spring 的事務管理范圍內。如果你在代碼里自己 new 了一個 Mapper 實例(雖然正常人不會這么干,但咱得防著新手踩坑),那事務肯定不會生效。

1.2 MyBatis-Plus 的特殊點:這些操作會影響事務嗎?

咱都知道 MyBatis-Plus 有很多貼心的增強功能,比如自動填充、樂觀鎖、邏輯刪除等,這些功能在事務中會不會搞事情呢?

  • 自動填充(MetaObjectHandler):放心,自動填充是在 Mapper 執行 SQL 之前完成的,屬于業務邏輯的一部分,只要在事務方法內調用 Mapper,填充的數據會跟著事務一起提交或回滾。
  • 樂觀鎖(@Version):重點來了!當使用樂觀鎖時,MyBatis-Plus 會在更新語句中添加版本號校驗條件。如果在事務中多個線程同時更新同一條數據,后提交的線程會因為版本號不一致導致更新失敗,此時事務會回滾,這是正?,F象,不是 bug 哦。
  • 邏輯刪除(@TableLogic):邏輯刪除本質上是執行 update 語句,將 deleted 字段標記為 1,和普通的 update 操作一樣,受事務管理控制,刪除操作會在事務提交時生效。

二、這些 "想當然" 的操作,分分鐘讓事務失效!

2.1 同類方法調用:別以為加了注解就萬事大吉

先看一段讓我栽跟頭的代碼:

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockService stockService;
    // 外層方法加了事務
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO orderDTO) {
        OrderEntity orderEntity = convertToEntity(orderDTO);
        orderMapper.insert(orderEntity); // 插入訂單
        updateStock(orderDTO.getProductId(), orderDTO.getQuantity()); // 調用內部方法扣庫存
    }
    // 內部方法沒加注解,以為會跟著外層事務走
    private void updateStock(Long productId, Integer quantity) {
        StockEntity stock = stockMapper.selectById(productId);
        if (stock.getStockQuantity() < quantity) {
            throw new BusinessException("庫存不足");
        }
        stock.setStockQuantity(stock.getStockQuantity() - quantity);
        stockMapper.updateById(stock); // 這里出問題了!
    }
}

看起來挺合理吧?外層方法有事務,內部方法應該跟著一起回滾。但實際情況是:當 updateStock 拋異常時,訂單數據居然已經插入數據庫了!為啥呢?原因解析:Spring 的 AOP 代理是基于接口或類的,當在同一個類中調用另一個方法時,實際上是通過 this 引用調用的,而不是通過代理對象調用。這時候,事務增強邏輯就不會生效,內部方法相當于在事務之外執行。

解決方案

  • 方法上移:把 updateStock 的邏輯直接寫在外層方法里,別搞內部調用。
  • 自我注入:在類中注入自己,通過代理對象調用方法:
@Service
public class OrderService {
    @Autowired
    private OrderService self; // 注入自己
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO orderDTO) {
        OrderEntity orderEntity = convertToEntity(orderDTO);
        orderMapper.insert(orderEntity);
        self.updateStock(orderDTO.getProductId(), orderDTO.getQuantity()); // 通過代理對象調用
    }
    private void updateStock(Long productId, Integer quantity) {
        // ... 邏輯不變
    }
}

不過這種方法有點反直覺,建議還是盡量避免同類內的方法調用,保持事務方法的原子性。

2.2 不同數據源:多數據源場景下事務會 "迷路"

現在微服務架構里,多數據源場景很常見,比如主庫寫、從庫讀,或者分庫分表。這時候如果在一個事務里操作多個數據源,@Transactional 還能生效嗎?

先看配置:

@Configuration
public class DataSourceConfig {
    @Bean("masterDataSource")
    @Primary
    public DataSource masterDataSource() {
        // 主數據源配置
    }
    @Bean("slaveDataSource")
    public DataSource slaveDataSource() {
        // 從數據源配置
    }
    @Bean("masterTransactionManager")
    @Primary
    public DataSourceTransactionManager masterTransactionManager(
            @Qualifier("masterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    @Bean("slaveTransactionManager")
    public DataSourceTransactionManager slaveTransactionManager(
            @Qualifier("slaveDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

然后在 Service 里:

@Service
public class MultiDataSourceService {
    @Autowired
    private MasterOrderMapper masterOrderMapper;
    @Autowired
    private SlaveStockMapper slaveStockMapper;
    // 以為加了@Transactional就能跨庫事務
    @Transactional(rollbackFor = Exception.class)
    public void updateBoth(String orderNo, Integer stockId) {
        MasterOrderEntity order = masterOrderMapper.selectByOrderNo(orderNo);
        order.setStatus("已支付");
        masterOrderMapper.updateById(order);
        SlaveStockEntity stock = slaveStockMapper.selectById(stockId);
        stock.setStockStatus("已扣除");
        slaveStockMapper.updateById(stock);
        if (orderNo.equals("異常訂單")) {
            throw new RuntimeException("模擬異常");
        }
    }
}

實際運行結果:主庫的訂單狀態更新了,但從庫的庫存狀態沒回滾!為啥呢?因為 @Transactional 默認使用的是主數據源的事務管理器,而跨數據源的事務需要分布式事務解決方案(如 Seata、XA 協議等),單純的 @Transactional 搞不定。解決方案

  • 明確指定事務管理器:如果只是操作同一個數據源內的不同庫(比如分庫),可以通過 @Transactional (transactionManager = "masterTransactionManager") 指定正確的事務管理器。
  • 分布式事務方案:涉及多個獨立數據源時,必須使用分布式事務框架,別指望 Spring 的本地事務能搞定。

2.3 枚舉類型坑:數據庫類型和 Java 枚舉對不上,事務不回滾

MyBatis-Plus 支持將 Java 枚舉映射到數據庫字段,比如:

public enum OrderStatus {
    NEW(0, "新建"),
    PAID(1, "已支付"),
    CANCELLED(2, "已取消");
    private Integer code;
    private String desc;
    // 構造器和getter省略
}
@TableField(typeHandler = OrderStatusTypeHandler.class)
private OrderStatus status;

然后在 Service 里:

@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(Long orderId, Integer statusCode) {
    OrderEntity order = orderMapper.selectById(orderId);
    order.setStatus(OrderStatus.valueOf(statusCode)); // 這里可能拋IllegalArgumentException
    orderMapper.updateById(order);
    // 假設后面還有其他操作
    int i = 1 / 0; // 模擬除零異常
}

當 statusCode 傳了一個不存在的枚舉值時,OrderStatus.valueOf 會拋 IllegalArgumentException,按理說整個事務應該回滾。但實際情況是:訂單狀態可能已經更新了,后面的除零異常導致方法報錯,但事務沒回滾!原因解析:Spring 默認只對 RuntimeException 及其子類回滾,CheckedException 不回滾。而 IllegalArgumentException 是 RuntimeException,按道理應該回滾啊?問題出在 MyBatis-Plus 的 TypeHandler 上,當枚舉映射錯誤時,MyBatis 會在執行 SQL 之前就拋出異常,此時事務可能還沒真正開啟,或者說異常發生在事務增強邏輯之外。

解決方案

  • 顯式指定回滾異常:@Transactional (rollbackFor = {Exception.class, IllegalArgumentException.class}),雖然有點粗暴,但能確保所有異常都回滾。
  • 提前校驗參數:在設置枚舉值之前,先檢查 statusCode 是否合法,避免在 MyBatis 處理時拋異常。

2.4 序列化陷阱:事務方法參數或返回值不能序列化

在分布式環境下(比如 Spring Cloud),如果 @Transactional 方法的參數或返回值包含無法序列化的對象,會導致事務失效,甚至應用啟動報錯。比如:

@Service
public class UserService {
    @Transactional(rollbackFor = Exception.class)
    public UserEntity updateUser(UserEntity user) {
        // 假設UserEntity里有一個ThreadLocal類型的字段
        userMapper.updateById(user);
        return user;
    }
}

如果 UserEntity 包含 ThreadLocal、Socket 等無法序列化的字段,當通過 RMI、Feign 等遠程調用時,就會報錯。雖然這不是 MyBatis-Plus 特有的問題,但在分布式場景下經常和 MyBatis-Plus 一起出現,必須注意。解決方案

  • 確保實體類可序列化:讓實體類實現 Serializable 接口,并且所有字段都是可序列化的。
  • 避免在遠程接口中使用事務方法:事務方法盡量只在本地調用,遠程接口專注于業務邏輯,別加 @Transactional。

三、深度解析:@Transactional 的核心參數,你真的懂嗎?

3.1 propagation:事務傳播行為,組隊打副本的策略

這是最容易搞錯的參數,決定了多個事務方法嵌套調用時的行為。類比組隊打副本:

  • REQUIRED(默認值):如果當前有事務,就加入;沒有就創建新事務。就像組隊打 BOSS,你要是已經在隊伍里,就跟著一起打;沒隊伍就自己建一個。
  • REQUIRES_NEW:不管有沒有事務,都創建新事務,掛起當前事務。相當于自己開個新隊伍,不管原來有沒有隊伍。
  • SUPPORTS:支持當前事務,沒有就以非事務方式執行。就是能組隊就組隊,組不了就單刷。
  • NOT_SUPPORTED:不支持事務,掛起當前事務,以非事務方式執行。就是拒絕組隊,自己單刷。
  • NEVER:必須沒有事務,否則拋異常。就是堅決不組隊,看到隊伍就跑。
  • NESTED:嵌套事務,在一個事務中開啟子事務,子事務回滾不影響父事務,父事務回滾子事務跟著回滾。相當于副本里的小 BOSS 戰,小 BOSS 滅了可以重來,整個副本失敗就全完。

舉個栗子:

@Transactional(propagation = Propagation.REQUIRED)
public void parentMethod() {
    childMethod();
    // 這里拋異常,parent和child都回滾
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
    // 這里拋異常,child回滾,parent不受影響(如果parent在child之后拋異常,child還是會回滾,因為parent的事務包含child)
}

注意:NESTED 需要數據庫支持,比如 MySQL 的 InnoDB 引擎支持 Savepoint 實現嵌套事務,而 REQUIRES_NEW 是通過新的事務連接實現的,性能上會有差異。

3.2 isolation:事務隔離級別,解決并發問題的關鍵

默認值是 Isolation.DEFAULT(使用數據庫默認隔離級別,MySQL 是 REPEATABLE_READ,Oracle 是 READ_COMMITTED)。常見的隔離級別:

  • READ_UNCOMMITTED:讀未提交,可能出現臟讀、不可重復讀、幻讀。
  • READ_COMMITTED:讀已提交,避免臟讀,可能出現不可重復讀、幻讀。
  • REPEATABLE_READ:可重復讀,避免臟讀、不可重復讀,可能出現幻讀(MySQL 通過 MVCC 解決了幻讀,Oracle 沒有)。
  • SERIALIZABLE:串行化,最高隔離級別,避免所有并發問題,但性能最差。

在 MyBatis-Plus 中使用時要注意:

  • 高隔離級別會影響性能,別一拍腦袋就設為 SERIALIZABLE。
  • 如果業務中存在幻讀風險(比如統計數據時插入新數據),MySQL 可以用 REPEATABLE_READ,Oracle 需要用 SERIALIZABLE 或應用層加鎖。

3.3 timeout:事務超時時間,別讓事務一直 "掛機"

默認值是 - 1(永不超時),但在實際項目中,必須設置合理的超時時間,避免長事務占用數據庫連接,導致連接池耗盡(這就是我之前踩的坑!)。比如:

@Transactional(timeout = 30) // 30秒超時
public void longRunningTransaction() {
    // 長時間運行的操作,比如批量插入、復雜計算
}

設置時要根據業務邏輯估算最長執行時間,預留一定緩沖,比如批量插入 10 萬條數據,測試發現平均 20 秒完成,超時時間可以設 30 秒。

3.4 readOnly:只讀事務,提升性能的小技巧

如果方法只是查詢數據,沒有寫操作,建議設置 readOnly=true:

@Transactional(readOnly = true)
public List<OrderEntity> listOrders() {
    return orderMapper.selectList(null);
}

這樣做有兩個好處:

  • 數據庫可以做優化,比如 MySQL 在只讀事務中不記錄 binlog(如果開啟了 binlog_format=ROW)。
  • 提醒開發者這個方法是只讀的,避免誤操作添加寫邏輯。

四、MyBatis-Plus 事務最佳實踐:這樣寫才規范

4.1 事務范圍:能小則小,別搞 "大包圍"

很多新手喜歡在 Service 層的入口方法上加 @Transactional,把整個方法都包在事務里,包括日志記錄、參數校驗、遠程調用等和數據庫無關的操作。這會導致事務持續時間過長,增加鎖競爭和超時風險。正確的做法是:

  • 事務只包裹真正的數據庫操作,無關邏輯放在事務外。
  • 批量操作時,合理拆分批次,避免單次事務處理太多數據(比如每次處理 1000 條數據,提交一次事務)。

4.2 異常處理:別吞掉回滾的 "信號"

@Transactional(rollbackFor = Exception.class)
public void processOrder() {
    try {
        // 數據庫操作
        orderMapper.insert(order);
        // 模擬異常
        int i = 1 / 0;
    } catch (Exception e) {
        // 錯誤做法:吞掉異常,不拋出去
        log.error("處理訂單失敗", e);
    }
}

這樣寫的后果是:事務不會回滾,因為 Spring 是根據方法是否拋異常來決定是否回滾的,異常被捕獲且沒有重新拋出,事務會認為執行成功,正常提交。正確的做法是:

@Transactional(rollbackFor = Exception.class)
public void processOrder() {
    try {
        orderMapper.insert(order);
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("處理訂單失敗", e);
        // 必須重新拋出異常,或者調用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        throw new RuntimeException("處理訂單失敗", e);
    }
}

或者在不需要處理異常的情況下,直接讓異常往上拋,別在事務方法內捕獲后不處理。

4.3 字段更新:注意 MyBatis-Plus 的更新策略

MyBatis-Plus 的 updateById、update 等方法,默認會更新所有非 null 字段,這在事務中可能導致意外結果。比如:

@Transactional(rollbackFor = Exception.class)
public void updateUserPartial(Long userId, UserEntity user) {
    // 假設user只有name字段有值,其他字段為null
    user.setId(userId);
    userMapper.updateById(user); // 會更新所有非null字段,包括name,其他字段不會被更新
}

這本來是正常行為,但如果在事務中,多個線程同時更新同一個用戶的不同字段,可能會出現更新順序問題。建議明確使用 updateWrapper,指定需要更新的字段:

userMapper.update(user, Wrappers.<UserEntity>update().set("name", user.getName()).eq("id", userId));

這樣更清晰,也避免因實體類字段變化導致的意外更新。

4.4 分布式場景:別把本地事務當萬能藥

前面提到的多數據源問題,本質上是分布式事務范疇。在微服務架構中,如果涉及跨服務、跨數據源的事務,必須使用分布式事務解決方案。這里簡單提一下常見方案:

  • TCC 模式(Try-Confirm-Cancel):適合強一致性場景,比如資金轉賬,實現復雜度高。
  • 可靠消息最終一致性:通過消息中間件保證事務最終一致,適合異步場景,比如訂單創建后通知庫存服務扣庫存。
  • Seata 框架:阿里巴巴開源的分布式事務解決方案,支持 AT 模式(自動生成回滾日志)、TCC 模式等,和 Spring Cloud 集成良好。

4.5 監控和日志:讓事務問題無處遁形

  • 開啟事務日志:在 application.properties 中配置:
# Spring事務日志
logging.level.org.springframework.transaction=DEBUG
# MyBatis-Plus執行日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

這樣可以在控制臺看到事務的開啟、提交、回滾過程,以及執行的 SQL 語句,方便排查問題。

  • 自定義注解和切面:可以寫一個 @TransactionalLog 注解,結合 AOP 記錄事務方法的執行時間、參數、異常信息等,幫助監控事務性能和異常情況。

五、總結:用好 @Transactional 的 "三板斧"

  1. 搞懂原理:知道 Spring 事務是怎么通過 AOP 代理和數據庫連接綁定實現的,明白 MyBatis-Plus 只是 ORM 工具,事務管理靠 Spring。
  2. 避開陷阱:記住同類方法調用、多數據源、枚舉類型、序列化這些常見坑,寫代碼時多檢查。
  3. 規范使用:合理設置事務傳播行為、隔離級別、超時時間,控制事務范圍,做好異常處理,分布式場景用對方案。
責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2024-12-20 16:49:15

MyBatis開發代碼

2023-10-31 08:01:48

Mybatis參數jdbcurl?

2025-02-27 09:45:47

2023-06-07 08:08:37

MybatisSpringBoot

2023-06-14 08:34:18

Mybatis死鎖框架

2023-07-29 22:02:06

MyBatis數據庫配置

2023-06-07 08:00:00

MySQL批量插入

2025-05-26 03:20:00

SpringMyBatis數據權限

2024-02-28 09:35:52

2024-07-31 09:56:20

2024-11-28 19:03:56

2023-01-12 09:13:49

Mybatis數據庫

2025-02-13 07:59:13

2023-01-17 09:13:08

Mybatis后端框架

2025-02-06 07:45:44

2023-03-27 07:39:07

內存溢出優化

2023-05-14 22:25:33

內存CPU

2022-05-20 12:24:45

分庫分表Java依賴

2021-09-29 08:23:56

項目css

2018-10-19 16:35:20

運維
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲欧美激情视频 | 久久国产欧美日韩精品 | 免费中文字幕日韩欧美 | 日韩成人中文字幕 | 国产精品久久久久久久久久久久 | 天堂视频中文在线 | av网站免费观看 | 国产精品a久久久久 | 91精品国产乱码久久久久久久久 | 成人在线视频免费观看 | 国产成人精品一区二区三区四区 | 精品九九| 五月天婷婷丁香 | 狠狠操狠狠干 | 成人一区二区三区在线观看 | 国产日韩欧美激情 | 亚洲国产二区 | 99精品免费在线观看 | 在线播放中文字幕 | 精品久久久久久久久久 | 国产精品高潮呻吟久久 | 成人黄色在线观看 | 91精品久久久久久久99 | 国产在线观看一区二区三区 | 亚洲成人99| 欧美一区二区大片 | 日韩精品国产精品 | 一区二区电影 | 久久久久国产精品一区二区 | 日韩免费高清视频 | 国产精品精品视频一区二区三区 | 精品一区二区免费视频 | 色狠狠一区 | 韩日三级| 国产一级毛片视频 | 狠狠干影院 | 国产一二三区在线 | 久久久久久国 | 国产精品日韩高清伦字幕搜索 | 亚洲天堂男人的天堂 | 韩国主播午夜大尺度福利 |