天天看點

日常開發踩坑:你的"事務"真的奏效了嗎?

日常開發踩坑:你的"事務"真的奏效了嗎?
日常開發踩坑:你的"事務"真的奏效了嗎?

對于“事務”,很多讀者尤其是業務研發通常采用“一筆帶過”的處理方式:方法上用注解聲明,然後就“安心”的寫代碼去了。

但事務真的奏效了嗎?複雜的嵌套事務應該如何處理?正确復原了嗎?本篇就針對這些坑點為讀者做科普和預警。

日常開發踩坑:你的"事務"真的奏效了嗎?
日常開發踩坑:你的"事務"真的奏效了嗎?

1

聲明問題導緻的事務失效

先用例子測試一下,定義使用者Controller,并注入Service來調用建立使用者方法:

@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private UserService userService;
//建立使用者方法
@PostMapping("/create")
public Result<OrderResVo> createUser(@RequestBody UserReqVo userReqVo){
  try{
      userService.createUser(userReqVo);
  }catch(Exception e){
      log.error("error in createUser",e.getMessage());
  }
}           

複制

建構Service服務,向controller暴露公有方法createUser,方法内部經過校驗邏輯後,調用聲明事務的私有方法doCreateUser實作入庫邏輯以及後續關聯業務處理邏輯:

@Service
public class UserService(){
@Autowired
private UserDao userDao;
public boolean createUser(UserReqVo userReqVo){
  //參數校驗
  ....
  //建立使用者
  this.doCreateUser(userReqVo);
}

//聲明事物
@Transactional
private void doCreateUser(UserReqVo userReqVo){
     //入庫 
     userDao.insertUser(userReqVo);
     //入庫之後的關聯業務邏輯處理
     .....
}
}           

複制

期望結果:當第18行的邏輯發生異常之後,16行的入庫操作進行復原。

實際結果:經過測試,發生異常的情況下,資料庫中的資料并未復原,事務并未奏效!

坑點:事務聲明注解@Transactional隻有定義在Public修飾的方法上才會奏效

于是嘗試修改一下Service層代碼第14行:

//private -> public
@Transactional
public void doCreateUser(UserReqVo userReqVo)
{
     //入庫 
     userDao.insertUser(userReqVo);
     //入庫之後的關聯業務邏輯處理
     .....
}           

複制

再次測試:發生異常的情況下,資料依然沒有復原!這又是為什麼呢?

原因在于:注解方式的事務聲明@Transactinal是基于AOP的,而AOP的底層是通過動态代理模式來實作,也就是說事務的復原邏輯其實是存在于代理類的方法增強邏輯中。

補課通道

設計模式的通俗了解--代理模式

坑點:想要事務生效必須通過代理類來調用事物方法doCreateUser(),而this并不是指代理類。

再次修改代碼:先建立UserService的代理(注入UserService),通過通過代理調用事務方法doCreateUser()

@Service
public class UserService(){
@Autowired
private UserDao userDao
//------add-------
@Autowired
private UserService userService;
//------add-------
public boolean createUser(UserReqVo userReqVo) {
  //參數校驗
  ....
  //建立使用者
  //------edit------
  userSservice.doCreateUser(userReqVo);
  //------edit------
}

//聲明事物
@Transactional
public void doCreateUser(UserReqVo userReqVo){
     //入庫 
     userDao.insertUser(userReqVo);
     //入庫之後的關聯業務邏輯處理
     .....
}
}           

複制

測試結果:當第24行的邏輯發生異常之後,22行的入庫操作正常復原,與期望結果一緻!

不過我們在UserService裡又注入了UserService自己,這很雞肋,是以再修改一下UserService:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo) {
  //參數校驗
  ....
  //建立使用者
  //入庫 
  userDao.insertUser(userReqVo);
  //入庫之後的關聯業務邏輯處理
  .....
}
}           

複制

2

異常處理導緻的事務失效

在實際開發中我們可能需要對代碼中可能的異常做catch操作:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo){
  try{
    //參數校驗
    ....
    //建立使用者
    //入庫 
    userDao.insertUser(userReqVo);
    //入庫之後的關聯業務邏輯處理
    .....
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
  }
}
}           

複制

測試結果:當代碼第14行發生異常後,進入catch,但12行的資料并未復原。

坑點:聲明事務的方法中如果自定義了異常捕獲catch邏輯,事務将無法復原。

解決方案:手動設定事務復原,修改catch中的代碼

...
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
    //-----add------
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();    
    //-----add------
  }
  ...           

複制

異常處理的另一種情況是直接抛出:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo) throws NullPointerException{
    //參數校驗
    ....
    //建立使用者
    //入庫 
    userDao.insertUser(userReqVo);
    //入庫之後的關聯業務邏輯處理
    .....
}
}           

複制

測試結果:當第13行發生空指針異常NullPointerException時,異常被抛出,事務也無法復原。

坑點:聲明事務的方法在發生指定抛出的異常類型時,事務無法復原。

解決方案:在@Transactional注解中聲明事務復原支援的異常類型

@Transactional(rollbackFor = Exception.class)           

複制

3

嵌套事務的復原邏輯錯誤

事務處理還有一個經常遇到的情況是:在方法中需要對多個表進行更新操作,這就涉及到多事務同時操作。

還是用開始的例子,我們在建立使用者之後增加日志記錄操作,将日志入庫:

@Service
public class UserService(){
@Autowired
private UserDao userDao;
//----add-----
//注入日志處理類
@Autowired
private LogService logService;
//----add-----
@Transactional
public boolean createUser(UserReqVo userReqVo){
  try{
    //參數校驗
    ....
    //建立使用者
    //入庫 
    userDao.insertUser(userReqVo);
    //入庫之後的關聯業務邏輯處理
    //----add-----
    //将操作記錄到日志表中
    logService.insertLog();
    //----add-----
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
  }
}
}           

複制

建構LogService類,日志入庫方法insertLog()聲明事務:

@Service
public class LogService(){
@Autowired
private LogDao logDao;
@Transactional
public boolean insertLog(){
  logDao.insertLog();
}
}           

複制

通過代碼可以看到,我在主事務方法中包含了日志入庫的子事務方法,那麼當子事務復原,按照之前講的:主事務做了catch,是以理論上主事務應該不會復原了。而實際的測試情況是:主事務依然發生了復原,這就是嵌套事務的特别之處。

日志處理并非主業務,我們更希望即便日志處理復原了也不應該影響建立使用者操作的正常送出。

坑點:如果我們希望子事務的復原不影響上級事務的送出,需要在子事務的@Transactional注解中聲明事務傳播政策

...
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean insertLog(){
...           

複制

修改之後日志處理復原的情況下,建立使用者操作依然正常送出,滿足業務需要。

以上就是“事務”處理的一些坑點,希望讀者看過本篇之後對這一部分重視起來,我們下期繼續踩坑。

日常開發踩坑:你的"事務"真的奏效了嗎?
日常開發踩坑:你的"事務"真的奏效了嗎?