概述
Spring 最成功、最吸引人的地方莫过于轻量级的声明式事务管理,Spring 的声明式事务管理是基于接口代理或动态字节码技术通过 AOP 来实现的:
- 对于基于接口动态代理的 AOP 事务增强来说,由于接口的方法是 public 的,这就要求实现类的实现方法必须是 public 的(不能是 protected、private),同时不能使用 static 的修饰符,所以,可以实施接口动态代理的方法只能是使用
public
或public final
修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施 AOP 增强,也就不能进行 Spring 事务增强了; - 基于 CGLib 字节码动态代理的方案是通过扩展被增强类、动态创建子类的方式进行 AOP 增强植入的,由于使用 final、static、private 修饰符的方法都不能被子类覆盖,相应的,这些方法将不能被实施 AOP 增强,所以,必须特别注意这些修饰符的使用,以免不小心成为事务管理的漏网之鱼。
动态代理策略 | 不能被事务增强的方法 |
---|---|
基于接口的动态代理 | 除 public 外的其它所有的方法, 此外 public static 也不能被增强 |
基于CGLib的动态代理 | private、static、final 修饰的方法 |
注解事务注意事项
- 声明式事务有
基于@Transactional注解(更灵活)
和基于XML配置(事务方法命名要遵循指定的规则)
两种方式,若这两种方式同时存在,为避免执行两次AOP切面,被@Transactional
注解标注的方法命名不能与XML配置方式中的事务方法命名规则相同。 - 默认情况下,如果在事务中抛出了运行时异常或者Error,则 Spring 将回滚事务;除此之外,Spring不会回滚事务;如果在事务中抛出其他类型的异常,并期望 Spring 能够回滚事务,可以指定 rollbackFor。
- 注解
@Transactional
的事务超时时间 timeout 默认值为-1(表示永不超时);所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。 - 注解
@Transactional
只有应用到public
方法,才能进行事务管理。 - 在Spring的AOP代理下,只有目标方法由外部调用,目标方法才由Spring生成的代理对象来管理,这会造成自调用问题。若同一类中的其他没有
@Transactional
注解的方法内部调用有@Transactional
注解的方法,有@Transactional
注解的方法的事务被忽略,不会发生回滚。 - 注解
@Transactional
只能应用到public
方法和自调用问题,是由于使用Spring AOP代理造成的,为解决这两个问题,使用AspectJ取代Spring AOP代理。
事务传播行为
事务传播行为 | 描述 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中(默认的事务传播行为) |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
其中比较容易混淆的是前三种,下面通过代码对这三种传播行为进行详细解释:
PROPAGATION_REQUIRED
1 |
|
控制台日志:1
2
3
4
5
6
7
8
9
10
11Creating new transaction with name [ServiceA.execute]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
Acquired Connection for JDBC transaction
Switching JDBC Connection to manual commit
====== Invoke serviceA.doSomething() ======
Participating in existing transaction
====== Invoke serviceB.execute() ======
====== Invoke serviceA.doSomethingElse() ======
Initiating transaction commit
Committing JDBC transaction
Releasing JDBC Connection after transaction
Returning JDBC Connection to DataSource
模拟伪代码:1
2
3
4
5
6
7
8
9Connection conn = DataSourceUtils.getConnection(dataSource);
try {
serviceA.execute();
conn.commit();
} catch (RuntimeException ex) {
conn.rollback();
} finally {
DataSourceUtils.releaseConnection(conn, dataSource);
}
PROPAGATION_REQUIRES_NEW
1 |
|
控制台日志:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Creating new transaction with name [ServiceA.execute]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
Acquired Connection JDBC transaction
Switching JDBC Connection to manual commit
====== Invoke serviceA.doSomething() ======
Suspending current transaction, creating new transaction with name [ServiceB.execute]
Acquired Connection for JDBC transaction
Switching JDBC Connection to manual commit
====== Invoke serviceB.execute() ======
Initiating transaction commit
Committing JDBC transaction
Releasing JDBC Connection after transaction
Returning JDBC Connection to DataSource
Resuming suspended transaction after completion of inner transaction
====== Invoke serviceA.doSomethingElse() ======
Initiating transaction commit
Committing JDBC transaction
Releasing JDBC Connection after transaction
Returning JDBC Connection to DataSource
模拟伪代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25TransactionalManager tm = null;
try {
tm = getTransactionalManager();
tm.begin(); // 开启第一个新事务
Transactional ts1 = getTransactional();
serviceA.doSomething();
tm.suspend(); // 挂起当前事务
try {
tm.begin(); // 开启第二个新事务
Transactional ts2 = getTransactional();
serviceB.execute();
ts2.commit();
} catch (RuntimeException ex) {
ts2.rollback(); // 回滚第二个事务
} finally {
// 释放资源
}
tm.resume(ts1); // 恢复第一个事务
serviceA.doSomethingElse();
ts1.commit();
} catch (RuntimeException ex) {
ts1.rollback(); // 回滚第一个事务
} finally {
// 释放资源
}
PROPAGATION_NESTED
1 |
|
控制台日志:1
2
3
4
5
6
7
8
9
10
11
12Creating new transaction with name [ServiceA.execute]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
Acquired Connection for JDBC transaction
Switching JDBC Connection to manual commit
====== Invoke serviceA.doSomething() ======
Creating nested transaction with name [ServiceB.execute]
====== Invoke serviceB.execute() ======
Releasing transaction savepoint
====== Invoke serviceA.doSomethingElse() ======
Initiating transaction commit
Committing JDBC transaction
Releasing JDBC Connection after transaction
Returning JDBC Connection to DataSource
模拟伪代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Connection conn = null;
try {
conn = getConnection();
conn.setAutoCommit(false);
serviceA.doSomething();
Savepoint savepoint = conn.setSavepoint();
try {
serviceB.execute();
} catch (RuntimeException ex) {
conn.rollback(savepoint);
} finally {
// 释放资源
}
serviceA.doSomethingElse();
conn.commit();
} catch (RuntimeException ex) {
conn.rollback();
} finally {
// 释放资源
}
事务隔离级别
在事务的ACID(原子性、一致性、隔离性、持久性)特性中,隔离性指的是不同事务先后提交并执行后,最终呈现出来的效果是串行的,也就是说,对于事务来说,它在执行过程中,感知到的数据变化应该只有自己操作引起的,不存在其他事务引发的数据变化。隔离性最简单的实现方式就是各个事务都串行执行,即如果前面的事务还没有执行完毕,后面的事务就都等待,但是这样的实现方式很明显并发效率不高,并不适合在实际环境中使用。为了解决这问题,SQL的标准制定者提出了不同的隔离级别:未提交读(READ-UNCOMMITTED)、已提交读(READ-COMMITTED)、可重复读(REPEATABLE-READ)、串行化读(SERIALIZABLE)。
四种事务隔离级别
- 未提交读(READ-UNCOMMITTED):在 READ-UNCOMMITTED 隔离级别下,一个事务可以读取另一个并发事务未提交的数据,这被称为脏读 (Dirty Read)。此级别的隔离性最低,可能会导致数据不一致的情况。
- 已提交读(READ-COMMITTED):在 READ-COMMITTED 隔离级别下,一个事务只能读取另一个已经提交的事务所做的修改,这样可以避免脏读。但是,可能会出现不可重复读 (Non-Repeatable Read) 的情况,即同一个事务在不同时间读取同一行数据时,数据的值不同(Oracle等多数数据库默认都是该级别)。
- 可重复读(REPEATABLE-READ):在 REPEATABLE-READ 隔离级别下,一个事务在执行期间多次读取同一行数据时,数据的值保持不变。此级别的隔离性较高,可以避免不可重复读的情况,但是可能会出现幻读 (Phantom Read) 的情况,即同一个事务在不同时间读取同一范围的数据时,数据的行数不同(MySQL默认级别)。
- 串行化读(SERIALIZABLE):在 SERIALIZABLE 隔离级别下,所有的并发事务按顺序执行,就像是串行执行一样。此级别的隔离性最高,可以避免脏读、不可重复读和幻读的情况,但是可能会导致性能问题。
三种读类型
- 脏读(DIRTY READ):脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问同一数据,然后使用了该数据。
- 不可重复读(NON-REPEATABLE READ):是指在一个事务内,多次读同一数据,在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读(PHANTOM READ):第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行,同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | Y | Y | Y |
已提交读(解决脏读) | N | Y | Y |
可重复读(解决不可重复读) | N | N | Y |
串行化读(解决幻读) | N | N | N |
MySQL的InnoDB引擎在可重复读级别通过间隙锁解决了幻读问题,通过MVCC解决了不可重复读的问题。
1 | SELECT @@global.tx_isolation, @@session.tx_isolation, @@session.autocommit; |
在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行
COMMIT
操作。因此要显式地开启一个事务务须使用命令BEGIN
或START TRANSACTION
或者执行命令SET AUTOCOMMIT = 0
用来禁止使用当前会话的自动提交。
常见问题
单条DML语句是否要加事务?
DML语句默认就是一个事务的,是原子操作,所以单条DML语句是不需要显式开启事务的。如果对一张表进行了多次 INSERT/UPDATE/DELETE 操作,那么就需要添加事务。准确地说,应该是一次任务中如果有多次 INSERT/UPDATE/DELETE 操作,并且这些操作彼此是不可分割的,要么全部成功要么全部失败,那么就需要使用事务进行管理。特别地,当这个任务中只有一次 INSERT/UPDATE/DELETE 操作时,可以不用显式地声明事务,毕竟一旦报错,数据肯定是没有入库。
查询语句是否要加事务?
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;如果你一次执行多条查询语句,例如统计查询、报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。