美文网首页
Spring事务管理

Spring事务管理

作者: 小马蛋 | 来源:发表于2019-11-15 14:11 被阅读0次

    上一篇我们讲了数据库(基于MySQL为蓝本)的事务还有与之对应的隔离级别,着重讲述了事务和隔离级别的概念和少量应用场景,这一篇主要讲一讲在web框架Spring里的使用。

    一、Spring事务接口与ORM对应关系

    由于Spring是web框架,所以它其实并不与MySQL数据库进行直连,一般都是通过ORM层框架,诸如JDBCiBatisMyBatisHibernateJPA等,与数据库进行连接的,而事务的管理都是交由各持久层框架自行处理的,Spring只是出具了事务管理器接口org.springframework.transaction.PlatformTransactionManager及不同的PlatformTransactionManager实现类,为不同的持久层框架提供对应的实现类,将管理事务的职责下放到各持久层框架,交由持久层框架处理。

    • org.springframework.jdbc.datasource.DataSourceTransactionManager:使用JDBCiBatisMyBatis进行持久化数据时使用

    • org.springframework.orm.hibernate5.HibernateTransactionManager:使用Hibernate5版本进行持久化数据时使用

    • org.springframework.orm.jpa.JpaTransactionManager:使用JPA进行持久化数据时使用

    • org.springframework.jdo.JdoTransactionManager:当持久化机制是jdo时使用

    • org.springframework.transaction.jta.JtaTransactionManager:使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用

    等等...


    Spring事务类图

    二、Spring事务的开启方式

    编程式:

    我不知道该怎么称呼它,因为自从工作以来,也没有遇到过,但是不代表人家不存在,也许很老的项目里依然存在。主要依靠手动处理来管理事务,对业务代码有侵入性。

    1)编程式,可以使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate

        代码不做展示,太冗余了
    

    声明式:

    声明式事务本质是建立在AOP基础之上的,在代理类实例里拦截目标方法,在方法执行前,开启或者加入一个事务,执行完毕或者发生异常后,根据具体情况进行提交或者回滚。
    相比较编程式事务,其最大的特点就是对业务代码没有侵入性,只需在配置文件中对事务进行配置管理(或者基于@Transactional注解的方式)即可,这样就可以将业务代码和事务管理隔离开。

    1)TransactionProxyFactoryBean声明式事务管理
    此方式是最传统,配置文件最臃肿、难以阅读的一种方式

        代码不做展示,太冗余了
    

    2)@Transactional注解事务管理
    基于注解的事务管理,在需要开启事务的类或方法上添加@Transactional,这种方式比较灵活,随用随加,但是容易到处乱用,凌乱不堪。更偏重灵活性。

        <!--  开启注解管理事务 -->
        <tx:annotation-driven transaction-manager="transactionManager"/>
    

    3)Aspectj AOP配置事务管理

        <!-- 事务AOP -->
        <aop:config>
            <!-- pointcut:切入点 aop:advisor 适配器,是要注入的方法和pointcut连接的桥梁 -->
            <aop:advisor pointcut="execution(* cn.x.x..*.service..*.*(..))" advice-ref="txAdvice" />
        </aop:config>
    
        <!-- 需要注入的方法,可以使用通配符进行匹配 -->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <tx:attributes>
                <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
                <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
                <tx:method name="update" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
                <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
            </tx:attributes>
        </tx:advice>
    
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource" />
        </bean>
    

    三、事务的配置信息

    基于Aspectj AOP配置事务管理进行配置讲解

    首先重温一下:在上一篇《MySQL事务及隔离级别》里,我们介绍了MySQL事务如果没有设置隔离级别,并发可能产生的几个安全问题:

    • 脏写

    • 脏读

    • 不可重复读

    • 幻读

    一些大佬,对此设计出了数据库事务的隔离级别,来确保根据具体情况,适时适当选用对应的策略,这种策略我们称之为事务的隔离级别,如下:

    • 读未提交 - 可能存在脏读、不可重复读、幻读

    • 读已提交 - 可能存在不可重复读、幻读

    • 可重复读 - 可能存在幻读(设计MySQL的大叔已经可以确保,在这个级别不出现幻读的情况)

    • 串行化 - 排队吃果果 很安全,但是真的影响效率

    属性设置

    <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"  read-only="" no-rollback-for="" timeout=""/>
    
    1、name 与事务属性关联的方法名

    可以使用通配符(*)关联一批相同事务属性的方法

    2、isolation 隔离级别

    默认值:DEFAULT

    • DEFAULT:使用数据库默认的事务隔离级别

    • READ_UNCOMMITTED:读未提交

    • READ_COMMITTED:读已提交

    • REPEATABLE_READ:可重复读

    • SERIALIZABLE: 串行化

    3、propagation 事务传播行为

    默认值:REQUIRED

    1)MANDATORY:该方法必须在事务中运行,如果当前事务不存在,则抛出异常

    2)NESTED:如果当前已经存在一个事务,则新建一个嵌套事务,该事务可以有多个回滚点,如果没有事务,则按REQUIRED属性执行,此方法对应的事务回滚不会对外部(嵌套)事务造成影响,但是外部事务回滚会影响内部事务。

    3)NEVER:该方法不会在事务中运行,否则抛出异常。

    4)NOT_SUPPORTED:此方法不需要事务,如果调用方处于一个事务中,那么该事务将被挂起,直到此方法执行完毕,被挂起的事务才恢复执行。

    5)REQUIRED:方法需要在事务中运行,如果运行时,该方法已经处于一个事务中,那么将加入到这个事务,否则自己创建一个事务。

    6)REQUIRED_NEW:当调用该方法时,无论此方法是否已经处在一个事务中,都会为重新创建一个新的事务,如已经存在一个事务了,那么该事务将被挂起,然后创建新的事务,直到此方法执行结束(事务被提交或者回滚),原先的事务才被执行。如果内部事务发生回滚,会影响已经挂起的事务。

    7)SUPPORTS:调用方在事务中,那么此方法也加入到事务中,如果没有没有事务,则非事务的方式执行。

    4、timeout 超时

    默认值 -1
    挺有意思的一个小东西,如果项目里使用MyBatis,即使设置了超时时间,也是没有任何效果的,暂时的解决办法是,使用MyBatis的超时设置或者使用JdbcTemplate进行数据库的操作。
    我相信,你一定对这个有兴趣

    5、read-only 只读

    默认值 false
    事务是否只读

    如果当前事务是只读,且在事务内进行增、删、改操作,则会抛出异常(Cannot execute statement in a READ ONLY transaction。)
    只读属性有什么用?

    6、rollback-for 触发回滚的异常

    可以有多个,以逗号相隔,如果是自定义异常,则需要类的完全限定名,我查阅很多资料,发现大家都趋向于下面的结论:

    如果发生的是检查时异常,那么事务不会自动回滚,如果是运行时异常,事务会自动回滚

    经过实际测试后发现,如果我们指定rollback-for="Exception",那么,抛出检查型异常一样可以触发事务的回滚。

    官方文档给出的信息是:

    默认情况下,检查时异常不会引起事务回滚,但是RuntimeException及其子类会引起事务回滚

    7、no-rollback-for 不触发回滚的异常

    和上面的类似,请自我揣摩~

    四、拓展

    声明式事务是建立在AOP基础上的,我们以下面的代码作为蓝本:

    假设事务的隔离级别是RR(Repeatable-Read)
    配置信息:
        <!-- 事务AOP -->
        <aop:config>
            <!-- pointcut:切入点 aop:advisor 适配器,是要注入的方法和pointcut连接的桥梁 -->
            <aop:advisor pointcut="execution(* x.x..*.service..*.*(..))" advice-ref="txAdvice" />
        </aop:config>
    
        <!-- 需要注入的方法,可以使用通配符进行匹配 -->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <tx:attributes>
                <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" />
                <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/>
            </tx:attributes>
        </tx:advice>
    
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource" />
        </bean>
    
    代码:
        public interface StudentService {
    
            List<Student> getStudents(String name);
    
            void insertStudent(Student student);
    
            deleteStudent(int id);
    
        }
    
        ===================分割线====================
    
        @Service
        public class StudentServiceImpl implements StudentService {
            
            @Autowired
            private StudentDao studentDao;
    
            @Override
            public List<Student> getStudents(String name) {
                Map<String, Object> params = Maps.newHashMap();
                params.put("name", name);
    
                return studentDao.getStudents(params);
            }
    
            @Override
            public void insertStudent(Student student) {
                studentDao.insert(student);
            }
            
            @Override
            public void deleteStudent(int id) {
                studentDao.delete(id);
            }
        }
    
        ===================分割线====================
    
        @RestController
        @RequestMapping("/student")
        public class StudentController {
            
            @Autowired
            private StudentService studentService;
    
            @GetMapping("/test")
            public void run() {
                student.getStudents("张三");
    
                Student student = Student.builder()
                             .id(1)
                             .name("张三")
                             .age(19)
                             .build();
    
                student.insertStudent(student);
                
                student.deleteStudent(student.getId());
            }
        }
    

    如上,StudentService 的 insert和get方法均开启了事务,但是deleteStudent方法没有开启事务。

    问题:

    1)在deleteStudent()里调用insertStudent(),试问,insertStudent()是否开启事务?为什么?

    还原代码:

        @Service
        public class StudentServiceImpl {
            
            @Autowired
            private StudentDao studentDao;
    
            public void insertStudent(Student student) {
                studentDao.insert(student);
            }
            
            public void deleteStudent(int id) {
                // 删除逻辑代码
    
                // 调用
                insertStudent(student);
            }
        }
    

    答案:

    不开启事务

    解读:

    ① 当StudentService.java文件进行编译的时候,编译器会将deleteStudent()方法里的insertStudent(student);编译成this.insertStudent(student);

    ② Controller层使用@Autowired注解进行StudentService的自动装配,但装配的实例并不是StudentServiceImpl类的实例,而是代理类的实例!声明式事务是建立在AOP基础上的,而AOP的实现原理就是代理模式!所以我们在Controller层所调用的方法会被代理类拦截。

    ③ 在代理类的拦截方法内,如果发现要调用的目标类方法是要开启事务的,那么就会在调用目标类方法之前开启事务,在目标类方法执行完毕或者发生异常后,根据情况执行回滚或不执行回滚。回归正题,当代理类发现deleteStudent()没有开启事务的要求,那么代理类是不会开启事务的。

    当我们在deleteStudent()里调用insertStudent()时,重点来了,jvm执行的是this.insertStudent()这个this是目标类本身,而不是代理类!但是事务的管理却在代理类里,所以,insertStudent()方法并不会开启事务!

    思考:
    通过上面的解读,我们知道了service上的事务是由代理类来管理的,类似的像@Async异步标签,其实也是由代理类进行管理的,在service内部调用打上@Async标签的内部方法,异步也不会起作用的。

    如果想调用内部方法,还想让事务/异步起作用,我们可以使用:xml配置方式暴露代理对象,通过代理对象AopContext.currentProxy()去调用方法。
    xml:

    <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
    
    @Service
    public class StudentServiceImpl {
            
        @Autowired
        private StudentDao studentDao;
    
        public void insertStudent(Student student) {
            studentDao.insert(student);
        }
            
        public void deleteStudent(int id) {
            // 删除逻辑代码
    
            // 调用
            (StudentService) AopContext.currentProxy()).insertStudent(student);
        }
    }
    

    2)在insertStudent()里调用deleteStudent(),试问,如果deleteStudent()抛出运行时异常,insertStudent()对应的操作是否回滚?为什么?

    还原代码:

        @Service
        public class StudentServiceImpl {
            
            @Autowired
            private StudentDao studentDao;
    
            public void insertStudent(Student student) {
                studentDao.insert(student);
                
                deleteStudent(student.getId());
            }
            
            public void deleteStudent(int id) {
                studentDao.delete(id);
                if (1 == 1)
                    throw new RuntimeException("测试回滚");
            }
        }
    

    答案:

    回滚

    解读:

    ① 当StudentService.java文件进行编译的时候,insertStudent(student)方法里的deleteStudent();变成this.deleteStudent();,没毛病,继续。

    ② Controller层调用insertStudent()时,会开启事务,一样没毛病,继续。

    ③ 在insertStudent()里调用this.deleteStudent(),相当于将deleteStudent()的代码块加入到当前事务中,所以如果deleteStudent()抛出运行时异常,那么insertStudent()对应的操作会回滚。

    相关文章

      网友评论

          本文标题:Spring事务管理

          本文链接:https://www.haomeiwen.com/subject/anxsictx.html