Spring 事务 学习

作者: jwfy | 来源:发表于2018-04-04 19:42 被阅读82次

    笔记简述
    本学习笔记主要是介绍了事务相关的基础知识,学习编程式事务和声明式事务等不同的事务使用方法。不过现在实际开发中,越来越多的服务都是分布式,单纯的spring事务已经无法解决数据问题了,但还是有必要去了解事务的知识点。
    Spring更多可查看Spring 源码学习

    目录

    Spring 事务 学习
    1、事务
    2、Spring 事务介绍
    2.1、TransactionDefinition 属性
    2.2、事务传递行为
    2.3、事务隔离级别
    3、Spring 事务使用方法
    3.1、编程式事务
    3.2、声明式事务
    3.2.1、TransactionInterceptor 方法
    3.2.2、TransactionProxyFactoryBean 方法
    3.2.3、命名空间 方法
    3.2.4、Transactional 注解 方法
    4、总结
    5、参考链接

    1、事务

    事务是值多个操作单元组成的集合,这多个操作单元组合在一起成为一个完整的工作单元,在执行过程中要么成功,要么失败,如果失败了则就相当于什么都没有发生一样,为了确保数据的完整性和一致性。就拿常见的取钱的例子吧,张三在ATM机器上取1000元钱,但是出现意外了,银行成功扣款,但是ATM机器却因为硬件故障导致出钞失败,张三就损失了1000元钱;如果银行扣款失败,但是却顺利取出了钱,银行就损失了1000元钱。在现实中这种情况需要绝对被解决,这就可以使用事务去解决了,银行扣款和ATM出钞分为2个操作单元,只有2个都成功了,才意味着成功取钱,否则就认为操作失败,所有的数据都回滚到发生之前。

    事务一般和数据连接绑定在一起使用,在操作数据库时,一般都是1、3、4步,但是加上事务必须得捕获异常,然后进行回滚操作,也就多了2、5两步了。其中回滚主要是使用类似binlog等方式恢复数据

    try{
        1. con = getConnection();  // 获取连接
        2. con.setAutoCommit(false);  // 设置是否进行自动提交
        3. doing....             // 拼接sql
        4. con.commit();      // 提交操作
    } catch(RuntimeException ex){  // 不一定就是运用时的异常才会被捕获,看用户自定义设置
        5. con.rollback();   //回滚事务
    } 
    

    多说一句,在接下来的学习中会了解到如今的实际场景中,其实事务使用的并不是很多。事务其实就类似于对一个数据库连接进行锁操作一样(这是自己描述的,并没有这个具体的说法,只是为了便于理解),而现在很多服务都是分布式架构的,必然存在多个数据库连接,而各个数据库连接之间没有关系,锁住或者控制一个数据库连接无法解决问题,况且还有更重要的幂等性问题

    事务包含了4个特性,分别是原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)

    • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
    • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
    • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
    • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

    2、Spring 事务介绍

    主要介绍下spring中的事务的基本情况。在spring中必须得由Spring中的某些对象接管原本的数据库连接,然后通过各种方式添加如上伪代码显示的2、5两步完成添加事务的操作。

    spring中提供事务的接口是PlatformTransactionManager接口类,具体实现有jdbc管理类等,具体如下图


    image
    // 获得需要的TransactionStatus对象,是一个事物的属性对象
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    // 提交
    void commit(TransactionStatus status) throws TransactionException;
    // 回滚
    void rollback(TransactionStatus status) throws TransactionException;
    

    2.1、TransactionDefinition 属性

    事务管理中必然需要管理各个事务的属性信息,而这些都存储在TransactionDefinition 接口类中

    public interface TransactionDefinition { 
        int getPropagationBehavior();
        //返回事务的传播行为。 
        int getIsolationLevel();
        //返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据。 
        int getTimeout();
        //返回事务必须在多少秒内完成,某些事务操作可能比较耗时,默认为-1
        boolean isReadOnly();
        //事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的。 
    }
    

    2.2、事务传递行为

    事务传递是指,在开始进行事务处理的时候,已经存在了一个上下文,此时该如何执行的这么一个过程。

    • TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
    • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行; 如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

    2.3、事务隔离级别

    事务隔离是指多个事务之间的隔离程度,当两个事务对数据库的同一条数据进行读写操作时,就会因为不同等级的隔离,出现不同的情况,当然隔离的程度越大性能消耗的也更多。

    • TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
    • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
    • TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
    • TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执 行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
    • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

    3、Spring 事务使用方法

    编写一些demo,实践中如何具体使用事务达到我们想要的目的(如下例子有参考网上实例,觉得这个例子挺好)

    数据库表结构


    image
    public class Money {
    
        private Long id;
        private String name;
        private Long moneyNum;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Long getMoneyNum() {
            return moneyNum;
        }
    
        public void setMoneyNum(Long moneyNum) {
            this.moneyNum = moneyNum;
        }
    
        @Override
        public String toString() {
            return "Money{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", moneyNum=" + moneyNum +
                    '}';
        }
    }
    
    @Component("moneyDao")
    public class MoneyDao {
    
        @Resource
        private JdbcTemplate jdbcTemplate;
    
        public void add(Long id, String name, Long moneyNum){
    
            String sql = "insert into test_money(id, name, money) value(?, ?, ?)";
    
            int record = jdbcTemplate.update(sql, id, name, moneyNum);
            System.out.println(record);
        }
    
        public void update(Long id, Long moneyNum){
            String sql = "update test_money set money = ? where id = ?";
    
            int record = jdbcTemplate.update(sql, moneyNum, id);
            System.out.println(record);
        }
    }
    
    <context:component-scan base-package="com.demo.jdbc" />
    
    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="......"/>
    </bean>
    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource" />
    </bean>
    

    3.1、编程式事务

    编程式代码也就是硬编码的形式,TransactionTemplate模板类用于简化事务管理,事务管理由模板类定义,而具体操作需要通过TransactionCallback回调接口或TransactionCallbackWithoutResult回调接口指定,通过调用模板类的参数类型为TransactionCallback或TransactionCallbackWithoutResult的execute方法来自动享受事务管理。

    • TransactionCallback 实现doInTransactionWithoutResult方法,里面填充事务需要管理的代码,有返回值
    • TransactionCallbackWithoutResult 继承自TransactionCallback接口,无需返回数据

    如果有看这两个代码,会发现其实调用的是同一个地方,只是无数据返回的是null罢了

    我们当前就选择无数据返回作为例子

    <!-- 配置SpringJdbc的事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!--配置事务管理模板,Spring为了简化事务管理的代码而提供的类-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"/>
        <!--  这里还可以设置事务隔离的级别属性-->
    </bean>
    
    public void change(){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                moneyDao.update(1L, 30L);
                // 添加70,移到李四上,当然这只是例子没有做查询减少的操作,直接更新罢了
                test();
                moneyDao.update(2L, 120L);
            }
        });
    }
    
    /**
     * 假设抛出了错误
     */
    private void test(){
        throw new IndexOutOfBoundsException("demo test");
    }
    

    如上代码所示,其中就有使用到上面说的TransactionTemplate模板类,在这里没有贴出具体的调用方法,自行注入执行,两个更新sql代码操作包裹在doInTransactionWithoutResult方法中。

    其中包含了抛出个运行时错误,如果没有事务,那第一个操作会成功,第二个会失败,但是这就导致了数据错误的情况,添加了事务,则同时成功或者回滚到操作前。

    在运行前还需要提一点的是,上一段话说了是抛出运行时错误,可是如果是sql本身或者事务具体执行的导致的异常呢?这个就需要取到TransactionStatus这个对象的数据,进行rollback操作,具体的原因后续的源码分析中会解答的。

    如下图,在有运行中抛出运行时错误之后,进行了回滚操作,并把异常,数据库的数据并没有更新


    image

    如果把抛出异常这步去掉,那么就可以正常更新数据了


    image image

    总结

    这种编程式的事务方法其实叫做基于 TransactionTemplate 的编程式事务管理,另外还有一种是基于底层 API 的编程式事务管理,他是利用了PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三个核心接口的API完成对事物的管理,不过本质来说只是表现形式不同而已,换汤不换药

    3.2、声明式事务

    在真正的开发中,如果需要管理的事务很多,使用编程式去硬编码完成,一方面使得代码耦合度提高了,另一方面再去修改成本也很大,维护难度提高了,最好还是无侵入式的方法最好,也就是我们现在所说的声明式事务,充分的使用spring的AOP功能,具体的AOP学习可以看Spring AOP学习,当然了按照spring的套路肯定提供了xml配置和注解两种方法了。

    3.2.1、TransactionInterceptor 方法

        <bean id="transactionInterceptor"
            class="org.springframework.transaction.interceptor.TransactionInterceptor">
            <property name="transactionManager" ref="transactionManager"/>
            <property name="transactionAttributes">
                <props>
                    <prop key="*">PROPAGATION_REQUIRED</prop>
                </props>
            </property>
        </bean>
    
        <bean id="moneyServiceProxy"
            class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="moneyService"/>
            <property name="interceptorNames">
                <list>
                    <idref bean="transactionInterceptor"/>
                </list>
            </property>
        </bean>
    

    在获取bean的时候,获取通过ProxyFactoryBean包装好的moneyServiceProxy,其他不需要做任何操作

    这里有一点需要主要的是,transactionInterceptor中的transactionAttributes属性信息,当前demo中的key是"*",实际上有各种可配置的,具体的配置是传播行为 [,隔离级别] [,只读属性] [,超时属性] [不影响提交的异常] [,导致回滚的异常]

    • 传播行为就是上面所述的传播行为的属性,隔离级别也是类似
    • 只读属性是readOnly字段
    • 超时属性是必须以TIMEOUT_开头,后面跟着一个数字,表示事务允许超时多少的最大表秒数
    • 不影响提交的异常是指出现该些异常,事务正常提交,不会发生回滚,需要加上+,例如+RuntimeException
    • 导致回滚的异常是值出现该些异常,事务将会回滚,需要加上-,例如-Exception

    同样的key中的表示方法名称,可以模糊匹配,如果是*则表示所有的函数都有事务,例如key="change",则意味着只有change函数需要添加事务

    3.2.2、TransactionProxyFactoryBean 方法

    上述的TransactionInterceptor虽然实现了无侵入式的方法,但是如果需要添加事务的类过多则就意味着所有的类都必须有这样的配置,spring提供了一个新的bean TransactionProxyFactoryBean,不过我个人觉得也没改善太多,就是把两个bean的内容组合到一起,减少了一些配置而已。

        <bean id="moneyServiceProxy2"
            class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
            <property name="target" ref="moneyService" />
            <property name="transactionManager" ref="transactionManager"/>
            <property name="transactionAttributes">
                <props>
                    <prop key="*">PROPAGATION_REQUIRED</prop>
                </props>
            </property>
        </bean>
    

    在具体使用的使用,使用moneyServiceProxy2这个bean即可,如果仔细查看配置的话,确实和TransactionInterceptor没有太多的区别

    3.2.3、命名空间 方法

    何所谓命名空间呢,就是利用spring本身的包含的各种NamespaceHandler去解析处理各种自定义的xml标签,自动注入各种bean去完成相关任务,在spring中提供了tx以及aop实现该功能

        <tx:advice id="transAdvice" transaction-manager="transactionManager">
            <!--配置事务传播性,隔离级别以及超时回滚等问题 -->
            <tx:attributes>
                <tx:method name="*" 
                    propagation="REQUIRED"
                    rollback-for="Exception" 
                    timeout="10"
                    read-only="true"
                    isolation="DEFAULT"
                    no-rollback-for="Exception" 
                    <!-- 以上都是配置的属性而已 -->
                />
            </tx:attributes>
        </tx:advice>
        
        <aop:config>
            <!--配置事务切点 -->
            <aop:pointcut id="services"
                expression="execution(* com.demo.jdbc.MoneyService.*(..))" />
            <aop:advisor pointcut-ref="services" advice-ref="transAdvice" />
        </aop:config>
    

    先是设置了一些包含事务的方法,并且设置有传递行为、隔离级别等属性,然后利用AOP的切面功能去实现事务处理

    3.2.4、Transactional 注解 方法

    既然xml配置都已经存在了,再支持注解的方法也是完全可以的

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
        ...
    

    可以支持在类上和方法上添加该注解,如果再类上面则该类中的public方法都会被添加上具体的事务方法的,例如如下配置

    @Transactional(propagation = Propagation.REQUIRED)
    public void change() {
        moneyDao.update(1L, 30L);        
        test();
        moneyDao.update(2L, 120L);
    }
    

    还有个点别忘记了,添加了注解还需要添加支持解析该注解的功能,在xml中添加上
    <tx:annotation-driven transaction-manager="transactionManager"/>

    4、总结

    主要是介绍了事务的基本信息以及如何具体的使用事务,从本质来说上面几种方法没有太多的差异,只是spring提供了更加便捷的方法去实现同样的功能,后续会学习源码层面,了解spring如何实现该功能的。以及异常如何被捕获回滚操作、幂等性的问题等还需要解决。

    5、参考链接

    相关文章

      网友评论

        本文标题:Spring 事务 学习

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