Spring 事务
一、分类
声明式事务 和 编程式事务 是两种事务管理的方式,它们在管理事务的实现和使用上有一些不同。
- 声明式事务
- 通过配置的方式来管理事务,通常通过 注解 或 XML配置 来声明事务的属性。
- 开发人员只需在方法或类上添加事务相关的注解,而无需手动编写事务管理的代码。
- Spring 提供了
@Transactional
注解来实现声明式事务,可以在需要添加事务管理的方法上添加该注解。
- 编程式事务(了解)
- 通过编程的方式来管理事务的,需要在代码中显式地编写事务管理的逻辑。
- 开发人员需要手动调用事务管理器的 API 来开启、提交、回滚事务,并在需要的地方捕获并处理事务异常。
- Spring 提供了
TransactionTemplate
类来简化编程式事务的使用,可以在代码中使用它来执行事务操作。
二、隔离级别
1、基本概念
级别 | 名称 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 数据库默认隔离级别 |
---|---|---|---|---|---|---|
1 | 读未提交 | Read Uncommitted | ✓ | ✓ | ✓ | - |
2 | 读已提交 | Read Committed | × | ✓ | ✓ | Oracle、SQL Server |
3 | 可重复读 | Repeatable Read | × | × | ✓ | MySQL |
4 | 串行化 | Serializable | × | × | × | - |
级别1最低,级别4最高。隔离级别越高,安全性越高,性能越低。(Spring默认采用数据库默认的隔离级别)
2、相关配置
// Spring事务隔离级别设置
@Transactional(isolation = Isolation.REPEATABLE_READ)
public enum Isolation {
DEFAULT(-1), // 数据库默认隔离级别(Spring默认)
READ_UNCOMMITTED(1), // 读未提交
READ_COMMITTED(2), // 读已提交
REPEATABLE_READ(4), // 可重复度
SERIALIZABLE(8); // 串行化
}
三、传播行为
1、基本概念
事务传播行为:指的就是当一个事务方法调用另一个方法时,事务应该如何传播。
传播行为(方法A) | 说明(以方法A调用方法B为例) |
---|---|
REQUIRED(默认) | 如果方法 A 有事务,则方法 B 加入该事务;否则方法 B 开始一个新事务(增、删、改) |
SUPPORTS | 如果方法 A 有事务,则方法 B 加入该事务;否则方法 B 以非事务的方式执行(查询) |
MANDATORY | 如果方法 A 有事务,则方法 B 加入该事务;否则会抛出异常 |
REQUIRES_NEW | 方法 B 会创建一个新的事务,不论方法 A 是否已经处于一个事务中。 |
NOT_SUPPORTED | 方法 B 会以非事务的方式执行,不论方法 A 是否已经处于一个事务中。 |
NEVER | 如果方法 A 有事务,则抛出异常;如果方法 A 没有事务,方法 B 以非事务的方式执行 |
NESTED | 如果方法 A 有事务,则在嵌套事务中执行;否则方法 B 开始一个新事务 |
嵌套事务:指在一个事务内部可以开启子事务,子事务是嵌套在父事务内部的,它们共享事务的回滚或提交。
-
子事务在父事务内部运行,但它有自己的保存点(Savepoint)。
如果子事务内部发生异常并触发回滚操作,只有子事务会回滚到自己的保存点,而父事务不受影响。
总之,嵌套事务可以提供更细粒度的事务控制,允许在父事务内部开启子事务,并且子事务可以独立于父事务进行回滚。
Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是再建立一个数据库连接,在此新数据库连接上执行sql。
2、相关配置
// Spring事务传播行为设置
@Transactional(propagation = Propagation.REQUIRED)
public enum Propagation {
REQUIRED(0), // 默认
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
四、超时时间
// Spring超时时间设置,单位s(默认值-1,表示没有超时限制)
@Transactional(timeout = -1)
五、只读
// Spring只读设置(默认为false)
@Transactional(readOnly=true)
一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务
-
如果一次执行单条查询语句
则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性。
-
如果一次执行多条查询语句(例如统计查询,报表查询)
这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
六、回滚规则
public @interface Transactional {
// 定义了哪些异常会导致事务回滚而哪些不会
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
以下是 rollbackFor()
方法的注释
By default, a transaction will be rolling back on {@link RuntimeException}
and {@link Error} but not on checked exceptions (business exceptions)
可以看到,默认情况下,事务只有遇到 运行时异常 RuntimeException
和 Error
才会回滚事务。
七、实现原理
Spring事务底层是基于 数据库事务 和 AOP机制 实现的
- 首先,对于使用了
@Transactional
注解的Bean,Spring会创建一个代理对象作为Bean,并放入IOC容器。 - 当调用代理对象的方法时,会先判断该方法上是否加了
@Transactional
注解。 - 如果没加,直接执行原方法;如果加了,执行以下逻辑:
- 利用事务管理器创建—个数据库连接。
- 修改数据库连接的
autocommit
属性为false
,禁止此连接的自动提交。 - 执行方法,方法中会执行sql…
- 方法出现异常,判断这个异常是否需要回滚:需要回滚,则回滚事务;不需要回滚,则提交事务。
八、失效分析
- 数据库引擎不支持事务。(如:MyISAM)
@Transactional
加到了非public的方法上(Spring AOP 默认只会处理 public 方法)@Transactional
的回滚规则不满足,默认只有遇到RuntimeException
和Error
才会回滚事务。@Transactional
的传播规则不满足,例如:NOT_SUPPORTED
和NEVER
。- 若同一类中,没有
@Transactional
注解的方法内部 调用有@Transactional
注解的方法,事务失效。
/**
* 内部调用导致Spring事务失效的解决方案:
* 1、自己注入自己
* 2、获取代理对象 AopContext.currentProxy()
* 3、带有事务的方法写到另一个service
*/
@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class TransactionServiceImpl implements TransactionService {
@Resource
private UserMapper userMapper;
@Lazy
@Resource
private TransactionService transactionService;
/**
* 事务失效
*/
@Override
public void test() {
// 调用的是普通对象的update() -> 事务失效
update();
}
/**
* 自己注入自己 解决事务失效
*/
@Override
public void test1() {
// 自己注入自己,注入的是代理对象 -> 调用代理对象的update() -> 事务生效
transactionService.update();
}
/**
* AopContext 解决事务失效
*
* 注意,必须要加 @EnableAspectJAutoProxy(exposeProxy = true),否则会抛异常
* java.lang.IllegalStateException: Cannot find current proxy:
* Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that
* AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
*/
@Override
public void test2() {
// 获取代理对象 -> 调用代理对象的update() -> 事务生效
TransactionService transactionService = (TransactionService) AopContext.currentProxy();
transactionService.update();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void update() {
UserEntity userEntity = userMapper.selectById(41);
userEntity.setUsername("嘿嘿嘿嘿");
userMapper.updateById(userEntity);
throw new NullPointerException();
}
}