美團二面:spring事務不生效的15種場景

張哥說技術發表於2023-04-03

來源:撿田螺的小男孩

前言

大家好,我是田螺

日常開發中,我們經常使用到spring事務。最近星球一位還有去美團面試,被問了這麼一道面試題: Spring 事務在哪幾種情況下會不生效? 今天田螺哥跟大家聊聊,spring事務不生效15種場景。

美團二面:spring事務不生效的15種場景

1. 你的service類沒有被Spring管理

//@Service (註釋了@Service)
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
     @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        //儲存tianluo實體資料庫記錄
        tianLuoMapper.save(tianluo);
        //儲存tianluo流水資料庫記錄
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事務不生效的原因:上面例子中, @Service註解註釋之後,spring事務(@Transactional)沒有生效,因為Spring事務是由AOP機制實現的,也就是說從Spring IOC容器獲取bean時,Spring會為目標類建立代理,來支援事務的。但是@Service被註釋後,你的service類都不是spring管理的,那怎麼建立代理類來支援事務呢
  • 解決方案:加上@Service註解。

2.沒有在Spring配置檔案中啟用事務管理器

@Configuration
public class AppConfig {
    // 沒有配置事務管理器
}

@Service
public class MyService {
    @Transactional
    public void doSomething() {
        // ...
    }
}
  • 事務不生效的原因:沒有在AppConfig中配置事務管理器,因此Spring無法建立事務代理物件,導致事務不生效。即使在MyService中新增了@Transactional註解,該方法也不會被Spring管理的事務代理攔截。
  • 解決方案:為了解決這個問題,應該在AppConfig中配置一個事務管器。例如:
@Configuration
public class AppConfig {
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

@Service
public class MyService {
    @Transactional
    public void doSomething() {
        // ...
    }
}

如果是Spring Boot專案,它預設會自動配置事務管理器並開啟事務支援。

3. 事務方法被final、static關鍵字修飾

@Service
public class TianLuoServiceImpl  {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public final void addTianLuo(TianLuo tianluo) {
         //儲存tianluo實體資料庫記錄
        tianLuoMapper.save(tianluo);
        //儲存tianluo流水資料庫記錄
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事務不生效的原因:如果一個方法被宣告為final或者static,則該方法不能被子類重寫,也就是說無法在該方法上進行動態代理,這會導致Spring無法生成事務代理物件來管理事務。
  • 解決方案addTianLuo事務方法不要用final修飾或者static修飾。

4. 同一個類中,方法內部呼叫

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
    
    public void addTianLuo(TianLuo tianluo){
     // 呼叫內部的事務方法
     this.executeAddTianLuo(tianluo);
   }

    @Transactional
    public void executeAddTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事務不生效的原因: 事務是透過Spring AOP代理來實現的,而在同一個類中,一個方法呼叫另一個方法時,呼叫方法直接呼叫目標方法的程式碼,而不是透過代理類進行呼叫。即以上程式碼,呼叫目標executeAddTianLuo方法不是透過代理類進行的,因此事務不生效。
  • 解決方案:可以新建多一個類,讓這兩個方法分開,分別在不同的類中。如下:
@Service
public class TianLuoExecuteServiceImpl implements TianLuoExecuteService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
    
    @Transactional
    public void executeAddTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}

@Service
public class TianLuoAddServiceImpl implements TianLuoAddService {

    @Autowired
    private TianLuoExecuteService tianLuoExecuteService;
    
    public void addTianLuo(User user){
     tianLuoExecuteService.executeAddTianLuo(user);
   }
}

當然,有時候你也可以在該 Service 類中注入自己,或者透過AopContext.currentProxy()獲取代理物件。

5.方法的訪問許可權不是public

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    private void addTianLuo(TianLuo tianluo) {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事務不生效的原因spring事務方法addTianLuo的訪問許可權不是public,所以事務就不生效啦,因為Spring事務是由AOP機制實現的,AOP機制的本質就是動態代理,而代理的事務方法不是public的話,computeTransactionAttribute()就會返回null,也就是這時事務屬性不存在了。大家可以看下AbstractFallbackTransactionAttributeSource的原始碼:
美團二面:spring事務不生效的15種場景
  • 解決方案addTianLuo事務方法的訪問許可權修改為public

6. 資料庫的儲存引擎不支援事務

Spring事務的底層,還是依賴於資料庫本身的事務支援。在MySQL中,MyISAM儲存引擎是不支援事務的,InnoDB引擎才支援事務。因此開發階段設計表的時候,確認你的選擇的儲存引擎是支援事務的

美團二面:spring事務不生效的15種場景

7 .配置錯誤的 @Transactional 註解

@Transactional(readOnly = true)
public void updateUser(User user) {
    userDao.updateUser(user);
}
  • 事務不生效的原因:雖然使用了@Transactional註解,但是註解中的readOnly=true屬性指示這是一個只讀事務,因此在更新User實體時會丟擲異常。
  • 解決方案:將readOnly屬性設定為false,或者移除了@Transactional註解中的readOnly屬性。

8.事務超時時間設定過短

@Transactional(timeout = 1)
public void doSomething() {
    //...
}
  • 事務不生效的原因:在上面的例子中,timeout屬性被設定為1秒,這意味著如果事務在1 秒內無法完成,則報事務超時了。

9. 使用了錯誤的事務傳播機制

@Service
public class TianLuoServiceImpl {
 
    @Autowired
    private TianLuoMapper tianLuoMapper;
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;
 
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public  void doInsertTianluo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        tianLuoFlowMapper.saveFlow(buildFlowByTianLuo(tianluo));
    }
}
  • 事務不生效的原因Propagation.NOT_SUPPORTED傳播特性不支援事務。
  • 解決方案:選擇正確的事務傳播機制。

幫大家複習一下,Spring提供了七種事務傳播機制。它們分別是:

  • REQUIRED(預設):如果當前存在一個事務,則加入該事務;否則,建立一個新事務。該傳播級別表示方法必須在事務中執行。
  • SUPPORTS:如果當前存在一個事務,則加入該事務;否則,以非事務的方式繼續執行。
  • MANDATORY:如果當前存在一個事務,則加入該事務;否則,丟擲異常。
  • REQUIRES_NEW:建立一個新的事務,並且如果存在一個事務,則將該事務掛起。
  • NOT_SUPPORTED:以非事務方式執行操作,如果當前存在一個事務,則將該事務掛起。
  • NEVER:以非事務方式執行操作,如果當前存在一個事務,則丟擲異常。
  • NESTED:如果當前存在一個事務,則在巢狀事務內執行。如果沒有事務,則按REQUIRED傳播級別執行。巢狀事務是外部事務的一部分,可以在外部事務提交或回滾時部分提交或回滾。

10. rollbackFor屬性配置錯誤

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(rollbackFor = Error.class)
    public void addTianLuo(TianLuo tianluo) {
        //儲存tianluo資料庫記錄
        tianLuoMapper.save(tianluo);
        //儲存tianluo流水資料庫記錄
        tianLuoFlowMapper.saveFlow(tianluo);
        //模擬異常丟擲
        throw new Exception();
    }
}
  • 事務不生效的原因: 其實rollbackFor屬性指定的異常必須是Throwable或者其子類。預設情況下,RuntimeExceptionError兩種異常都是會自動回滾的。但是因為以上的程式碼例子,指定了rollbackFor = Error.class,但是丟擲的異常又是Exception,而Exception和Error沒有任何什麼繼承關係,因此事務就不生效。
美團二面:spring事務不生效的15種場景

大家可以看一下Transactional註解原始碼哈:

美團二面:spring事務不生效的15種場景
  • 解決方案rollbackFor屬性指定的異常與丟擲的異常匹配。

11.事務註解被覆蓋導致事務失效

public interface MyRepository {
    @Transactional
    void save(String data);
}

public class MyRepositoryImpl implements MyRepository {
    @Override
    public void save(String data) {
        // 資料庫操作
    }
}

public class MyService {

    @Autowired
    private MyRepository myRepository;

    @Transactional
    public void doSomething(String data) {
        myRepository.save(data);
    }
}

public class MyTianluoService extends MyService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doSomething(String data) {
        super.doSomething(data);
    }
}
  • 事務失效的原因MyTianluoServiceMyService的子類,並且覆蓋了doSomething()方法。在該方法中,使用了不同的傳播行為(REQUIRES_NEW)來覆蓋父類的@Transactional註解。在這種情況下,當呼叫MyTianluoServicedoSomething()方法時,由於子類方法中的註解覆蓋了父類的註解,Spring框架將不會在父類的方法中啟動事務。因此,當MyRepositorysave()方法被呼叫時,事務將不會被啟動,也不會回滾。這將導致資料不一致的問題,因為在MyRepositorysave()方法中進行的資料庫操作將不會回滾。

12.巢狀事務的坑

@Service
public class TianLuoServiceInOutService {

    @Autowired
    private TianLuoFlowService tianLuoFlowService;
    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        tianLuoFlowService.saveFlow(tianluo);
    }
}

@Service
public class TianLuoFlowService {

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void saveFlow(TianLuo tianLuo) {
        tianLuoFlowMapper.save(tianLuo);
        throw new RuntimeException();
    }
}

以上程式碼使用了巢狀事務,如果saveFlow出現執行時異常,會繼續往上拋,到外層addTianLuo的方法,導致tianLuoMapper.save也會回滾啦。如果不想因為被內部巢狀的事務影響,可以用try-catch包住,如下:

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        tianLuoMapper.save(tianluo);
        try {
            tianLuoFlowService.saveFlow(tianluo);
        } catch (Exception e) {
          log.error("save tian luo flow fail,message:{}",e.getMessage());
        }
    }

13. 事務多執行緒呼叫

@Service
public class TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowService tianLuoFlowService;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        //儲存tianluo資料庫記錄
        tianLuoMapper.save(tianluo);
        //多執行緒呼叫
        new Thread(() -> {
            tianLuoFlowService.saveFlow(tianluo);
        }).start();
    }
}

@Service
public class TianLuoFlowService {

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void save(TianLuo tianLuo) {
        tianLuoFlowMapper.saveFlow(tianLuo);
    }
}
  • 事務不生效原因:這是因為Spring事務是基於執行緒繫結的,每個執行緒都有自己的事務上下文,而多執行緒環境下可能會存在多個執行緒共享同一個事務上下文的情況,導致事務不生效。Spring事務管理器透過使用執行緒本地變數(ThreadLocal)來實現執行緒安全。大家有興趣的話,可以去看下原始碼哈.

在Spring事務管理器中,透過TransactionSynchronizationManager類來管理事務上下文。TransactionSynchronizationManager內部維護了一個ThreadLocal物件,用來儲存當前執行緒的事務上下文。在事務開始時,TransactionSynchronizationManager會將事務上下文繫結到當前執行緒的ThreadLocal物件中,當事務結束時,TransactionSynchronizationManager會將事務上下文從ThreadLocal物件中移除。

美團二面:spring事務不生效的15種場景

14.異常被捕獲並處理了,沒有重新丟擲

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) {
        try {
            //儲存tianluo資料庫記錄
            tianLuoMapper.save(tianluo);
            //儲存tianluo flow資料庫記錄
            tianLuoFlowMapper.saveFlow(tianluo);
        } catch (Exception e) {
            log.error("add TianLuo error,id:{},message:{}", tianluo.getId(),e.getMessage());
        }
    }

}
  • 事務不生效的原因: 事務中的異常已經被業務程式碼捕獲並處理,而沒有被正確地傳播回事務管理器,事務將無法回滾。我們可以從spring原始碼(TransactionAspectSupport這個類)中找到答案:
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {

 //這方法會省略部分程式碼,只留關鍵程式碼哈
  @Nullable
 protected Object invokeWithinTransaction(Method method, @Nullable Class

invokeWithinTransaction方法中,當Spring catch到Throwable異常的時候,就會呼叫completeTransactionAfterThrowing()方法進行事務回滾的邏輯。但是,在TianLuoServiceImpl類的spring事務方法addTianLuo中,直接把異常catch住了,並沒有重新throw出來,因此 Spring自然就catch不到異常啦,因此事務回滾的邏輯就不會執行,事務就失效了。

  • 解決方案:在spring事務方法中,當我們使用了try-catch,如果catch住異常,記錄完異常日誌什麼的,一定要重新把異常丟擲來,正例如下:
@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;

    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional(rollbackFor = Exception.class)
    public void addTianLuo(TianLuo tianluo) {
        try {
            //儲存tianluo資料庫記錄
            tianLuoMapper.save(tianluo);
            //儲存tianluo flow資料庫記錄
            tianLuoFlowMapper.saveFlow(tianluo);
        } catch (Exception e) {
            log.error("add TianLuo error,id:{},message:{}", tianluo.getId(),e.getMessage());
            throw e;
        }
    }
}

15. 手動拋了別的異常

@Service
public class TianLuoServiceImpl implements TianLuoService {

    @Autowired
    private TianLuoMapper tianLuoMapper;
    
    @Autowired
    private TianLuoFlowMapper tianLuoFlowMapper;

    @Transactional
    public void addTianLuo(TianLuo tianluo) throws Exception {
        //儲存tianluo資料庫記錄
        tianLuoMapper.save(tianluo);
        //儲存tianluo流水資料庫記錄
        tianLuoFlowMapper.saveFlow(tianluo);
        throw new Exception();
    }
}
  • 失效的原因:上面的程式碼例子中,手動拋了Exception異常,但是是不會回滾的,因為Spring預設只處理RuntimeException和Error,對於普通的Exception不會回滾,除非,用rollbackFor屬性指定配置。
  • 解決方案:新增屬性配置@Transactional(rollbackFor = Exception.class)

註解為事務範圍的方法中,事務的回滾僅僅對於unchecked的異常有效。對於checked異常無效。也就是說事務回滾僅僅發生在,出現RuntimeException或Error的時候。通俗一點就是:程式碼中出現的空指標等異常,會被回滾。而檔案讀寫、網路超時問題等,spring就沒法回滾了。




來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2943381/,如需轉載,請註明出處,否則將追究法律責任。

相關文章