Spring 事务管理

作为CURD 码农,事务管理是我开发中经常写的或者考虑的内容,本文将结合spring 框架讲一下我在开发中遇到的问题。

事务

事务的四个特点:Atomicity,Consistency,Isolation,Durability。

Atomictiy :一个事务当成一个单位,所有的数据库操作要么全部执行成功,要么全部执行失败。
Consistency: 保证事务将数据库从一种有效状态带到另一种有效状态。根据所有定义的规则和约束,写入数据库的所有数据必须是有效的。
Isolation:当多个事务并发处理数据时,根据不同的隔离级别,事务之间数据的可见性不一样。四个隔离级别:Read Uncommited,Read Commited, Repeatable Read, Serializable。

事务并发问题:

  1. 脏读,在事务A 读到了事务B 未提交的数据,如果事务B 因为异常回滚了,读取到的就是脏数据。当前事务隔离级别未读未提交
  2. 不可重复读,在事务A读到的数据第一次与第二次读到的数据不一致;因为读到的数据在事务B 修改提交之后,在事务A 就能跟读到。当前事务隔离级别为读已提交
  3. 幻读,在事务中,第一次读到的数据,与第二次读到的数据条目不一致,第一次可能是一条,第二次读到了两条;因为另外的事务读到的; 当前事务隔离级别为可重复读
    MySQL (InnoDB): 默认为REPEATABLE READ

Durability: 一个事务一旦提交就会被保存,即使是断电,系统crash了

Spring 事务托管

使用@Transacational 注解托管事务,有AOP 动态代理编织方法;实现事务自动创建自动提交自动回滚。

在使用注解之前要确保在Spring 配置中启用了事务管理。

@Transactional 注解

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
@Target({ElementType.TYPE, ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
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;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

String timeoutString() default "";

boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};

}

@Transactional 注解可以在方法上使用,也可以在类上使用。

在类上使用时,所有的public 方法都会被事务管理,但是颗粒度较大,使用不当会造成没有必要的事务开销。

transactionManager:事务管理器
timeout: 事务超时时间,默认-1,单位秒
rollbackFor : 指定异常回滚
nonRollbackFor: 指定异常不回滚
label: 标签,描述用
readOnly : 是否只读,标识之后事务使用只读模式
isolation 隔离级别: 虽然在代码里面可以设置隔离级别,但是实际的隔离级别以数据库设置的为准,所以一般也不会在编码的时候设置这个参数.
propagation 传播级别

  • REQUIRED: 默认传播级别,支持当前事务,如果当前存在事务就继续使用,如果不存在则创建一个新事务
  • SUPPORTS:支持当前事务,如果当前不存在事务,就保持不在事务里面
  • MANDATORY:支持当前事务,如果事务不存在,则抛出异常
  • REQUIRES_NEW:创建一个新的事务,如果当前存在一个事务,则suspend 当前事务
  • NOT_SUPPORTED:保证当前无事务存在,如果存在事务则将当前事务suspend
  • NEVER:无事务执行,如果存在事务则抛出异常
  • NESTED :如果当前事务存在,则嵌套在当前事务中执行。

@Transactional 注解失效

为什么我加了@Transactional 注解但是事务没生效,导致数据不一致?

前面我们提到,Spring 事务管理是通过AOP 动态代理实现的,如果没有生效,应该要看看使用了注解的方法或者类,是否被spring 管理.

手工管理事务

PlatformTransactionManager

另外一种常见的方法就是手工管理事务,主要是下面三个组件

  • PlatformTransactionManager
  • TranscationDefinition, 事务相关属性定义,属性与注解的一致
    • DefaultTransactionDefinition,
  • TransactionStatus,事务状态对象
    • TransactionExecution : 事务当前执行状态
    • SavepointManager: 编程式事务管理的接口

示例代码如下:

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
@Service
public class TransactionDemoService {
@Autowired
PlatformTransactionManager platformTransactionManager;

public void func(int a) {
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
defaultTransactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
defaultTransactionDefinition.setTimeout(100);

TransactionStatus transaction = platformTransactionManager.getTransaction(defaultTransactionDefinition);
try {

if(a == 100) {
return ;
}
Object savepoint = transaction.createSavepoint();
transaction.rollbackToSavepoint(savepoint);

Object secondSavepoint = transaction.createSavepoint();
transaction.releaseSavepoint(secondSavepoint);

platformTransactionManager.commit(transaction);
} catch (Exception e) {
platformTransactionManager.rollback(transaction);
} finally {
if (!transaction.isCompleted()) {
platformTransactionManager.rollback(transaction);
}
}
}
}

ps: 使用try-catch-finally 方法管理事务 ,避免因为其他业务逻辑直接return 而没有提交事务

使用TransactionTemplate

Spring 也提供了TransactionTemplate 进行手工事务管理

源码:TransactionTemplate#execute 方法

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
@Override  
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@Autowired
private TransactionTemplate transactionTemplate;

public void func() {

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);

Object execute = transactionTemplate.execute(callback -> {
Object savepoint = callback.createSavepoint();

try {
//事务操作
return null;
} catch (Exception e) {
callback.rollbackToSavepoint(savepoint);

}
return null;
});
}

一些实践

  1. 避免长事务
  2. 先做业务逻辑判断,再进行数据库写操作
  3. 避免大事务
  4. 减少慢SQL,查询要走索引
  5. 扫表要进行深分页,加主键索引作为查询条件
  6. 考虑主从延迟,避免空指针,从库查不到查主库

参考