@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) {
// 覆蓋了類別層級的設定,用 readOnly = true
}
}

標在類別上的話,所有 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) {
// 使用 REQUIRES_NEW 和 60 秒超時,覆蓋了類別層級的設定
}

public void deleteUser(String userId) {
// 使用類別層級的 REQUIRED 和 30 秒超時
}
}

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) {
// 你的程式碼
}
}

// Spring 生成的代理類別(簡化版)
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
private void saveToCache(User user) {
cache.put(user.getId(), user);
}

// 這個也無法被代理,因為是 final
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) {
// 直接在這個方法做所有事,就不用內部呼叫其他 @Transactional 方法了
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) // 使用 savepoint(如果支援)
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) {
// 讀操作,Spring 會進行一些優化
return userRepository.findById(userId);
}

@Transactional // 預設 readOnly = false
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)  // 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 {
// 拋出 ValidationException 時不會回滾
}

預設 Spring 只會在 RuntimeException 和 Error 時回滾。如果你想要在 checked exception 時也回滾,要用 rollbackFor

常見的坑整理

  1. 同一個類別內部呼叫 - 最常見的坑!一定要用 self 或代理
  2. Private 或 final 方法 - 無法被代理
  3. Static 方法 - 無法被代理
  4. 內部類別的方法 - 有時候也會有問題
  5. 外部線程中的方法呼叫 - 代理對象可能不同

重點整理

  • @Transactional 用 CGLIB 動態代理實現,代理透過繼承原類別
  • 類別層級的設定對所有 public 方法生效,方法層級覆蓋類別層級
  • 關鍵:同一個類別內部呼叫不經過代理,@Transactional 不會生效
  • 解決辦法:注入自己或用 AopContext.currentProxy()
  • Private 和 final 方法無法被代理
  • 要注意 propagation、isolation、timeout、rollbackFor 等設定

下次再遇到交易相關的詭異行為,先檢查是不是自我呼叫的問題。