今天,該篇博客主要聊聊業務代碼中與數據庫事務相關的坑。
Spring 針對 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事務 API,實現了一致的編程模型,而 Spring 的聲明式事務功能更是提供了極其方便的事務配置方式,配合 Spring Boot 的自動配置,大多數 Spring Boot 項目只需要在方法上標記 @Transactional 註解,即可一鍵開啓方法的事務性配置。
大多數業務開發同學都有事務的概念,也知道如果整體考慮多個數據庫操作要麼成功要麼失敗時,需要通過數據庫事務來實現多個操作的一致性和原子性。但,在使用上大多僅限於爲方法標記 @Transactional,不會去關注事務是否有效、出錯後事務是否正確回滾,也不會考慮複雜的業務代碼中涉及多個子業務邏輯時,怎麼正確處理事務。事務沒有被正確處理,一般來說不會過於影響正常流程,也不容易在測試階段被發現。但當系統越來越複雜、壓力越來越大之後,就會帶來大量的數據不一致問題,隨後就是大量的人工介入查看和修復數據(也就是所謂的髒數據)。
小心Spring 的事務可能沒有生效
在使用 @Transactional 註解開啓聲明式事務時, 第一個最容易忽略的問題是,很可能事務並沒有生效。
事務失效場景一:
定義一個 UserService 類,負責業務邏輯處理。如果不清楚 @Transactional 的實現方式,只考慮代碼邏輯的話,這段代碼看起來沒有問題。
定義一個入口方法 createUserWrong1 來調用另一個私有方法 createUserPrivate,私有方法上標記了 @Transactional 註解。當傳入的用戶名包含 test 關鍵字時判斷爲用戶名不合法,拋出異常,讓用戶創建操作失敗,期望事務可以回滾:
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
//一個公共方法供Controller調用,內部調用事務性的私有方法
public int createUserWrong1(String name) {
try {
this.createUserPrivate(new UserEntity(name));
} catch (Exception ex) {
log.error("create user failed because {}", ex.getMessage());
}
return userRepository.findByName(name).size();
}
//標記了@Transactional的private方法
@Transactional
private void createUserPrivate(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test"))
throw new RuntimeException("invalid username!");
}
}
}
調用接口後發現,即便用戶名不合法,用戶也能創建成功。刷新瀏覽器,多次發現有十幾個的非法用戶註冊。
這裏給出 @Transactional 生效原則 1,除非特殊配置(比如使用 AspectJ 靜態織入實現AOP),否則只有定義在 public 方法上的 @Transactional 才能生效。原因是,Spring默認通過動態代理的方式實現 AOP,對目標方法進行增強,private 方法無法代理到,Spring 自然也無法動態增強事務處理邏輯。
測試發現,調用新的 createUserWrong2 方法事務同樣不生效。這裏,我給出@Transactional 生效原則 2,必須通過代理過的類從外部調用目標方法才能生效。
調用方式:userService.createUserPublic(new UserEntity(name)); 原因是:通過 this 自調用,沒有機會走到 Spring 的代理類;後兩種改進方案調用的是 Spring 注入的 UserService,通過代理調用纔有機會對 createUserPublic 方法進行動態增強。
事務即便生效也不一定能回滾
通過 AOP 實現事務處理可以理解爲,使用 try…catch…來包裹標記了 @Transactional 註解的方法,當方法出現了異常並且滿足一定條件的時候,在 catch 裏面我們可以設置事務回滾,沒有異常則直接提交事務。
這裏的“一定條件”,主要包括兩點。
第一,只有異常傳播出了標記了 @Transactional 註解的方法,事務才能回滾。在 Spring的 TransactionAspectSupport 裏有個 invokeWithinTransaction 方法,裏面就是處理事務的邏輯。可以看到,只有捕獲到異常才能進行後續事務處理:
第二,默認情況下,出現 RuntimeException(非受檢異常)或 Error 的時候,Spring
纔會回滾事務。
①如果你希望自己捕獲異常進行處理的話,也沒關係,可以手動設置讓當前事務處於回滾狀態:
@Transactional
public void createUserRight1(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("create user failed", ex);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
②在註解中聲明,期望遇到所有的 Exception 都回滾事務(來突破默認不回滾受檢異常的限制):
@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
userRepository.save(new UserEntity(name));
otherTask();
}
同樣也是可以回滾。
總結
業務代碼中最常見的使用數據庫事務的方式,即 Spring 聲明式事務,與你總結了使用上可能遇到的三類坑,包括:
第一,因爲配置不正確,導致方法上的事務沒生效。我們務必確認調用 @Transactional 註解標記的方法是 public 的,並且是通過 Spring 注入的 Bean 進行調用的。
第二,因爲異常處理不正確,導致事務雖然生效但出現異常時沒回滾。Spring 默認只會對標記 @Transactional 註解的方法出現了 RuntimeException 和 Error 的時候回滾,如果我們的方法捕獲了異常,那麼需要通過手動編碼處理事務回滾。如果希望 Spring 針對其他異常也可以回滾,那麼可以相應配置@Transactional 註解的 rollbackFor 和noRollbackFor 屬性來覆蓋其默認設置。