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>
原文地址:https://www.cnblogs.com/sout-ch233/p/13622378.html