spring事務失效的幾種場景以及原因

linyb極客之路發表於2022-03-01

前言

spring事務失效場景可能大家在很多文章都看過了,所以今天就水一篇,看大家能不能收穫一些不一樣的東西。直接進入主題

spring事務失效場景以及原因

1、場景一:service沒有託管給spring

public class TranInvalidCaseWithoutInjectSpring {

    private UserService userService;

    public TranInvalidCaseWithoutInjectSpring(UserService userService) {
        this.userService = userService;
    }

    @Transactional
    public boolean add(User user){
        boolean isSuccess = userService.save(user);
        int i = 1 % 0;
        return isSuccess;
    }
}
    @Test
    public void testServiceWithoutInjectSpring(){
        boolean randomBoolean = new Random().nextBoolean();
        TranInvalidCaseWithoutInjectSpring tranInvalidCaseWithoutInjectSpring;
        if(randomBoolean){
            tranInvalidCaseWithoutInjectSpring = applicationContext.getBean(TranInvalidCaseWithoutInjectSpring.class);
            System.out.println("service已經被spring託管");
        }else{
            tranInvalidCaseWithoutInjectSpring = new TranInvalidCaseWithoutInjectSpring(userService);
            System.out.println("service沒被spring託管");
        }

        boolean isSuccess = tranInvalidCaseWithoutInjectSpring.add(user);
        Assert.assertTrue(isSuccess);

    }

失效原因: spring事務生效的前提是,service必須是一個bean物件
解決方案: 將service注入spring

2、場景二:丟擲受檢異常

@Service
public class TranInvalidCaseByThrowCheckException {

    @Autowired
    private UserService userService;


    @Transactional
    public boolean add(User user) throws FileNotFoundException {
        boolean isSuccess = userService.save(user);
        new FileInputStream("1.txt");
        return isSuccess;
    }
    }
 @Test
    public void testThrowCheckException() throws Exception{
        boolean randomBoolean = new Random().nextBoolean();
        boolean isSuccess = false;
        TranInvalidCaseByThrowCheckException tranInvalidCaseByThrowCheckException = applicationContext.getBean(TranInvalidCaseByThrowCheckException.class);
        if(randomBoolean){
            System.out.println("配置@Transactional(rollbackFor = Exception.class)");
            isSuccess = tranInvalidCaseByThrowCheckException.save(user);
        }else{
            System.out.println("配置@Transactional");
            tranInvalidCaseByThrowCheckException.add(user);
        }

        Assert.assertTrue(isSuccess);

    }

失效原因: spring預設只會回滾非檢查異常和error異常
解決方案: 配置rollbackFor

3、場景三:業務自己捕獲了異常

 @Transactional
    public boolean add(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {

        }
        return isSuccess;
    }
  @Test
    public void testCatchExecption() throws Exception{
        boolean randomBoolean = new Random().nextBoolean();
        boolean isSuccess = false;
        TranInvalidCaseWithCatchException tranInvalidCaseByThrowCheckException = applicationContext.getBean(TranInvalidCaseWithCatchException.class);
        if(randomBoolean){
            randomBoolean = new Random().nextBoolean();
            if(randomBoolean){
                System.out.println("將異常原樣丟擲");
                tranInvalidCaseByThrowCheckException.save(user);
            }else{
                System.out.println("設定TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();");
                tranInvalidCaseByThrowCheckException.addWithRollBack(user);
            }
        }else{
            System.out.println("業務自己捕獲了異常");
            tranInvalidCaseByThrowCheckException.add(user);
        }

        Assert.assertTrue(isSuccess);

    }

失效原因: spring事務只有捕捉到了業務丟擲去的異常,才能進行後續的處理,如果業務自己捕獲了異常,則事務無法感知
解決方案:
1、將異常原樣丟擲;
2、設定TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

4、場景四:切面順序導致

@Service
public class TranInvalidCaseWithAopSort {

    @Autowired
    private UserService userService;

    @Transactional
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }



}
@Aspect
@Component
@Slf4j
public class AopAspect {


    @Around(value = " execution (* com.github.lybgeek.transcase.aopsort..*.*(..))")
    public Object around(ProceedingJoinPoint pjp){

        try {
            System.out.println("這是一個切面");
           return pjp.proceed();
        } catch (Throwable throwable) {
            log.error("{}",throwable);
        }

        return null;
    }
}

失效原因: spring事務切面的優先順序順序最低,但如果自定義的切面優先順序和他一樣,且自定義的切面沒有正確處理異常,則會同業務自己捕獲異常的那種場景一樣
解決方案:
1、在切面中將異常原樣丟擲;
2、在切面中設定TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

5、場景五:非public方法

@Service
public class TranInvalidCaseWithAccessPerm {

        @Autowired
        private UserService userService;

        @Transactional
        protected boolean save(User user){
            boolean isSuccess = userService.save(user);
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                throw new RuntimeException();
            }
            return isSuccess;
        }

}
public class TranInvalidCaseWithAccessPermTest {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        TranInvalidCaseWithAccessPerm tranInvalidCaseWithAccessPerm = context.getBean(TranInvalidCaseWithAccessPerm.class);
        boolean isSuccess = tranInvalidCaseWithAccessPerm.save(UserUtils.getUser());

        System.out.println(isSuccess);

    }
}

失效原因: spring事務預設生效的方法許可權都必須為public

解決方案:
1、將方法改為public;
2、修改TansactionAttributeSource,將publicMethodsOnly改為false【這個從原始碼跟蹤得出結論】
3、開啟 AspectJ 代理模式【從spring文件得出結論】

文件如下
Method visibility and @Transactional
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

具體步驟:

1、在pom引入aspectjrt座標以及相應外掛

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.9</version>
    <configuration>
        <showWeaveInfo>true</showWeaveInfo>
        <aspectLibraries>
            <aspectLibrary>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
             <goals>
              <goal>compile</goal>       <!-- use this goal to weave all your main classes -->
              <goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
            </goals>
        </execution>
    </executions>
</plugin> 

2、在啟動類上加上如下配置

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

注: 如果是在idea上執行,則需做如下配置
在這裡插入圖片描述
4、直接用TransactionTemplate

示例:

    @Autowired
    private TransactionTemplate transactionTemplate;

    private void process(){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                processInTransaction();
            }
        });

    }

6、場景六:父子容器

失效原因: 子容器掃描範圍過大,將未加事務配置的serivce掃描進來

解決方案:
1、父子容器個掃個的範圍;
2、不用父子容器,所有bean都交給同一容器管理

注: 因為示例是使用springboot,而springboot啟動預設沒有父子容器,只有一個容器,因此就該場景就演示示例了

7、場景七:方法用final修飾

    @Transactional
    public final boolean add(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 因為spring事務是用動態代理實現,因此如果方法使用了final修飾,則代理類無法對目標方法進行重寫,植入事務功能

解決方案:
1、方法不要用final修飾

8、場景八:方法用static修飾

  @Transactional
    public static boolean save(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 原因和final一樣

解決方案:
1、方法不要用static修飾

9、場景九:呼叫本類方法

   public boolean save(User user) {
        return this.saveUser(user);
    }

    @Transactional
    public boolean saveUser(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 本類方法不經過代理,無法進行增強

解決方案:
1、注入自己來呼叫;
2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()

10、場景十:多執行緒呼叫

 @Transactional(rollbackFor = Exception.class)
    public boolean save(User user) throws ExecutionException, InterruptedException {

        Future<Boolean> future = executorService.submit(() -> {
            boolean isSuccess = userService.save(user);
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                throw new Exception();
            }
            return isSuccess;
        });
        return future.get();


    }

失效原因: 因為spring的事務是通過資料庫連線來實現,而資料庫連線spring是放在threadLocal裡面。同一個事務,只能用同一個資料庫連線。而多執行緒場景下,拿到的資料庫連線是不一樣的,即是屬於不同事務

11、場景十一:錯誤的傳播行為

 @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

失效原因: 使用的傳播特性不支援事務

12、場景十二:使用了不支援事務的儲存引擎

失效原因: 使用了不支援事務的儲存引擎。比如mysql中的MyISAM

13、場景十三:資料來源沒有配置事務管理器

注: 因為springboot,他預設已經開啟事務管理器。org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration。因此示例略過

14、場景十四:被代理的類過早例項化

@Service
public class TranInvalidCaseInstantiatedTooEarly implements BeanPostProcessor , Ordered {

    @Autowired
    private UserService userService;


    @Transactional
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

失效原因: 當代理類的例項化早於AbstractAutoProxyCreator後置處理器,就無法被AbstractAutoProxyCreator後置處理器增強

總結

本文列舉了14種spring事務失效的場景,其實這14種裡面有很多都是歸根結底都是屬於同一類問題引起,比如因為動態代理原因、方法限定符原因、異常型別原因等

demo連結

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-transaction-invalid-case

相關文章