事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可以出现 | 可以出现 | 可以出现 |
读提交 | 不允许出现 | 可以出现 | 可以出现 |
可重复读 | 不允许出现 | 不允许出现 | 可以出现 |
序列化 | 不允许出现 | 不允许出现 | 不允许出现 |
注意点
上述隔离级别都是这么定义的,但是InnoDB和Falcon存储引擎已经通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了幻读问题。
行锁的类型
InnoDB 实现了两种类型的行级锁:
-
共享锁
(也称为 S 锁):允许事务读取一行数据。
可以使用 SQL 语句
select * from tableName where … lock in share mode;
手动加 S 锁。 -
独占锁
(也称为 X 锁):允许事务删除或更新一行数据。
可以使用 SQL 语句
select * from tableName where … for update
; 手动加 X 锁。
S 锁和 S 锁是兼容的,X 锁和其它锁都不兼容,举个例子,事务 T1 获取了一个行 r1 的 S 锁,另外事务 T2 可以立即获得行 r1 的 S 锁,此时 T1 和 T2 共同获得行 r1 的 S 锁,此种情况称为锁兼容,但是另外一个事务 T2 此时如果想获得行 r1 的 X 锁,则必须等待 T1 对行 r 锁的释放,此种情况也成为锁冲突。
为了实现多粒度的锁机制,InnoDB 还有两种内部使用的意向锁,由 InnoDB 自动添加,且都是表级别的锁。
- 意向共享锁(IS):事务即将给表中的各个行设置共享锁,事务给数据行加 S 锁前必须获得该表的 IS 锁。
- 意向排他锁(IX):事务即将给表中的各个行设置排他锁,事务给数据行加 X 锁前必须获得该表 IX 锁。
意向锁的主要目的是为了使得行锁和表锁共存。表 2 列出了行级锁和表级意向锁的兼容性。
行级锁和表级意向锁的兼容性
锁类型 | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
行锁的算法
读锁和写锁都是行级锁,InnoDB的行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁,InnoDB行锁分为3中情形:
- 记录锁(Record Lock):对索引项加锁。
- 间隙锁(Gap Lock):对索引项之间的“间隙”、第一条记录前的“间隙”或最后一条记录后的“间隙”加锁。
- 临键锁(Next-key Lock):前两种的结合,对记录及其前面的间隙加锁。
InnoDB这种行锁的实现特点意味着,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表一样
一致性非锁定读和多版本并发控制
一致性非锁定读(consistent nonlocking read)是指InnoDB通过行多版本控制(Multi Version Concurrency Control, MVCC)的方法来读取当前执行时间数据库中行的数据。
即如果读取的行正在执行变更操作,这时读取不会等待行锁的释放,而是会读取行的一个快照数据。快照是指该行的一个历史数据,通过undo操作来完成。这种方式极大提高了数据库的并发性,这也是InnoDB的默认设置。
快照是当前行的一个历史版本,但可能存在多个版本,行数据存在多个快照数据,这种技术成为行多版本技术,由此带来的并发控制,称为多版本并发控制(MVCC)。InnoDB在READ COMMITED 和 REPEATABLE READ隔离级别时,会使用非锁定的一致性读,但是在这两种隔离级别使用的快照数据定义却不同:
- READ COMMITED: 总是读取最新一份快照
- REPEATABLE READ: 总是读取事务开始时的行数据版本
我们执行一个示例:
一致性非锁定读 |
||
---|---|---|
时间 | 会话A | 会话B |
1 | BEGIN | |
2 | select * from z where a = 3; | |
3 | BEGIN | |
4 | update z set b=2 where a=3; | |
5 | select * from z where a = 3; | |
6 | COMMIT; | |
7 | select * from z where a = 3; | |
8 | COMMIT; |
在这个例子中我们可以清晰的看到0、1、2三种隔离级别的区别:
#在事务开始前我们可以分别调整为0、1、2三种隔离级别,来查看不同的输出
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 (0.00 sec)
# A会话:T1事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from z where a = 3;
+---+------+
| a | b |
+---+------+
| 3 | 1 |
+---+------+
1 row in set (0.00 sec)
# B会话:T2事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update z set b=2 where a=3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
# A会话:T1事务,如果此时隔离级别是READ-UNCOMMITTED,因为此刻事务2可能会回滚,所以出现了脏读
mysql> select * from z where a=3;
+---+------+
| a | b |
+---+------+
| 3 | 2 |
+---+------+
1 row in set (0.00 sec)
# A会话:T1事务,如果此时隔离级别是大于READ-UNCOMMITTED的更高级别
mysql> select * from z where a=3;
+---+------+
| a | b |
+---+------+
| 3 | 1 |
+---+------+
1 row in set (0.00 sec)
# B会话:T2事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
# A会话:T1事务,如果此时隔离级别是READ-COMMITTED,因为数据和事务开始时读取的出现了不一致,因此称为不可重复读,能够读到其他事务的结果,违反了事务的隔离性
mysql> select * from z where a=3;
+---+------+
| a | b |
+---+------+
| 3 | 2 |
+---+------+
1 row in set (0.00 sec)
# A会话:T1事务,如果此时隔离级别是大于READ-COMMITTED的更高级别
mysql> select * from z where a=3;
+---+------+
| a | b |
+---+------+
| 3 | 1 |
+---+------+
1 row in set (0.00 sec)
# A会话:T1事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
总结
未提交读 ReadUnCommited 就是读写都不加任何锁
已提交读 ReadCommitted 读取 不加锁 写入 加行锁(如果按索引过滤,那么只对筛选数据进行加行锁,但是如果不是按索引进行过滤,就对所有记录加行锁(但是Mysql对此进行了优化,会将不满足条件的记录释放锁)),可以防止更新丢失。
可重复读 RepeatableRead 就是多次读取数据,结果都一样,如果是已提交读,事务过程中,如果有其他事务commited了查询范围内的数据,那么是可以读取到新的写入操作的,但是可重复读就防止了这种情况。就InnoDB引擎来说,通过多版本并发控制(MVVC,只使用在已提交读,可重复读两种事务隔离级别中)可以实现一致性读取,通过临键锁(行锁,间隙锁的组合)的方式可以防止其他事务对相关数据的写入。这里主要是该事务使用中的行,以及相关间隙数据,对这些数据行加锁,其他事务不可以新增,删除,修改。
序列化 SERIALIZABLE 就是每一个读取都会加读锁,写入就添加写锁