是五月呀!

事务隔离性

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

1. 四种隔离级别

MySQL/InnoDB提供SQL标准所描述的所有四个事务隔离级别:

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

按照SQL:1992 事务隔离级别,InnoDB默认是可重复读的(REPEATABLE READ)。

2. 设置和查看mysql的隔离级别

可以在命令行用–transaction-isolation选项,或在选项文件里,为所有连接设置默认隔离级别。

例如,可以在my.inf文件的[mysqld]节里类似如下设置该选项:

1
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}

用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。它的语法如下:

1
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

注意:默认的行为(不带session和global)是为下一个(未开始)事务设置隔离级别。如果你使用GLOBAL关键字,语句在全局对从那点开始创建的所有新连接(除了不存在的连接)设置默认事务级别。你需要SUPER权限来做这个。使用SESSION 关键字为将来在当前连接上执行的事务设置默认事务级别。 任何客户端都能自由改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。

可以用下列语句查询全局和会话事务隔离级别:

1
2
3
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;

3. 事务隔离性相关的问题

3.1 脏读

脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
session 1:

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
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
row in set (0.00 sec)
mysql> select @@session.tx_isolation;
+-----------------------+
| @@session.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into ttd values(1);
Query OK, 1 row affected (0.05 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 |
+------+
row in set (0.00 sec)

session 2:

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> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
row in set (0.00 sec)
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ | --------该隔离级别下(除了 read uncommitted)
+-----------------------+
row in set (0.00 sec)
mysql> select * from ttd;
Empty set (0.00 sec) --------不会出现脏读
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED | --------该隔离级别下
+------------------------+
row in set (0.00 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 | --------出现脏读
+------+
row in set (0.00 sec)

结论:session 2 在READ-UNCOMMITTED 下读取到session 1 中未提交事务修改的数据.

3.2 不可重复读

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
session 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED |
+------------------------+
row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 |
+------+
row in set (0.00 sec)

session 2 :

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 @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 |
+------+
row in set (0.00 sec)
mysql> insert into ttd values(2); ------也可以更新数据
Query OK, 1 row affected (0.00 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 |
| 2 |
+------+
rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.02 sec)

session 2 提交后,查看session 1 的结果;

session 1:

1
2
3
4
5
6
7
8
mysql> select * from ttd;
+------+
| id |
+------+
| 1 | --------和第一次的结果不一样,READ-COMMITTED 级别出现了不重复读
| 2 |
+------+
rows in set (0.00 sec)

3.3 可重复读

session 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from ttd;
+------+
| id |
+------+
| 1 |
| 2 |
+------+
rows in set (0.00 sec)

session 2 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into ttd values(3);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.03 sec)

session 2 提交后,查看session 1 的结果;

session 1:

1
2
3
4
5
6
7
8
9
10
mysql> select * from ttd;
+------+
| id |
+------+
| 1 | --------和第一次的结果一样,REPEATABLE-READ级别没出现重复读
| 2 |
+------+
rows in set (0.00 sec)
(commit session 1 之后 再select * from ttd 可以看到session 2 插入的数据3)

3.4 幻读

第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

1
2
3
4
5
6
7
8
9
10
11
12
mysql>CREATE TABLE `t_bitfly` (
`id` bigint(20) NOT NULL default '0',
`value` varchar(32) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_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
t Session A Session B
|
| START TRANSACTION; START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| empty set
| INSERT INTO t_bitfly
| VALUES (1, 'a');
|
| SELECT * FROM t_bitfly;
| empty set
| COMMIT;
|
| SELECT * FROM t_bitfly;
| empty set
|
| INSERT INTO t_bitfly VALUES (1, 'a');
| ERROR 1062 (23000):
| Duplicate entry '1' for key 1
v (shit, 刚刚明明告诉我没有这条记录的)

如此就出现了幻读,以为表里没有数据,其实数据已经存在了,傻乎乎的提交后,才发现数据冲突了。

实验二:

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
t Session A Session B
|
| START TRANSACTION; START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id | value |
| +------+-------+
| | 1 | a |
| +------+-------+
| INSERT INTO t_bitfly
| VALUES (2, 'b');
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id | value |
| +------+-------+
| | 1 | a |
| +------+-------+
| COMMIT;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id | value |
| +------+-------+
| | 1 | a |
| +------+-------+
|
| UPDATE t_bitfly SET value='z';
| Rows matched: 2 Changed: 2 Warnings: 0
| (怎么多出来一行)
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id | value |
| +------+-------+
| | 1 | z |
| | 2 | z |
| +------+-------+

本事务中第一次读取出一行,做了一次更新后,另一个事务里提交的数据就出现了。也可以看做是一种幻读。
当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。

4. 生产环境遇到的问题

4.1 事务未提交导致数据读取失败

背景:项目做了读写分离,事务标注的方法会使用主库进行写操作。项目中将缓存管理抽离出来,例如UserManager,其中使用单独的读库配置来加载数据到缓存。
在用户注册逻辑中,最外层有事务注解:

1
2
3
4
5
6
7
8
9
@Transactional
public void register(String userName) {
// 1. 保存到数据库中
userDao.save(userName);
// 2. 预热缓存,并从缓存中读取数据,这里读取到的user未空
User user = userManager.get(userName);
// 3. 使用user,报错NPE
user.getUserName();
}

方法使用事务注解,将使用主库进行数据写操作。当事务还没有被提交时,userManager尝试读取从库中的数据到缓存,是读取不到的。