zoukankan      html  css  js  c++  java
  • 5、Spring事务

    学习资源:动力节点的2020最新Spring框架教程【IDEA版】-Spring框架从入门到精通



    1. 什么是事务
      事务是指一组sql语句的集合, 集合中有多条sql语句可能是insert , update ,select ,delete, 我们希望这些多个 sql 语句都能成功,或者都失败,控制这些sql语句的执行是一致的,作为一个整体执行
    2. 什么时候使用事务
      当数据库操作涉及得到多个表,或者是多个 insert,update,delete 的 sql 语句,需要保证这些语句都是成功才能完成功能,或者都失败,保证操作是符合要求的
      在 java 程序中,控制事务应该是在 service 类的业务方法上,因为业务方法会调用多个 dao 方法,执行多条 sql 语句。
    3. 不同数据库访问技术处理事务的方式
      1. jdbc访问数据库,处理事务:Connection conn; conn.commit(); conn.rollback();
      2. mybatis访问数据库,处理事务:SqlSession.commit(); SqlSession.rollback();
      3. hibernate访问数据库,处理事务:Session.commit(); Session.rollback();
      4. ......
    4. 不同的数据库访问技术处理事务的弊端
      1. 不同的数据库访问技术,处理事务使用的对象、方法不同,开发人员需要了解不同数据库访问技术及使用事务的原理
      2. 掌握多种数据库中事务的处理逻辑,什么时候提交事务、什么时候回滚事务
      3. 一种技术可能有多种处理事务方法
    5. 怎么解决弊端
      spring 提供了一种处理事务的统一模型, 能使用统一的步骤、方式完成多种不同数据库访问技术的事务处理。
      使用spring的事务处理机制,可以完成mybatis访问数据库的事务处理
      使用spring的事务处理机制,可以完成hibernate访问数据库的事务处理
      ......

    spring事务处理


    事务原本是数据库中的概念,在 Dao 层,但一般情况下,需要将事务提升到业务层,即 Service 层,这样做是为了能够使用事务的特性来管理具体的业务。

    在 Spring 中通常可以通过以下两种方式来实现对事务的管理:

    1. 使用 Spring 的事务注解管理事务,适用于中小项目
    2. 使用 AspectJ 的 AOP 配置管理事务

    1、Spring 事务管理 API

    Spring 的事务管理,主要用到两个事务相关的接口。

    1.1、事务管理器接口

    事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

    image-20200902223116468

    1.1.1、接口实现类

    • DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
    • HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。

    1.1.2、Spring 的回滚方式

    • 发生 运行时异常 和 error 回滚事务,发生受查(编译)异常提交事务。
    • 对于受查异常,程序员也可以手工设置其回滚方式。

    1.1.3、回顾错误与异常类型

    image-20200902223407852

    Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时, 才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。

    Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、ThreadDeath、 NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出)的, JVM 一般会终止线程。

    程序在编译和运行时出现的另一类错误称之为异常,它是 JVM 通知程序员的一种方式。通过这种方式,让程序员知道已经或可能出现错误,要求程序员对其进行处理。

    异常分为运行时异常与受查异常。

    运行时异常,是 RuntimeException 类或其子类, 即只有在运行时才出现的异常。如,NullPointerException、 ArrayIndexOutOfBoundsException、 IllegalArgumentException 等均属于运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。但只要代码编写足够仔细,程序足够健壮,运行时异常是可以避免的。

    受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理,则无法通过编译。如 SQLException, ClassNotFoundException, IOException 等都属于受查异常。RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception的子类,即用户自定义的异常也属受查异常。程序员在定义异常时,只要未明确声明定义的为 RuntimeException 的子类,那么定义的就是受查异常。


    1.2、事务定义接口

    事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别事务传播行为事务默认超时时限,及对它们的操作。

    image-20200902223758333

    1.2.1、5 个事务隔离级别常量

    这些常量均是以 ISOLATION_ 开头。即形如 ISOLATION_XXX

    • DEFAULT: 采用 DB 默认的事务隔离级别。 MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。
    • READ_UNCOMMITTED: 读未提交。未解决任何并发问题。
    • READ_COMMITTED: 读已提交。解决脏读,存在不可重复读与幻读。
    • REPEATABLE_READ: 可重复读。解决脏读、不可重复读,存在幻读。
    • SERIALIZABLE: 串行化。不存在并发问题。

    1.2.2、7 个事务传播行为常量

    所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。

    如, A 事务中的方法 doSome() 调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

    事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX

    • PROPAGATION_REQUIRED:"我"需要事务
      指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。如该传播行为加在 doOther() 方法上。若 doSome() 方法在调用 doOther() 方法时就是在事务内运行的,则 doOther() 方法的执行也加入到该事务内执行。若 doSome() 方法在调用 doOther() 方法时没有在事务内执行,则 doOther() 方法会创建一个事务,并在其中执行。

    image-20200902224257620

    • PROPAGATION_REQUIRES_NEW:"我"需要属于自己的事务
      总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。

    image-20200902224413188

    • PROPAGATION_SUPPORTS:"我"支持事务
      指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。

    image-20200902224453573

    • PROPAGATION_MANDATORY
    • PROPAGATION_NESTED
    • PROPAGATION_NEVER
    • PROPAGATION_NOT_SUPPORTED

    1.2.3、事务默认超时时限

    常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限, sql 语句的执行时长,默认值为 -1 ,表示时限无限长。

    注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。


    2、搭建测试环境

    2.1、创建两个测试用的表:

    • sale 销售记录表

    image-20200903112835991

    • goods 商品表

    image-20200903112842521

    image-20200903112849139


    2.2、maven 依赖

    <dependencies>
        <!-- 测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
        <!-- ioc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <!-- 事务 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- 整合 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.4</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <!-- 德鲁伊连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
    </dependencies>
    
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory><!--所在的目录-->
                <includes><!--包括目录下的.properties,.xml 文件都会扫描到-->
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    

    2.3、创建实体类

    package com.chen.pojo;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Goods {
    
        private Integer id;
        private String name;
        private Integer amount;
        private Float price;
    }
    
    package com.chen.pojo;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Sale {
    
        private Integer id;
        private Integer gid;
        private Integer nums;
    }
    

    2.4、创建 dao 接口

    package com.chen.dao;
    
    public interface GoodsDao {
        
        int updateGoods(Goods goods);
        Goods selectGoods(Integer goodsId);
    }
    
    package com.chen.dao;
    
    public interface SaleDao {
        int insertSale(Sale sale);
    }
    

    2.5、创建 mapper

    <mapper namespace="">
        <update id="updateGoods">
            update goods set amount = amount - #{amount} where id=#{id}
        </update>
        <select id="selectGoods" resultType="com.chen.pojo.Goods">
            select * from goods where id=#{goodId}
        </select>
    </mapper>
    
    <mapper namespace="">
        <insert id="insertSale">
            insert into sale(gid, nums) values(#{gid}, #{nums})
        </insert>
    </mapper>
    

    2.6、创建 mybati-config

    <configuration>
    
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
    
        <!--  给每一个pojo单独起别名  -->
        <typeAliases>
            <package name="com.chen.pojo"/>
        </typeAliases>
    
        <!-- sql映射文件的位置 -->
        <mappers>
            <package name="com.chen.dao"/>
        </mappers>
    
    </configuration>
    

    2.7、定义异常类

    定义 service 层可能会抛出的异常类 NotEnoughException ,直接继承运行时异常类即可

    package com.chen.myError;
    
    public class NotEnoughException extends RuntimeException{
    
        public NotEnoughException() {
            super();
        }
        
        public NotEnoughException(String msg) {
                super(msg); 
        }
    }
    

    2.8、创建 service 接口及实现类

    package com.chen.service;
    
    public interface BuyGoosService {
        void buyGoods(Integer goodsId, Integer amount);
    }
    
    package com.chen.service.impl;
    
    @Setter
    public class BuyGoosServiceImpl implements BuyGoosService {
    
        private GoodsDao goodsDao;
        private SaleDao saleDao;
    
        @Override
        public void buyGoods(Integer goodsId, Integer amount) {
    
            // 正常逻辑应该是先判断参数是否正确,再记录、售出
            Sale sale = new Sale();
            sale.setGid(goodsId);
            sale.setNums(amount);
            saleDao.insertSale(sale);
            Goods goods = goodsDao.selectGoods(goodsId);
    
            if (goods == null) {
                throw new NullPointerException("无此商品");
            }
            if (goods.getAmount() < amount) {
                throw new NotEnoughException("库存不足");
            }
    
            goods = new Goods();
            goods.setAmount(amount);
            goods.setId(goodsId);
            goodsDao.updateGoods(goods);
        }
    }
    

    2.9、创建 spring 配置文件

    jdbc.url=jdbc:mysql://localhost:3306/ssm?useSSL=true&useUnicode=true&characterEncoding=UTF-8
    #用户名
    jdbc.username=root
    #用户密码
    jdbc.password=
    #新版本的MySQL8驱动
    jdbc.driver=com.mysql.cj.jdbc.Driver
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    
        <property name="driverClassName" value="${jdbc.driver}"/>
    
        <property name="filters" value="stat"/>
    
        <property name="maxActive" value="20"/>
        <property name="initialSize" value="1"/>
        <property name="maxWait" value="60000"/>
        <property name="minIdle" value="1"/>
    
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>
    
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
    
        <property name="poolPreparedStatements" value="true"/>
        <property name="maxOpenPreparedStatements" value="20"/>
    
        <property name="asyncInit" value="true"/>
    </bean>
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <property name="basePackage" value="com.chen.dao,com.chen.dao2"/>
    </bean>
    
    <bean id="buyGoodsService" class="com.chen.service.impl.BuyGoodsServiceImpl">
        <property name="goodsDao" ref="goodsDao"/>
        <property name="saleDao" ref="saleDao"/>
    </bean>
    
    <!-- 事务 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    

    2.10、测试

    无事务管理下测试

    @Test
    public void test1(){
    
        String resource = "applicationContext.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(resource);
        BuyGoodsService buyGoodsService = (BuyGoodsService) context.getBean("buyGoodsService");
        // 正常购买
        buyGoodsService.buyGoods(1001, 10);
    }
    
    @Test
    public void test2(){
    
        String resource = "applicationContext.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(resource);
        BuyGoodsService buyGoodsService = (BuyGoodsService) context.getBean("buyGoodsService");
        // 非正常购买,但 Sale 却被修改了
        buyGoodsService.buyGoods(1000, 10);
    }
    
    @Test
    public void test3(){
    
        String resource = "applicationContext.xml";
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(resource);
        BuyGoodsService buyGoodsService = (BuyGoodsService) context.getBean("buyGoodsService");
        // 非正常购买,但是 Sale 却被修改了
        buyGoodsService.buyGoods(1001, 10000);
    }
    

    3、Spring 管理事务的两种方式

    1. Spring 的事务注解管理事务,适合中小项目
    2. AspectJ 的 AOP 配置管理事务,适合大型项目,

    3.1、使用 Spring 的事务注解管理事务

    通过 @Transactional 注解方式, 可将事务织入到相应 sevice 的 public 方法中,实现事务管理。

    该注解属性:

    • propagation: 用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED。
    • isolation: 用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。
    • readOnly: 用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。
    • timeout: 用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
    • rollbackFor: 指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
    • rollbackForClassName: 指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
    • noRollbackFor: 指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
    • noRollbackForClassName: 指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

    需要注意的是, @Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注 @Transactional, 虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。若 @Transaction 注解在类上,则表示该类上所有的 public 方法均将在执行时织入事务.

    实现 spring 注解的事务管理步骤:

    1. 配置文件中声明事务管理器
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    1. 开启注解驱动
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    1. 在业务方法上使用 @Transactional ,并配置事务通知的属性,一般使用默认值即可。
    @Transactional(propagation = Propagation.REQUIRED, 
                   rollbackFor = {NotEnoughException.class, NullPointerException.class}, 
                   readOnly = false)
    public void buyGoods(Integer goodsId, Integer amount) {
    
        Sale sale = new Sale();
        sale.setGid(goodsId);
        sale.setNums(amount);
        saleDao.insertSale(sale);
        Goods goods = goodsDao.selectGoods(goodsId);
    
        if (goods == null) {
            throw new NullPointerException("无此商品");
        }
        if (goods.getAmount() < amount) {
            throw new NotEnoughException("库存不足");
        }
    
        goods = new Goods();
        goods.setAmount(amount);
        goods.setId(goodsId);
        goodsDao.updateGoods(goods);
    }
    

    3.2、使用 AspectJ 的 AOP 配置管理事务

    实现声明式事务管理的步骤:

    1. maven 依赖
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    
    1. 在容器中添加事务管理器
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    1. 配置业务方法的事务通知属性(隔离级别、传播行为、超时时间),注意此时并没有指定给哪些类的方法配置事务
      一般使用默认属性即可。
      spring 事务匹配优先顺序:完整方法名 > 带 * 的方法名 > *
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <!--tx:attributes:配置事务属性-->
        <tx:attributes>
            <!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
                name:方法名称,1)完整的方法名称,不带有包和类。
                              2)方法可以使用通配符,* 表示任意字符
                propagation:传播行为,枚举值
                isolation:隔离级别
                rollback-for:你指定的异常类名,全限定类名。 发生异常一定回滚
            -->
            <!-- 完整方法名,指定一个方法 -->
            <tx:method name="buyGoods" propagation="REQUIRED" isolation="DEFAULT"
                       rollback-for="java.lang.NullPointerException,com.bjpowernode.excep.NotEnoughException"/>
    
            <!--使用通配符,需要业务方法有命名规则,可以指定很多的方法-->
            <!--指定添加方法-->
            <tx:method name="add*" propagation="REQUIRES_NEW" />
            <!--指定修改方法-->
            <tx:method name="modify*" />
            <!--删除方法-->
            <tx:method name="remove*" />
            <!--查询方法,query,search,find-->
            <tx:method name="*" propagation="SUPPORTS" read-only="true" />
        </tx:attributes>
    </tx:advice>
    
    1. 配置增强(切入)器
    <aop:config>
        <!--    配置切入点表达式:指定哪些包中的方法,要使用事务
                id:切入点表达式的名称,唯一值
                expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象
                com.chen.service
                com.crm.service
                com.service
    	-->
        <aop:pointcut id="servicePoint" expression="execution(* *..service..*.*(..))"/>
        <!--   配置增强器:关联 adivce 和 pointcut
               advice-ref:通知,上面tx:advice哪里的配置
               pointcut-ref:切入点表达式的id
    	-->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="servicePoint" />
    </aop:config>
    
  • 相关阅读:
    10年测试专家深度解读接口测试
    测试技术大牛谈成长经历:一个好的软件测试工程师应该做到这些!
    一位测试老鸟的工作经验分享
    又一名程序员倒下,网友:我们只是新时代农民工
    软件测试工程师这样面试,拿到offer的几率是80%
    App测试流程及测试点(个人整理版)
    自动化测试是什么?
    软件测试工程师的职业技能分析
    月薪15k的测试员需要学习什么技术?
    面向对象
  • 原文地址:https://www.cnblogs.com/sout-ch233/p/13622378.html
Copyright © 2011-2022 走看看