別在 MyBatis-Plus 里瞎用 @Transactional!這鍋背到我差點(diǎn)被踢出項(xiàng)目組
兄弟們,咱今天來嘮嘮 MyBatis-Plus 里那個(gè)讓人又愛又恨的 @Transactional 注解。先給大家講個(gè)真實(shí)的 "血淚教訓(xùn)":上個(gè)月我在項(xiàng)目里一頓操作猛如虎,對(duì)著 Service 層狂甩 @Transactional 注解,自以為事務(wù)管理穩(wěn)如老狗,結(jié)果線上接連爆雷 —— 訂單創(chuàng)建成功但庫存沒扣,用戶退款申請(qǐng)?zhí)峤涣说?cái)務(wù)系統(tǒng)沒同步,最慘的是某天凌晨收到運(yùn)維大哥的奪命連環(huán) call,說數(shù)據(jù)庫連接池被撐爆了。當(dāng)時(shí)項(xiàng)目經(jīng)理看我的眼神,恨不得把我連人帶電腦一起踢出項(xiàng)目組...
痛定思痛之后,我抱著 MyBatis-Plus 官方文檔和 Spring 事務(wù)源碼啃了三天三夜,終于摸清了這個(gè)注解在 MyBatis-Plus 里的各種坑。今兒個(gè)咱就掰開揉碎了聊,保證讓你看完再也不會(huì)在事務(wù)管理上栽跟頭,說不定還能反向指導(dǎo)新人避坑,妥妥的升職加薪小技巧?。?/span>
一、先搞明白:MyBatis-Plus 的事務(wù)和 Spring 到底啥關(guān)系?
好多小伙伴可能跟我當(dāng)初一樣犯迷糊:MyBatis-Plus 不是 ORM 框架嗎,為啥要用 Spring 的 @Transactional 注解?這就得從 MyBatis-Plus 的出身說起了 —— 它本質(zhì)上是 MyBatis 的增強(qiáng)工具,本身并不提供事務(wù)管理功能,而是完全依賴 Spring 的事務(wù)管理機(jī)制。所以咱們聊的 @Transactional,本質(zhì)上還是 Spring 的注解,只不過在 MyBatis-Plus 的使用場(chǎng)景里,有些細(xì)節(jié)需要特別注意。
1.1 底層原理:Spring 是怎么玩轉(zhuǎn)事務(wù)的?
這里咱用個(gè)接地氣的比喻:Spring 的事務(wù)管理就像一場(chǎng)舞臺(tái)劇,@Transactional 注解就是導(dǎo)演給演員(方法)貼的標(biāo)簽,告訴幕后的 AOP 代理(替身演員):"這一段戲需要開啟事務(wù),要是演砸了(拋異常)得回滾??!" 具體來說:
- 代理模式:Spring 會(huì)給加了 @Transactional 的類生成動(dòng)態(tài)代理(JDK 代理或 CGLIB 代理),當(dāng)調(diào)用目標(biāo)方法時(shí),實(shí)際執(zhí)行的是代理類中的事務(wù)增強(qiáng)邏輯。
- 事務(wù)攔截器:org.springframework.transaction.interceptor.TransactionInterceptor 是核心攔截器,它會(huì)在方法執(zhí)行前開啟事務(wù),執(zhí)行過程中監(jiān)控異常,執(zhí)行完畢后根據(jù)情況提交或回滾事務(wù)。
- 數(shù)據(jù)源綁定:通過 ThreadLocal 將數(shù)據(jù)庫連接與當(dāng)前線程綁定,確保同一個(gè)事務(wù)內(nèi)使用的是同一個(gè)數(shù)據(jù)庫連接。
這里有個(gè)關(guān)鍵知識(shí)點(diǎn):MyBatis-Plus 的 Mapper 方法在執(zhí)行時(shí),必須通過 Spring 容器獲取的 Mapper 代理對(duì)象調(diào)用,才能保證處于 Spring 的事務(wù)管理范圍內(nèi)。如果你在代碼里自己 new 了一個(gè) Mapper 實(shí)例(雖然正常人不會(huì)這么干,但咱得防著新手踩坑),那事務(wù)肯定不會(huì)生效。
1.2 MyBatis-Plus 的特殊點(diǎn):這些操作會(huì)影響事務(wù)嗎?
咱都知道 MyBatis-Plus 有很多貼心的增強(qiáng)功能,比如自動(dòng)填充、樂觀鎖、邏輯刪除等,這些功能在事務(wù)中會(huì)不會(huì)搞事情呢?
- 自動(dòng)填充(MetaObjectHandler):放心,自動(dòng)填充是在 Mapper 執(zhí)行 SQL 之前完成的,屬于業(yè)務(wù)邏輯的一部分,只要在事務(wù)方法內(nèi)調(diào)用 Mapper,填充的數(shù)據(jù)會(huì)跟著事務(wù)一起提交或回滾。
- 樂觀鎖(@Version):重點(diǎn)來了!當(dāng)使用樂觀鎖時(shí),MyBatis-Plus 會(huì)在更新語句中添加版本號(hào)校驗(yàn)條件。如果在事務(wù)中多個(gè)線程同時(shí)更新同一條數(shù)據(jù),后提交的線程會(huì)因?yàn)榘姹咎?hào)不一致導(dǎo)致更新失敗,此時(shí)事務(wù)會(huì)回滾,這是正?,F(xiàn)象,不是 bug 哦。
- 邏輯刪除(@TableLogic):邏輯刪除本質(zhì)上是執(zhí)行 update 語句,將 deleted 字段標(biāo)記為 1,和普通的 update 操作一樣,受事務(wù)管理控制,刪除操作會(huì)在事務(wù)提交時(shí)生效。
二、這些 "想當(dāng)然" 的操作,分分鐘讓事務(wù)失效!
2.1 同類方法調(diào)用:別以為加了注解就萬事大吉
先看一段讓我栽跟頭的代碼:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockService stockService;
// 外層方法加了事務(wù)
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
OrderEntity orderEntity = convertToEntity(orderDTO);
orderMapper.insert(orderEntity); // 插入訂單
updateStock(orderDTO.getProductId(), orderDTO.getQuantity()); // 調(diào)用內(nèi)部方法扣庫存
}
// 內(nèi)部方法沒加注解,以為會(huì)跟著外層事務(wù)走
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); // 這里出問題了!
}
}
看起來挺合理吧?外層方法有事務(wù),內(nèi)部方法應(yīng)該跟著一起回滾。但實(shí)際情況是:當(dāng) updateStock 拋異常時(shí),訂單數(shù)據(jù)居然已經(jīng)插入數(shù)據(jù)庫了!為啥呢?原因解析:Spring 的 AOP 代理是基于接口或類的,當(dāng)在同一個(gè)類中調(diào)用另一個(gè)方法時(shí),實(shí)際上是通過 this 引用調(diào)用的,而不是通過代理對(duì)象調(diào)用。這時(shí)候,事務(wù)增強(qiáng)邏輯就不會(huì)生效,內(nèi)部方法相當(dāng)于在事務(wù)之外執(zhí)行。
解決方案:
- 方法上移:把 updateStock 的邏輯直接寫在外層方法里,別搞內(nèi)部調(diào)用。
- 自我注入:在類中注入自己,通過代理對(duì)象調(diào)用方法:
@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()); // 通過代理對(duì)象調(diào)用
}
private void updateStock(Long productId, Integer quantity) {
// ... 邏輯不變
}
}
不過這種方法有點(diǎn)反直覺,建議還是盡量避免同類內(nèi)的方法調(diào)用,保持事務(wù)方法的原子性。
2.2 不同數(shù)據(jù)源:多數(shù)據(jù)源場(chǎng)景下事務(wù)會(huì) "迷路"
現(xiàn)在微服務(wù)架構(gòu)里,多數(shù)據(jù)源場(chǎng)景很常見,比如主庫寫、從庫讀,或者分庫分表。這時(shí)候如果在一個(gè)事務(wù)里操作多個(gè)數(shù)據(jù)源,@Transactional 還能生效嗎?
先看配置:
@Configuration
public class DataSourceConfig {
@Bean("masterDataSource")
@Primary
public DataSource masterDataSource() {
// 主數(shù)據(jù)源配置
}
@Bean("slaveDataSource")
public DataSource slaveDataSource() {
// 從數(shù)據(jù)源配置
}
@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就能跨庫事務(wù)
@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("模擬異常");
}
}
}
實(shí)際運(yùn)行結(jié)果:主庫的訂單狀態(tài)更新了,但從庫的庫存狀態(tài)沒回滾!為啥呢?因?yàn)?@Transactional 默認(rèn)使用的是主數(shù)據(jù)源的事務(wù)管理器,而跨數(shù)據(jù)源的事務(wù)需要分布式事務(wù)解決方案(如 Seata、XA 協(xié)議等),單純的 @Transactional 搞不定。解決方案:
- 明確指定事務(wù)管理器:如果只是操作同一個(gè)數(shù)據(jù)源內(nèi)的不同庫(比如分庫),可以通過 @Transactional (transactionManager = "masterTransactionManager") 指定正確的事務(wù)管理器。
- 分布式事務(wù)方案:涉及多個(gè)獨(dú)立數(shù)據(jù)源時(shí),必須使用分布式事務(wù)框架,別指望 Spring 的本地事務(wù)能搞定。
2.3 枚舉類型坑:數(shù)據(jù)庫類型和 Java 枚舉對(duì)不上,事務(wù)不回滾
MyBatis-Plus 支持將 Java 枚舉映射到數(shù)據(jù)庫字段,比如:
public enum OrderStatus {
NEW(0, "新建"),
PAID(1, "已支付"),
CANCELLED(2, "已取消");
private Integer code;
private String desc;
// 構(gòu)造器和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);
// 假設(shè)后面還有其他操作
int i = 1 / 0; // 模擬除零異常
}
當(dāng) statusCode 傳了一個(gè)不存在的枚舉值時(shí),OrderStatus.valueOf 會(huì)拋 IllegalArgumentException,按理說整個(gè)事務(wù)應(yīng)該回滾。但實(shí)際情況是:訂單狀態(tài)可能已經(jīng)更新了,后面的除零異常導(dǎo)致方法報(bào)錯(cuò),但事務(wù)沒回滾!原因解析:Spring 默認(rèn)只對(duì) RuntimeException 及其子類回滾,CheckedException 不回滾。而 IllegalArgumentException 是 RuntimeException,按道理應(yīng)該回滾啊?問題出在 MyBatis-Plus 的 TypeHandler 上,當(dāng)枚舉映射錯(cuò)誤時(shí),MyBatis 會(huì)在執(zhí)行 SQL 之前就拋出異常,此時(shí)事務(wù)可能還沒真正開啟,或者說異常發(fā)生在事務(wù)增強(qiáng)邏輯之外。
解決方案:
- 顯式指定回滾異常:@Transactional (rollbackFor = {Exception.class, IllegalArgumentException.class}),雖然有點(diǎn)粗暴,但能確保所有異常都回滾。
- 提前校驗(yàn)參數(shù):在設(shè)置枚舉值之前,先檢查 statusCode 是否合法,避免在 MyBatis 處理時(shí)拋異常。
2.4 序列化陷阱:事務(wù)方法參數(shù)或返回值不能序列化
在分布式環(huán)境下(比如 Spring Cloud),如果 @Transactional 方法的參數(shù)或返回值包含無法序列化的對(duì)象,會(huì)導(dǎo)致事務(wù)失效,甚至應(yīng)用啟動(dòng)報(bào)錯(cuò)。比如:
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public UserEntity updateUser(UserEntity user) {
// 假設(shè)UserEntity里有一個(gè)ThreadLocal類型的字段
userMapper.updateById(user);
return user;
}
}
如果 UserEntity 包含 ThreadLocal、Socket 等無法序列化的字段,當(dāng)通過 RMI、Feign 等遠(yuǎn)程調(diào)用時(shí),就會(huì)報(bào)錯(cuò)。雖然這不是 MyBatis-Plus 特有的問題,但在分布式場(chǎng)景下經(jīng)常和 MyBatis-Plus 一起出現(xiàn),必須注意。解決方案:
- 確保實(shí)體類可序列化:讓實(shí)體類實(shí)現(xiàn) Serializable 接口,并且所有字段都是可序列化的。
- 避免在遠(yuǎn)程接口中使用事務(wù)方法:事務(wù)方法盡量只在本地調(diào)用,遠(yuǎn)程接口專注于業(yè)務(wù)邏輯,別加 @Transactional。
三、深度解析:@Transactional 的核心參數(shù),你真的懂嗎?
3.1 propagation:事務(wù)傳播行為,組隊(duì)打副本的策略
這是最容易搞錯(cuò)的參數(shù),決定了多個(gè)事務(wù)方法嵌套調(diào)用時(shí)的行為。類比組隊(duì)打副本:
- REQUIRED(默認(rèn)值):如果當(dāng)前有事務(wù),就加入;沒有就創(chuàng)建新事務(wù)。就像組隊(duì)打 BOSS,你要是已經(jīng)在隊(duì)伍里,就跟著一起打;沒隊(duì)伍就自己建一個(gè)。
- REQUIRES_NEW:不管有沒有事務(wù),都創(chuàng)建新事務(wù),掛起當(dāng)前事務(wù)。相當(dāng)于自己開個(gè)新隊(duì)伍,不管原來有沒有隊(duì)伍。
- SUPPORTS:支持當(dāng)前事務(wù),沒有就以非事務(wù)方式執(zhí)行。就是能組隊(duì)就組隊(duì),組不了就單刷。
- NOT_SUPPORTED:不支持事務(wù),掛起當(dāng)前事務(wù),以非事務(wù)方式執(zhí)行。就是拒絕組隊(duì),自己?jiǎn)嗡ⅰ?/span>
- NEVER:必須沒有事務(wù),否則拋異常。就是堅(jiān)決不組隊(duì),看到隊(duì)伍就跑。
- NESTED:嵌套事務(wù),在一個(gè)事務(wù)中開啟子事務(wù),子事務(wù)回滾不影響父事務(wù),父事務(wù)回滾子事務(wù)跟著回滾。相當(dāng)于副本里的小 BOSS 戰(zhàn),小 BOSS 滅了可以重來,整個(gè)副本失敗就全完。
舉個(gè)栗子:
@Transactional(propagation = Propagation.REQUIRED)
public void parentMethod() {
childMethod();
// 這里拋異常,parent和child都回滾
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void childMethod() {
// 這里拋異常,child回滾,parent不受影響(如果parent在child之后拋異常,child還是會(huì)回滾,因?yàn)閜arent的事務(wù)包含child)
}
注意:NESTED 需要數(shù)據(jù)庫支持,比如 MySQL 的 InnoDB 引擎支持 Savepoint 實(shí)現(xiàn)嵌套事務(wù),而 REQUIRES_NEW 是通過新的事務(wù)連接實(shí)現(xiàn)的,性能上會(huì)有差異。
3.2 isolation:事務(wù)隔離級(jí)別,解決并發(fā)問題的關(guān)鍵
默認(rèn)值是 Isolation.DEFAULT(使用數(shù)據(jù)庫默認(rèn)隔離級(jí)別,MySQL 是 REPEATABLE_READ,Oracle 是 READ_COMMITTED)。常見的隔離級(jí)別:
- READ_UNCOMMITTED:讀未提交,可能出現(xiàn)臟讀、不可重復(fù)讀、幻讀。
- READ_COMMITTED:讀已提交,避免臟讀,可能出現(xiàn)不可重復(fù)讀、幻讀。
- REPEATABLE_READ:可重復(fù)讀,避免臟讀、不可重復(fù)讀,可能出現(xiàn)幻讀(MySQL 通過 MVCC 解決了幻讀,Oracle 沒有)。
- SERIALIZABLE:串行化,最高隔離級(jí)別,避免所有并發(fā)問題,但性能最差。
在 MyBatis-Plus 中使用時(shí)要注意:
- 高隔離級(jí)別會(huì)影響性能,別一拍腦袋就設(shè)為 SERIALIZABLE。
- 如果業(yè)務(wù)中存在幻讀風(fēng)險(xiǎn)(比如統(tǒng)計(jì)數(shù)據(jù)時(shí)插入新數(shù)據(jù)),MySQL 可以用 REPEATABLE_READ,Oracle 需要用 SERIALIZABLE 或應(yīng)用層加鎖。
3.3 timeout:事務(wù)超時(shí)時(shí)間,別讓事務(wù)一直 "掛機(jī)"
默認(rèn)值是 - 1(永不超時(shí)),但在實(shí)際項(xiàng)目中,必須設(shè)置合理的超時(shí)時(shí)間,避免長(zhǎng)事務(wù)占用數(shù)據(jù)庫連接,導(dǎo)致連接池耗盡(這就是我之前踩的坑?。?。比如:
@Transactional(timeout = 30) // 30秒超時(shí)
public void longRunningTransaction() {
// 長(zhǎng)時(shí)間運(yùn)行的操作,比如批量插入、復(fù)雜計(jì)算
}
設(shè)置時(shí)要根據(jù)業(yè)務(wù)邏輯估算最長(zhǎng)執(zhí)行時(shí)間,預(yù)留一定緩沖,比如批量插入 10 萬條數(shù)據(jù),測(cè)試發(fā)現(xiàn)平均 20 秒完成,超時(shí)時(shí)間可以設(shè) 30 秒。
3.4 readOnly:只讀事務(wù),提升性能的小技巧
如果方法只是查詢數(shù)據(jù),沒有寫操作,建議設(shè)置 readOnly=true:
@Transactional(readOnly = true)
public List<OrderEntity> listOrders() {
return orderMapper.selectList(null);
}
這樣做有兩個(gè)好處:
- 數(shù)據(jù)庫可以做優(yōu)化,比如 MySQL 在只讀事務(wù)中不記錄 binlog(如果開啟了 binlog_format=ROW)。
- 提醒開發(fā)者這個(gè)方法是只讀的,避免誤操作添加寫邏輯。
四、MyBatis-Plus 事務(wù)最佳實(shí)踐:這樣寫才規(guī)范
4.1 事務(wù)范圍:能小則小,別搞 "大包圍"
很多新手喜歡在 Service 層的入口方法上加 @Transactional,把整個(gè)方法都包在事務(wù)里,包括日志記錄、參數(shù)校驗(yàn)、遠(yuǎn)程調(diào)用等和數(shù)據(jù)庫無關(guān)的操作。這會(huì)導(dǎo)致事務(wù)持續(xù)時(shí)間過長(zhǎng),增加鎖競(jìng)爭(zhēng)和超時(shí)風(fēng)險(xiǎn)。正確的做法是:
- 事務(wù)只包裹真正的數(shù)據(jù)庫操作,無關(guān)邏輯放在事務(wù)外。
- 批量操作時(shí),合理拆分批次,避免單次事務(wù)處理太多數(shù)據(jù)(比如每次處理 1000 條數(shù)據(jù),提交一次事務(wù))。
4.2 異常處理:別吞掉回滾的 "信號(hào)"
@Transactional(rollbackFor = Exception.class)
public void processOrder() {
try {
// 數(shù)據(jù)庫操作
orderMapper.insert(order);
// 模擬異常
int i = 1 / 0;
} catch (Exception e) {
// 錯(cuò)誤做法:吞掉異常,不拋出去
log.error("處理訂單失敗", e);
}
}
這樣寫的后果是:事務(wù)不會(huì)回滾,因?yàn)?Spring 是根據(jù)方法是否拋異常來決定是否回滾的,異常被捕獲且沒有重新拋出,事務(wù)會(huì)認(rèn)為執(zhí)行成功,正常提交。正確的做法是:
@Transactional(rollbackFor = Exception.class)
public void processOrder() {
try {
orderMapper.insert(order);
int i = 1 / 0;
} catch (Exception e) {
log.error("處理訂單失敗", e);
// 必須重新拋出異常,或者調(diào)用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException("處理訂單失敗", e);
}
}
或者在不需要處理異常的情況下,直接讓異常往上拋,別在事務(wù)方法內(nèi)捕獲后不處理。
4.3 字段更新:注意 MyBatis-Plus 的更新策略
MyBatis-Plus 的 updateById、update 等方法,默認(rèn)會(huì)更新所有非 null 字段,這在事務(wù)中可能導(dǎo)致意外結(jié)果。比如:
@Transactional(rollbackFor = Exception.class)
public void updateUserPartial(Long userId, UserEntity user) {
// 假設(shè)user只有name字段有值,其他字段為null
user.setId(userId);
userMapper.updateById(user); // 會(huì)更新所有非null字段,包括name,其他字段不會(huì)被更新
}
這本來是正常行為,但如果在事務(wù)中,多個(gè)線程同時(shí)更新同一個(gè)用戶的不同字段,可能會(huì)出現(xiàn)更新順序問題。建議明確使用 updateWrapper,指定需要更新的字段:
userMapper.update(user, Wrappers.<UserEntity>update().set("name", user.getName()).eq("id", userId));
這樣更清晰,也避免因?qū)嶓w類字段變化導(dǎo)致的意外更新。
4.4 分布式場(chǎng)景:別把本地事務(wù)當(dāng)萬能藥
前面提到的多數(shù)據(jù)源問題,本質(zhì)上是分布式事務(wù)范疇。在微服務(wù)架構(gòu)中,如果涉及跨服務(wù)、跨數(shù)據(jù)源的事務(wù),必須使用分布式事務(wù)解決方案。這里簡(jiǎn)單提一下常見方案:
- TCC 模式(Try-Confirm-Cancel):適合強(qiáng)一致性場(chǎng)景,比如資金轉(zhuǎn)賬,實(shí)現(xiàn)復(fù)雜度高。
- 可靠消息最終一致性:通過消息中間件保證事務(wù)最終一致,適合異步場(chǎng)景,比如訂單創(chuàng)建后通知庫存服務(wù)扣庫存。
- Seata 框架:阿里巴巴開源的分布式事務(wù)解決方案,支持 AT 模式(自動(dòng)生成回滾日志)、TCC 模式等,和 Spring Cloud 集成良好。
4.5 監(jiān)控和日志:讓事務(wù)問題無處遁形
- 開啟事務(wù)日志:在 application.properties 中配置:
# Spring事務(wù)日志
logging.level.org.springframework.transaction=DEBUG
# MyBatis-Plus執(zhí)行日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
這樣可以在控制臺(tái)看到事務(wù)的開啟、提交、回滾過程,以及執(zhí)行的 SQL 語句,方便排查問題。
- 自定義注解和切面:可以寫一個(gè) @TransactionalLog 注解,結(jié)合 AOP 記錄事務(wù)方法的執(zhí)行時(shí)間、參數(shù)、異常信息等,幫助監(jiān)控事務(wù)性能和異常情況。
五、總結(jié):用好 @Transactional 的 "三板斧"
- 搞懂原理:知道 Spring 事務(wù)是怎么通過 AOP 代理和數(shù)據(jù)庫連接綁定實(shí)現(xiàn)的,明白 MyBatis-Plus 只是 ORM 工具,事務(wù)管理靠 Spring。
- 避開陷阱:記住同類方法調(diào)用、多數(shù)據(jù)源、枚舉類型、序列化這些常見坑,寫代碼時(shí)多檢查。
- 規(guī)范使用:合理設(shè)置事務(wù)傳播行為、隔離級(jí)別、超時(shí)時(shí)間,控制事務(wù)范圍,做好異常處理,分布式場(chǎng)景用對(duì)方案。