千寻

道路很长, 开始了就别停下!

0%

Spring事务传播机制与隔离级别

Spring事务传播性机制

什么是事务的传播机制

事务的传播性一般在事务嵌套时候使用,比如在事务A里面调用了另外一个使用事务的方法,那么这俩个事务是各自作为独立的事务执行提交,还是内层的事务合并到外层的事务一块提交那,这就是事务传播性要确定的问题。下面一一介绍比较常用的事务传播机制。

首先奉上事务拦截器TransactionInterceptor事务处理流程图:

Spring事务的7种传播机制

1. PROPAGATION_REQUIRED

Spring默认的事务传播机制,如果外层有事务则当前事务加入到外层事务,一块提交一块回滚,如果外层没有事务则当前开启一个新事务。这个机制可以满足大多数业务场景。

若当前存在事务,则加入该事务,若不存在事务,则新建一个事务。
试验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
class C1(){
@Transactional(propagation = Propagation.REQUIRED)
function A(){
C2.B();
}
}

class C2(){
@Transactional(propagation = Propagation.REQUIRED)
function B(){
do something;
}
}
  • 若B方法抛出异常,A方法进行捕获,A会抛出异常,因为C2标志回滚,C1标志提交,产生冲突。

  • 若B方法抛出异常,B方法内部捕获,A、B都不会回滚。

  • 若A或B抛出异常,但没有捕获,则A、B都回滚。

  • A、B可操作同一条记录,因为处于同一个事务中。

平时我们都是在bo里面调用数据库操作,在rpc和screen调用bo,所以bo层不应该catch掉异常,而应该抛出来,在rpc和screen层catch异常。

2. PROPAGATION_REQUIRES_NEW

每次都新开启一个事务,同时把外层的事务挂起,当前新事务执行完毕后在恢复上层事务的执行。

新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
class C1(){
@Transactional(propagation = Propagation.REQUIRED)
function A(){
C2.B();
}
}

class C2(){
@Transactional(propagation = Propagation.REQUIRE_NEW)
function B(){
do something;
}
}
  • 若B方法抛出异常,A方法进行捕获,B方法回滚,A方法不受B异常影响。

  • 若B方法抛出异常,B方法内部捕获,A、B都不会回滚。

  • 若A方法抛出异常,不会影响B正常执行。

  • 若B方法抛出异常,A、B方法都没有处理,则A、B都会回滚。

  • A、B不可操作同一条记录,因为处于不同事务中,会产生死锁。

3. PROPAGATION_SUPPORTS

如果外层有事务则加入该事务,如果不存在也不会创建新事务,直接使用非事务方式执行。

4. PROPAGATION_NOT_SUPPORTED

不支持事务,如果外层存在事务则挂起外层事务 ,然后执行当前逻辑,执行完毕后,恢复外层事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
class C1(){
@Transactional(propagation = Propagation.REQUIRED)
function A(){
C2.B();
}
}

class C2(){
@Transactional(propagation = Propagation.NOT_SUPPORTED)
function B(){
do something;
}
}
  • A、B不可操作同一条记录,因为A是事务执行,B在A尚未提交前再操作同一条记录,会产生死锁。

5. PROPAGATION_NEVER

不支持事务,以非事务的方式执行,如果外层存在事务,则直接抛出异常。

1
IllegalTransactionStateException( “Existing transaction found for transaction marked with propagation ‘never'”)

6. PROPAGATION_MANDATORY

强制事务执行,只能在已经存在事务的方法中被调用,如果在不存在事务的方法中被调用,直接抛出异常。

1
IllegalTransactionStateException( “No existing transaction found for transaction marked with propagation ‘mandatory'”);

7. PROPAGATION_NESTED

如果外层存在事务,则嵌套在外层事务中执行。如果外层没有事务,则新建一个事务,类似于REQUIRED_NEW。
该传播机制特点是可以保存状态保存点,当事务回滚后会回滚到某一个保存点上,从而避免所有嵌套事务都回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
class C1(){
@Transactional(propagation = Propagation.REQUIRED)
function A(){
C2.B();
}
}

class C2(){
@Transactional(propagation = Propagation.NESTED)
function B(){
do something;
}
}
  • 若B方法抛出异常,A方法进行捕获,B方法回滚,A方法正常执行。

  • 若A或者B抛出异常,不做任何处理的话,A、B都要回滚。

  • A、B可操作同一条记录,因为处于同一个事务中。

总结:

只有为PROPAGATION_REQUIRED||PROPAGATION_REQUIRES_NEW||PROPAGATION_NESTED时候才可能开启一个新事务。

  • PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  • PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。

NESTED和REQUIRED_NEW的区别:

  • PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.

  • PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交.

那么绘制一个表格来表现他们的差异

  1. 定义serviceA.methodA()以PROPAGATION_REQUIRED修饰;
  2. 定义serviceB.methodB()以表格中三种方式修饰;
  3. methodA中调用methodB
异常状态 PROPAGATION_REQUIRES_NEW(两个独立事务) PROPAGATION_NESTED (B的事务嵌套在A的事务中) PROPAGATION_REQUIRED(同一个事务)
methodA抛异常 methodB正常 A回滚,B正常提交 A与B一起回滚 A与B一起回滚
methodA正常 methodB抛异常 1.如果A中捕获B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交;2.如果A未捕获B的异常,默认则会将B的异常向上抛,则B先回滚,A再回滚 B先回滚,A再正常提交 A与B一起回滚
methodA抛异常 methodB抛异常 B先回滚,A再回滚 A与B一起回滚 A与B一起回滚
methodA正常 methodB正常 B先提交,A再提交 A与B一起提交 A与B一起提交

二、事务隔离性

2.1 什么是事务的隔离性

事务的隔离性是指多个事务并发执行的时候相互之间不受到彼此的干扰,是事务acid中i,根据隔离程度对隔离性有会分类。在具体介绍事务隔离性前有必要介绍几个名词说明数据库并发操作存在的问题。

2.1.1 脏读

所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,具体来说假如有两个事务A和B同时更新一个数据d=1,事务B先执行了select获取到d=1,然后更新d=2但是没有提交,这时候事务A在B没有提交的情况下执行搜索结果d=2,这就是脏读。

2.1.2 不可重复读

所谓不可重复读是指一个事务内在未提交的前提下多次搜索一个数据,搜出来的结果不一致。发生不可重复读的原因是在多次搜索期间这个数据被其他事务更新了。

2.1.3 幻读

所谓幻读是指同一个事务内多次查询(注意查询的sql不一定一样)返回的结果集的不一样(比如新增或者少了一条数据),比如同一个事务A内第一次查询时候有n条记录,但是第二次同等条件下查询却又n+1条记录,这就好像产生了幻觉,为啥两次结果不一样那。其实和不可重复读一样,发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据。不同在于不可重复读是数据内容被修改了,幻读是数据变多了或者少了。

2.2、事务隔离级别

为了解决事务并发带来的问题,才有了sql规范中的四个事务隔离级别,不同隔离级别对上面三个问题部分或者全部做了避免。注意:下面试验用的两个终端都是同时执行了begin为了模拟事务并发。

2.2.1 Read Uncommitted

读未提交隔离级别,就是指一个事务中可以读取其他事务未提交的数据,这个级别会导致脏读。 本文都是以mysql为例引擎InnoDB,mysql默认事务隔离级别为Repeatable_Read:

1
2
3
4
5
6
mysql> show global variables like '%isolation%';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

下面我们更改隔离级别为Read Uncommitted :

1
2
3
4
5
6
7
8
9
10
11
12
mysql> set global transaction_isolation='read-uncommitted';
Query OK, 0 rows affected (0.02 sec)

mysql> set session transaction_isolation='read-uncommitted';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| READ-UNCOMMITTED | READ-UNCOMMITTED |
+--------------------------------+-------------------------+

试验: 下面我们打开两个mysql终端,并且关闭自动提交.
终端一:

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
mysql> show variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+

mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like '%autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
+------+------+

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test_user values(2,'2');
Query OK, 1 row affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)

终端二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)

终端一我们开启了一个事务,并且插入了一条数据但是没有提交事务,但是终端二却查询出来了。

终端一执行rollback:

1
2
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

终端二查询:

1
2
3
4
5
6
7
mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.00 sec)

在终端1回滚后,终端二有搜不到了,所以有可能在终端一没有回滚时候终端二已经获取并使用终端一的数据,而终端一回滚后,数据已经被使用过了,所以导致了脏读。

总结:该隔离级别会导致 脏读,不可重复读,幻读,是最低级的隔离级别,一般不用的。

2.2.2 Read Committed

读已提交隔离级别,一个事务只能读取到其他事务已经提交的数据,可能导致同一个事务中多次搜查结果不一样。

试验:修改事务隔离级别为Read Committed,

1
2
3
4
5
6
7
8
9
10
11
12
mysql> set global transaction_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)

mysql> set session transaction_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| READ-COMMITTED | READ-COMMITTED |
+--------------------------------+-------------------------+

终端一:

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test_user values(2,'2');
Query OK, 1 row affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+

终端二:

1
2
3
4
5
6
mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
+------+------+

由于终端一执行后没有commit,所以终端二查询不到。

下面终端一执行commit:

1
2
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

终端二再次执行查询:

1
2
3
4
5
6
7
8
mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)

终端一提交后,终端二就可以搜查出来了。

总结:该隔离级别会导致不可重复读和幻读,避免了脏读,oracle默认是该隔离级别。实际项目使用mybaits时候虽然隔离级别是read committed,但是在一个事务中多次搜索还是会是同一个结果,这是因为mybatis一级缓存的原因

2.2.3 Repeatable Read

可重复读隔离级别,一个事务内多次查询数据时候查询的数据内容和第一次查询的一致也就是说第一次查询出来的数据没有被修改,而不管其他事务有没有对这些数据新修改。但是可能其他事务新增一条数据,导致一个事务内查询的结果集里面多了一条记录。mysql默认隔离级别就是这个。
试验: 首先修改事务隔离级别为可重复读:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> set global transaction_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)

mysql> set session transaction_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+

模拟修改数据情况: 终端一:

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
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)

mysql> update test_user set name='3' where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 3 |
+------+------+

终端二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select @@global.transaction_isolation,@@transaction_isolation;;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+

可以知道终端一已经提交的数据在终端二的事务中还是查不到(注意终端二执行begin要在终端一执行commit前,因为我们要模拟并发事务)。

下面在模拟下新增数据情况 终端一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)

mysql> insert into test_user values(3,'3');
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+

终端二:

1
2
3
4
5
6
7
8
9
10
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+

终端一插入了一条记录并且提交,但是终端二还是查询不到新增的记录
总结:这里有点奇怪,按照其他资料显示该隔离级别应该是避免了 脏读,不可重复读,但是还存在幻读

2.2.4 Serializable

串行化隔离级别,就是多个事务串行化一个个按照顺序执行,这种不存在并发情况,所以可以避免所有事务并发问题。

1
2
3
4
5
6
7
8
9
10
11
12
mysql> set global transaction_isolation='serializable';
Query OK, 0 rows affected (0.00 sec)

mysql> set session transaction_isolation='serializable';
Query OK, 0 rows affected (0.00 sec)

mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| SERIALIZABLE | SERIALIZABLE |
+--------------------------------+-------------------------+

终端一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| SERIALIZABLE | SERIALIZABLE |
+--------------------------------+-------------------------+

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test_user;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+------+------+
4 rows in set (0.00 sec)

终端二:

1
2
3
4
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test_user values(5,5);

可以看到终端一打开一个事务后,事务二的insert语句会等待直到事务一提交或者超时。
超时:

1
2
3
4
5
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test_user values(5,5);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction