MySQL InnoDB 存储引擎 - 锁

发布于: 2020 年 06 月 26 日

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 数据,

  1. 事务T1 第一次 执行 select * from table where id >= 1,返回 1,2,5三条数据,

  2. 此时事务T2执行 insert into table (id) value (1) 或 INSERT INTO table SELECT 4 后,

  3. 事务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、死锁发生的概率

死锁发生的因素有:

  • 系统中事务的数量,数量越多发生死锁的概率越大;

  • 每个事务操作的数量,每个事务操作的数量越多,发生死锁的概率越大;

  • 操作数据的集合,越小则发生死锁的概率越大;

发布于: 2020 年 06 月 26 日 阅读数: 12
用户头像

Albert

关注

还未添加个人签名 2018.08.31 加入

还未添加个人简介

评论

发布
暂无评论
MySQL InnoDB 存储引擎 - 锁