零、环境准备
mysql版本:8.0.20node
调试IDE:Visual Studio Codemysql
1、问题引入
看以下一条sql语句:sql
# table T (id int, name varchar(20)) delete from T where id = 10;
MySQL在执行的过程当中,是如何加锁呢?数据库
再看下面这条语句:并发
select * from T where id = 5;
那这条语句呢?其实这其中包含太多知识点了。要回答这两个问题,首先须要了解一些知识。mvc
2、相关知识回顾
2.1 多版本并发控制
在MySQL默认存储引擎InnoDB中,实现的是基于多版本的并发控制协议——MVCC(Multi-Version Concurrency Control)(注:与MVVC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。less
其中MVCC最大的好处是:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是很是重要的,极大的提升了系统的并发性能,在现阶段,几乎全部的RDBMS,都支持MVCC。其实,MVCC就一句话总结:同一份数据临时保存多个版本的一种方式,进而实现并发控制。性能
2.2 当前读和快照读
在MVCC并发控制中,读操做能够分为两类:快照读与当前读。fetch
- 快照读(简单的select操做):读取的是记录中的可见版本(多是历史版本),不用加锁。这你就知道第二个问题的答案了吧。
- 当前读(特殊的select操做、insert、delete和update):读取的是记录中最新版本,而且当前读返回的记录都会加上锁,这样保证了了其余事务不会再并发修改这条记录。
2.3 汇集索引
也叫作聚簇索引。在InnoDB中,数据的组织方式就是聚簇索引:完整的记录,储存在主键索引中,经过主键索引,就能够获取记录中全部的列。大数据
2.4 最左前缀原则
也就是最左优先,这条原则针对的是组合索引和前缀索引,理解:
一、在MySQL中,进行条件过滤时,是按照向右匹配直到遇到范围查询(>,<,between,like)就中止匹配,好比说a = 1 and b = 2 and c > 3 and d = 4 若是创建(a, b, c, d)顺序的索引,d是用不到索引的,若是创建(a, b, d, c)索引就都会用上,其中a,b,d的顺序能够任意调整。
二、= 和 in 能够乱序,好比 a = 1 and b = 2 and c = 3 创建(a, b, c)索引能够任意顺序,MySQL的查询优化器会优化索引能够识别的形式。
2.5 两阶段锁
传统的RDMS加锁的一个原则,就是2PL(Two-Phase Locking,二阶段锁)。也就是说锁操做分为两个阶段:加锁阶段和解锁阶段,而且保证加锁阶段和解锁阶段不想交。也就是说在一个事务中,无论有多少条增删改,都是在加锁阶段加锁,在 commit 后,进入解锁阶段,才会所有解锁。
2.6 隔离级别
MySQL/InnoDB中,定义了四种隔离级别:
- Read Uncommitted:能够读取未提交记录。此隔离级别不会使用。
- Read Committed(RC):针对当前读,RC隔离级别保证了对读取到的记录加锁(记录锁),存在幻读现象。
- Repeatable Read(RR):针对当前读,RR隔离级别保证对读取到的记录加锁(记录锁),同时保证对读取的范围加锁,新的知足查询条件的记录不可以插入(间隙锁),不存在幻读现象。
- Serializable:从MVCC并发控制退化为基于锁的并发控制。不区别快照读和当前读,全部的读操做都是当前读,读加读锁(S锁),写加写锁(X锁)。在该隔离级别下,读写冲突,所以并发性能急剧降低,在MySQL/InnoDB中不建议使用。
2.7 Gap锁和Next-Key锁
在InnoDB中完整行锁包含三部分:
- 记录锁(Record Lock):记录锁锁定索引中的一条记录。
- 间隙锁(Gap Lock):间隙锁要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或最后一个索引记录后面的值。
- Next-Key Lock:Next-Key锁时索引记录上的记录锁和在记录以前的间隙锁的组合。
3、案例分析过程
SQL1: select * from t1 where id = 10;(不加锁。由于MySQL是使用多版本并发控制的,读不加锁。)
SQL2: delete from t1 where id = 10;(需根据多种状况进行分析)
假设t1表上有索引,执行计划必定会选择使用索引进行过滤 (索引扫描),根据如下组合,来进行分析。
- 组合一:id列是主键,RC隔离级别
- 组合二:id列是二级惟一索引,RC隔离级别
- 组合三:id列是二级非惟一索引,RC隔离级别
- 组合四:id列上没有索引,RC隔离级别
- 组合五:id列是主键,RR隔离级别
- 组合六:id列是二级惟一索引,RR隔离级别
- 组合七:id列是二级非惟一索引,RR隔离级别
- 组合八:id列上没有索引,RR隔离级别
- 组合九:Serializable隔离级别
注:在前面八种组合下,也就是RC,RR隔离级别下,SQL1:select操做均不加锁,采用的是快照读,所以在下面的讨论中就忽略了,主要讨论SQL2:delete操做的加锁。
组合一: id主键 + RC
id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只须要将主键上,id = 10的记录加上X锁便可。以下图所示:
结论:id是主键时,此SQL只须要在id=10这条记录上加X锁便可。
示例:
#准备数据 mysql> create table t1 (id int,name varchar(10)); mysql> alter table t1 add primary key (id); mysql> insert into t1 values(1,'a'),(4,'c'),(7,'b'),(10,'a'),(20,'d'),(30,'b'); mysql> select * from t1; +----+------+ | id | name | +----+------+ | 1 | a | | 4 | c | | 7 | b | | 10 | a | | 20 | d | | 30 | b | +----+------+ 6 rows in set (0.00 sec) 会话1 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> delete from t1 where id=10; Query OK, 1 row affected (0.00 sec) 会话2 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t1; +----+------+ | id | name | +----+------+ | 1 | a | | 4 | c | | 7 | b | | 10 | a | | 20 | d | | 30 | b | +----+------+ 6 rows in set (0.00 sec) mysql> update t1 set name='a1' where id=10; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set name='a1' where id=11; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 mysql> update t1 set name='a1' where id=7; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 从示例中能够看到会话1执行的delete操做,只对id=10加了X锁。
组合二:id惟一索引 + RC
id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 须要加什么锁呢?见下图:
此组合中,id是unique索引,而主键是name列。此时,加锁的状况因为组合一有所不一样。因为id是unique索引,所以delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),而后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。
为何聚簇索引上的记录也要加锁?试想一下,若是并发的一个SQL,是经过主键索引来更新:update t1 set id = 100 where name = 'd';此时,若是delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除须要串行执行的约束。
示例:
准备数据 mysql> create table t1 (id int,name varchar(10)); Query OK, 0 rows affected (0.06 sec) mysql> ALTER TABLE test.t1 ADD UNIQUE INDEX idx_id (id); Query OK, 0 rows affected (0.07 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> ALTER TABLE test.t1 ADD PRIMARY KEY (name); Query OK, 0 rows affected (0.11 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> insert into t1 values(1,'f'),(2,'zz'),(3,'b'),(5,'a'),(6,'c'),(10,'d'); Query OK, 6 rows affected (0.01 sec) Records: 6 Duplicates: 0 Warnings: 0 会话1 mysql> begin; Query OK, 0 rows affected (0.01 sec) mysql> delete from t1 where id=10; Query OK, 1 row affected (0.00 sec) 会话2 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t1; +------+------+ | id | name | +------+------+ | 1 | f | | 2 | zz | | 3 | b | | 5 | a | | 6 | c | | 10 | d | +------+------+ 6 rows in set (0.00 sec) mysql> update t1 set id =100 where name='d'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id =100 where name='c'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id =101 where name='a'; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0
结论:若id列是unique列,其上有unique索引。那么SQL须要加两个X锁,一个对应于id unique索引上的id = 10的记录,另外一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。
组合三:id非惟一索引 + RC
id列是一个普通索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?一样见下图:
由上图能够看出,首先,id列索引上,知足id = 10查询的记录,均加上X锁。同时,这些记录对应的主键索引上的记录也加上X锁。与组合二的惟一区别,组合二最多只有一个知足条件的记录,而在组合三中会将全部知足条件的记录所有加上锁。
结论:若id列上有非惟一索引,那么对应的全部知足SQL查询条件的记录,都会加上锁。同时,这些记录在主键索引上也会加上锁。
示例:
准备数据 mysql> create table t1 (id int,name varchar(10)); mysql> ALTER TABLE test.t1 ADD PRIMARY KEY (name); mysql> alter table t1 add index idx_id (id); mysql> insert into t1 values(2,'zz'),(6,'c'),(10,'b'),(10,'d'),(11,'f'),(15,'a'); 会话1 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t1 where id=10; Query OK, 2 rows affected (0.00 sec) 会话2 mysql> select * from t1; +------+------+ | id | name | +------+------+ | 2 | zz | | 6 | c | | 10 | b | | 10 | d | | 11 | f | | 15 | a | +------+------+ 6 rows in set (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update t1 set id=11 where name='b'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id=11 where name='d'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id=11 where name='f'; Query OK, 0 rows affected (0.00 sec) Rows matched: 1 Changed: 0 Warnings: 0 mysql> update t1 set id=11 where name='c'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
组合四:id无索引+RC
相对于前面的组合,该组合相对特殊,由于id列上无索引,因此在 where id = 10 这个查询条件下,无法经过索引来过滤,所以只能全表扫描作过滤。对于该组合,MySQL又会进行怎样的加锁呢?看下图:
因为id列上无索引,所以只能走聚簇索引,进行全表扫描。由图能够看出知足条件的记录只有两条,可是,聚簇索引上的记录都会加上X锁。但在实际操做中,MySQL进行了改进,在进行过滤条件时,发现不知足条件后,会调用 unlock_row 方法,把不知足条件的记录放锁(违背了2PL原则)。这样作,保证了最后知足条件的记录加上锁,可是每条记录的加锁操做是不能省略的。
结论:若id列上没有索引,MySQL会走聚簇索引进行全表扫描过滤。因为是在MySQl Server层面进行的,所以每条记录不管是否知足条件,都会加上X锁,可是,为了效率考虑,MySQL在这方面进行了改进,在扫描过程当中,若记录不知足过滤条件,会进行解锁操做。同时优化违背了2PL原则。
示例:
准备数据 mysql> create table t1 (id int,name varchar(10)); mysql> ALTER TABLE test.t1 ADD PRIMARY KEY (name); mysql> insert into t1 values(5,'a'),(3,'b'),(10,'d'),(2,'f'),(10,'g'),(9,'zz'); 会话1 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t1 where id=10; Query OK, 2 rows affected (0.00 sec) 会话2 mysql> select * from t1; +------+------+ | id | name | +------+------+ | 5 | a | | 3 | b | | 10 | d | | 2 | f | | 10 | g | | 9 | zz | +------+------+ 6 rows in set (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update t1 set id=6 where name='a'; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id=6 where name='b'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id=6 where name='d'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id=6 where name='f'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id=6 where name='g'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id=6 where name='zz'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id=6 where name='zzf'; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 实验结果与推倒的结论不一致, 实验结果看出只锁住了id=10的两行。
组合五:id主键+RR
id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:”id主键 + RC“一致。
结论:id是主键是,此SQL语句只须要在id = 10这条记录上加上X锁便可。
示例:
mysql> create table t1 (id int,name varchar(10)); mysql> alter table t1 add primary key (id); mysql> insert into t1 values(1,'a'),(4,'c'),(7,'b'),(10,'a'),(20,'d'),(30,'b'); mysql> select * from t1; +----+------+ | id | name | +----+------+ | 1 | a | | 4 | c | | 7 | b | | 10 | a | | 20 | d | | 30 | b | +----+------+ 6 rows in set (0.00 sec) mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) 会话1 mysql> begin; Query OK, 0 rows affected (0.01 sec) mysql> delete from t1 where id=10; Query OK, 1 row affected (0.00 sec) 会话2 mysql> select * from t1; +----+------+ | id | name | +----+------+ | 1 | a | | 4 | c | | 7 | b | | 10 | a | | 20 | d | | 30 | b | +----+------+ 6 rows in set (0.00 sec) mysql> update t1 set name='a1' where id=10; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set name='a1' where id=11; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 mysql> update t1 set name='a1' where id=7; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
组合六:id惟一索引+RR
id惟一索引 + RR的加锁与id惟一索引,RC一致。两个X锁,id惟一索引知足条件的记录上一个,对应的聚簇索引上的记录一个。
示例:
准备数据 mysql> create table t1 (id int,name varchar(10)); mysql> ALTER TABLE test.t1 ADD UNIQUE INDEX idx_id (id); mysql> ALTER TABLE test.t1 ADD PRIMARY KEY (name); mysql> insert into t1 values(1,'f'),(2,'zz'),(3,'b'),(5,'a'),(6,'c'),(10,'d'); 会话1 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t1 where id=10; Query OK, 1 row affected (0.01 sec) 会话2 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t1; +------+------+ | id | name | +------+------+ | 1 | f | | 2 | zz | | 3 | b | | 5 | a | | 6 | c | | 10 | d | +------+------+ 6 rows in set (0.00 sec) mysql> update t1 set id =100 where name='d'; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update t1 set id =100 where name='c'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> update t1 set id =101 where name='a'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
组合七:id不惟一索引+RR
在组合一到组合四中,隔离级别是Read Committed下,会出现幻读状况,可是在该组合Repeatable Read级别下,不会出现幻读状况,这是怎么回事呢?而MySQL又是如何给上述语句加锁呢?看下图:
该组合和组合三看起来很类似,但差异很大,在该组合中加入了一个间隙锁(Gap锁)。这个Gap锁就是相对于RC级别下,RR级别下不会出现幻读状况的关键。实质上,Gap锁不是针对于记录自己的,而是记录之间的Gap。所谓幻读,就是同一事务下,连续进行屡次当前读,且读取一个范围内的记录(包括直接查询全部记录结果或者作聚合统计),发现结果不一致(标准档案通常指记录增多, 记录的减小应该也算是幻读)。
那么该如何解决这个问题呢?如何保证屡次当前读返回一致的记录,那么就须要在多个当前读之间,其余事务不会插入新的知足条件的记录并提交。为了实现该结果,Gap锁就应运而生。
如图所示,有些位置能够插入新的知足条件的记录,考虑到B+树的有序性,知足条件的记录必定是具备连续性的。所以会在 [4, b], [10, c], [10, d], [20, e] 之间加上Gap锁。
Insert操做时,如insert(10, aa),首先定位到 [4, b], [10, c]间,而后插入在插入以前,会检查该Gap是否加锁了,若是被锁上了,则Insert不能加入记录。所以经过第一次当前读,会把知足条件的记录加上X锁,还会加上三把Gap锁,将可能插入知足条件记录的3个Gap锁上,保证后续的Insert不能插入新的知足 id = 10 的记录,也就解决了幻读问题。
而在组合五,组合六中,一样是RR级别,可是不用加上Gap锁,在组合五中id是主键,组合六中id是Unique键,都能保证惟一性。一个等值查询,最多只能返回一条知足条件的记录,并且新的相同取值的记录是没法插入的。
结论:在RR隔离级别下,id列上有非惟一索引,对于上述的SQL语句;首先,经过id索引定位到第一条知足条件的记录,给记录加上X锁,而且给Gap加上Gap锁,而后在主键聚簇索引上知足相同条件的记录加上X锁,而后返回;以后读取下一条记录重复进行。直至第一条出现不知足条件的记录,此时,不须要给记录加上X锁,可是须要给Gap加上Gap锁,最后返回结果。
示例:
准备数据 mysql> create table t1 (id int,name varchar(10)); mysql> ALTER TABLE test.t1 ADD PRIMARY KEY (name); mysql> alter table t1 add index idx_id (id); mysql> insert into t1 values(1,'a'),(4,'b'),(10,'c'),(20,'e'),(10,'d'); 会话1 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t1 where id=10; Query OK, 2 rows affected (0.00 sec) 会话2 mysql> select * from t1; +------+------+ | id | name | +------+------+ | 1 | a | | 4 | b | | 10 | c | | 10 | d | | 20 | e | +------+------+ 5 rows in set (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into t1 values(6,'aa'); Query OK, 1 row affected (0.00 sec) mysql> insert into t1 values(6,'bb'); Query OK, 1 row affected (0.01 sec) mysql> insert into t1 values(6,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(7,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(8,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(9,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(10,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(11,'cc'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into t1 values(11,'ff'); Query OK, 1 row affected (0.00 sec) mysql> insert into t1 values(11,'g'); Query OK, 1 row affected (0.00 sec)
组合八:id无索引+RR
该组合中,id列上无索引,只能进行全表扫描,那么该如何加锁,看下图:
如图,能够看出这是一个很恐怖的事情,全表每条记录要加X锁,每一个Gap加上Gap锁,若是表上存在大量数据时,又是什么情景呢?这种状况下,这个表,除了不加锁的快照读,其余任何加锁的并发SQL,均不能执行,不能更新,删除,插入,这样,全表锁死。
固然,和组合四同样,MySQL进行了优化,就是semi-consistent read。semi-consistent read开启的状况下,对于不知足条件的记录,MySQL会提早放锁,同时Gap锁也会释放。而semi-consistent read是如何触发:要么在Read Committed隔离级别下;要么在Repeatable Read隔离级别下,设置了 innodb_locks_unsafe_for_binlog 参数。
示例:
准备数据 mysql> create database cgwtest; mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; //mysql> insert into t1 values(5,'a'),(3,'b'),(10,'d'),(2,'f'),(10,'g'),(9,'zz'); mysql> insert into t values(1,1),(5,5),(10,10); 会话1 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t where d=5; Query OK, 1 rows affected (0.00 sec) 会话2 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t; +----+------+ | id | d | +----+------+ | 1 | 1 | | 5 | 5 | | 10 | 10 | +----+------+ 3 rows in set (0.00 sec) mysql> insert into t values(2,2); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
(注:如下流程和源码是主流程和重点关注的点!)
1,delete源码实现过程:
/* Basic lock modes */ enum lock_mode { LOCK_IS = 0, /* intention shared */ LOCK_IX, /* intention exclusive */ LOCK_S, /* shared */ LOCK_X, /* exclusive */ LOCK_AUTO_INC, /* locks the auto-inc counter of a table in an exclusive mode */ LOCK_NONE, /* this is used elsewhere to note consistent read */ LOCK_NUM = LOCK_NONE, /* number of lock modes */ LOCK_NONE_UNSET = 255 };
ut_ad(gap_mode == LOCK_ORDINARY || gap_mode == LOCK_GAP || gap_mode == LOCK_REC_NOT_GAP); #define ut_ad(EXPR) ut_a(EXPR) /** Debug statement. Does nothing unless UNIV_DEBUG is defined. */调试断言
核心方法:
/** Sets a lock on a record. mostly due to we cannot reposition a record in R-Tree (with the nature of splitting) @param[in] pcur cursor @param[in] rec record @param[in] index index @param[in] offsets rec_get_offsets(rec, index) @param[in] sel_mode select mode: SELECT_ORDINARY, SELECT_SKIP_LOKCED, or SELECT_NO_WAIT @param[in] mode lock mode @param[in] type LOCK_ORDINARY, LOCK_GAP, or LOC_REC_NOT_GAP @param[in] thr query thread @param[in] mtr mtr @return DB_SUCCESS, DB_SUCCESS_LOCKED_REC, or error code */ UNIV_INLINE dberr_t sel_set_rec_lock(btr_pcur_t *pcur, const rec_t *rec, dict_index_t *index, const ulint *offsets, select_mode sel_mode, ulint mode, ulint type, que_thr_t *thr, mtr_t *mtr) { trx_t *trx; dberr_t err = DB_SUCCESS; const buf_block_t *block; block = btr_pcur_get_block(pcur); trx = thr_get_trx(thr); trx_mutex_enter(trx); ut_ad(trx_can_be_handled_by_current_thread(trx)); bool too_many_locks = (UT_LIST_GET_LEN(trx->lock.trx_locks) > 10000); trx_mutex_exit(trx); if (too_many_locks) { if (buf_LRU_buf_pool_running_out()) { return (DB_LOCK_TABLE_FULL); } } if (index->is_clustered()) { err = lock_clust_rec_read_check_and_lock( lock_duration_t::REGULAR, block, rec, index, offsets, sel_mode, static_cast<lock_mode>(mode), type, thr); } else { if (dict_index_is_spatial(index)) { if (type == LOCK_GAP || type == LOCK_ORDINARY) { ut_ad(0); ib::error(ER_IB_MSG_1026) << "Incorrectly request GAP lock " "on RTree"; return (DB_SUCCESS); } err = sel_set_rtr_rec_lock(pcur, rec, index, offsets, sel_mode, mode, type, thr, mtr); } else { err = lock_sec_rec_read_check_and_lock( lock_duration_t::REGULAR, block, rec, index, offsets, sel_mode, static_cast<lock_mode>(mode), type, thr); } } return (err); }
dberr_t lock_clust_rec_read_check_and_lock( const lock_duration_t duration, const buf_block_t *block, const rec_t *rec, dict_index_t *index, const ulint *offsets, const select_mode sel_mode, const lock_mode mode, const ulint gap_mode, que_thr_t *thr) { dberr_t err; ulint heap_no; ut_ad(rec_offs_validate(rec, index, offsets)); if (srv_read_only_mode || index->table->is_temporary()) { return (DB_SUCCESS); } heap_no = page_rec_get_heap_no(rec); if (heap_no != PAGE_HEAP_NO_SUPREMUM) { lock_rec_convert_impl_to_expl(block, rec, index, offsets);//隐示锁转显示锁 } DEBUG_SYNC_C("after_lock_clust_rec_read_check_and_lock_impl_to_expl"); lock_mutex_enter();//系统锁 if (duration == lock_duration_t::AT_LEAST_STATEMENT) { lock_protect_locks_till_statement_end(thr); } ut_ad(mode != LOCK_X || lock_table_has(thr_get_trx(thr), index->table, LOCK_IX)); ut_ad(mode != LOCK_S || lock_table_has(thr_get_trx(thr), index->table, LOCK_IS)); err = lock_rec_lock(false, sel_mode, mode | gap_mode, block, heap_no, index, thr); MONITOR_INC(MONITOR_NUM_RECLOCK_REQ); lock_mutex_exit(); ut_ad(lock_rec_queue_validate(false, block, rec, index, offsets)); DEBUG_SYNC_C("after_lock_clust_rec_read_check_and_lock"); ut_ad(err == DB_SUCCESS || err == DB_SUCCESS_LOCKED_REC || err == DB_LOCK_WAIT || err == DB_DEADLOCK || err == DB_SKIP_LOCKED || err == DB_LOCK_NOWAIT); return (err); }
/** Tries to lock the specified record in the mode requested. If not immediately possible, enqueues a waiting lock request. This is a low-level function which does NOT look at implicit locks! Checks lock compatibility within explicit locks. This function sets a normal next-key lock, or in the case of a page supremum record, a gap type lock. @param[in] impl if true, no lock is set if no wait is necessary: we assume that the caller will set an implicit lock @param[in] sel_mode select mode: SELECT_ORDINARY, SELECT_SKIP_LOCKED, or SELECT_NO_WAIT @param[in] mode lock mode: LOCK_X or LOCK_S possibly ORed to either LOCK_GAP or LOCK_REC_NOT_GAP @param[in] block buffer block containing the record @param[in] heap_no heap number of record @param[in] index index of record @param[in,out] thr query thread @return DB_SUCCESS, DB_SUCCESS_LOCKED_REC, DB_LOCK_WAIT, DB_DEADLOCK, DB_SKIP_LOCKED, or DB_LOCK_NOWAIT */ static dberr_t lock_rec_lock(bool impl, select_mode sel_mode, ulint mode, const buf_block_t *block, ulint heap_no, dict_index_t *index, que_thr_t *thr) { ut_ad(lock_mutex_own()); ut_ad(!srv_read_only_mode); /* Implicit locks are equivalent to LOCK_X|LOCK_REC_NOT_GAP, so we can omit creation of explicit lock only if the requested mode was LOCK_REC_NOT_GAP */ ut_ad(!impl || ((mode & LOCK_REC_NOT_GAP) == LOCK_REC_NOT_GAP)); /* We try a simplified and faster subroutine for the most common cases */ switch (lock_rec_lock_fast(impl, mode, block, heap_no, index, thr)) { case LOCK_REC_SUCCESS return (DB_SUCCESS); case LOCK_REC_SUCCESS_CREATED: return (DB_SUCCESS_LOCKED_REC); case LOCK_REC_FAIL: return ( lock_rec_lock_slow(impl, sel_mode, mode, block, heap_no, index, thr)); default: ut_error; } }
delete语句调用堆栈:
lock_rec_lock(bool impl, select_mode sel_mode, ulint mode, const buf_block_t * block, ulint heap_no, dict_index_t * index, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\lock\lock0lock.cc:1667) lock_clust_rec_read_check_and_lock(const lock_duration_t duration, const buf_block_t * block, const rec_t * rec, dict_index_t * index, const ulint * offsets, const select_mode sel_mode, const lock_mode mode, const ulint gap_mode, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\lock\lock0lock.cc:5701) sel_set_rec_lock(btr_pcur_t * pcur, const rec_t * rec, dict_index_t * index, const ulint * offsets, select_mode sel_mode, ulint mode, ulint type, que_thr_t * thr, mtr_t * mtr) (\root\mysql-8.0.20\storage\innobase\row\row0sel.cc:1184) row_search_mvcc(unsigned char * buf, page_cur_mode_t mode, row_prebuilt_t * prebuilt, ulint match_mode, const ulint direction) (\root\mysql-8.0.20\storage\innobase\row\row0sel.cc:5214) ha_innobase::general_fetch(ha_innobase * const this, uchar * buf, uint direction, uint match_mode) (\root\mysql-8.0.20\storage\innobase\handler\ha_innodb.cc:9949) ha_innobase::rnd_next(ha_innobase * const this, uchar * buf) (\root\mysql-8.0.20\storage\innobase\handler\ha_innodb.cc:10226) handler::ha_rnd_next(handler * const this, uchar * buf) (\root\mysql-8.0.20\sql\handler.cc:2966) TableScanIterator::Read(TableScanIterator * const this) (\root\mysql-8.0.20\sql\records.cc:423) Sql_cmd_delete::delete_from_single_table(Sql_cmd_delete * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_delete.cc:503) Sql_cmd_delete::execute_inner(Sql_cmd_delete * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_delete.cc:823) Sql_cmd_dml::execute(Sql_cmd_dml * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_select.cc:725) mysql_execute_command(THD * thd, bool first_level) (\root\mysql-8.0.20\sql\sql_parse.cc:3471) mysql_parse(THD * thd, Parser_state * parser_state) (\root\mysql-8.0.20\sql\sql_parse.cc:5306) dispatch_command(THD * thd, const COM_DATA * com_data, enum_server_command command) (\root\mysql-8.0.20\sql\sql_parse.cc:1776) do_command(THD * thd) (\root\mysql-8.0.20\sql\sql_parse.cc:1274) handle_connection(void * arg) (\root\mysql-8.0.20\sql\conn_handler\connection_handler_per_thread.cc:302) pfs_spawn_thread(void * arg) (\root\mysql-8.0.20\storage\perfschema\pfs.cc:2854) libpthread.so.0!start_thread (未知源:0) libc.so.6!clone (未知源:0)
执行删除:
row_upd_clust_step(upd_node_t * node, que_thr_t * const thr) (\root\mysql-8.0.20\storage\innobase\row\row0upd.cc:2982) row_upd(upd_node_t * node, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0upd.cc:3175) row_upd_step(que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0upd.cc:3306) row_update_for_mysql_using_upd_graph(const unsigned char * mysql_rec, row_prebuilt_t * prebuilt) (\root\mysql-8.0.20\storage\innobase\row\row0mysql.cc:2347) row_update_for_mysql(const unsigned char * mysql_rec, row_prebuilt_t * prebuilt) (\root\mysql-8.0.20\storage\innobase\row\row0mysql.cc:2443) ha_innobase::delete_row(ha_innobase * const this, const uchar * record) (\root\mysql-8.0.20\storage\innobase\handler\ha_innodb.cc:9374) handler::ha_delete_row(handler * const this, const uchar * buf) (\root\mysql-8.0.20\sql\handler.cc:7894) Sql_cmd_delete::delete_from_single_table(Sql_cmd_delete * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_delete.cc:528) Sql_cmd_delete::execute_inner(Sql_cmd_delete * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_delete.cc:823) Sql_cmd_dml::execute(Sql_cmd_dml * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_select.cc:725) mysql_execute_command(THD * thd, bool first_level) (\root\mysql-8.0.20\sql\sql_parse.cc:3471) mysql_parse(THD * thd, Parser_state * parser_state) (\root\mysql-8.0.20\sql\sql_parse.cc:5306) dispatch_command(THD * thd, const COM_DATA * com_data, enum_server_command command) (\root\mysql-8.0.20\sql\sql_parse.cc:1776) do_command(THD * thd) (\root\mysql-8.0.20\sql\sql_parse.cc:1274) handle_connection(void * arg) (\root\mysql-8.0.20\sql\conn_handler\connection_handler_per_thread.cc:302) pfs_spawn_thread(void * arg) (\root\mysql-8.0.20\storage\perfschema\pfs.cc:2854) libpthread.so.0!start_thread (未知源:0) libc.so.6!clone (未知源:0)
2,insert源码实现过程:
核心方法:
/** Checks if some other transaction has a conflicting explicit lock request in the queue, so that we have to wait. @return lock or NULL */ static const lock_t *lock_rec_other_has_conflicting( ulint mode, /*!< in: LOCK_S or LOCK_X, possibly ORed to LOCK_GAP or LOC_REC_NOT_GAP, LOCK_INSERT_INTENTION */ const buf_block_t *block, /*!< in: buffer block containing the record */ ulint heap_no, /*!< in: heap number of the record */ const trx_t *trx) /*!< in: our transaction */ { ut_ad(lock_mutex_own()); ut_ad(!(mode & ~(ulint)(LOCK_MODE_MASK | LOCK_GAP | LOCK_REC_NOT_GAP | LOCK_INSERT_INTENTION))); ut_ad(!(mode & LOCK_PREDICATE)); ut_ad(!(mode & LOCK_PRDT_PAGE)); RecID rec_id{block, heap_no}; const bool is_supremum = rec_id.is_supremum(); return (Lock_iter::for_each(rec_id, [=](const lock_t *lock) { return (!(lock_rec_has_to_wait(trx, mode, lock, is_supremum))); })); }
/** Iterate over all the locks on a specific row @param[in] rec_id Iterate over locks on this row @param[in] f Function to call for each entry @param[in] hash_table The hash table to iterate over @return lock where the callback returned false */ template <typename F> static const lock_t *for_each(const RecID &rec_id, F &&f, hash_table_t *hash_table = lock_sys->rec_hash) { ut_ad(lock_mutex_own()); auto list = hash_get_nth_cell(hash_table, hash_calc_hash(rec_id.m_fold, hash_table)); for (auto lock = first(list, rec_id); lock != nullptr; lock = advance(rec_id, lock)) { ut_ad(lock->is_record_lock()); if (!std::forward<F>(f)(lock)) { return (lock); } } return (nullptr); } };
/** Checks if a lock request for a new lock has to wait for request lock2. @return true if new lock has to wait for lock2 to be removed */ UNIV_INLINE bool lock_rec_has_to_wait( const trx_t *trx, /*!< in: trx of new lock */ ulint type_mode, /*!< in: precise mode of the new lock to set: LOCK_S or LOCK_X, possibly ORed to LOCK_GAP or LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION */ const lock_t *lock2, /*!< in: another record lock; NOTE that it is assumed that this has a lock bit set on the same record as in the new lock we are setting */ bool lock_is_on_supremum) /*!< in: true if we are setting the lock on the 'supremum' record of an index page: we know then that the lock request is really for a 'gap' type lock */ { ut_ad(trx && lock2); ut_ad(lock_get_type_low(lock2) == LOCK_REC); const bool is_hp = trx_is_high_priority(trx); if (trx != lock2->trx && !lock_mode_compatible(static_cast<lock_mode>(LOCK_MODE_MASK & type_mode), lock_get_mode(lock2))) { /* If our trx is High Priority and the existing lock is WAITING and not high priority, then we can ignore it. */ if (is_hp && lock2->is_waiting() && !trx_is_high_priority(lock2->trx)) { return (false); } /* We have somewhat complex rules when gap type record locks cause waits */ if ((lock_is_on_supremum || (type_mode & LOCK_GAP)) && !(type_mode & LOCK_INSERT_INTENTION)) { /* Gap type locks without LOCK_INSERT_INTENTION flag do not need to wait for anything. This is because different users can have conflicting lock types on gaps. */ return (false); } if (!(type_mode & LOCK_INSERT_INTENTION) && lock_rec_get_gap(lock2)) { /* Record lock (LOCK_ORDINARY or LOCK_REC_NOT_GAP does not need to wait for a gap type lock */ return (false); } if ((type_mode & LOCK_GAP) && lock_rec_get_rec_not_gap(lock2)) { /* Lock on gap does not need to wait for a LOCK_REC_NOT_GAP type lock */ return (false); } if (lock_rec_get_insert_intention(lock2)) { /* No lock request needs to wait for an insert intention lock to be removed. This is ok since our rules allow conflicting locks on gaps. This eliminates a spurious deadlock caused by a next-key lock waiting for an insert intention lock; when the insert intention lock was granted, the insert deadlocked on the waiting next-key lock. Also, insert intention locks do not disturb each other. */ return (false); } return (true); } return (false); }
调用堆栈
lock_rec_other_has_conflicting(ulint mode, const buf_block_t * block, ulint heap_no, const trx_t * trx) (\root\mysql-8.0.20\storage\innobase\lock\lock0lock.cc:805) lock_rec_insert_check_and_lock(ulint flags, const rec_t * rec, buf_block_t * block, dict_index_t * index, que_thr_t * thr, mtr_t * mtr, ulint * inherit) (\root\mysql-8.0.20\storage\innobase\lock\lock0lock.cc:5291) btr_cur_ins_lock_and_undo(ulint flags, btr_cur_t * cursor, dtuple_t * entry, que_thr_t * thr, mtr_t * mtr, ulint * inherit) (\root\mysql-8.0.20\storage\innobase\btr\btr0cur.cc:2621) btr_cur_optimistic_insert(ulint flags, btr_cur_t * cursor, ulint ** offsets, mem_heap_t ** heap, dtuple_t * entry, rec_t ** rec, big_rec_t ** big_rec, que_thr_t * thr, mtr_t * mtr) (\root\mysql-8.0.20\storage\innobase\btr\btr0cur.cc:2841) row_ins_clust_index_entry_low(uint32_t flags, ulint mode, dict_index_t * index, ulint n_uniq, dtuple_t * entry, que_thr_t * thr, bool dup_chk_only) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:2515) row_ins_clust_index_entry(dict_index_t * index, dtuple_t * entry, que_thr_t * thr, bool dup_chk_only) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:3095) row_ins_index_entry(dict_index_t * index, dtuple_t * entry, uint32_t & multi_val_pos, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:3286) row_ins_index_entry_step(ins_node_t * node, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:3424) row_ins(ins_node_t * node, que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:3542) row_ins_step(que_thr_t * thr) (\root\mysql-8.0.20\storage\innobase\row\row0ins.cc:3666) row_insert_for_mysql_using_ins_graph(const unsigned char * mysql_rec, row_prebuilt_t * prebuilt) (\root\mysql-8.0.20\storage\innobase\row\row0mysql.cc:1585) row_insert_for_mysql(const unsigned char * mysql_rec, row_prebuilt_t * prebuilt) (\root\mysql-8.0.20\storage\innobase\row\row0mysql.cc:1715) ha_innobase::write_row(ha_innobase * const this, uchar * record) (\root\mysql-8.0.20\storage\innobase\handler\ha_innodb.cc:8530) handler::ha_write_row(handler * const this, uchar * buf) (\root\mysql-8.0.20\sql\handler.cc:7837) write_record(THD * thd, TABLE * table, COPY_INFO * info, COPY_INFO * update) (\root\mysql-8.0.20\sql\sql_insert.cc:2111) Sql_cmd_insert_values::execute_inner(Sql_cmd_insert_values * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_insert.cc:621) Sql_cmd_dml::execute(Sql_cmd_dml * const this, THD * thd) (\root\mysql-8.0.20\sql\sql_select.cc:725) mysql_execute_command(THD * thd, bool first_level) (\root\mysql-8.0.20\sql\sql_parse.cc:3471) mysql_parse(THD * thd, Parser_state * parser_state) (\root\mysql-8.0.20\sql\sql_parse.cc:5306) dispatch_command(THD * thd, const COM_DATA * com_data, enum_server_command command) (\root\mysql-8.0.20\sql\sql_parse.cc:1776)
结论:在Repeatable Read隔离级别下,若是进行全表扫描的当前读,那么会锁上表上的全部记录,而且全部的Gap加上Gap锁,杜绝全部的 delete/update/insert 操做。固然在MySQL中,能够触发 semi-consistent read来缓解锁开销与并发影响,可是semi-consistent read自己也会带来其余的问题,不建议使用。
组合九:Serializable
在最后组合中,对于上诉的删除SQL语句,加锁过程和组合八一致。可是,对于查询语句(好比select * from T1 where id = 10)来讲,在RC,RR隔离级别下,都是快照读,不加锁。在Serializable隔离级别下,不管是查询语句也会加锁,也就是说快照读不存在了,MVCC降级为Lock-Based CC。
结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于全部的状况,而是和隔离级别有关。在Serializable隔离级别下,全部的操做都会加锁。
4、其它:
1. 数据库事务ACID特性
数据库事务的4个特性:
原子性(Atomic): 事务中的多个操做,不可分割,要么都成功,要么都失败; All or Nothing.
一致性(Consistency): 事务操做以后, 数据库所处的状态和业务规则是一致的; 好比a,b帐户相互转帐以后,总金额不变;
隔离性(Isolation): 多个事务之间就像是串行执行同样,不相互影响;
持久性(Durability): 事务提交后被持久化到永久存储.
2. 隔离性
其中 隔离性 分为了四种:
READ UNCOMMITTED:能够读取未提交的数据,未提交的数据称为脏数据,因此又称脏读。此时:幻读,不可重复读和脏读均容许;
READ COMMITTED:只能读取已经提交的数据;此时:容许幻读和不可重复读,但不容许脏读,因此RC隔离级别要求解决脏读;
REPEATABLE READ:同一个事务中屡次执行同一个select,读取到的数据没有发生改变;此时:容许幻读,但不容许不可重复读和脏读,因此RR隔离级别要求解决不可重复读;
SERIALIZABLE: 幻读,不可重复读和脏读都不容许,因此serializable要求解决幻读;
3. 几个概念
脏读:能够读取未提交的数据。RC 要求解决脏读;
不可重复读:同一个事务中屡次执行同一个select, 读取到的数据发生了改变(被其它事务update而且提交);
可重复读:同一个事务中屡次执行同一个select, 读取到的数据没有发生改变(通常使用MVCC实现);RR各级级别要求达到可重复读的标准;
幻读:同一个事务中屡次执行同一个select, 读取到的数据行发生改变。也就是行数减小或者增长了(被其它事务delete/insert而且提交)。SERIALIZABLE要求解决幻读问题;
这里必定要区分 不可重复读 和 幻读:
不可重复读的重点是修改:
一样的条件的select, 你读取过的数据, 再次读取出来发现值不同了
幻读的重点在于新增或者删除:
一样的条件的select, 第1次和第2次读出来的记录数不同
从结果上来看, 二者都是为屡次读取的结果不一致。但若是你从实现的角度来看, 它们的区别就比较大:
对于前者, 在RC下只须要锁住知足条件的记录,就能够避免被其它事务修改,也就是 select for update, select in share mode; RR隔离下使用MVCC实现可重复读;
对于后者, 要锁住知足条件的记录及全部这些记录之间的gap,也就是须要 gap lock。
而ANSI SQL标准没有从隔离程度进行定义,而是定义了事务的隔离级别,同时定义了不一样事务隔离级别解决的三大并发问题:
Isolation Level |
Dirty Read |
Unrepeatable Read |
Phantom Read |
Read UNCOMMITTED |
YES |
YES |
YES |
READ COMMITTED |
NO |
YES |
YES |
READ REPEATABLE |
NO |
NO |
YES |
SERIALIZABLE |
NO |
NO |
NO |
4. 数据库的默认隔离级别
除了MySQL默认采用RR隔离级别以外,其它几大数据库都是采用RC隔离级别。
可是他们的实现也是极其不同的。Oracle仅仅实现了RC 和 SERIALIZABLE隔离级别。默认采用RC隔离级别,解决了脏读。可是容许不可重复读和幻读。其SERIALIZABLE则解决了脏读、不可重复读、幻读。
MySQL的实现:MySQL默认采用RR隔离级别,SQL标准是要求RR解决不可重复读的问题,可是由于MySQL采用了gap lock,因此实际上MySQL的RR隔离级别也解决了幻读的问题。那么MySQL的SERIALIZABLE是怎么回事呢?其实MySQL的SERIALIZABLE采用了经典的实现方式,对读和写都加锁。
5. MySQL 中RC和RR隔离级别的区别
MySQL数据库中默认隔离级别为RR,可是实际状况是使用RC 和 RR隔离级别的都很多。好像淘宝、网易都是使用的 RC 隔离级别。那么在MySQL中 RC 和 RR有什么区别呢?咱们该如何选择呢?为何MySQL将RR做为默认的隔离级别呢?
5.1 RC 与 RR 在锁方面的区别
1> 显然 RR 支持 gap lock(next-key lock),而RC则没有gap lock。由于MySQL的RR须要gap lock来解决幻读问题。而RC隔离级别则是容许存在不可重复读和幻读的。因此RC的并发通常要好于RR;
2> RC 隔离级别,经过 where 条件过滤以后,不符合条件的记录上的行锁,会释放掉(虽然这里破坏了“两阶段加锁原则”);可是RR隔离级别,即便不符合where条件的记录,也不会是否行锁和gap lock;因此从锁方面来看,RC的并发应该要好于RR;另外 insert into t select ... from s where 语句在s表上的锁也是不同的。
2563 2048 + 512 + 3