@Transactional 是 Spring 最常用的註解之一,但很多人對它的底層原理不太了解,導致寫出一些表面上沒問題,但實際上交易沒有生效的程式碼。我就踩過好幾次這種坑,最後才徹底理解它的運作方式。
類別層級 vs 方法層級
@Transactional 可以標在類別上,也可以標在方法上。
標在類別上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service @Transactional public class UserService { public void updateUser(User user) { } public void deleteUser(String userId) { } @Transactional(readOnly = true) public User getUser(String userId) { } }
|
標在類別上的話,所有 public 方法都會有交易。
標在方法上
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class UserService { @Transactional public void updateUser(User user) { } public void deleteUser(String userId) { } }
|
只有標注的方法才有交易。
優先順序
方法層級的設定會覆蓋類別層級的設定:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service @Transactional(propagation = Propagation.REQUIRED, timeout = 30) public class UserService { @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 60) public void updateUser(User user) { } public void deleteUser(String userId) { } }
|
CGLIB 動態代理的運作原理
@Transactional 之所以能生效,是因為 Spring 用了動態代理。當你把 @Transactional 標在某個類別上時,Spring 會在 runtime 時生成一個代理類別,這個代理會包裝交易邏輯。
簡單講,Spring 做的是這樣的事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Service @Transactional public class UserService { public void updateUser(User user) { } }
public class UserServiceProxy extends UserService { @Override public void updateUser(User user) { Transaction tx = dataSource.getConnection().getTransaction(); tx.begin(); try { super.updateUser(user); tx.commit(); } catch (Exception e) { tx.rollback(); throw e; } } }
|
Spring 用的是 CGLIB(Code Generation Library)來生成這個代理類別,而不是 JDK 的動態代理。CGLIB 的優點是可以代理普通類別(JDK proxy 只能代理 interface)。
為什麼 Private 和 Final 方法無法被代理
因為代理是透過繼承你的類別來實現的,所以 private 和 final 的方法就無法被重寫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Service @Transactional public class UserService { private void saveToCache(User user) { cache.put(user.getId(), user); } public final void updateUser(User user) { saveToCache(user); } }
|
即使你呼叫 updateUser(),Spring 的代理會攔截這個呼叫,但是 saveToCache() 的呼叫就不會被代理,因為它是 private。
同一個類別內部呼叫的坑(最常見的坑!)
這是最常見的坑,也是最容易被忽視的。假設你有這樣的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Service public class OrderService { @Transactional public void createOrder(Order order) { saveOrder(order); notifyCustomer(order); } @Transactional public void saveOrder(Order order) { } @Transactional public void notifyCustomer(Order order) { } }
|
表面上看起來沒問題,但實際上 saveOrder() 和 notifyCustomer() 這兩個方法的 @Transactional 不會生效!
為什麼?因為 createOrder() 內部直接呼叫 this.saveOrder(),這是一個內部方法呼叫(self-invocation),不會經過代理。Spring 代理只能攔截外部呼叫。
1 2 3 4 5 6
| 外部呼叫: OrderServiceProxy.createOrder() -> 代理攔截,開啟交易 -> OrderService.createOrder() -> this.saveOrder() // 直接呼叫,不經過代理! -> this.notifyCustomer() // 直接呼叫,不經過代理!
|
解決方案一:注入自己
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Service public class OrderService { @Autowired private OrderService self; @Transactional public void createOrder(Order order) { self.saveOrder(order); self.notifyCustomer(order); } @Transactional public void saveOrder(Order order) { } @Transactional public void notifyCustomer(Order order) { } }
|
這樣 self.saveOrder() 就會經過代理,@Transactional 才會生效。
解決方案二:用 AopContext.currentProxy()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class OrderService { @Transactional public void createOrder(Order order) { OrderService proxy = (OrderService) AopContext.currentProxy(); proxy.saveOrder(order); proxy.notifyCustomer(order); } @Transactional public void saveOrder(Order order) { } @Transactional public void notifyCustomer(Order order) { } }
|
但要注意,使用 AopContext 需要在 Spring config 裡面啟用暴露代理:
1 2 3 4
| @Configuration @EnableAspectJAutoProxy(exposeProxy = true) public class AppConfig { }
|
或者在 application.yml:
1 2 3 4
| spring: aop: proxy-target-class: true expose-proxy: true
|
解決方案三:直接在 createOrder() 標注 @Transactional
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Service public class OrderService { @Transactional public void createOrder(Order order) { saveOrder(order); notifyCustomer(order); } private void saveOrder(Order order) { } private void notifyCustomer(Order order) { } }
|
這樣最簡單,就一個交易,不用處理多層交易的複雜性。
交易的傳播行為
@Transactional 有個 propagation 參數,用來定義如果已經在一個交易中,該怎麼處理新的交易要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Transactional(propagation = Propagation.REQUIRED) public void methodA() { }
@Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() { }
@Transactional(propagation = Propagation.NESTED) public void methodC() { }
|
常見的有:
- REQUIRED(預設):如果已有交易就加入,沒有就新建
- REQUIRES_NEW:總是新建一個交易,暫停現有的交易
- NESTED:使用 savepoint,允許部分回滾
讀寫優化
1 2 3 4 5 6 7 8 9 10
| @Transactional(readOnly = true) public User getUser(String userId) { return userRepository.findById(userId); }
@Transactional public void updateUser(User user) { userRepository.save(user); }
|
readOnly = true 可以幫助 Spring 進行最佳化,比如某些資料庫會在讀操作上使用較寬鬆的隔離級別。
隔離級別
1 2 3 4
| @Transactional(isolation = Isolation.READ_COMMITTED) public void updateUser(User user) { }
|
常見的隔離級別有:
- READ_UNCOMMITTED:最低,可能有髒讀
- READ_COMMITTED:避免髒讀
- REPEATABLE_READ:避免髒讀和不可重複讀
- SERIALIZABLE:最高,但效能最差
超時設定
1 2 3 4
| @Transactional(timeout = 30) public void longRunningOperation() { }
|
如果操作超過 30 秒,Spring 會自動回滾。但要注意,超時的精確行為取決於底層資料庫。
異常和回滾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Transactional public void updateUser(User user) { userRepository.save(user); if (user.getAge() < 0) { throw new IllegalArgumentException("Age cannot be negative"); } }
@Transactional(rollbackFor = { SQLException.class }) public void updateDatabase() throws SQLException { }
@Transactional(noRollbackFor = { ValidationException.class }) public void validateAndSave(User user) throws ValidationException { }
|
預設 Spring 只會在 RuntimeException 和 Error 時回滾。如果你想要在 checked exception 時也回滾,要用 rollbackFor。
常見的坑整理
- 同一個類別內部呼叫 - 最常見的坑!一定要用 self 或代理
- Private 或 final 方法 - 無法被代理
- Static 方法 - 無法被代理
- 內部類別的方法 - 有時候也會有問題
- 外部線程中的方法呼叫 - 代理對象可能不同
重點整理
- @Transactional 用 CGLIB 動態代理實現,代理透過繼承原類別
- 類別層級的設定對所有 public 方法生效,方法層級覆蓋類別層級
- 關鍵:同一個類別內部呼叫不經過代理,@Transactional 不會生效
- 解決辦法:注入自己或用 AopContext.currentProxy()
- Private 和 final 方法無法被代理
- 要注意 propagation、isolation、timeout、rollbackFor 等設定
下次再遇到交易相關的詭異行為,先檢查是不是自我呼叫的問題。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️