深入理解mysql四种隔离级别及底层实现原理(MVCC和锁)
1) 锁的分类
- Shared Locks(共享锁/S锁)
若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- Exclusive Locks(排它锁/X锁)
若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
注意:排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是不加锁的就没办法控制了。
- Record Locks(行锁)
行锁,顾名思义,是加在索引行
(对!是索引行!不是数据行!)上的锁。比如select * from user where id=1 and id=10 for update
,就会在id=1
和id=10
的索引行上加Record Lock。
- Gap Locks(间隙锁)
间隙锁,它会锁住两个索引之间的区域。比如select * from user where id>1 and id<10 for update
,就会在id为(1,10)的索引区间上加Gap Lock。
- Next-Key Locks(间隙锁)
也叫间隙锁,它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from user where id>=1 and id<=10 for update
,就会在id为[1,10]的索引闭区间上加Next-Key Lock。
这样组合起来就有,行级共享锁,表级共享锁,行级排它锁,表级排它锁。
2) 什么时候会加锁?
在数据库增删改查四种操作中,insert、delete和update都是会加排它锁(Exclusive Locks)的,而select只有显式声明才会加锁:
- select: 即最常用的查询,是不加任何锁的
- select ... lock in share mode: 会加共享锁(Shared Locks)
- select ... for update: 会加排它锁
3) 四种隔离级别
不同的隔离级别是在数据可靠性和并发性之间的均衡取舍,隔离级别越高,对应的并发性能越差,数据越安全可靠。
- READ UNCOMMITTED
顾名思义,事务之间可以读取彼此未提交的数据。机智如你会记得,在前文有说到所有写操作都会加排它锁,那还怎么读未提交呢?
机智如你,前面我们介绍排它锁的时候,有这种说明: 排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是对不加锁的读就不起作用了。
READ UNCOMMITTED
隔离级别下, 读不会加任何锁。而写会加排他锁,并到事务结束之后释放。
实例1:
查-写:查并没有阻止写,表明查肯定并没有加锁,要不写肯定就阻塞了。写很明显,会加排它锁的。
实例2: 写-写:阻塞,表明,写会加排它锁。
- READ COMMITTED
顾名思义,事务之间可以读取彼此已提交的数据。
InnoDB在该隔离级别(READ COMMITTED)写数据时,使用排它锁, 读取数据不加锁而是使用了MVCC机制。
因此,在读已提交的级别下,都会通过MVCC获取当前数据的最新快照,不加任何锁,也无视任何锁(因为历史数据是构造出来的,身上不可能有锁)。
但是,该级别下还是遗留了不可重复读和幻读问题: MVCC版本的生成时机: 是每次select时。这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读,即:重复读时,会出现数据不一致问题,后面我们会讲解超支现象,就是这种引起的。
- REPEATABLE READ
READ COMMITTED级别不同的是MVCC版本的生成时机,即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。
但是因为MVCC的快照只对读操作有效,对写操作无效,举例说明会更清晰一点: 事务A依次执行如下3条sql,事务B在语句1和2之间,插入10条age=20的记录,事务A就幻读了。
1. select count(1) from user where age=20;
-- return 0: 当前没有age=20的
2. update user set name=test where age=20;
-- Affects 10 rows: 因为事务B刚写入10条age=20的记录,而写操作是不受MVCC影响,能看到最新数据的,所以更新成功,而一旦操作成功,这些***作的数据就会对当前事务可见
3. select count(1) from user where age=20;
-- return 10: 出现幻读
REPEATABLE READ级别,可以防止大部分的幻读,但像前边举例读-写-读的情况,使用不加锁的select依然会幻读。
- SERIALISABLE
大杀器,该级别下,会自动将所有普通select
转化为select ... lock in share mode
执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。
读-写,写-写均互斥。
4)总结:几类读异常
读-写-读,引起的异常
-
脏读:读取了脏数据(不存在的数据)。 事务一读 事务二写(未提交) 事务二读(脏数据) 事务二回滚
-
不可重复读:既可以读取修改的数据,也可以读取新增的数据(幻读)。 事务一读 事务二写(更新已提交) 事务二读(数据不一致,不可重复读)
-
幻读:仅可以读取新增的数据,但是无法读取修改的数据; 事务一读 事务二写(新增已提交) 事务二读(数据不一致,幻读)