精华内容
下载资源
问答
  • 2022-03-19 11:27:30

     

    目录

    1、service没有托管给spring 

     2、抛出受检异常

     3、业务自己捕获了异常

    4、切面顺序导致

     5、非public方法

    具体步骤:

    1、在pom引入aspectjrt坐标以及相应插件

     2、在启动类上加上如下配置

    3、直接用TransactionTemplate

     6、父子容器

    7、方法用final修饰

    8、方法用static修饰

    9、调用本类方法

    10、多线程调用

    11、错误的传播行为

    12、使用了不支持事务的存储引擎

    13、数据源没有配置事务管理器

    14、被代理的类过早实例化

    总结


    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上运行,则需做如下配置

    在这里插入图片描述

    3、直接用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:点我查看

    更多相关内容
  • Spring事务操作示例(四种方式),包含完整代码和数据库文件(基于MySQL,在项目sql文件夹中),可运行,学习Spring事务详见博客:http://blog.csdn.net/daijin888888/article/details/51822257
  • Spring事务和MySQL事务详解面试

    万次阅读 多人点赞 2020-09-10 23:56:36
    文章目录数据库事务事务是什么事务的四大特性MySQL事务隔离级别查看MySQL当前事务隔离级别MySQL默认操作模式为自动提交模式JDBC处理事务Spring事务Spring的事务传播PROPAGATION_REQUIREDPROPAGATION_...

    数据库事务

    事务是什么

    是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合。

    事务的四大特性

    1. 原子性
      事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做

    2. 一致性
      事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。

    3. 隔离性
      一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

    4. 持续性
      也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

    MySQL事务隔离级别

    隔离级别隔离级别的值导致的问题
    Read-Uncommitted0导致脏读
    Read-Committed1避免脏读,允许不可重复读和幻读
    Repeatable-Read2MySQL默认的隔离级别。避免脏读,不可重复读,允许幻读
    Serializable3串行化读,事务只能一个一个执行,避免了 脏读、不可重复读、幻读。执行效率慢,使用时慎重

    1. 脏读

    一个事务对数据进行了增删改查,但是未提交事务。另一个事物可以读取到未提交的数据,如果第一个事务进行了回滚,那么第二个事务就读到了脏数据。

    例子:

    领导给张三发工资,10000元已打到张三账户,但该事务还未提交,正好这时候张三去查询工资,发现10000元已到账。这时领导发现张三工资算多了5000元,于是回滚了事务,修改了金额后将事务提交。最后张三实际到账的只有5000元。

    2. 不可重复度

    一次事务发生了两次读操作,两个读操作之间发生了另一个事务对数据修改操作,这时候第一次和第二次读到的数据不一致。

    不可重复度关注点在数据更新和删除,通过行级锁可以实现可重复读的隔离级别。

    例子:

    张三需要转正1000元,系统读到卡余额有2000元,此时张三老婆正好需要转正2000元,并且在张三提交事务前把2000元转走了,当张三提交转账是系统提示余额不足。

    3. 幻读

    幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。

    相对于不可重复读,幻读更关注其它事务的新增数据。通过行级锁可以避免不可重复读,但无法解决幻读的问题,想要解决幻读,只能通过Serializable隔离级别来实现。

    例子:

    张三老婆准备打印张三这个月的信用卡消费记录,经查询发现消费了两次共1000元,而这时张三刚按摩完准备结账,消费了1000元,这时银行记录新增了一条1000元的消费记录。当张三老婆将消费记录打印出来时,发现总额变为了2000元,这让张三老婆很诧异。

    4. 串行化读

    Serializable是最高的隔离级别,性能很低,一般很少用。在这级别下,事务是串行顺序执行的,不仅避免了脏读、不可重复读,还避免了幻读。

    查看MySQL当前事务隔离级别

    MySQL InnoDB默认的事务隔离级别为REPEATABLE-READ

    mysql> select @@tx_isolation;
    +-----------------+
    | @@tx_isolation  |
    +-----------------+
    | REPEATABLE-READ |
    +-----------------+
    

    MySQL默认操作模式为自动提交模式

    除非显示的开启一个事务,否则每个查询都被当成一个单独的事务自动执行。可以通脱设置autocommit的值改变默认的提交模式。

    1. 查看当前提交模式
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | ON    |
    +---------------+-------+
    
    1. 关闭自动提交。0代表关闭,1代表开启。
    mysql> set autocommit = 0;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> show variables like 'autocommit';
    +---------------+-------+
    | Variable_name | Value |
    +---------------+-------+
    | autocommit    | OFF   |
    +---------------+-------+
    

    JDBC处理事务

    Connection connection = null;
    PreparedStatement pstmt = null;
    ResultSet resultSet = null;
    
    try {
        Class.forName("com.mysql.jdbc.Driver");
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbname?characterEncoding=utf-8","username", "password");
    
        connection.setAutoCommit(false);
        
        // others ......
        
        connection.commit();
    } catch (Exception e) {
        connection.rollback();
    } finally {
        connection.setAutoCommit(true);
        // close connection
    }
    

    Spring事务

    Spring事务本质是对数据库事务的支持,如果数据库不支持事务(例如MySQL的MyISAM引擎不支持事务),则Spring事务也不会生效。

    Spring的事务传播

    事务传播行为是指一个事务方法A被另一个事务方法B调用时,这个事务A应该如何处理。事务A应该在事务B中运行还是另起一个事务,这个有事务A的传播行为决定。

    事务传播属性定义TransactionDefinition

    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    
    常量名称常量解释
    PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,就新建一个事务。这是Spring 默认的事务的传播。
    PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
    PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,就抛出异常。
    PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后, 不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获, 也可以不处理回滚操作。 使用JtaTransactionManager作为事务管理器
    PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。使用JtaTransactionManager作为事务管理器
    PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
    PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。

    PROPAGATION_REQUIRED

    如果存在一个事务,则支持当前事务,如果没有事务则开启事务。
    image

    如下例子,单独调用methodB时,当前上下文没有事务,所以会开启一个新的事务。

    调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务A中来。

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        methodB();
        // do something
    }
     
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodB() {
        // do something
    }
    

    PROPAGATION_SUPPORTS

    如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行.
    image

    单独的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        methodB();
        // do something
    }
     
    // 事务属性为SUPPORTS
    @Transactional(propagation = Propagation.SUPPORTS)
    public void methodB() {
        // do something
    }
    

    PROPAGATION_MANDATORY

    如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
    image

    当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”)

    当调用methodA时,methodB则加入到methodA的事务中,以事务方式执行。

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        methodB();
        // do something
    }
    
    @Transactional(propagation = Propagation.MANDATORY)
    public void methodB() {
        // do something
    }
    

    PROPAGATION_REQUIRES_NEW

    使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
    它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。
    image

    从下面代码可以看出,事务B与事务A是两个独立的事务,互不相干。事务B是否成功并不依赖于 事务A。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了

    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        doSomeThingA();
        methodB();
        doSomeThingB();
        // do something else
    }
     
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // do something
    }
    

    当调用methodA(),相当于

    public static void main(){
        TransactionManager tm = null;
        try{
            //获得一个JTA事务管理器
            tm = getTransactionManager();
            tm.begin();//开启一个新的事务
            Transaction ts1 = tm.getTransaction();
            doSomeThing();
            tm.suspend();//挂起当前事务
            try{
                tm.begin();//重新开启第二个事务
                Transaction ts2 = tm.getTransaction();
                methodB();
                ts2.commit();//提交第二个事务
            } Catch(RunTimeException ex) {
                ts2.rollback();//回滚第二个事务
            } finally {
                //释放资源
            }
            //methodB执行完后,恢复第一个事务
            tm.resume(ts1);
            doSomeThingB();
            ts1.commit();//提交第一个事务
        } catch(RunTimeException ex) {
            ts1.rollback();//回滚第一个事务
        } finally {
            //释放资源
        }
    }
    

    PROPAGATION_NOT_SUPPORTED

    总是非事务地执行,并挂起任何存在的事务。

    使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。

    image

    PROPAGATION_NEVER

    总是非事务地执行,如果存在一个活动事务,则抛出异常。

    PROPAGATION_NESTED

    如果一个活动的事务存在,则运行在一个嵌套的事务中。

    如果没有活动事务,则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。

    这是一个嵌套事务,使用JDBC3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。 需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。

    image

    @Transactional(propagation = Propagation.REQUIRED)
    methodA(){
        doSomeThingA();
        methodB();
        doSomeThingB();
    }
     
    @Transactional(propagation = Propagation.NEWSTED)
    methodB(){
        // do something
    }
    

    单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,则相当于:

    main(){
        Connection con = null;
        Savepoint savepoint = null;
        try{
            con = getConnection();
            con.setAutoCommit(false);
            doSomeThingA();
            savepoint = con2.setSavepoint();
            try{
                methodB();
            } catch(RuntimeException ex) {
                con.rollback(savepoint);
            } finally {
                //释放资源
            }
            doSomeThingB();
            con.commit();
        } catch(RuntimeException ex) {
            con.rollback();
        } finally {
            //释放资源
        }
    }
    

    当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。

    需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

    Spring事务的隔离级别

    事务隔离级别定义TransactionDefinition

    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    
    隔离级别解释
    ISOLATION_DEFAULT这是个 PlatfromTransactionManager 默认的隔离级别, 使用数据库默认的事务隔离级别。另外四个与 JDBC 的 隔离级别相对应。
    ISOLATION_READ_UNCOMMITTED这是事务最低的隔离级别,它允许另外一个事务可以看 到这个事务未提交的数据。这种隔离级别会产生脏读, 不可重复读和幻像读。
    ISOLATION_READ_COMMITTED保证一个事务修改的数据提交后才能被另外一个事务读 取。另外一个事务不能读取该事务未提交的数据。 ISOLATION_REPEATABLE_READ
    ISOLATION_SERIALIZABLE这是花费最高代价但是最可靠的事务隔离级别。事务被 处理为顺序执行。

    Spring事务基本配置样例

    <aop:aspectj-autoproxy proxy-target-class="true"/>
    	
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref="dataSource"/>
    </bean>
    
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    	<tx:attributes>
    		<tx:method name="add*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
    		<tx:method name="remove*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
    		<tx:method name="edit*" propagation="REQUIRED" rollback-for="Exception,RuntimeException,SQLException"/>
    		<tx:method name="login" propagation="NOT_SUPPORTED"/>
    		<tx:method name="query*" read-only="true"/>
    	</tx:attributes>
    </tx:advice>
    
    <aop:config>
    	<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
        <aop:aspect ref="dataSource">
            <aop:pointcut id="transactionPointcut" expression="execution(public * com.gupaoedu..*.service..*Service.*(..))" />
        </aop:aspect>
    </aop:config>
    
    展开全文
  • 深入理解 Spring 事务原理

    千次阅读 2021-03-10 00:30:42
    作者:xiaolyuhhttps://my.oschina.net/xiaolyuh/blog/3109049Spring事务的基本原理Spring事务的本质其实就是数据库对事务的支持,没...

    作者:xiaolyuh

    https://my.oschina.net/xiaolyuh/blog/3109049

    Spring事务的基本原理

    Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

    1. 获取连接 Connection con = DriverManager.getConnection()

    2. 开启事务con.setAutoCommit(true/false);

    3. 执行CRUD

    4. 提交事务/回滚事务 con.commit() / con.rollback();

    5. 关闭连接 conn.close();

    使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。

    下面简单地介绍下,注解方式为例子

    1. 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。

    2. spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。

    3. 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

    Spring的事务机制

    所有的数据访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或者在发生错误的时候回滚数据。

    而Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现,如表所示。

    数据访问技术及实现

    在程序中定义事务管理器的代码如下:

    @Bean   
    public PlatformTransactionManager transactionManager() {   
      
     JpaTransactionManager transactionManager = new JpaTransactionManager();   
     transactionManager.setDataSource(dataSource());   
     return transactionManager;   
    }  
    

    声名式事务

    Spring支持声名式事务,即使用注解来选择需要使用事务的方法,它使用@Transactional注解在方法上表明该方法需要事务支持。这是一个基于AOP的实现操作。

    @Transactional   
    public void saveSomething(Long  id, String name) {   
        //数据库操作   
    }  
    

    在此处需要特别注意的是,此@Transactional注解来自org.springframework.transaction.annotation包,而不是javax.transaction。

    AOP 代理的两种实现:

    • jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到;

    • cglib是子类,private的方法照样不会出现在子类里,也不能被拦截。

    Java 动态代理。

    具体有如下四步骤:

    1. 通过实现 InvocationHandler 接口创建自己的调用处理器;

    2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;

    3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;

    4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

    GCLIB代理

    cglib(Code Generation Library)是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。

    • cglib封装了asm,可以在运行期动态生成新的class(子类)。

    • cglib用于AOP,jdk中的proxy必须基于接口,cglib却没有这个限制。

    原理区别:

    java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

    1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP

    2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP

    3. 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

    如果是类内部方法直接不是走代理,这个时候可以通过维护一个自身实例的代理。

    @Service  
    public class PersonServiceImpl implements PersonService {  
        @Autowired  
        PersonRepository personRepository;  
      
        // 注入自身代理对象,在本类内部方法调用事务的传递性才会生效  
        @Autowired  
        PersonService selfProxyPersonService;  
      
        /**  
         * 测试事务的传递性  
         *  
         * @param person  
         * @return  
         */  
        @Transactional  
        public Person save(Person person) {  
            Person p = personRepository.save(person);  
            try {  
                // 新开事务 独立回滚  
                selfProxyPersonService.delete();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            try {  
                // 使用当前事务 全部回滚  
                selfProxyPersonService.save2(person);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            personRepository.save(person);  
      
            return p;  
        }  
      
        @Transactional  
        public void save2(Person person) {  
            personRepository.save(person);  
            throw new RuntimeException();  
        }  
      
        @Transactional(propagation = Propagation.REQUIRES_NEW)  
        public void delete() {  
            personRepository.delete(1L);  
            throw new RuntimeException();  
        }  
    }  
    

    Spring 事务的传播属性

    所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:

    数据库隔离级别

    脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。

    不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。

    幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。

    总结:

    隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

    大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle

    少数数据库默认隔离级别为:Repeatable Read 比如:MySQL InnoDB

    Spring中的隔离级别

    事务的嵌套

    通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。

    假设外层事务 Service A 的 Method A() 调用 内层Service B 的 Method B()

    PROPAGATION_REQUIRED(spring 默认)

    如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。

    假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。

    这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。

    PROPAGATION_REQUIRES_NEW

    比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。

    那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。

    他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。

    PROPAGATION_SUPPORTS

    假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

    PROPAGATION_NESTED

    现在的情况就变得比较复杂了, ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:

    a、捕获异常,执行异常分支逻辑

    void methodA() {   
      
            try {   
      
                ServiceB.methodB();   
      
            } catch (SomeException) {   
      
                // 执行其他业务, 如 ServiceC.methodC();   
      
            }   
      
        }  
    

    这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。

    b、 外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback

    另外三种事务传播属性基本用不到,在此不做分析。

    总结

    对于项目中需要使用到事务的地方,我建议开发者还是使用spring的TransactionCallback接口来实现事务,不要盲目使用spring事务注解,如果一定要使用注解,那么一定要对spring事务的传播机制和隔离级别有个详细的了解,否则很可能发生意想不到的效果。

    Spring Boot 对事务的支持

    通过org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration类。我们可以看出Spring Boot自动开启了对注解事务的支持 Spring

    只读事务(@Transactional(readOnly = true))的一些概念

    • 概念:

    从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。

    @Transcational(readOnly=true) 这个注解一般会写在业务类上,或者其方法上,用来对其添加事务控制。当括号中添加readOnly=true, 则会告诉底层数据源,这个是一个只读事务,对于JDBC而言,只读事务会有一定的速度优化。而这样写的话,事务控制的其他配置则采用默认值,事务的隔离级别(isolation) 为DEFAULT,也就是跟随底层数据源的隔离级别,事务的传播行为(propagation)则是REQUIRED,所以还是会有事务存在,一代在代码中抛出RuntimeException,依然会导致事务回滚。

    • 应用场合:

    1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;

    2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

    【注意是一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务】

    参考

    • http://www.codeceo.com/article/spring-transactions.html

    • http://www.cnblogs.com/fenglie/articles/4097759.html

    • https://www.zhihu.com/question/39074428/answer/88581202

    • http://blog.csdn.net/andyzhaojianhui/article/details/51984157

    END

    推荐好文

    强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!
    
    分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!
    能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!
    
    
    
    
    展开全文
  • 聊聊spring事务失效的12种场景,太坑了

    千次阅读 多人点赞 2021-09-04 13:29:15
    为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。 确实,spring事务用起来贼爽,就用一个简单的注解:@Transactional,就能轻松搞定事务。我猜大部分小...

    请添加图片描述

    前言

    对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。

    在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

    确实,spring事务用起来贼爽,就用一个简单的注解:@Transactional,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。

    但如果你使用不当,它也会坑你于无形。

    今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。不信,让我们一起看看。

    最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

    BAT大佬写的刷题笔记,让我offer拿到手软

    一 事务不生效

    1.访问权限问题

    众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

    但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:

    @Service
    public class UserService {
        
        @Transactional
        private void add(UserModel userModel) {
             saveData(userModel);
             updateData(userModel);
        }
    }
    

    我们可以看到add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。

    说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

    protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
          return null;
        }
    
        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
    
        // First try is the method in the target class.
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
        if (txAttr != null) {
          return txAttr;
        }
    
        // Second try is the transaction attribute on the target class.
        txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
        if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
          return txAttr;
        }
    
        if (specificMethod != method) {
          // Fallback is to look at the original method.
          txAttr = findTransactionAttribute(method);
          if (txAttr != null) {
            return txAttr;
          }
          // Last fallback is the class of the original method.
          txAttr = findTransactionAttribute(method.getDeclaringClass());
          if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
            return txAttr;
          }
        }
        return null;
      }
    

    也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。

    2. 方法用final修饰

    有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:

    @Service
    public class UserService {
    
        @Transactional
        public final void add(UserModel userModel){
            saveData(userModel);
            updateData(userModel);
        }
    }
    

    我们可以看到add方法被定义成了final的,这样会导致事务失效。

    为什么?

    如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

    但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

    注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

    3.方法内部调用

    有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Transactional
        public void add(UserModel userModel) {
            userMapper.insertUser(userModel);
            updateStatus(userModel);
        }
    
        @Transactional
        public void updateStatus(UserModel userModel) {
            doSameThing();
        }
    }
    

    我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

    由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

    那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

    3.1 新加一个Service方法

    这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

    @Servcie
    public class ServiceA {
       @Autowired
       prvate ServiceB serviceB;
    
       public void save(User user) {
             queryData1();
             queryData2();
             serviceB.doSave(user);
       }
     }
    
     @Servcie
     public class ServiceB {
    
        @Transactional(rollbackFor=Exception.class)
        public void doSave(User user) {
           addData1();
           updateData2();
        }
    
     }
    

    3.2 在该Service类中注入自己

    如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

    @Servcie
    public class ServiceA {
       @Autowired
       prvate ServiceA serviceA;
    
       public void save(User user) {
             queryData1();
             queryData2();
             serviceA.doSave(user);
       }
    
       @Transactional(rollbackFor=Exception.class)
       public void doSave(User user) {
           addData1();
           updateData2();
        }
     }
    

    可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

    答案:不会。

    其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。但有些坑,如果你想进一步了解循环依赖问题,可以看看我之前文章《spring:我是如何解决循环依赖的?》。

    3.3 通过AopContent类

    在该Service类中使用AopContext.currentProxy()获取代理对象

    上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

    @Servcie
    public class ServiceA {
    
       public void save(User user) {
             queryData1();
             queryData2();
             ((ServiceA)AopContext.currentProxy()).doSave(user);
       }
    
       @Transactional(rollbackFor=Exception.class)
       public void doSave(User user) {
           addData1();
           updateData2();
        }
     }
    

    4.未被spring管理

    在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

    通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

    当然创建bean实例的方法还有很多,有兴趣的小伙伴可以看看我之前写的另一篇文章《@Autowired的这些骚操作,你都知道吗?

    如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如:

    //@Service
    public class UserService {
    
        @Transactional
        public void add(UserModel userModel) {
             saveData(userModel);
             updateData(userModel);
        }    
    }
    

    从上面的例子,我们可以看到UserService类没有加@Service注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。

    5.多线程调用

    在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?

    @Slf4j
    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
        @Autowired
        private RoleService roleService;
    
        @Transactional
        public void add(UserModel userModel) throws Exception {
            userMapper.insertUser(userModel);
            new Thread(() -> {
                roleService.doOtherThing();
            }).start();
        }
    }
    
    @Service
    public class RoleService {
    
        @Transactional
        public void doOtherThing() {
            System.out.println("保存role表数据");
        }
    }
    

    从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

    这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

    如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

    private static final ThreadLocal<Map<Object, Object>> resources =
    
      new NamedThreadLocal<>("Transactional resources");
    

    我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

    6.表不支持事务

    周所周知,在mysql5之前,默认的数据库引擎是myisam

    它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。

    有些老项目中,可能还在用它。

    在创建表的时候,只需要把ENGINE参数设置成MyISAM即可:

    CREATE TABLE `category` (
      `id` bigint NOT NULL AUTO_INCREMENT,
      `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
      `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
      `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
      `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
    

    myisam好用,但有个很致命的问题是:不支持事务

    如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。

    此外,myisam还不支持行锁和外键。

    所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。

    有时候我们在开发的过程中,发现某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务。

    7.未开启事务

    有时候,事务没有生效的根本原因是没有开启事务。

    你看到这句话可能会觉得好笑。

    开启事务不是一个项目中,最最最基本的功能吗?

    为什么还会没有开启事务?

    没错,如果项目已经搭建好了,事务功能肯定是有的。

    但如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?

    当然原因有很多,但没有开启事务,这个原因极其容易被忽略。

    如果你使用的是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。

    你所要做的事情很简单,只需要配置spring.datasource相关参数即可。

    但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。

    具体配置如下信息:

       
    <!-- 配置事务管理器 --> 
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
        <property name="dataSource" ref="dataSource"></property> 
    </bean> 
    <tx:advice id="advice" transaction-manager="transactionManager"> 
        <tx:attributes> 
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes> 
    </tx:advice> 
    <!-- 用切点把事务切进去 --> 
    <aop:config> 
        <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
        <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
    </aop:config> 
    

    默默的说一句,如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。

    二 事务不回滚

    1.错误的传播特性

    其实,我们在使用@Transactional注解时,是可以指定propagation参数的。

    该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:

    • REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
    • SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
    • MANDATORY 如果当前上下文中存在事务,否则抛出异常。
    • REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
    • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
    • NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
    • NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

    如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

    @Service
    public class UserService {
    
        @Transactional(propagation = Propagation.NEVER)
        public void add(UserModel userModel) {
            saveData(userModel);
            updateData(userModel);
        }
    }
    

    我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

    目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

    2.自己吞了异常

    事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常。比如:

    @Slf4j
    @Service
    public class UserService {
        
        @Transactional
        public void add(UserModel userModel) {
            try {
                saveData(userModel);
                updateData(userModel);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }
    

    这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

    如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

    3.手动抛了别的异常

    即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

    @Slf4j
    @Service
    public class UserService {
        
        @Transactional
        public void add(UserModel userModel) throws Exception {
            try {
                 saveData(userModel);
                 updateData(userModel);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new Exception(e);
            }
        }
    }
    

    上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

    因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

    4.自定义了回滚异常

    在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。

    但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

    @Slf4j
    @Service
    public class UserService {
        
        @Transactional(rollbackFor = BusinessException.class)
        public void add(UserModel userModel) throws Exception {
           saveData(userModel);
           updateData(userModel);
        }
    }
    

    如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

    即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

    这是为什么呢?

    因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。

    5.嵌套事务回滚多了

    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private RoleService roleService;
    
        @Transactional
        public void add(UserModel userModel) throws Exception {
            userMapper.insertUser(userModel);
            roleService.doOtherThing();
        }
    }
    
    @Service
    public class RoleService {
    
        @Transactional(propagation = Propagation.NESTED)
        public void doOtherThing() {
            System.out.println("保存role表数据");
        }
    }
    

    这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

    why?

    因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

    怎么样才能只回滚保存点呢?

    @Slf4j
    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private RoleService roleService;
    
        @Transactional
        public void add(UserModel userModel) throws Exception {
    
            userMapper.insertUser(userModel);
            try {
                roleService.doOtherThing();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }
    

    可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

    三 其他

    1 大事务问题

    在使用spring事务时,有个让人非常头疼的问题,就是大事务问题。

    通常情况下,我们会在方法上@Transactional注解,填加事务功能,比如:

    @Service
    public class UserService {
        
        @Autowired 
        private RoleService roleService;
        
        @Transactional
        public void add(UserModel userModel) throws Exception {
           query1();
           query2();
           query3();
           roleService.save(userModel);
           update(userModel);
        }
    }
    
    
    @Service
    public class RoleService {
        
        @Autowired 
        private RoleService roleService;
        
        @Transactional
        public void save(UserModel userModel) throws Exception {
           query4();
           query5();
           query6();
           saveData(userModel);
        }
    }
    

    @Transactional注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

    上面的这个例子中,在UserService类中,其实只有这两行才需要事务:

    roleService.save(userModel);
    update(userModel);
    

    在RoleService类中,只有这一行需要事务:

    saveData(userModel);
    

    现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。

    如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

    关于大事务问题的危害,可以阅读一下我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,上面有详细的讲解。


    最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

    BAT大佬写的刷题笔记,让我offer拿到手软

    2.编程式事务

    上面聊的这些内容都是基于@Transactional注解的,主要说的是它的事务问题,我们把这种事务叫做:声明式事务

    其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务。例如:

    
       @Autowired
       private TransactionTemplate transactionTemplate;
       
       ...
       
       public void save(final User user) {
             queryData1();
             queryData2();
             transactionTemplate.execute((status) => {
                addData1();
                updateData2();
                return Boolean.TRUE;
             })
       }
    

    在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。

    相较于@Transactional注解声明式事务,我更建议大家使用,基于TransactionTemplate的编程式事务。主要原因如下:

    1. 避免由于spring aop问题,导致事务失效的问题。
    2. 能够更小粒度的控制事务的范围,更直观。

    请添加图片描述

    建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

    展开全文
  • Spring事务传播机制

    千次阅读 2022-01-13 03:44:59
    当我们在使用Spring所提供的事务功能时,如果是仅仅处理单个的事务,是比较容易把握事务的提交与回滚,不过一旦引入嵌套事务后,多个事务的回滚和提交就会变得复杂起来,各个事务之间是如何相互影响的
  • ---- 推荐访问权限问题方法用final修饰未被spring管理多线程调用表不支持事务未开启事务事务不回滚错误的传播特性自己吞了异常手动抛了别的异常自定义了回滚异常嵌套事务回滚多了其他常见问题编程式事务事务问题 ...
  • spring 事务机制总结

    千次阅读 2020-06-28 19:37:58
    目录 为什么会有传播机制 传播机制生效条件 传播机制类型 示例代码 ...spring事务的控制,是使用 aop 切面实现的,我们不用关心事务的开始,提交 ,回滚,只需要在方法上加 @Transactional 注解,
  • 一文带你深入理解 Spring 事务原理

    千次阅读 2020-12-25 07:24:00
    正文 Spring事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行: 获取连接 Connection...
  • 详解spring事务失效的12种场景详解spring事务失效的12种场景前言一 事务不生效1.访问权限问题2. 方法用final修饰3.方法内部调用3.1 新加一个Service方法3.2 在该Service类中注入自己3.3 通过AopContent类4.未被...
  • spring事务的传播机制

    千次阅读 多人点赞 2020-08-03 09:41:24
    spring事务是封装在数据库事务之上的一种事务处理机制,它有两种管理方式:编程式事务和声明式事务。在平时使用中,我们大多使用@Transactional声明式事务来管理,这也是spring推荐的方式,下面例子也统一采用此种...
  • 数据库事务和spring事务的区别

    千次阅读 2022-03-17 11:45:24
    数据库事务和spring事务 本质上其实是同一个概念,spring的事务是对数据库的事务的封装,最后本质的实现还是在数据库,假如数据库不支持事务的话,spring的事务是没有作用的.数据库的事务说简单就只有开启,回滚和关闭,...
  • 详细整理Spring事务失效的具体场景及解决方案

    万次阅读 多人点赞 2020-08-04 22:25:14
    好多小伙伴可能只是简单了解一下,遇到事务失效的情况,便会无从下手,溪源此篇文章给大家整理了一下常见Spring事务失效的场景,希望开发过程尽量避免踩坑,造成时间精力的浪费。 溪源按照最基本的使用方式以及常见...
  • Spring事务失效的8种情况

    千次阅读 2021-11-16 13:47:17
    Spring事务失效的8种情况 总结: 1、数据库引擎不支持事务 从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。 2、没有被 Spring ...
  • } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { //Spring事务会开启一个Connection并与当前线程绑定 ConnectionHolder conHolder = (ConnectionHolder) ...
  • Spring事务与数据库事务的交互原理

    千次阅读 2021-01-27 11:01:33
    Spring事务和数据库事务到底是什么关系?Spring事务是如何传递到数据库的?首先,我们来回忆一下jdbc这个api,这是java操作数据库的入口,是java操作数据库的通道,是我们与数据库打交道的必经之路。那么,是不是说...
  • Spring事务@Transactional注解原理

    千次阅读 2021-07-02 17:46:58
    Spring 事务管理分为编程式和声明式两种。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体的逻辑与事务处理解耦。 声明式事务有两种方式,一种是在配置文件(XML)中做相关的事务规则声明,另一...
  • Spring事务的隔离级别

    千次阅读 2022-04-04 22:10:52
    Spring事务的隔离级别一、Spring事务的隔离级别 一、Spring事务的隔离级别   1.ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别...
  • Spring 事务隔离级别和传播行为

    千次阅读 2021-11-15 23:50:35
    一、spring支持的事务声明方式 编程式事务 当系统需要明确的,细粒度的控制各个事务的边界,应选择编程式事务。 声明式事务 当系统对于事务的控制粒度较粗时,应该选择声明式事务 二、spring支持7种事务传播...
  • spring事务传播机制

    千次阅读 2021-07-26 15:12:52
    spring事务传播机制前言一、7种事务传播类型1.1 支持当前事务1.2 不支持当前事务1.3 NESTED二、示例2.1 required2.读入数据总结 前言 spring事务传播行为的含义: 简单的理解就是多个事务方法相互调用时,事务如何...
  • spring 事务实现方式有哪些?

    万次阅读 多人点赞 2019-07-18 19:16:25
    spring 事务实现方式有哪些? 编程式事务管理,在代码中调用 commit()、rollback()等事务管理相关的方法 maven pom.xml文件 <dependency> <groupId>org.springframework</groupId> <...
  • Spring 事务失效的场景

    千次阅读 2020-08-11 14:38:25
    数据库事务1.1 事务的ACID特性1.2 多事务的并发进行造成的问题1.3 MySQL事务的隔离级别1.4 Spring事务的传播特性1.5 Spring事务的失效场景1.5.1 本类中没有事务的方法调用含有事务的方法1.5.2 rollbackFor属性使用...
  • spring事务实现的几种方式

    千次阅读 2021-07-26 10:07:57
    spring事务处理 前言 1.、事务几种实现方式 (1)编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。 ...
  • Spring事务失效常见场景

    千次阅读 2022-03-24 12:21:02
     Spring的声明式事务是基于动态代理实现的,我们无法重写final修饰的方法;  不管是JDK动态代理还是Cglib的动态代理,就是要通过代理的方式获取到代理的具体对象,而static方法修饰的方法是属于类的,不属于任何...
  • spring-04 spring事务底层原理分析

    千次阅读 2019-02-26 10:31:54
    数据库的事务的基本特性1.1 演示1.1.1 sql准备1.1.2 设置连接参数1.2 数据库默认隔离级别1.3 查看mysql 的默认隔离级别2.spring对事务的支持与使用2.1 spring 事务相关API说明2.1.1 如何使用spring事务2.2.2 接口...
  • Spring事务实现的方式及底层原理

    千次阅读 2022-04-03 11:45:42
    spring事务的实现方式 spring框架提供了两种事务实现方式:编程式事务、声明式事务 编程式事务:在代码中进行事务控制。优点:精度高。缺点:代码耦合度高 声明式事务:通过@Transactional注解实现事务控制 spring...
  • spring事务的开启方法

    千次阅读 2022-03-14 17:46:31
    AOP方式自动开启事务。 不会自动开启事务,通过类添加注解@EnableTransactionManagement方法上添加 @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) 开启事务
  • Spring事务嵌套机制

    万次阅读 2019-07-10 14:35:13
    Spring事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务.结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。 其实这是不认识Spring事务传播机制而造成的误解...
  • spring事务失效原因及解决方案

    千次阅读 2022-02-17 14:27:01
    spring事务的注意事项 spring的声明式事务是基于代理模式的。其实代理模式相当简单, 就是将另一个类包裹在我们的类外面, 在调用我们创建的方法之前, 先经过外面的方法, 进行一些处理, 返回之前, 再进行一些操作。...
  • Spring事务隔离级别与设置

    千次阅读 2021-06-18 00:42:17
    Spring事务隔离级别比数据库事务隔离级别多一个default 1) DEFAULT (默认) 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。2) READ_...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 350,808
精华内容 140,323
关键字:

spring事物

spring 订阅
友情链接: Dll2C.zip