Spring-Transaction事务
参考文献
- Spring 事务失效的 8 大场景,看看都遇到过几个?
- Spring官方推荐的@Transactional还能导致生产事故?原来姿势完全错了
- 当Transactional碰到锁,有个大坑,要小心.
- 几行烂代码,我赔了16万。
Spring事务隔离级别
- Spring提供事5种务隔离级别
@Transaction(isolation=Isolation.DEFAULT)
: 默认的事务隔离级别,即使用数据库的事务隔离级别.@Transaction(isolation=Isolation.READ_UNCOMMITTED)
: 读未提交,这是最低的事务隔离级别,允许其他事务读取未提交的数据,这种级别的事务隔离会产生脏读,不可重复读和幻读.@Transaction(isolation=Isolation.READ_COMMITTED)
: 读已提交,这种级别的事务隔离能读取其他事务已修改的数据,不能读取未提交的数据,会产生不可重复读和幻读.@Transaction(isolation=Isolation.REPEATABLE_READ)
: 可重复读,这种级别的事务隔离可以防止不可重复读和脏读,但是会产生幻读.@Transaction(isolation=Isolation.SERIALIZABLE)
: 串行化,这是最高级别的事务隔离,会避免脏读,不可重复读和幻读.在这种隔离级别下,事务会按顺序进行.
Spring事务传播行为
- 事务传播行为是指如果多个事务同事存在,Spring就会处理这些事务的行为.
- 事务传播行为分为7种(这7种传播行为有个前提,它们的事务管理器是同一个的时候,才会有下面描述的表现行为):
REQUIRED
: 如果当前存在事务,就加入该事务;如果当前没有事务,就创建一个新的事务,这是Spring默认的事务传播行为.SUPPORTS
: 如果当前存在事务,就加入该事务;如果当前没有事务,就以非事务的方式继续运行.MANDATORY
: 如果当前事务存在事务,就加入该事务;如果当前事务,就抛出异常.REQUIRES_NEW
: 创建一个新的事务,如果当前存在事务,就把当前事务挂起.新建事务和被挂起的事务没有任何关系,是两个独立的事务.外层事务回滚失败时,不能回滚内层事务执行结果,外层事务不能相互干扰.NOT_SUPPORTED
: 以非事务方式运行,如果当前存在事务,就把当前事务挂起.- 事务挂起: 对当前存在的事务现场生成一个快照,然后将事务现场清理干净,然后重新开启一个新事物,新事物执行完毕之后,将事务现场清理干净,然后根据前面的快照恢复旧事物.
NEVER
: 以非事务方式运行,如果当前存在事务,就抛出异常.NESTED
: 如果当前存在事务,就创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,该取值就等价于REQUIRED
- 若当前存在事务,
NESTED
创建的事务为当前事务的子事务,有单独的保存点,所以NESTED
方法抛出异常被回滚,不会影响到外层事务
- 若当前存在事务,
声明式事务属性
value
: 存放String类型的值,主要用来指定不同的事务管理器,满足在同一个系统中存在不同的事务管理器.transactionManager
: 与value
类似,也是用来选择事务管理器.propagation
: 事务传播行为,默认为Propagation.REQUIRED
isolation
: 事务的隔离级别,默认为Isolation.DEFAULT
timeout
: 事务的超时时间,默认为-1,如果超过了设置的时间还没有执行完成,就会自动回滚当前事务.readOnly
: 当前事务是不是自读事务,默认值是false.通常可以设置读取数据的事务的属性值为true.rollbackFor
: 可以设置触发事务的指定异常,允许指定多个类型的异常.noRollbackFor
: 与rollbackFor
相反,可以设置不触发事务的指定异常,允许指定多个类型的异常.
事务回滚规则
- Spring的事务回滚通常是根据当前事务抛出异常的时候,Spring事务管理器捕捉到未经处理的异常,然后根据规则来决定当前事务是否回滚.
- 若捕获的异常正好是
noRollbackFor
属性的异常,那么将不会被捕获.在默认配置下,Spring只有捕获运行时异常(RuntimeException)的子类才会进行回滚.
Spring对事务的抽象
-
Spring事务抽象的关键就是事务策略的概念,事务策略是通过
TransactionManager
接口定义的.TransactionManager
本身只是一个标记接口,它有两个直接子接口-
ReactiveTransactionManager
:这个接口主要用于在响应式编程模型下 -
PlatformTransactionManager
:命令式编程模型下使用这个接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public interface PlatformTransactionManager extends TransactionManager {
/**
* 开启事务
* Return a currently active transaction or create a new one, according to
* the specified propagation behavior.
* <p>Note that parameters like isolation level or timeout will only be applied
* to new transactions, and thus be ignored when participating in active ones.
* <p>Furthermore, not all transaction definition settings will be supported
* by every transaction manager: A proper transaction manager implementation
* should throw an exception when unsupported settings are encountered.
* <p>An exception to the above rule is the read-only flag, which should be
* ignored if no explicit read-only mode is supported. Essentially, the
* read-only flag is just a hint for potential optimization.
* @param definition the TransactionDefinition instance (can be {@code null} for defaults),
* describing propagation behavior, isolation level, timeout etc.
* @return transaction status object representing the new or current transaction
* @throws TransactionException in case of lookup, creation, or system errors
* @throws IllegalTransactionStateException if the given transaction definition
* cannot be executed (for example, if a currently active transaction is in
* conflict with the specified propagation behavior)
* @see TransactionDefinition#getPropagationBehavior
* @see TransactionDefinition#getIsolationLevel
* @see TransactionDefinition#getTimeout
* @see TransactionDefinition#isReadOnly
*/
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
-
TransactionDefinition
-
它的主要完成了对事务定义的抽象,这些定义有些是数据库层面本身就有的,例如隔离级别、是否只读、超时时间、名称.
-
使用申明式事务的时候,会通过
@Transactional
这个注解去申明某个方法需要进行事务管理,在@Transactional
中可以定义事务的属性,这些属性实际上就会被封装到一个TransactionDefinition
中,当然封装的时候肯定不是直接使用的接口,而是这个接口的一个实现类RuleBasedTransactionAttribute
.
TransactionExecution
-
这个接口也是用于描述事务的状态,TransactionStatus是在其上做的扩展,内部定义了以下几个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package org.springframework.transaction;
/**
* Common representation of the current state of a transaction.
* Serves as base interface for {@link TransactionStatus} as well as
* {@link ReactiveTransaction}.
*
* @author Juergen Hoeller
* @since 5.2
*/
public interface TransactionExecution {
/**
* Return whether the present transaction is new; otherwise participating
* in an existing transaction, or potentially not running in an actual
* transaction in the first place.
* 判断当前事务是否是一个新的事务,不是一个新事务的话,那么需要加入到已经存在的事务中
*/
boolean isNewTransaction();
/**
* Set the transaction rollback-only. This instructs the transaction manager
* that the only possible outcome of the transaction may be a rollback, as
* alternative to throwing an exception which would in turn trigger a rollback.
*
*/
void setRollbackOnly();
/**
* Return whether the transaction has been marked as rollback-only
* (either by the application or by the transaction infrastructure).
* 事务是否被标记成RollbackOnly,如果被标记成了RollbackOnly,意味着事务只能被回滚
*/
boolean isRollbackOnly();
/**
* Return whether this transaction is completed, that is,
* whether it has already been committed or rolled back.
* 是否事务完成,回滚或提交都意味着事务完成了
*/
boolean isCompleted();
}
SavepointManager
- 定义了管理保存点(
Savepoint
)的方法,隔离级别为NESTED
时就是通过设置回滚点来实现的
1 | package org.springframework.transaction; |
TransactionStatus
- 主要用于描述Spring事务的状态,继承了
TransactionExecution
和SavepointManager
这些接口,额外提供了两个方法
1 | /** |
PlatformTransactionManager
-
AbstractPlatformTransactionManager
,Spring提供的一个事务管理的基类,提供了事务管理的模板,实现了Spring事务管理的一个标准流程-
判断当前是否已经存在一个事务
-
应用合适的事务传播行为
-
在必要的时候挂起/恢复事务
-
提交时检查事务是否被标记成为
rollback-only
-
在回滚时做适当的修改(是执行真实的回滚/还是将事务标记成
rollback-only
)
-
-
AbstractPlatformTransactionManager
提供了三个常见的子类完整类路径 说明 org.springframework.jdbc.datasource.DataSourceTransactionManager
使用 Spring JDBC
或者MyBatis
进行持久化数据时使用org.springframework.orm.hibernate5.HibernateTransactionManager
使用 Hibernate
进行持久化数据使用org.springframework.orm.jpa.JpaTransactionManager
使用 JPA
进行持久化数据使用
编程式事务
通过PlatfromTransactionManager
控制事务
1 |
|
定义事务管理器PlatformTransactionManager
- 事务管理器相当于一个管理员,这个管理员就是用来控制事务的,比如开启事务,提交事务,回滚事务等等
1 | public interface PlatformTransactionManager extends TransactionManager { |
JpaTransactionManager
: 如果用Jpa
来操作db,那么需要用这个管理器来帮控制事务。DataSourceTransactionManager
: 如果用是指定数据源的方式,比如操作数据库用的是:JdbcTemplate、mybatis、ibatis
,那么需要用这个管理器来控制事务。HibernateTransactionManager
: 如果用Hibernate
来操作db
,那么需要用这个管理器帮控制事务。JtaTransactionManager
: 如果用的是Java
中的Jta
来操作db
,这种通常是分布式事务,此时需要用这种管理器来控制事务。
定义事务属性DefaultTransactionDefinition
- 定义事务属性,比如事务隔离级别、事务超时时间、事务传播方式、是否是只读事务等等。
Spring
中使用TransactionDefinition
接口来表示事务的定义信息,有个子类比较常用:DefaultTransactionDefinition
开启事务
- 调用事务管理器的
getTransaction
方法,即可以开启一个事务
通过TransactionTemplate
控制事务
1 |
|
声明式事务
-
启用
Spring
的注解驱动事务管理功能,添加@EnableTransactionManagement
-
当spring容器启动的时候,发现有
@EnableTransactionManagement
注解,此时会拦截所有bean的创建,扫描看一下bean上是否有@Transaction
注解(类,或者父类,或者接口,或者方法中有这个注解都可以),如果有这个注解,Spring会通过Aop的方式给bean生成代理对象,代理对象中会增加一个拦截器,拦截器会拦截bean中public
方法执行,会在方法执行之前启动事务,方法执行完毕之后提交或者回滚事务.1
org.springframework.transaction.interceptor.TransactionInterceptor#invoke
-
-
定义事务管理器
JpaTransactionManager
: 如果用Jpa
来操作db,那么需要用这个管理器来帮控制事务。DataSourceTransactionManager
: 如果用是指定数据源的方式,比如操作数据库用的是:JdbcTemplate、mybatis、ibatis
,那么需要用这个管理器来控制事务。HibernateTransactionManager
: 如果用Hibernate
来操作db
,那么需要用这个管理器帮控制事务。JtaTransactionManager
: 如果用的是Java
中的Jta
来操作db
,这种通常是分布式事务,此时需要用这种管理器来控制事务。
-
需要使用事务的目标上加
@Transaction
注解@Transaction
放在接口上,那么接口的实现类中所有public
都被Spring自动加上事务@Transaction
放在类上,那么当前类以及其下无限级子类中所有pubilc
方法将被Spring自动加上事务@Transaction
放在public
方法上,那么该方法将被Spring自动加上事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public Transactional {
// 指定事务管理器的bean名称,如果容器中有多个事务管理器PlatformTransactionManager,需要告诉Spring当前配置使用哪个事务管理器
String value() default "";
String transactionManager() default "";
String[] label() default {};
// 事务的传播属性
Propagation propagation() default Propagation.REQUIRED;
// 事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务执行的超时时间(秒)
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
// 是否是只读事务
boolean readOnly() default false;
// 定义0个或者更多异常类,这个异常类必须是Throwable的子类,当方法抛出这些异常及其子类异常的时候,Spring会让事务回滚
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
Spring
的@Transactional
注解控制事务哪些场景下不生效?
数据库引擎是否支持事务(Mysql 的 MyIsam引擎不支持事务)
注解所在的类是否被加载为 Bean(是否被spring 管理)
所用数据源是否加载了事务管理器
- 如果没有启用事务管理,则@Transactional注解将无效.在Spring中,需要通过
@EnableTransactionManagement
注解启用事务管理.
注解所在的方法是否为 public 修饰的
Method visibility and
@Transactional
When you use transactional proxies with Spring’s standard configuration, you should apply the@Transactional
annotation only to methods withpublic
visibility. If you do annotateprotected
,private
, or package-visible methods with the@Transactional
annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. If you need to annotate non-public methods, consider the tip in the following paragraph for class-based proxies or consider using AspectJ compile-time or load-time weaving (described later).When using@EnableTransactionManagement
in a@Configuration
class,protected
or package-visible methods can also be made transactional for class-based proxies by registering a customtransactionAttributeSource
bean like in the following example. Note, however, that transactional methods in interface-based proxies must always bepublic
and defined in the proxied interface.
1
2
3
4
5
6
7
8
9
10
11
12 /**
* Register a custom AnnotationTransactionAttributeSource with the
* publicMethodsOnly flag set to false to enable support for
* protected and package-private @Transactional methods in
* class-based proxies.
*
* @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
*/
TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}The Spring TestContext Framework supports non-private
@Transactional
test methods by default. See Transaction Management in the testing chapter for examples.
@Transactional
注解添加在private、static、final
方法上,这些方法不会被 Spring 代理,因此不会进行事务管理.
1 | /** |
是否存在自身调用的问题
1 | //示例1 |
- 事务都会失效,因为发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了.
@Transactional
的扩展配置propagation
是否正确
1 |
|
- 如果不同事务的
propagation
属性不一致,可能会导致注解失效的问题,具体表现为子事务的操作不会受到父事务的控制,或者出现死锁或数据不一致等问题. - 例如,如果父事务的
propagation
属性为REQUIRED
,而子事务的propagation
属性为REQUIRES_NEW
,那么子事务会开启一个新的事务,不受父事务的控制,可能会导致数据不一致的问题. - 为了避免注解失效的问题,可以根据具体的业务需求和场景选择合适的
propagation
属性,确保嵌套事务的正确性和一致性.例如,如果需要子事务受父事务的控制,可以将子事务的propagation
属性设置为REQUIRED
;如果需要子事务独立于父事务,可以将子事务的propagation
属性设置为REQUIRES_NEW
. - 需要注意的是,使用
propagation
属性时,需要根据具体的业务需求和场景进行相应的配置,避免出现注解失效的问题.同时,在进行嵌套事务时,也需要注意事务的传播行为和隔离级别等问题,避免出现死锁或数据不一致等问题.
异常被catch
捕获导致@Transactional
失效
-
这个也是出现比较多的场景:把异常吃了,然后又不抛出来,事务也不会回滚!
1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderServiceImpl implements OrderService {
public void updateOrder(Order order) {
try {
// update order
} catch {
}
}
}
异常类型错误
1 |
|
- 这样事务也是不生效的,因为默认回滚的是:
RuntimeException
,如果想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class)
这个配置仅限于Throwable
异常类及其子类.
1 | // org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn |
错误的调用方式
- 在一个类中,一个没有被
@Transactional
注解标注的方法调用了被@Transactional
注解标注的方法,被调用的方法的事务会失效. - 跨方法调用,如果在一个事务中调用了另一个被 @Transactional 注解标注的方法,并且被调用方法使用了不同的事务传播行为,那么事务可能会失效.
使用@Transactional
注解注意点
@Transactional
注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截.在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务.- 当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于
@Transactional
注解包裹的整个方法都是使用同一个connection连接.如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放.一旦类似操作过多,就会导致数据库连接池耗尽.
并发场景下使用@Transactional
1 |
|
-
上述代码会出现并发问题,具体现象为:
线程A 线程B 获取锁 开启事务 判断数据是否存在 数据不存在,插入数据 解锁 获取锁 提交事务 判断数据是否存在(此时线程A尚未提交事务,插入的数据尚未进入数据库) 数据不存在,插入数据(唯一索引校验,重复插入报错) -
主要原因为: 事务范围大于锁的范围,即事务范围不在锁范围内
-
解决方法:
-
将锁的范围设置大于事务范围
-
具体方法有:
-
采用编程式事务,使用
TransactionTemplate
或TransactionManager
来控制事务,再在外层使用锁进行锁定.1
2
3
4
5
6transactionTemplate.execute(new TransactionCallback<Object>() {
public Object doInTransaction(TransactionStatus status) {
return null;
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TransactionUtil {
private PlatformTransactionManager transactionManager;
public <T> boolean transactional(Consumer<T> consumer) {
// 定义事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
consumer.accept(null);
transactionManager.commit(status);
return true;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("编程式事务业务异常回滚", e);
return false;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29fun preventRepeat(
policyInformation: PolicyInformation,
responseBody: GatewayResponseMessage.GatewayResponseBody
) {
val policyNo = policyInformation.policy.policyNo
val lock = redissonClient.getLock(policyNo)
if (lock.tryLock()) {
try {
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
val informationOptional = policyWithOrderInformationMapper.queryInfoByPolicyNo(policyNo)
if (informationOptional.isPresent) {
responseBody.msg = ORDER_ALREADY_EXISTS
responseBody.code = SUCCESS_CODE
} else {
createOrderAndSavePolicyInfo(policyInformation, responseBody)
}
}
})
} catch (e: Exception) {
LOG.error(e.message)
} finally {
lock.unlock()
}
} else {
responseBody.msg = CREATE_ORDER_FAILURE
responseBody.code = Response.ERROR_CODE.toString()
}
} -
使用事务的传播机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fun test(request: RequestMessage): ResponseMessage {
val policyNo = policyInformation.policy.policyNo
if (reentrantLock.tryLock()) {
try {
lock(policyNo, responseBody, policyInformation)
} catch (e: Exception) {
LOG.error(e.message)
} finally {
reentrantLock.unlock()
}
} else {
responseBody.msg = CREATE_ORDER_FAILURE
responseBody.code = Response.ERROR_CODE.toString()
}
return ResponseMessage().apply {
this.sign = request.sign
this.head = request.head
this.body = responseBody
}
}
private fun test2(
policyNo: String,
responseBody: GatewayResponseMessage.GatewayResponseBody,
policyInformation: PolicyInformation
) {
val informationOptional = policyWithOrderInformationMapper.queryInfoByPolicyNo(policyNo)
if (informationOptional.isPresent) {
responseBody.msg = ORDER_ALREADY_EXISTS
responseBody.code = SUCCESS_CODE
} else {
createOrderAndSavePolicyInfo(policyInformation, responseBody)
}
}
-
-
避免长事务
- 长事务: 运行时间比较长,长时间未提交的事务
长事务会引发哪些问题?
- 数据库连接池被占满,应用无法获取连接资源;
- 容易引发数据库死锁;
- 数据库回滚时间长;
- 在主从架构中会导致主从延时变大.
如何避免长事务
-
对事务方法进行拆分,尽量爱让事务变小,变快,减小事务的颗粒度.
-
Spring进行事务管理的方式:
-
声明式事务: 在方法上使用
@Transactional
注解进行事务管理的操作叫声明式事务.- 优点: 使用简单
- 缺点: 事务的颗粒度是整个方法,无法进行精细化控制.
-
编程式事务:基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作
-
在Spring项目中可以使用
TransactionTemplate
类的对象,手动控制事务.1
2
3
4
5
6
7
8
9
10
11
private TransactionTemplate transactionTemplate;
public void xxx(TempBean tempBean) {
transactionTemplate.execute(transactionStatus -> {
tempDao.save(tempBean);
//保存明细表
tempDetailDao.save(tempBean.getDetail());
return Boolean.TRUE;
});
} -
优点: 可以精细化控制事务的范围.
-
-
-
避免长事务的最简单的方法就是不要使用声明式事务
@Transactional
,而是使用编程式事务手动控制事务范围.