Spring 对事务的支持
- spring 除了提供了对AOP的支持外,使得我们可以通过编程进行事务的管理,spring 还提供了一套API,使得我们可以通过声明实现事务的管理。
- spring 对于事务的支持,底层还是通过动态代理的,是基于面向切面编程AOP的,spring对AOP进行了封装。
事务概述
- 什么是事务
-
- 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
- 多条DML要么同时成功,要么同时失败,这叫做事务。
- 事务:Transaction(tx)
- 事务的四个处理过程:
-
- 第一步:开启事务 (start transaction)
- 第二步:执行核心业务代码
- 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
- 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
- 事务的四个特性:
-
- A 原子性:事务是最小的工作单元,不可再分。
- C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
- I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
- D 持久性:成功的操作对数据库的影响永久保存,持久性是事务结束的标志。
Spring 实现事务的两种方式
- 编程式事务:通过编写代码的方式来实现事务的管理。
- 声明式事务:spring框架封装的一套用于处理事务的API
- 基于注解方式
- 基于XML配置方式
Spring 事务管理 API
Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
PlatformTransactionManager接口:spring事务管理器的核心接口。
- 第三方ORM框架可以通过提供该接口的实现类,让Spring可以帮助第三方ORM框架进行事务的管理
在Spring6中PlatformTransactionManager接口有两个实现:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
- JtaTransactionManager:支持分布式事务管理。
如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring内置写好了,可以直接用。)
事务的属性
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
// 事务的传播行为
Propagation propagation() default Propagation.REQUIRED;
// 事务的隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务的超时时间,默认-1表示不限时
int timeout() default -1;
String timeoutString() default "";
// 属性值为true,表示当前事务是一个只读事务
boolean readOnly() default false;
// 设置当程序出现什么样的异常的时候进行回滚
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
// 设置当程序出现什么样的异常的时候不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
事务传播行为
- 事务的传播行为:在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
事务传播行为在spring框架中被定义为枚举类型:
使用以下代码进行测试。
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
测试代码
引入依赖。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--引入jdbcTemplate依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.6</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<!--引入德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
<!--引入@Resource注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.0</version>
</dependency>
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.example.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db01"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开始注解的方式控制事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
接口
public interface AccountService {
/**
* 转账的业务代码
*/
void transform(Integer fromAct, Integer toAct, Double money);
/**
* 保存账户
* @param account
* @return
*/
int save(Account account);
}
两个实现类
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDAO")
private AccountDAO accountDAO;
@Override
@Transactional
public void transform(Integer fromAct, Integer toAct, Double money) {
Account fromAccount = accountDAO.selectByAct(fromAct);
if (fromAccount.getBalance() < money) {
throw new RuntimeException("余额不足");
}
Account toAccount = accountDAO.selectByAct(toAct);
fromAccount.setBalance(fromAccount.getBalance() - money);
toAccount.setBalance(toAccount.getBalance() + money);
int count = accountDAO.update(fromAccount);
// 模拟异常
int i = 1 / 0;
count += accountDAO.update(toAccount);
if (count != 2) {
throw new RuntimeException("转账失败");
}
}
@Resource(name = "accountService2")
private AccountService accountService2;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
}
@Service("accountService2")
public class AccountServiceImpl2 implements AccountService {
@Resource(name = "accountDAO")
private AccountDAO accountDAO;
@Override
@Transactional
public void transform(Integer fromAct, Integer toAct, Double money) {
}
@Override
@Transactional(propagation = Propagation.NESTED)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
}
测试类启动
@Test
public void propagationTest() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
accountService.save(new Account(1, "act-001", 1000.0));
}
声明:以下不同级别的测试只需要变更实现类!实现类!实现类!
1. REQUIRED
支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】执行到当前业务方法时,如果原先有事务就加入原来的事务,如果原先没有事务,就自己新建一个事务。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
以上的结果表明确实是在同一个事务中,并且第一个当中也俘获异常排除了干扰。
2. REQUIRES_NEW
开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】不管原先是否有事务,都开启一个新的事务,与原先的事务并列,挂起暂停原先的事务,执行控制新的事务。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
结果确实表面第二个事务对其并没有影响到,使用 debug 或者 引入 log4j (启用debug模式)也可以证明确实是新开了一个事务。
需要注意的是,NESTED这种隔离级别内层事务不会影响到外层事务,外层却会影响到内层(也就是当外层报错,内层嵌套的事务也会回滚)。
以下给出证明:
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
// try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
String s = null;
s.toString();
return i;
}
@Override
@Transactional(propagation = Propagation.NESTED)
public int save(Account account) {
int insert = accountDAO.insert(account);
// String s = null;
// s.toString();
return insert;
}
3. NESTED
如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样(也就是新建以一个事务)。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样新建一个。】原先没有事务就新建一个事务,原先有事务就在原先的事务内新建一个独立的嵌套事务。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
@Override
@Transactional(propagation = Propagation.NESTED)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
使用 debug 或者 引入 log4j (启用debug模式)也可以证明是开启一个嵌套事务。
4. SUPPORTS
支持当前事务(外层事务),如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】如果原先有事务就加入原来的事务,如果原先没有事务,那就不开启事务,不进行事务控制。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
结果显示,内层导致外层一起回滚了,处于一个事务当中。
以上都是外层有事务的情况,下面测一下没有事务的情况。
@Override
// @Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
} catch (Exception e) {
// throw new RuntimeException(e);
}
return i;
}
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
以上的结果表明了,当外层没有事务时,SUPPORTS这种策略不会新开事务,此时遇到了报错,不会回滚,数据还是插进去了,并且由于外层俘获了异常(其实有没有俘获都一样,没有事务就没有回滚),外层数据一样也插进去了。
5. MANDATORY
必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】如果原先有事务就加入原来的事务,如果原先没有事务,那就抛异常。
@Override
// @Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
// try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
return i;
}
@Override
@Transactional(propagation = Propagation.MANDATORY)
public int save(Account account) {
int insert = accountDAO.insert(account);
// String s = null;
// s.toString();
return insert;
}
以上是外层没有事务的情况,结果显示:外层没有事务,内层直接报错,导致外层直接中断程序了(此时没有俘获异常,控制变量达到效果),但是由于外层没有事务,数据库还是插入了数据。
报错信息(抛的异常)如下:
外层有事务没什么好测的,加入事务罢了。
6. NOT_SUPPORTED
以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】不进行事务控制,原先有事务,就暂停挂起事务。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
// try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
return i;
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public int save(Account account) {
int insert = accountDAO.insert(account);
String s = null;
s.toString();
return insert;
}
结果分析:内层不支持事务,此时外层的事务又挂起了,因此当内层报错时,数据已经存下去了,外层没有俘获异常,报错了,外层事务回滚。
7. NEVER
以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int save(Account account) {
// 保存act-001
int i = accountDAO.insert(account);
// 保存act-002
// try {
i += accountService2.save(new Account(2, "act-002", 1000.0));
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
return i;
}
@Override
@Transactional(propagation = Propagation.NEVER)
public int save(Account account) {
int insert = accountDAO.insert(account);
// String s = null;
// s.toString();
return insert;
}
此时确实抛出异常了。
外层未俘获,因此回滚。
总结
代码没有执行到业务方法的结尾花括号,那么业务方法就没结束,事务也不会结束,因为没有执行到结尾花括号,永远不知道后面有什么代码。
对于在一个事务内捕获出现的异常,依然还会进行当前事务的回滚,捕获异常无效,因为当前事务不知道捕获异常后面还有什么DML语句,为了保证数据安全,出现异常只能回滚。
事务隔离级别
事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。
- 事务隔离,防止多事务并发的情况下,多个事务同时操作同一个表时互相干扰的一种机制
事务隔离级别在Spring中定义为枚举类型:(数值越大越强)
public enum Isolation {
DEFAULT(-1), // 默认隔离级别
READ_UNCOMMITTED(1), // 读未提交
READ_COMMITTED(2), // 读已提交(Oracle默认隔离级别)
REPEATABLE_READ(4), // 可重复读(MySQL默认隔离级别)
SERIALIZABLE(8); // 序列化
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
在Spring代码中如何设置隔离级别
- 设置当前事务的隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
数据库中读取数据存在的三大问题(三大读问题)
- 脏读:读取到没有提交到数据库的数据,叫做脏读。事务1对数据库表中的数据进行了修改,但是还没有进行提交,此时事务2就可以读到事务1修改后没有提交的数据,这个时候就发生了脏读。
- 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。对于两个事务Session A、Session B,Session A 读取 了一个字段,然后 Session B 更新 了该字段并且进行了提交。 之后 Session A 再次读取 同一个字段, 值就不同 了。那就意味着发生了不可重复读。
- 幻读:读到的数据是假的。对于两个事务Session A、Session B, Session A 从一个表中 读取 了一个字段, 然后 Session B 在该表中 插入 了一些新的行。 之后, 如果 Session A 再次读取 同一个表, 就会多出几行。那就意味着发生了幻读。幻读是针对于向数据库中进行数据插入的操作,当前从数据库中读到的数据记录的条数比之前读取到的数据记录条数多了,好像出现了幻觉一样,凭空多了一些记录出来幻读强调的是读到了之前没有读到的记录。
事务隔离级别包括四个级别
- 读未提交:READ_UNCOMMITTED这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
- 读提交:READ_COMMITTED解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。同一个事务内前后读取到的不一样
- 可重复读:REPEATABLE_READ解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
- 序列化:SERIALIZABLE解决了幻读问题,事务排队执行。不支持并发,事务排队执行,效率低。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交 | 有 | 有 | 有 |
读提交 | 无 | 有 | 有 |
可重复读 | 无 | 无 | 有 |
序列化 | 无 | 无 | 无 |
事务超时
在Spring代码中如何设置事务超时
@Transactional(timeout = 10)
代码表示设置事务的超时时间为10秒。
- 表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。
- timeout默认值为-1,表示没有时间限制。
Q:这里有个坑,事务的超时时间指的是哪段时间?
A:在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。
如果想把所有代码执行时间都记入超时时间,可以在最后添加一个无关紧要的DML语句。
只读事务
- 代码如下:@Transactional(readOnly = true)
-
- readOnly属性默认值为false
- 将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。
- 该特性的==作用==是:启动spring的优化策略。提高select语句执行效率。
- 如果该事务中确实没有增删改操作,建议设置为只读事务,提高select语句的执行效率
设置哪些异常回滚事务
-
- 代码如下:
- @Transactional(rollbackFor = RuntimeException.class)
-
- rollbackFor属性为Class数组类型
- 表示只有发生RuntimeException异常或该异常的子类异常才回滚。
设置哪些异常不回滚事务
-
- 代码如下:
- @Transactional(noRollbackFor = NullPointerException.class)
-
- noRollbackFor属性为Class数组类型
- 表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。