1. 隔离级别概述

隔离级别用于限定事务内外的哪些改变是可见的,哪些是不可见的。

MySQL实现了标准定义的4类隔离级别。
隔离级别从低到高依次是:Read Uncommitted, Read Committed, Repeatable Read, Serializable

低级别的隔离一般支持更高的并发,并且系统开销更低。

MySQL的默认事务隔离级别是 Repeatable Read。

文中针对每一种隔离级别包含实战部份,涉及的表结构和数据如下:

create table product(
	id bigint not null auto_increment,
	name varchar(100) not null,
	stock int not null default 0,
	primary key (id),
	index product_name(name)
);
insert into product(id,name,stock) values(1,'Java并发编程的艺术',100);

需要用到的一些命令的说明

# 查看当前会话事务隔离级别
select @@tx_isolation;

# 查看全局的事务隔离级别
select @@global.tx_isolation;

# 设置事务全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

# 设置事务会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABL;

2. Read Uncommitted (读取未提交内容)

概念:所有事务都可以看到其它未提交事务的执行结果。

问题:脏读

实战:
step1: 会话1

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

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

mysql>

setp2: 会话2

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

mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

mysql> update product set stock=99 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

setp3: 回到会话1

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|    99 |
+-------+
1 row in set (0.00 sec)

可以看到, 事务隔离级别为 READ UNCOMMITTED 时,会话1读取到了会话2未提交的库存数据修改。

3. Read Committed (读取已提交内容)

概念:一个事务只能看到已经提交事务所做的改变。

问题:一个事务中,同一个select可能返回不同的结果。(其它事务提交,可能修改了数据)

实战:
step1: 会话1

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

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

mysql>

setp2: 会话2

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

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> update product set stock=99 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>

step3: 会话1

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

step4: 会话2

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

step5: 会话1

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|    99 |
+-------+
1 row in set (0.00 sec)

可以看到,事务隔离级别为 READ COMMITTED 时,会话1只有在会话2提交后,才能读到会话2对库存数据的修改。

4. Repeatable Read (可重读)

概念:MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行.

问题:幻读 (指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。)

InnoDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。(见本文后面章节)

实战:
step1: 会话1

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

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

step2: 会话2

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

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> update product set stock=99 where id = 1;
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)

step3: 会话1

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|   100 |
+-------+
1 row in set (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|    99 |
+-------+
1 row in set (0.00 sec)

可以看到,事务隔离级别为 REPEATABLE READ 时,会话1 在执行过程中,反复的读取的库存数据都是一致的,即使这个过程中,会话2 提交了库存数据修改事务。

5. Serializable (可串行化)

概念:强制事务排序,使之不可能相互冲突,从而解决幻读问题。

问题:在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

实战:
setp1: 会话1

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)

mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE   |
+----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> select stock from product where id = 1;
+-------+
| stock |
+-------+
|    99 |
+-------+
1 row in set (0.00 sec)

step2: 会话2

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

mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE   |
+----------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> update product set stock = 98 where id =1;

上面更新语句被卡住,在等待锁

setp3: 会话1

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

step4: 会话2

Query OK, 1 row affected (5.46 sec)
Rows matched: 1  Changed: 1  Warnings: 0

可以看到,事务隔离级别为 SERIALIZABLE 时,会话1会话2 事务串行执行。

6. MVCC

多版本并发控制 (Multiversion Concurrency Control)

MVCC是行级锁的一个变种,它在很多情况下避免了加锁操作,所以开销更低。

MVCC只在 REPEATABLE READREAD COMMITTED 两个隔离级别下工作,其它两个隔离级别与MVCC不兼容。

MVCC通过保存数据在某个时间点的快照来实现的,不管事务要执行多长时间,每个事务看到的数据都是一致的。

实现机制:
在每行记录后面添加两个隐藏的列:创建时版本号,删除时版本号

每开始一个新的事务,系统版本号都会自动递增,将自增后的系统版本号作为该事务的事务版本号。

SELECT
根据下面2个条件检查每行记录:
a. 只查找早于当前事务版本号的数据行(即行的创建时版本号小于或等于当前事务的版本号)。这样可以确保事务事务读取的行,要么是在事务开始前已经存在,要么是事务自身插入或者修改过的

b. 行的删除时版本号要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT
新插入的每一行保存当前事务版本号作为行版本号(创建时版本号字段)。

DELETE
为删除的每一行保存当前事务版本号做为删除标识(删除时版本号字段)

UPDATE
插入一行新记录,保存当前事务版本号作为行版本号(创建时版本号字段),同时保存当前事务版本号到原来的行,作为行删除标识。(删除时版本号字段)

保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。