5、Spring事务
学习资源:动力节点的2020最新Spring框架教程【IDEA版】-Spring框架从入门到精通
- 什么是事务:
事务是指一组sql语句的集合, 集合中有多条sql语句可能是insert , update ,select ,delete, 我们希望这些多个 sql 语句都能成功,或者都失败,控制这些sql语句的执行是一致的,作为一个整体执行 - 什么时候使用事务:
当数据库操作涉及得到多个表,或者是多个 insert,update,delete 的 sql 语句,需要保证这些语句都是成功才能完成功能,或者都失败,保证操作是符合要求的
在 java 程序中,控制事务应该是在 service 类的业务方法上,因为业务方法会调用多个 dao 方法,执行多条 sql 语句。 - 不同数据库访问技术处理事务的方式:
- jdbc访问数据库,处理事务:
Connection conn;
conn.commit();
conn.rollback();
- mybatis访问数据库,处理事务:
SqlSession.commit();
SqlSession.rollback();
- hibernate访问数据库,处理事务:
Session.commit();
Session.rollback();
- ......
- jdbc访问数据库,处理事务:
- 不同的数据库访问技术处理事务的弊端:
- 不同的数据库访问技术,处理事务使用的对象、方法不同,开发人员需要了解不同数据库访问技术及使用事务的原理
- 掌握多种数据库中事务的处理逻辑,什么时候提交事务、什么时候回滚事务
- 一种技术可能有多种处理事务方法
- 怎么解决弊端:
spring 提供了一种处理事务的统一模型, 能使用统一的步骤、方式完成多种不同数据库访问技术的事务处理。
使用spring的事务处理机制,可以完成mybatis访问数据库的事务处理
使用spring的事务处理机制,可以完成hibernate访问数据库的事务处理
......
事务原本是数据库中的概念,在 Dao 层,但一般情况下,需要将事务提升到业务层,即 Service 层,这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
- 使用 Spring 的事务注解管理事务,适用于中小项目
- 使用 AspectJ 的 AOP 配置管理事务
1、Spring 事务管理 API
Spring 的事务管理,主要用到两个事务相关的接口。
1.1、事务管理器接口
事务管理器是 PlatformTransactionManager
接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
1.1.1、接口实现类
- DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
- HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
1.1.2、Spring 的回滚方式
- 发生 运行时异常 和 error 回滚事务,发生受查(编译)异常提交事务。
- 对于受查异常,程序员也可以手工设置其回滚方式。
1.1.3、回顾错误与异常类型
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
中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作。
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()
方法会创建一个事务,并在其中执行。
- PROPAGATION_REQUIRES_NEW:"我"需要属于自己的事务
总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
- PROPAGATION_SUPPORTS:"我"支持事务
指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
- PROPAGATION_MANDATORY
- PROPAGATION_NESTED
- PROPAGATION_NEVER
- PROPAGATION_NOT_SUPPORTED
1.2.3、事务默认超时时限
常量 TIMEOUT_DEFAULT
定义了事务底层默认的超时时限, sql 语句的执行时长,默认值为 -1 ,表示时限无限长。
注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值一般就使用默认值即可。
2、搭建测试环境
2.1、创建两个测试用的表:
- sale 销售记录表
- goods 商品表
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 管理事务的两种方式
- Spring 的事务注解管理事务,适合中小项目
- 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 注解的事务管理步骤:
- 配置文件中声明事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 开启注解驱动
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在业务方法上使用 @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 配置管理事务
实现声明式事务管理的步骤:
- maven 依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
- 在容器中添加事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 配置业务方法的事务通知属性(隔离级别、传播行为、超时时间),注意此时并没有指定给哪些类的方法配置事务
一般使用默认属性即可。
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>
- 配置增强(切入)器
<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>