MySQL InnoDB 存储引擎 - 锁
1、数据库中锁的作用
数据库系统使用锁是为了支持对【共享资源】进行【并发访问】,提供【数据】的【完整性】和【一致性】;
2、Lock 与 Latch
3、InnoDB 存储引擎中的锁
3-1、锁的类型
InnoDB 存储引擎实现了两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据;
排他锁(X Lock),允许事务删除或更新一行数据;
不同的锁之间兼容性不相同:
锁兼容(Lock Compatible):一个事务获得一行数据【共享锁】,另一个事务也可以获得同一行的【共享锁】;
锁不兼容:一个事务获得行的排他锁,必须等待其他事务释放共享锁或排他锁;
S 和 X 锁都是行锁,兼容是指对【同一记录】(row)锁的兼容性情况;
3-2、一致性【非锁定读】
一致性非锁定读(consistent nonlocking read) 是指 InnoDB 存储引擎通过行多版本控制(multi versioning) 的方式来读取当前执行时间数据库中行的数据。
如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会因此等待行上锁的释放,而是去读取行的一个【快照数据】。
之所以称其为【非锁定读】,因为不需要等待访问行上 X 锁的释放。
优点:
【快照数据】是指该行之前版本的数据,该实现通过 undo 段来完成,而 undo 用来在事务中回滚数据,因此【快照数据本身没有额外开销】;
读取【快照数据】不需要上锁,因为没有事务需要对历史数据进行修改操作;
【非锁定读】提高了数据库的并发性;
这是 InnoDB 引擎默认读取方式,但不是每个事务隔离级别都采用非锁定的一致性读;在事务隔离级别 READ COMMITTED 和 REPEATABLE READ(InnoDB 存储引擎的默认事务隔离级别),InnoDB 存储引擎使用非锁定的一致性读;对于快照数据,READ COMMITTED 下读取最新一份快照数据,REPEATABLE READ 读取【事务开始】时的行数据版本;
3-2-1、多版本并发控制(Multi Version Concurrency Control, MVCC)
快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本;一个行记录可能不止一个快照数据,一般称这种技术为行多版本技术;
关于多版本并发控制,
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了 InnoDB 的并发度。在内部实现中,与 Postgres 在数据行上实现多版本不同,InnoDB 是在 undolog 中实现的,通过 undolog 可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在 InnoDB 内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
3-3、一致性【锁定读】
对数据库读取操作进行加锁以保证数据逻辑的一致性;
InnoDB 存储引擎对于 SELECT 语句支持两种一致性的锁定读(locking read):
SELECT ... FOR UPDATE:读取行记录加一个 X 锁;其他事务不能对已锁定的行加任何锁;
SELECT ... LOCK IN SHARE MODE:对读取的行记录加一个 S 锁,其他事务可以对行加 S 锁,但如果加 X 锁,则会被阻塞;
对于一致性【非锁定读】,即使读取的行已被执行 SELECT ... FOR UPDATE,也可以进行读取;
4、锁的算法
4-1、行锁的 3 种算法
InnoDB 存储引擎有 3 种行锁算法,分别是:
Record Lock:单个行记录上的锁;
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身;
如果索引有 10,11,13,20 这四个值,在 Next-Key Locking 下索引的区间为:
(-∞,10],即 -∞ < Lock <= 10,因为要锁定记录本身;
(10, 11],
(11, 13],
(13, 20],
(20, +∞)
关于 Next-Key Lock:
Next-Key Lock 设计目的是为了解决 幻读(Phantom Problem);
Next-Key Lock 如果降级为 Record Lock 仅在查询的列是【唯一索引】的情况下;对于【唯一键值】的锁定,Next-Key Lock 降级为【Record Lock】仅存在于查询【所有的】唯一索引列;查询其中一个还是用 Next-Key Lock;
4-2、解决 Phantom Problem
幻读(Phantom Problem)是指在【同一事务下】,连续执行两次【同样的 SQL 语句】可能导致不同的结果,第二次的 SQL 语句可能会返回之前不存在的行;
当表中存在 1,2,5 数据,
事务 T1 第一次 执行 select * from table where id >= 1,返回 1,2,5 三条数据,
此时事务 T2 执行 insert into table (id) value (1) 或 INSERT INTO table SELECT 4 后,
事务 T1 再次执行 select * from table where id >= 1,返回 1,2,4,5;
4-2-1、采用 Next-Key Locking 算法避免 幻读(Phantom Problem)
InnoDB 引擎采用 Next-Key Locking 算法避免幻读,当执行 SELECT * FROM table WHERE id >= 1 FOR UPDATE 时,是对 [1, +∞) 这个范围加了 X 锁,因此对这个范围的插入都是不被允许的,从而避免幻读;
事务不同隔离级别,采取的加锁算法不同:
可重复读(REPEATABLE READ),采用 Next-Key Locking 方式加锁;
读已提交(READ COMMITTED),采用 Record Lock;
5、锁问题
5-1、脏读
脏读:事务对缓冲池中行记录的修改,并且还没有被提交;也就是,在不同事务下,当前事务可以读到另外事务未提交的数据;
脏读是指【未提交的】数据,如果读到脏数据,即一个事务可以读到【另一个事务】【未提交的】数据,违反了数据库的隔离性;
脏读出现在【事务隔离级别】设置为【读未提交(READ UNCOMMITTED)】;
5-2、不可重复读
不可重复读,是指在【一个事务内】【多次读取同一数据集合】,事务未结束时,另一事务访问该数据集合并做了 DML 操作并提交,在第一个事务中两次读数据之间,由于第二个事务修改,第一个事务中每次读到的数据可能不同;
不可重复读,违反了数据库【事务一致性】的要求;
不可重复读和脏读的区别:脏读是读到【未提交】的数据,不可重复读读到【已提交】的数据;
不可重复读 出现在【事务隔离级别】设置为【读已提交(READ COMMITTED)】;
InnoDB 存储引擎中,通过使用 Next-Key Lock 算法 避免 【不可重复读】,MySQL 官方将【不可重复读】定义为 Phantom Problem,即幻像问题;
在【 Next-Key Lock】算法下,对于索引扫描,不仅锁住扫描的索引,还锁住索引覆盖的范围(GAP),因此在这个范围内的插入都是不允许的,从而避免不可重复读问题;
5-3、丢失更新
丢失更新,一个事务的更新操作会被另一个事务的更新操作覆盖,从而导致数据不一致;
例如:
1)事务 T1 将 行记录 R 更新为 V1,但事务 T1 并未提交;
2) 同时,事务 T2 将 行记录 R 更新为 V2,事务 T2 未提交;
3) 事务 T1 提交;
4) 事务 T2 提交;
还有一种情况:
1)事务 T1 查询一行数据,并返回界面给 User1;
2) 事务 T2 查询同一行数据,并返回用户界面给 User2 展示;
3) User1 修改这行记录,更新数据库并提交;
4) User2 修改这行记录,更新数据库并提交;
User1 修改的数据【丢失】了;
例如银行转账,账户 A 里有 1 万元,此时 User1 向另一个账户 B 转账 9000,由于网络问题,需要等待;而 User2 向账户 C 转账 1000,结果可能导致 账户余额是 9000,User1 转账更新账户 A 的结果被 User2 的转账操作覆盖了;
要避免这种情况,最好将操作变成【串行化】;
6、阻塞
7、死锁
7-1、死锁的概念
死锁是指两个或两个以上的事务在执行过程中,因【争夺锁资源】而造成的一种【相互等待】的现象;
7-1-1、解决死锁问题的方法
1、不要有等待
将任何等待都回滚,并且事务重新开始;
优点:简单
缺点:
导致并发能力下降,甚至任何事务不能进行;
导致资源浪费;
2、超时
当两个事务互相等待时,当一个等待时间超过设置的阈值时,其中一个事务进行回滚,另一个事务就能继续执行;
在 InnoDB 引擎中,参数【innodb_lock_wait_timeout】用来设置超时时间;
缺点:
超时的事务权重比较大,事务更新行数多,占用较多的 undo log,回滚该事务所占用时间可能会很多;
3、等待图(wait-for graph)
【等待图】比起【超时】的解决方案,是一种更为【主动的】死锁检测方式。
等待图要求数据库保存两个信息:
锁的信息链表
事务等待链表
通过上述链表可以构造出一张图,图中若存在【回路】,就代表存在死锁,因此资源间相互发生等待;
等待图中【边】的定义:
一个事务 T1 等待另一个事务 T2 所占用的资源;
一个事务 T1【最终等待】另一个事务 T2 所占用的资源,也就是事务之间在等待【相同的资源】,而事务 T1 发生在事务 T2 后面;
总结:
【等待图】的【节点】由事务组成;
【等待图】的【边】:由事务等待被另一事务占用的相同资源,事务到另一事务存在一个边;
举例:
由上图看出,
当前存在 4 个事务,因此在【等待图】中有 4 个节点;
事务 T2 对 Row1 占 X 锁,事务 T1 对 Row1 占 S 锁,事务 T1 在 T2 之后,且需要等待 T2 释放资源,因此存在事务 T1 指向事务 T2 的边;
事务 T2 等待事务 T1、T4 占用的 Row2,因此存在 T2 到 T1、T4 的边;
同样,T3 也有到 T1、T2、T4 的边;
因此当前事务【等待图】如下:
事务请求锁等待都会判断是否存在【回路】,若存在则有死锁;存储引擎选择回滚 undo 量最小的事务;
死锁检测采用【深度优先算法】实现;
7-1-2、死锁发生的概率
死锁发生的因素有:
系统中事务的数量,数量越多发生死锁的概率越大;
每个事务操作的数量,每个事务操作的数量越多,发生死锁的概率越大;
操作数据的集合,越小则发生死锁的概率越大;
版权声明: 本文为 InfoQ 作者【Arthur】的原创文章。
原文链接:【http://xie.infoq.cn/article/8aa06e0b3a054c0a9b6134866】。未经作者许可,禁止转载。
评论