一、"在同一事務中" 的核心含義
"在同一事務中" 指多個數據庫操作共享同一個事務上下文,具有以下特點:
- 原子性保證:所有操作要么全部成功提交,要么全部失敗回滾。
- 隔離性共享:操作使用相同的隔離級別(如 READ COMMITTED)。
- 資源共享:操作使用同一個數據庫連接,且事務狀態(如鎖)保持一致。
二、代碼中如何表示 "在同一事務中"
1. Spring 框架中的實現方式
在 Spring 中,主要通過 **@Transactional
注解或編程式事務 ** 來控制事務邊界。
示例 1:使用@Transactional
注解(聲明式事務)
@Service
public class OrderService {@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate PaymentService paymentService;// 該方法開啟一個事務,內部所有操作都在同一事務中@Transactional(propagation = Propagation.REQUIRED) // 默認值可不寫public void createOrder(Order order) {// 操作1:保存訂單orderRepository.save(order);// 操作2:扣減庫存(假設在同一事務中)inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 操作3:調用支付服務(默認加入當前事務)paymentService.processPayment(order);// 若以上任一操作失敗,整個事務回滾}
}
關鍵點:
@Transactional
注解標記的方法會被 Spring AOP 攔截,自動開啟、提交或回滾事務。- 默認傳播行為
Propagation.REQUIRED
表示:若當前無事務,則創建新事務;若已有事務,則加入該事務。
示例 2:跨方法調用保持同一事務
因為@Transactional(propagation = Propagation.REQUIRED)是默認有的
@Service
public class OrderService {@Autowiredprivate PaymentService paymentService;// 外層事務方法@Transactionalpublic void processOrder(Order order) {// 操作1:創建訂單createOrder(order);// 操作2:調用支付服務(默認加入當前事務)paymentService.processPayment(order);// 若此處拋出異常,createOrder和processPayment都會回滾}// 內層方法(默認加入外層事務)public void createOrder(Order order) {// 訂單創建邏輯}
}
關鍵點:
- 同一個類中的方法調用(如
processOrder
調用createOrder
)默認共享事務,因為 Spring AOP 通過代理對象實現事務增強。 - 若
createOrder
單獨標記@Transactional
,且調用者無事務,則createOrder
會創建新事務。
2. 編程式事務(手動控制事務邊界)
適用于需要更細粒度控制事務的場景。
示例 3:使用 TransactionTemplate(Spring 早期方式)
@Service
public class TransactionExample {@Autowiredprivate TransactionTemplate transactionTemplate;@Autowiredprivate UserRepository userRepository;public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {transactionTemplate.execute(status -> {try {// 操作1:扣減轉出用戶余額User fromUser = userRepository.findById(fromUserId).orElseThrow();fromUser.setBalance(fromUser.getBalance().subtract(amount));userRepository.save(fromUser);// 模擬異常if (amount.compareTo(new BigDecimal("1000")) > 0) {throw new RuntimeException("金額過大");}// 操作2:增加轉入用戶余額User toUser = userRepository.findById(toUserId).orElseThrow();toUser.setBalance(toUser.getBalance().add(amount));userRepository.save(toUser);return true;} catch (Exception e) {// 手動回滾(實際中通常自動回滾)status.setRollbackOnly();throw e;}});}
}
關鍵點:
transactionTemplate.execute()
包裹的所有操作在同一事務中。- 異常會觸發事務回滾,成功則自動提交。
3. 使用 PlatformTransactionManager(更底層的方式)
關鍵點:
- 通過
PlatformTransactionManager
手動控制事務的開始、提交和回滾。 - 適合需要動態調整事務屬性的場景。
三、常見問題與注意事項
1.事務傳播行為的影響:
若子方法使用REQUIRES_NEW
,則會創建新事務,與外層事務隔離。
示例:
@Transactional
public void parentMethod() {// 外層事務childService.childMethod(); // 若childMethod使用REQUIRES_NEW,則不在同一事務中
}
2.異常處理與事務回滾:
Spring 默認只對RuntimeException
和Error
回滾事務,檢查異常(如IOException
)不會觸發回滾。
可通過@Transactional(rollbackFor = Exception.class)
擴大回滾范圍。
3.同一個類中的方法調用:
Spring AOP 通過代理對象實現事務增強,若methodA
調用methodB
(同一類中),methodB
的@Transactional
會失效。
解決方案:
@Service
public class SelfCallExample {@Autowiredprivate SelfCallExample self; // 注入自身代理@Transactionalpublic void methodA() {// 正確方式:通過代理調用self.methodB();}@Transactionalpublic void methodB() {// ...}
}
四、總結
"在同一事務中" 的核心是共享事務上下文,在代碼中通過以下方式實現:
- 聲明式事務:使用
@Transactional
注解標記方法,默認傳播行為REQUIRED
確保操作在同一事務中。 - 編程式事務:通過
TransactionTemplate
或PlatformTransactionManager
手動控制事務邊界。 - 跨方法調用:確保方法間通過代理對象調用,且子方法不使用
REQUIRES_NEW
等隔離傳播行為。
合理控制事務邊界是保證數據一致性的關鍵,需根據業務場景選擇合適的事務管理方式。
通俗易懂地理解 "同一事務" 與代碼示例
一、"同一事務" 的通俗解釋
比喻:想象你在銀行柜臺辦理轉賬業務,整個流程包括:
- 驗證轉出賬戶余額
- 扣減轉出賬戶金額
- 增加轉入賬戶金額
- 記錄交易日志
這四個步驟必須要么全部成功,要么全部失敗(例如,若扣錢成功但加錢失敗,銀行會回滾整個操作)。這就是 "在同一事務中" 的含義 ——一組不可分割的操作,共享同一個 "原子性" 保障。
二、代碼示例:如何在 Spring 中實現 "同一事務"
1. 最常見場景:一個方法內的多個操作
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate LogRepository logRepository;// 整個方法在同一事務中@Transactionalpublic void transferMoney(Long fromId, Long toId, BigDecimal amount) {// 操作1:扣錢User fromUser = userRepository.findById(fromId).orElseThrow();fromUser.setBalance(fromUser.getBalance().subtract(amount));userRepository.save(fromUser);// 模擬網絡延遲或其他異常// if (true) throw new RuntimeException("模擬異常");// 操作2:加錢User toUser = userRepository.findById(toId).orElseThrow();toUser.setBalance(toUser.getBalance().add(amount));userRepository.save(toUser);// 操作3:記錄日志(與轉賬共享同一事務)Log log = new Log("轉賬", amount, fromId, toId);logRepository.save(log);}
}
關鍵點:
@Transactional
標記整個方法,內部的 3 個數據庫操作共享同一事務。- 若中間拋出異常(如取消注釋第 16 行),則所有操作都回滾,錢不會平白消失。
2. 跨方法調用保持同一事務
@Service
public class OrderService {@Autowiredprivate ProductService productService;@Autowiredprivate InventoryService inventoryService;// 主事務方法@Transactionalpublic void createOrder(Order order) {// 操作1:保存訂單orderRepository.save(order);// 操作2:扣減庫存(調用其他服務的方法)inventoryService.reduceStock(order.getProductId(), order.getQuantity());// 操作3:更新商品銷量(調用其他服務的方法)productService.updateSales(order.getProductId(), order.getQuantity());// 若此處拋出異常,整個事務回滾// throw new RuntimeException("訂單創建失敗");}
}@Service
public class InventoryService {// 該方法默認加入調用者的事務public void reduceStock(Long productId, Integer quantity) {Inventory inventory = inventoryRepository.findByProductId(productId);inventory.setStock(inventory.getStock() - quantity);inventoryRepository.save(inventory);}
}
關鍵點:
createOrder
方法上的@Transactional
使整個調用鏈在同一事務中。reduceStock
和updateSales
雖然在不同類中,但默認加入外層事務,共享原子性。- 若訂單保存成功,但扣庫存失敗,則整個操作回滾,不會出現 "有訂單但沒扣庫存" 的情況。
3. 同一類中方法調用的陷阱與解決方案
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate UserService self; // 注入自身代理// 錯誤示例:同一類中方法調用,事務不生效@Transactionalpublic void wrongUpdate(User user) {// 保存用戶基本信息userRepository.save(user);// 調用同一類中的方法(事務不會生效)updateLastLoginTime(user.getId());// 若此處拋出異常,updateLastLoginTime的操作不會回滾}@Transactionalpublic void updateLastLoginTime(Long userId) {User user = userRepository.findById(userId).orElseThrow();user.setLastLoginTime(new Date());userRepository.save(user);}// 正確示例:通過代理調用,事務生效@Transactionalpublic void correctUpdate(User user) {userRepository.save(user);// 通過代理調用,事務生效self.updateLastLoginTime(user.getId());}
}
關鍵點:
- Spring 通過代理對象實現事務增強,同一類中直接調用方法(如
wrongUpdate
)會導致內層方法的@Transactional
失效。 - 解決方案:通過
@Autowired
注入自身代理(self
),或拆分到不同 Service 類中。
三、常見問題與避坑指南
1. 為什么要在同一事務中?
反例:若轉賬操作不在同一事務中:
- 扣錢成功 → 系統崩潰 → 加錢失敗
- 結果:錢平白消失,用戶炸鍋!
正例:在同一事務中,要么都成功,要么都失敗,錢不會消失。
2. 如何驗證是否在同一事務中?
@Service
public class TestService {@Autowiredprivate DataSource dataSource;@Transactionalpublic void testTransaction() {try (Connection conn = dataSource.getConnection()) {// 輸出false表示自動提交已關閉,在事務中System.out.println("是否自動提交: " + conn.getAutoCommit());} catch (SQLException e) {e.printStackTrace();}// 模擬異常throw new RuntimeException("回滾測試");}
}
關鍵點:
- 在
@Transactional
方法中,數據庫連接的autoCommit
為false
,表示事務已開啟。 - 若拋出異常,數據庫會自動回滾。
3. 哪些情況會導致事務失效?
- 非 public 方法:
@Transactional
只對 public 方法生效。 - 同一類中直接調用:如前面示例中的
wrongUpdate
。 - 異常被吞掉:
@Transactional
public void wrongMethod() {try {// 數據庫操作throw new RuntimeException("異常");} catch (Exception e) {// 錯誤:異常被捕獲,事務不會回滾}
}
四、總結
"同一事務" 的本質:
- 一組操作共享原子性(要么都成功,要么都失敗)。
- 共享數據庫連接和事務狀態。
實現方式:
- 用
@Transactional
標記方法,確保多個操作在同一方法內或跨方法調用。 - 注意同一類中方法調用的代理問題。
- 避免手動捕獲異常導致事務不回滾。
記住:只要看到@Transactional
,就像給代碼上了 "保險",要么全成功,要么全失敗!
通俗易懂理解:新方法的事務選擇(加入現有事務還是新建事務)
一、用生活場景比喻事務傳播行為
場景假設:你和朋友一起做飯(現有事務),這時來了一個新幫手(新方法)。
-
情況 1:新幫手加入你們的小組(加入現有事務)
你們共用一口鍋、一套工具,任何一個人犯錯(比如菜炒糊了),整個做飯過程都可能取消(回滾)。
對應代碼:新方法和現有方法在同一個事務中,共享成功或失敗。 -
情況 2:新幫手自己開小灶(新建事務)
他自己用另一口鍋做飯,即使你們的菜炒糊了,他的飯依然能單獨做好端出來;反之,他把飯做糊了,不影響你們的菜。
對應代碼:新方法開啟獨立事務,與外層事務互不影響。
二、代碼示例:兩種事務傳播行為的對比
1. 新方法加入現有事務(默認行為:PROPAGATION_REQUIRED)
@Service
public class OrderService {@Autowiredprivate PaymentService paymentService;// 外層事務(主業務:創建訂單+支付)@Transactionalpublic void createOrderWithPayment(Order order) {// 操作1:保存訂單(現有事務)orderRepository.save(order);// 操作2:調用支付方法(默認加入現有事務)paymentService.pay(order.getOrderId(), order.getAmount());// 若此處拋出異常,整個事務回滾(訂單和支付都失敗)// throw new RuntimeException("訂單創建失敗");}
}@Service
public class PaymentService {// 未指定傳播行為,默認PROPAGATION_REQUIRED(加入現有事務)@Transactionalpublic void pay(Long orderId, BigDecimal amount) {// 支付操作Payment payment = new Payment(orderId, amount);paymentRepository.save(payment);// 若此處拋出異常,外層事務一起回滾// throw new RuntimeException("支付失敗");}
}
關鍵點:
- 外層
createOrderWithPayment
開啟事務,內層pay
方法默認加入這個事務。 - 異常連鎖反應:內層拋異常 → 外層事務回滾;外層拋異常 → 內層操作也回滾。
2. 新方法創建新事務(PROPAGATION_REQUIRES_NEW)
@Service
public class OrderService {@Autowiredprivate PaymentService paymentService;// 外層事務(主業務:創建訂單)@Transactionalpublic void createOrder(Order order) {// 操作1:保存訂單orderRepository.save(order);try {// 操作2:調用支付方法(新建獨立事務)paymentService.payWithNewTransaction(order.getOrderId(), order.getAmount());} catch (Exception e) {// 支付失敗不影響訂單保存log.error("支付失敗,但訂單已創建", e);}// 外層拋出異常,不影響內層已提交的支付// throw new RuntimeException("訂單創建失敗");}
}@Service
public class PaymentService {// 明確指定新建事務@Transactional(propagation = Propagation.REQUIRES_NEW)public void payWithNewTransaction(Long orderId, BigDecimal amount) {// 支付操作Payment payment = new Payment(orderId, amount);paymentRepository.save(payment);// 內層拋異常,僅回滾支付操作,不影響外層訂單throw new RuntimeException("支付失敗(獨立回滾)");}
}
關鍵點:
payWithNewTransaction
用REQUIRES_NEW
開啟新事務,與外層事務隔離。- 異常隔離:
- 內層拋異常 → 僅回滾支付操作,訂單保存成功;
- 外層拋異常 → 訂單回滾,但已提交的支付操作不回滾(因為內層事務已獨立提交)。
三、常見應用場景對比
場景 | 選擇加入現有事務(REQUIRED) | 選擇新建事務(REQUIRES_NEW) |
---|---|---|
典型案例 | 轉賬(扣錢 + 加錢必須同時成功 / 失敗) | 訂單創建時記錄日志(即使訂單失敗,日志也要保存) |
核心需求 | 操作必須整體成功或失敗 | 操作需要獨立于外層邏輯 |
資源消耗 | 更省資源(共用數據庫連接) | 消耗更多資源(新建連接 + 事務) |
異常處理 | 內層異常會導致外層回滾 | 內層異常不影響外層,外層異常不影響內層 |