InnoDB中锁的模式

 

Ⅰ、总览

  • S行级共享锁
    lock in share mode
  • X行级排它锁
    增删改
  • IS意向共享锁
  • IX意向排他锁
  • AI自增锁

Ⅱ、锁之间的兼容性

X IX S IS
X × × × ×
IX × ×
S × ×
IS ×

2.1 意向锁

意向锁揭示了下一层级请求的锁类型,意向锁全兼容

  • IS:事务想要获得一张表中某几行的共享锁
  • IX:事务想要获得一张表中某几行的排它锁

InnoDB存储引擎中意向锁都是表锁,是不是读下来很懵逼?

如果没有意向锁,当你去锁一张表的时候,你就需要对表下的所有记录都进行加锁操作,且对其他事务刚刚插入的记录(游标已经扫过的范围)就没法在上面加锁了,此时就没有实现锁表的功能

对一棵树加锁的概念:
从上往下的,先加意向锁再加记录锁,内存操作,很快,释放操作则是从记录锁开始从下往上进行释放

假设数据库四个层级,库,表,页,记录

假如此时有事务tx1需要在记录A上进行加X锁:
1. 在该记录所在的数据库上加一把意向锁IX
2. 在该记录所在的表上加一把意向锁IX
3. 在该记录所在的页上加一把意向锁IX
4. 最后在该记录A上加上一把X锁
假如此时有事务tx2需要对记录B(假设和记录A在同一个页中)加S锁:
1. 在该记录所在的数据库上加一把意向锁IS
2. 在该记录所在的表上加一把意向锁IS
3. 在该记录所在的页上加一把意向锁IS
4. 最后在该记录B上加一把S锁
假如此时有事务tx3需要在记录A上进行加S锁:
1. 在该记录所在的数据库上加一把意向锁IS
2. 在该记录所在的表上加一把意向锁IS
3. 在该记录所在的页上加一把意向锁IS
4. 发现该记录被锁定(tx1的X锁),那么tx3需要等待,直到tx1进行commit

tips:

  • 共享锁和排它锁不是说只能加在记录级别上,是可以加在各个级别上的
    innodb表锁的获取:
    lock table l read; lock table l write; unlock tables; 这是server层的锁(mdl锁)
    从原理上讲innodb也是可以对表加X锁的,但是没有一个具体的命令来触发,也可以把lock table l read; 理解为加X锁

    通常来说不需要加表级别的锁,mysqldump都不加,ddl不支持online的时候就是先对一张表先加一个S锁,现在不一样了

  • 为什么意向锁都是互相兼容的?因为在当前级别上并没有加锁啊

但是在MySQL中没有数据库级别的锁和页级别的锁,这就意味着一共就两层,所有的意向锁都是表锁,意向锁是innodb层级的

tips:
MySQL8.0中所有的锁都在innodb层,现在的锁一部分在innodb层一部分在server层,server层的不好理解

Ⅱ、自增锁

  • 一个表一个自增列,自增锁做自增并发处理
  • auto_increment pk 代表这个列的自增有一把锁
  • 在事务提交前释放
    其他锁在事务提交时才释放
  • Think about
    insert ... select ...

tips:
MySQL的自增存在一个回溯的问题,5.7版本之前都是非持久化的,都是服务启动时候执行下面这个sql获取自增值,从下个位置开始继续自增,如果数据库重启了,之前的自增值可能被重复使用,8.0已解决,这个值会被写到元数据表(innodb引擎)中。

select max(auto_inc_col) from t for update;

2.1 自增列的约束

(root@localhost) [test]> create table t (a int auto_increment, b int) engine = innodb;
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key
(root@localhost) [test]> create table t (a int auto_increment, b int, key(b,a)) engine = innodb;
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key
(root@localhost) [test]> create table t (a int auto_increment, b int, key(a,b)) engine = innodb;
Query OK, 0 rows affected (0.04 sec)

InnoDB自增列必须被定义为一个key,且必须是这个key的开始部分

WHY?

select max(auto_inc_col) from t for update;

避免重启执行上面这句的时候扫全表 ,myisam是非聚集索引的,不是用这个方式来采集自增值的,8.0虽然持久化了,但还是有这个限制

经测试,myisam自增列也需要被定义为一个key,但是不需要是key的开始部分

2.2 自增的参数

(root@localhost) [test]> show variables like 'auto_increment%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| auto_increment_increment | 1 | -- 步长
| auto_increment_offset | 1 | --初始值
+--------------------------+-------+
2 rows in set (0.01 sec)

多节点全局唯一
N台服务器:A:[offset = 1, increment=N] , B:[offset = 2, increment=N] , C:[offset = 3, increment=N]...N:[offset = N, increment=N]

注意,这不能用来做多主,如果有额外的唯一索引就保证不了全局唯一了

2.3 自增锁分析

session1:

(root@localhost) [test]> create table t_ai_l(a int auto_increment, b int, primary key(a));
Query OK, 0 rows affected (0.02 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into t_ai_l values(NULL, 10);
Query OK, 1 row affected (0.00 sec) 事务不提交

session2:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into t_ai_l values(NULL, 20);
Query OK, 1 row affected (0.00 sec)

咦?没等待耶,amazing!

AI锁在事务提交前就释放了,类似latch,使用完就释放了

session1&2:

(root@localhost) [test]> rollback;
Query OK, 0 rows affected (0.02 sec)

session1:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into t_ai_l values(NULL, 30);
Query OK, 1 row affected (0.00 sec) (root@localhost) [test]> commit;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from t_ai_l;
+---+------+
| a | b |
+---+------+
| 3 | 30 |
+---+------+
1 row in set (0.00 sec)

可以看到虽然rollback,但AI锁是提交过了的,自增值不会跟着回滚,这样自增值就不连续,但连续也没什么用

也就是说,仅仅是这条sql执行的这段时间里,其他session是不可以对这个表操作的,插入过程太长,对insert也会阻塞

执行这条sql的时候,自增是被锁住的,所以插进去之后都是连续的值

2.4 利用sleep()分析自增锁

session1:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into t_ai_l (a,b) select NULL, sleep(1) from tmp limit 10000;
~~~

session2:

(root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421958478908128, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31217775, ACTIVE 10 sec
mysql tables in use 2, locked 2
4 lock struct(s), heap size 1136, 11 row lock(s), undo log entries 10
MySQL thread id 2255, OS thread handle 140482757068544, query id 3006342 localhost root User sleep
insert into t_ai_l (a,b) select NULL, sleep(1) from tmp limit 10000
TABLE LOCK table `test`.`tmp` trx id 31217775 lock mode IS
RECORD LOCKS space id 1408 page no 4 n bits 624 index PRIMARY of table `test`.`tmp` trx id 31217775 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000001cd15db; asc ;;
2: len 7; hex d4000001760110; asc v ;;
3: len 4; hex 80000001; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd15dc; asc ;;
2: len 7; hex d5000001300110; asc 0 ;;
3: len 4; hex 80000002; asc ;; ... TABLE LOCK table `test`.`t_ai_l` trx id 31217775 lock mode AUTO-INC
TABLE LOCK table `test`.`t_ai_l` trx id 31217775 lock mode IX
...

插入数据过程分析:

  • tmp表被加了IS锁,表中记录被加S锁,注意不会一次性所有记录加锁,是被查到的记录就被锁住,最终事务结束后释放所有锁
  • t_ai_l表上有两个锁AUTO-INC和IX

session2:

(root@localhost) [test]> insert into t_ai_l (a,b) select NULL, sleep(1) from tmp limit 10000;
~~~

session3:

(root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421958478909040, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31218060, ACTIVE 15 sec setting auto-inc lock
mysql tables in use 2, locked 2
LOCK WAIT 3 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 2255, OS thread handle 140482757068544, query id 3006385 localhost root Sending data
insert into t_ai_l (a,b) select NULL, b from tmp limit 10000
------- TRX HAS BEEN WAITING 15 SEC FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `test`.`t_ai_l` trx id 31218060 lock mode AUTO-INC waiting
------------------
TABLE LOCK table `test`.`tmp` trx id 31218060 lock mode IS
RECORD LOCKS space id 1408 page no 4 n bits 624 index PRIMARY of table `test`.`tmp` trx id 31218060 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000001cd15db; asc ;;
2: len 7; hex d4000001760110; asc v ;;
3: len 4; hex 80000001; asc ;; TABLE LOCK table `test`.`t_ai_l` trx id 31218060 lock mode AUTO-INC waiting
---TRANSACTION 31218051, ACTIVE 40 sec
mysql tables in use 2, locked 2
4 lock struct(s), heap size 1136, 40 row lock(s), undo log entries 39
MySQL thread id 2254, OS thread handle 140482756536064, query id 3006383 localhost root User sleep
insert into t_ai_l (a,b) select NULL, sleep(1) from tmp limit 10000
TABLE LOCK table `test`.`tmp` trx id 31218051 lock mode IS
RECORD LOCKS space id 1408 page no 4 n bits 624 index PRIMARY of table `test`.`tmp` trx id 31218051 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000001cd15db; asc ;;
2: len 7; hex d4000001760110; asc v ;;
3: len 4; hex 80000001; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd15dc; asc ;;
2: len 7; hex d5000001300110; asc 0 ;;
3: len 4; hex 80000002; asc ;;
...

insert into t_ai_l (a,b) select NULL, b from tmp limit 10000 在等待三个锁

  • t_ai_l表上的AUTO-INC锁
  • tmp表上的IS锁
  • tmp表中第一条记录上的S锁

这样设计的初衷是希望批量插入的自增值是连续的,但实际上是牺牲了并发度的

2.5 自增锁的分类

- 说明
insert-like 所有插入语句都属于此类
simple inserts 插入之前能确定插入多少行(insert into table_1 values(NULL, 1), (NULL, 2);)
bulk inserts 插入之前不确定插入多少行(insert into table_1 select * from t;)
mixed-mode inserts 插入内容部分自增部分确定(insert ... on duplicate key update不推荐)

2.6 如何提升自增并发度

(root@localhost) [test]> show variables like 'innodb_autoinc_lock_mode';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 1 |
+--------------------------+-------+
1 row in set (0.00 sec)

此参数可设置为[0|1|2]

  • 0 sql语句执行完释放AI锁,若数据量大sql执行完之前其他事务是无法插入的,保证了在此sql语句内插入的数据自增值是连续的
  • 1(default,大部分情况用1) 对于bulk inserts,和设置0一样

simple inserts则可以并发插入,在sql运行完之前确定自增值之后就可以释放AI锁了

                            +
bulk inserts | simple inserts
|
+-------------------------------------------------------+
|
acquire AI_Lock | acquire AI_Lock
|
insert ... select ... | ai = ai + M
|
ai = ai + N | release AI_Lock
|
release AI_Lock | insert ... select ...
+
bulk inserts不知道要插入多少行,所以只能等insert结束后,才知道N的值,然后一次性(ai + N)
simple inserts知道插入的行数(M),所以可以先(ai + M),然后将锁释放掉,给别的事务用,然后自己慢慢插入数据
  • 2 所有自增都可以并发(不同于Simple inserts的方式 ) 同一sql语句自增可能不连续

row-based binlog

for (i = ai; until_no_rec; i++) {
acquire AI_Lock # 插入前申请锁
insert one record... # 只插入一条记录
ai = ai + 1 # 自增值+1
release AI_Lock # 释放锁
}
并发度增加了,但性能不一定变好,尤其是单线程的时候,频繁申请和释放锁会导致开销大
虽然不连续,但插入进去至少是单调递增所以基本满足业务需求

tips:
这种情况严格意义上是不连续,但由于并发度不够再加上limit是预先批量申请分配这种不阻塞不是很好演示,所以看上去是连续的,其实不是,limit大一点应该是可以的,但等待时间太长了,也可以通过mysqlslap测测

InnoDB中锁的查看

 

Ⅰ、 show engine innodb status\G

1.1 实力分析一波

锁介绍的那篇中已经提到了这个命令,现在我们开一个参数,更细致的分析一下这个命令

(root@localhost) [(none)]> set global innodb_status_output_locks=1;
Query OK, 0 rows affected (0.00 sec)
(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> delete from l where a = 2;
Query OK, 1 row affected (0.00 sec) (root@localhost) [test]> update l set b = b + 1 where a = 4;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0 (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 30217412, ACTIVE 37 sec
2 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 355, OS thread handle 140483080300288, query id 1263 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 30217412 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 30217412 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 32
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd14c4; asc ;;
2: len 7; hex 2400000fc21499; asc $ ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001cd14c4; asc ;;
2: len 7; hex 2400000fc214c8; asc $ ;;
3: len 4; hex 80000007; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;;
...

解析:

  • table lock IX 意向排他锁(意向锁都是表锁)
  • record locks 记录锁
    -->space id 表空间
    -->page no 第几个页,所有的记录开始写都是从表的第四个页开始写,第四个页也是聚集索引的root page
    -->index PRIMARY 表示在主键上加了一把锁
    -->lock_mode 锁的模式
    -->locks rec but not gap 这个先不看
    -->heap no 2 PHYSICAL RECORD: n_fields 6 锁住记录的heap no为2的物理记录,这个记录一共6个列
    -->compact format 这条记录的存储格式是compact(dynamic也是compact)
    -->info bits 0表示这条记录没有被删除;非0表示被修改或者被删除(32)

Q? 表中是四个列,为什么这把是6个列?

  • 如果没有主键的话,会多一个隐藏列row_id,这里有主键row_id就是主键那不谈
  • 6个字节的表示事务id,7个字节表示回滚指针,这两个列就是隐藏列

1.2、趁热打铁,分析一下等待的情况

session1:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> delete from l where a = 2;
Query OK, 1 row affected (0.00 sec)

session2:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a=2 for update;
hang住了

session3:

...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421958478909040, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 30217455, ACTIVE 1741 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 396, OS thread handle 140483215816448, query id 2340 localhost root statistics
select * from l where a=2 for update
------- TRX HAS BEEN WAITING 27 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 30217455 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 32
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd14ee; asc ;;
2: len 7; hex 230000013d27d5; asc # =' ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; ------------------
TABLE LOCK table `test`.`l` trx id 30217455 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 30217455 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 32
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd14ee; asc ;;
2: len 7; hex 230000013d27d5; asc # =' ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; ---TRANSACTION 30217454, ACTIVE 1821 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 355, OS thread handle 140483080300288, query id 2339 localhost root
TABLE LOCK table `test`.`l` trx id 30217454 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 30217454 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 32
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001cd14ee; asc ;;
2: len 7; hex 230000013d27d5; asc # =' ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;;
...
  • 找到LOCK WAIT
    LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) 两个锁结构,一个记录锁
  • 找到TRX HAS BEEN WAITING 27 SEC FOR THIS LOCK TO BE GRANTED
    等的是主键是2的这条记录上的锁,锁的类型是排他锁
  • 再往下看,找到hold住2这条记录的事务,根据thread id 355可以找到对应的线程
    这个355就是show processlist;对应的id,我们去session1上看下便知
    (root@localhost) [test]> show processlist;
+-----+------+-----------+------+---------+------+----------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+------+-----------+------+---------+------+----------+------------------+
| 355 | root | localhost | test | Query | 0 | starting | show processlist |
| 396 | root | localhost | test | Sleep | 1321 | | NULL |
+-----+------+-----------+------+---------+------+----------+------------------+
2 rows in set (0.00 sec) 注意再thread_id表中就不一样了,是对应proceelist_id
(root@localhost) [test]> select thread_id,processlist_id,thread_os_id from performance_schema.threads where processlist_id is not NULL;
+-----------+----------------+--------------+
| thread_id | processlist_id | thread_os_id |
+-----------+----------------+--------------+
| 27 | 1 | 10574 |
| 381 | 355 | 18745 |
| 422 | 396 | 10592 |
+-----------+----------------+--------------+
3 rows in set (0.00 sec)
分别表示内部线程号(自增的),对应show processlist里的id,进程号

Ⅱ、简单点,上面是不是太专业了

2.1 利用三张表写一个sql脚本

重复之前的步骤,一边开一个事务删除2这条记录不提交,另一边用for update查2这条记录
(root@localhost) [(none)]> SELECT
-> r.trx_id waiting_trx_id,
-> r.trx_mysql_thread_id waiting_thread,
-> r.trx_query wating_query,
-> b.trx_id blocking_trx_id,
-> b.trx_mysql_thread_id blocking_thread,
-> b.trx_query blocking_query
-> FROM
-> information_schema.innodb_lock_waits w
-> INNER JOIN
-> information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
-> INNER JOIN
-> information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
+----------------+----------------+--------------------------------------+-----------------+-----------------+----------------+
| waiting_trx_id | waiting_thread | wating_query | blocking_trx_id | blocking_thread | blocking_query |
+----------------+----------------+--------------------------------------+-----------------+-----------------+----------------+
| 30217455 | 396 | select * from l where a=2 for update | 30217454 | 355 | NULL |
+----------------+----------------+--------------------------------------+-----------------+-----------------+----------------+
1 row in set, 1 warning (0.02 sec)

2.2 走sys库看一把,更简单

5.7才有sys库,不过5.6也可以自行把sys库弄进去

(root@localhost) [(none)]> select * from sys.innodb_lock_waits\G
*************************** 1. row ***************************
wait_started: 2018-06-03 00:52:01
wait_age: 00:00:14
wait_age_secs: 14
locked_table: `test`.`l`
locked_index: PRIMARY
locked_type: RECORD
waiting_trx_id: 30217455
waiting_trx_started: 2018-06-03 00:11:13
waiting_trx_age: 00:41:02
waiting_trx_rows_locked: 5
waiting_trx_rows_modified: 0
waiting_pid: 396
waiting_query: select * from l where a=2 for update
waiting_lock_id: 30217455:1358:3:2
waiting_lock_mode: X
blocking_trx_id: 30217454
blocking_pid: 355
blocking_query: NULL
blocking_lock_id: 30217454:1358:3:2
blocking_lock_mode: X
blocking_trx_started: 2018-06-03 00:09:53
blocking_trx_age: 00:42:22
blocking_trx_rows_locked: 1
blocking_trx_rows_modified: 1
sql_kill_blocking_query: KILL QUERY 355
sql_kill_blocking_connection: KILL 355
1 row in set, 3 warnings (0.09 sec)

tips:

  • waiting_lock_id: 30217455:1358:3:2 这个东西表示 事务ID:space:page_No:heap_no,其他得比较简单不用说了
  • blocking_query是null,waiting_query是知道的,为什么?
    因为blocking的语句已经执行结束了,只是事务没提交罢了
    线上大部分时间是看不到这个blocking_query的
    即使show engine innodb status\G也是只能看到在等待哪条记录上的锁释放,而看不到是哪条sql导致的这个问题
  • 最下面的KILL QUERY和KILL的区别是?
    KILL QUERY是杀这个查询,KILL是直接杀连接

Ⅲ、锁超时

刚才模拟锁等待过程中出现了下面得报错

(root@localhost) [test]> select * from l where a=2 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

这叫锁等待超时,开发人员通常把这个和死锁混为一谈

lock持有的时间是以事务为单位的,事务提交后才会把事务里所有的锁释放,这是无法避免的,不过可以通过一个参数来控制超时时间

(root@localhost) [test]> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set (0.00 sec)

默认50s,建议设置为3s左右即可

Ⅲ、强行分析heap no

innodb中页里面的记录是逻辑有序的

一个页中,第一条插入的记录heap no是2,后面插入的heap no递增,这样在堆中就是有序的了,但是记录之间又是逻辑有序的,通过指针连接

heap no表示插入时的顺序,用来表示一个page中的record是什么时候插入的,所以加锁的定位是space->page_no->heap_no

一个page中,一条记录都没有,innodb默认会生成两条虚拟伪记录,min和max,min的heap_no是0,max的heap_no是1,所以用户插入的记录heap_no都是从2开始

max上是可以加锁的,min上面通常不加锁

Ⅳ、InnoDB中锁的管理

  • 每个事务每个page(不是每条记录)有一个锁的对象,通过位图(lock bitmap )的方式来管理,位图是基于每个page的
    page里面哪条record加锁了,就会把这条record的heap_no设置为1,heap_no就表示一个位图,表示第几位,所以innodb的锁是占用内存的,但是不是一个锁一个锁来管理锁的存储的(mysql上一个page的锁差不多30个字节就够了,网上都说的是100)
  • 没有锁升级(like oracle)
    sqlserver有锁升级,sqlserver是每个锁一个锁对象,innodb是每个page一个锁对象,所以锁的空间占用上,oracle<mysql<sqlserver

补充sqlserver和innodb全表更新对比
sqlserver每个记录一个锁对象

如果占用10字节,300w个page,每个page100条记录

InnoDB:300M(300w*100/1000/1000)

sqlserver:3G(300w10010/1000/1000)

tips:

  • sqlserver锁升级
    一个事务持有5000(默认)行锁升级到表锁,锁升级也不是一点都不好,毕竟内存变小了
 
 
 
 
 

InnoDB中锁的算法(1)

 

Ⅰ、InnoDB锁算法的介绍

首先明确一点,锁锁住的是什么?锁锁住的是索引

  • Record Lock
    单个行记录上的锁
  • Gap Lock
    锁定一个范围,但不包含记录本身
  • Next-key Lock
    Gap Lock + Record Lock 锁定一个范围,并且锁定记录本身

Ⅱ、模拟加锁场景

(root@localhost) [test]> desc l;
+-------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| a | int(11) | NO | PRI | NULL | |
| b | int(11) | YES | MUL | NULL | |
| c | int(11) | YES | UNI | NULL | |
| d | int(11) | YES | | NULL | |
+-------+---------+------+-----+---------+-------+
4 rows in set (0.00 sec) (root@localhost) [test]> select * from l;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 2 | 4 | 6 | 8 |
| 4 | 6 | 8 | 10 |
| 6 | 8 | 10 | 12 |
| 8 | 10 | 12 | 14 |
+---+------+------+------+
4 rows in set (0.02 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a = 2 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 2 | 4 | 6 | 8 |
+---+------+------+------+
1 row in set (0.03 sec) 对主键为2的这条记录加锁,这里可以表示三个意思
①record lock:对2加X锁 ②gap lock:对(负无穷,2)加X锁
thd1:hold 2 x gap
thd2:hold 2 x record
上面两个是兼容的,也就是说,thd2直接操作2这条记录是可以操作的,不需要等待
thd3:insert 1,这个线程就要wait,因为1在这个范围内 ③next-key lock 锁住(负无穷,2] oralce中只有record lock,没有别的意思

一般来说,此处我们根据不同事务隔离级别来分析这个加锁情况如下:

  • rc
    所有某条记录的加锁都是record锁,所有insert不用等待,并发度更好
    --->lock_mode X locks rec but not gap
  • rr
    所有对某条记录加锁都用的next-key locking,insert 并行性能或许有点差
    --->lock_mode X

特殊情况:
会把加锁模式优化为record lock,前提是锁住的那个index是unique的,并且只返回(锁住)一条记录

(a,b)复合索引,查a=? 用的还是next-key locking,查a=?,b=?就会用record lock

Ⅲ、正儿八经的分析几个场景看看

3.1 对主键加锁

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a <=2 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 2 | 4 | 6 | 8 |
+---+------+------+------+
1 row in set (0.01 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 31220336, ACTIVE 16 sec
2 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 416, OS thread handle 139830453040896, query id 5627 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220336 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220336 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001c1b939; asc 9;;
2: len 7; hex e0000001a80110; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001c1b93a; asc :;;
2: len 7; hex e1000001a90110; asc ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;;
...

按道理我们锁住的应该是(负无穷,2],但实际上锁住的范围已经到了4这条记录,此时插入3是插不进去的,为什么?

为了保证解决幻读,要把2到它后面这条记录4这段范围锁住,这时候如果新插入一个2,在原来的2后面是插不进来的,如果4不锁住,新开一个线程可以删除4,又可以新插入一个4

rc的话就是只锁住记录本身,如下:

(root@localhost) [(none)]> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec) (root@localhost) [(none)]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a <=2 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 2 | 4 | 6 | 8 |
+---+------+------+------+
1 row in set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 31220337, ACTIVE 6 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 443, OS thread handle 139830452774656, query id 5649 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220337 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220337 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001c1b939; asc 9;;
2: len 7; hex e0000001a80110; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;;
...

唯一索引和主键情况一样

3.2 对二级索引加锁

先看rc事务隔离级别

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 6 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 4 | 6 | 8 | 10 |
+---+------+------+------+
1 row in set (0.02 sec (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 31220338, ACTIVE 35 sec
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 443, OS thread handle 139830452774656, query id 5653 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220338 lock mode IX
RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220338 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000004; asc ;; RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220338 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001c1b93a; asc :;;
2: len 7; hex e1000001a90110; asc ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;;
...

先对二级索引b加record锁:lock_mode X locks rec but not gap锁住了(6,4),6是二级索引,4是主键值

再对聚集索引加锁也是record locks,锁聚集索引index primary,锁住了a=4

再分析rr隔离级别下的情况,如下:

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 6 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 4 | 6 | 8 | 10 |
+---+------+------+------+
1 row in set (0.01 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 31220340, ACTIVE 5 sec
4 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 444, OS thread handle 139830446065408, query id 5673 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220340 lock mode IX
RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220340 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000004; asc ;; RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220340 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001c1b93a; asc :;;
2: len 7; hex e1000001a90110; asc ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;; RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220340 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 4; hex 80000006; asc ;;
...

这个就稍微有点复杂了,依稀可以看到是加了三个锁,我们挨个分析一波

  • 第一个锁锁住索引b(4,6],next-key lock锁 lock_mode X
  • 第二个锁是对主键a=4这条唯一记录的主键上加一个记录锁(因为唯一),lock_mode X locks rec but not gap
  • 第三个锁是gap before rec 锁住了b(6,8),也就是对8加了gap

为什么要锁住(6,8)?

假设不锁住这块,一个线程插入了(3,6),只锁住(4,6]那就可以插入了,那原来的线程第一次返回的只有一条b=6的记录,那第二次执行就出现了两条b=6,就幻读了

tips:
新插入的6是在(6,8)这个范围里的,新插入的相同的记录,都在已存在的记录后面 4 6 6(新插) 8

 
 
 

InnoDB中锁的算法(2)

 

Ⅰ、上节回顾

session1:

(root@localhost) [test]> select * from l;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 2 | 4 | 6 | 8 |
| 4 | 6 | 8 | 10 |
| 6 | 8 | 10 | 12 |
| 8 | 10 | 12 | 14 |
+---+------+------+------+
4 rows in set (0.00 sec) (root@localhost) [test]> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 6 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 4 | 6 | 8 | 10 |
+---+------+------+------+
1 row in set (0.09 sec) pk 2 4 6 8
key 4 6 8 10
二级索引锁住的是(4,6]&&(6,8)
主键锁住的是4

session2:

(root@localhost) [(test)]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into l values (3,4,14,20); hang~~~

session1:

(root@localhost) [(none)]> show engine innodb status\G
...
MySQL thread id 1087, OS thread handle 139830446065408, query id 7300 localhost root update
insert into l values (3,4,14,20)
------- TRX HAS BEEN WAITING 19 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220594 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000004; asc ;;
...
4在被锁住的范围之内,所以插不进去

那(1,4,14,20)能插入吗?先公布答案,是可以插入的

为什么?
这里有一个隐晦的问题

二级索引排序的时候包含主键值排序,锁的时候范围也是包含主键值,如下:

((4,2),(6,4)],((6,4),(8,6))

(4,3)在这个范围内,所以不能插,而(4,1)不在这范围内,可以插

insert (5,4,14,20)也会阻塞,(4,5)在范围中

Ⅱ、走索引查询的记录不存在

2.1 rr事务隔离级别

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 12 for update;
Empty set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
---TRANSACTION 31220600, ACTIVE 7 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1104, OS thread handle 139830452774656, query id 7383 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220600 lock mode IX
RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220600 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
...
heap no 1 0和1的heap no表示的是min和max的记录,虚拟的
n_fields 1 只有一个列,伪列
key min 4 6 8 10 max

在max上加了锁,范围为(10,正无穷),意味着插入任意大于10的记录都是不可以的

rr级别时,如果搜索一条记录搜不到,就会在max上加锁,意味着这条没查到的记录之后的范围都插不了

为什么呢?
如果第一次读12没有读到不把10往后的都锁住,那一个线程插入了12这条记录,第二次再去读就会读到12,就发生了幻读

再来
session1:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 7 for update;
Empty set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
···
---TRANSACTION 31220601, ACTIVE 51 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1104, OS thread handle 139830452774656, query id 7387 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220601 lock mode IX
RECORD LOCKS space id 1358 page no 5 n bits 72 index b of table `test`.`l` trx id 31220601 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 4; hex 80000006; asc ;;
···
在8上面加了一个gap锁,8本身是不锁的

session2:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 8 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 6 | 8 | 10 | 12 |
+---+------+------+------+
1 row in set (0.00 sec)
这时候8这条记录上又有了Next-key Lock锁,锁住6到8,8本身也被锁住,8上面两把锁是不抵触的

2.2 rc事务隔离级别

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where b = 12 for update;
Empty set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
------------
TRANSACTIONS
------------
Trx id counter 31220604
Purge done for trx's n:o < 31220593 undo n:o < 0 state: running but idle
History list length 35
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421305875781456, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220603, ACTIVE 6 sec
1 lock struct(s), heap size 1136, 0 row lock(s)
MySQL thread id 1106, OS thread handle 139830446065408, query id 7436 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220603 lock mode IX
...

会发现并没有锁,读到没有12这条记录,直接就释放了,rc不解决幻读,这么看rc的insert并发性能会更好

Ⅲ、不走索引的情况怎么锁?

3.1 rc事务隔离级别

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.01 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where d = 10 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 4 | 6 | 8 | 10 |
+---+------+------+------+
1 row in set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421305875781456, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220604, ACTIVE 11 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1106, OS thread handle 139830446065408, query id 7446 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220604 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220604 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001c1b93a; asc :;;
2: len 7; hex e1000001a90110; asc ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;;
...

会产生一个记录锁,对d=10这条记录对应的主键加锁

3.2 rr事务隔离级别

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where d = 10 for update;
+---+------+------+------+
| a | b | c | d |
+---+------+------+------+
| 4 | 6 | 8 | 10 |
+---+------+------+------+
1 row in set (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421305875781456, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220606, ACTIVE 22 sec
2 lock struct(s), heap size 1136, 5 row lock(s)
MySQL thread id 1106, OS thread handle 139830446065408, query id 7459 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220606 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 72 index PRIMARY of table `test`.`l` trx id 31220606 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001c1b939; asc 9;;
2: len 7; hex e0000001a80110; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000001c1b93a; asc :;;
2: len 7; hex e1000001a90110; asc ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000008; asc ;;
5: len 4; hex 8000000a; asc ;; Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 6; hex 000001c1b93f; asc ?;;
2: len 7; hex e40000015d0110; asc ] ;;
3: len 4; hex 80000008; asc ;;
4: len 4; hex 8000000a; asc ;;
5: len 4; hex 8000000c; asc ;; Record lock, heap no 5 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000008; asc ;;
1: len 6; hex 000001c1b940; asc @;;
2: len 7; hex e50000015f0110; asc _ ;;
3: len 4; hex 8000000a; asc ;;
4: len 4; hex 8000000c; asc ;;
5: len 4; hex 8000000e; asc ;;
...

Next-key Lock锁住了主键的2,4,6,8

(负无穷,2],(2,4],(4,6],(6,8],(8,正无穷)

这并不是表锁(没有表升级),只是表现形式类似整个锁住,如果表有100w记录,会产生100w个lock,锁模式是Next-key Locking,任何记录的插入和更新都是不可以的,锁的代价很大

因为后面插入任何一条记录都可以是d=10,所以任何一条记录都要被锁住,随便插一个d=10的记录都给插那就乱套了,又幻读了,那就不允许任何记录插入吧

tips:
rr本身不解决幻读,只有在序列化的事务隔离级别之下才解决幻读,但是MySQL的锁比较特殊,在rr隔离级别下就解决幻读

 
 
 
 

InnoDB中锁的算法(3)

 

Ⅰ、隐式锁vs显示锁

session1:

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into l values (16,18,20,22);
Query OK, 1 row affected (0.00 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875781456, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220665, ACTIVE 24 sec
1 lock struct(s), heap size 1136, 0 row lock(s), undo log entries 1
MySQL thread id 1185, OS thread handle 139830020065024, query id 7781 localhost root starting
show engine innodb status
TABLE LOCK table `test`.`l` trx id 31220665 lock mode IX
...

会发现插入的这条记录上没有锁,只能看到一把意向锁

session2:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a = 16 for update;
hang~~~ ???

session1:

(root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220670, ACTIVE 18 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1184, OS thread handle 139830453040896, query id 7783 localhost root statistics
select * from l where a = 16 for update
------- TRX HAS BEEN WAITING 18 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220670 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000010; asc ;;
1: len 6; hex 000001dc63b9; asc c ;;
2: len 7; hex b4000001a10110; asc ;;
3: len 4; hex 80000012; asc ;;
4: len 4; hex 80000014; asc ;;
5: len 4; hex 80000016; asc ;; ------------------
TABLE LOCK table `test`.`l` trx id 31220670 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220670 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000010; asc ;;
1: len 6; hex 000001dc63b9; asc c ;;
2: len 7; hex b4000001a10110; asc ;;
3: len 4; hex 80000012; asc ;;
4: len 4; hex 80000014; asc ;;
5: len 4; hex 80000016; asc ;; ---TRANSACTION 31220665, ACTIVE 252 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1185, OS thread handle 139830020065024, query id 7781 localhost root
TABLE LOCK table `test`.`l` trx id 31220665 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220665 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000010; asc ;;
1: len 6; hex 000001dc63b9; asc c ;;
2: len 7; hex b4000001a10110; asc ;;
3: len 4; hex 80000012; asc ;;
4: len 4; hex 80000014; asc ;;
5: len 4; hex 80000016; asc ;;
...

这里可以发现这条记录上的锁又出来了,为什么?

原因:innodb做了优化,这个锁叫隐式锁,这条记录不需要加锁就知道上面有锁,因为这条记录对应的事务还在事务活跃列表中

  • 显式锁(explicit-lock)
    select * from t where rowd = xxx for update;
  • 隐式锁(implicit-lock)
    不创建锁对象若没有锁冲突,发生等待则转化为显示锁,这样锁的开销就进一步下降了,几乎很少

小结:
insert操作一开始是隐式锁,不创建锁对象,发生等待的时候才转化为显式锁,查到a=16这条记录在活跃事务列表中,就是没提交,说明上面有锁,这时候创建锁对象,即延迟创建锁对象,如果在延迟过程中,没有对这条记录加锁,就不用创建锁对象,这样就节省内存了

Ⅱ、插入意向锁

session1:

(root@localhost) [test]> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec) (root@localhost) [test]> select * from l;
+----+------+------+------+
| a | b | c | d |
+----+------+------+------+
| 2 | 4 | 6 | 8 |
| 4 | 6 | 8 | 10 |
| 6 | 8 | 10 | 12 |
| 8 | 10 | 12 | 14 |
| 10 | 12 | 14 | 16 |
| 20 | 22 | 24 | 26 |
+----+------+------+------+
6 rows in set (0.00 sec) (root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> select * from l where a < 20 for update;
+----+------+------+------+
| a | b | c | d |
+----+------+------+------+
| 2 | 4 | 6 | 8 |
| 4 | 6 | 8 | 10 |
| 6 | 8 | 10 | 12 |
| 8 | 10 | 12 | 14 |
| 10 | 12 | 14 | 16 |
+----+------+------+------+
5 rows in set (0.00 sec)

session2:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into l values (14 ,16, 18, 20);
~~~

session3:

(root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220676, ACTIVE 27 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1184, OS thread handle 139830453040896, query id 7811 localhost root update
insert into l values (14 ,16, 18, 20)
------- TRX HAS BEEN WAITING 27 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220676 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 6; hex 000001dc63c1; asc c ;;
2: len 7; hex ba000001970110; asc ;;
3: len 4; hex 80000016; asc ;;
4: len 4; hex 80000018; asc ;;
5: len 4; hex 8000001a; asc ;; ------------------
TABLE LOCK table `test`.`l` trx id 31220676 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220676 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 6; hex 000001dc63c1; asc c ;;
2: len 7; hex ba000001970110; asc ;;
3: len 4; hex 80000016; asc ;;
4: len 4; hex 80000018; asc ;;
5: len 4; hex 8000001a; asc ;; ---TRANSACTION 31220675, ACTIVE 75 sec
2 lock struct(s), heap size 1136, 6 row lock(s)
MySQL thread id 1185, OS thread handle 139830020065024, query id 7809 localhost root
TABLE LOCK table `test`.`l` trx id 31220675 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220675 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 000001c1b939; asc 9;;
2: len 7; hex e0000001a80110; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;; 篇幅原因省略下面不相关记录锁
...

这时候能看到插入意向锁了

gap before rec insert intention waiting

session1:

(root@localhost) [test]> commit;
Query OK, 0 rows affected (0.01 sec) (root@localhost) [test]> show engine innodb status\G
...
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421305875783280, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421305875782368, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 31220677, ACTIVE 17 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 1184, OS thread handle 139830453040896, query id 7815 localhost root
TABLE LOCK table `test`.`l` trx id 31220677 lock mode IX
RECORD LOCKS space id 1358 page no 3 n bits 80 index PRIMARY of table `test`.`l` trx id 31220677 lock_mode X locks gap before rec insert intention
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000014; asc ;;
1: len 6; hex 000001dc63c1; asc c ;;
2: len 7; hex ba000001970110; asc ;;
3: len 4; hex 80000016; asc ;;
4: len 4; hex 80000018; asc ;;
5: len 4; hex 8000001a; asc ;;
...

可以看到,对20这条记录加了一个gap锁,但是是insert intention的

睁大眼睛啊
session1:

(root@localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec) (root@localhost) [test]> insert into l values (15, 17, 19, 20);
Query OK, 1 row affected (0.00 sec)

哦嚯,插入成功,美鸡鸡

Ⅲ、捋一下为什么可以插?

a列:2 4 6 8 10 20
step1:
20这条记录上有一个X锁,next-key-locking,锁住xxx...(10,20]这几个范围

step2:
插入14这条记录,会对20这条记录加一个gap锁,即(14,20),但是这个gap锁有个insert intention的属性

step3:
第一个事务commit,事务2持有了上面这把(14,20)的insert intention的gap锁
这时候插15是能插入的,就因为insert intention

gap锁是用来阻塞的,之前的理解(14,20)之间是不能插入15的,但是有了上面说的这个特性,就表示插入非阻塞,即允许插入,意义在于提升了插入性能

如果没有insert intention,那插入14时(14,20)上面就是加一个gap锁,事务1提交则事务2获取这个gap锁,插入15,是插不了的,性能下降了

tips:
插入14地时候为什么这里会阻塞呢?因为14要在20上加一个gap锁,为什么要加gap锁来判断到底能不能插,一条记录能不能插就看它后面这条记录上有没有锁,这个锁是不是gap的,如果是那就不能插,只是一个record锁那就能插,而这个例子20这条记录上本身是有gap的所以就等待了

总结:

  • insert intention用来判断当前事务能否插入,并不阻塞后面其他线程在这个范围的插入操作,提升了并发插入的性能
  • gap insert intention互相之间本身是兼容的
  • insert在等待的时候(被阻塞)才会加gap insert intention锁,不等待是没任何锁的
  • rc没有next-key-lock锁,没有上面的情况,锁住20表示只锁住记录本身,没有锁住一个范围,14是可以直接插的

InnoDB中锁的模式,锁的查看,算法的更多相关文章

  1. C#中的几种锁:用户模式锁、内核模式锁、动态计数、监视锁

    参考网址: https://blog.csdn.net/weixin_43989331/article/details/105356008 C#中的几种锁:用户模式锁.内核模式锁.动态计数.监视锁介绍 ...

  2. SQLServer中的事务与锁

    事务:保持逻辑数据一致性与可恢复性,必不可少的利器. 锁:多用户访问同一数据库资源时,对访问的先后次序权限管理的一种机制,没有他事务或许将会一塌糊涂,不能保证数据的安全正确读写. 死锁:是数据库性能的 ...

  3. MySQL的中的全局锁、表级锁、行锁

    MySQL的中的全局锁.表级锁.行锁 学习极客时间-林晓彬老师-MySQL实战45讲 学习整理 全局锁 对整个数据库实例加锁.通过使用Flush tables with read lock (FTWR ...

  4. Innodb中怎么查看锁信息

    一.前言 上一篇说了下innodb中锁的大概意思, 这篇说说怎么查看加的哪些锁.不然后续出现死锁或者锁等待都不知道为什么. 二.底层基础表信息 在学会如何查看有哪些锁信息时, 需要了解一些基础表信息, ...

  5. InnoDB中锁的查看

    Ⅰ. show engine innodb status\G 1.1 实力分析一波 锁介绍的那篇中已经提到了这个命令,现在我们开一个参数,更细致的分析一下这个命令 (root@localhost) [ ...

  6. MySQL/InnoDB中,对于锁的认识

    MySQL/InnoDB的加锁,一直是一个面试中常问的话题.例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?我在工作过程中,也会经常用到,乐观锁,排它锁,等.于是今天就对 ...

  7. InnoDB的锁机制浅析(二)—探索InnoDB中的锁(Record锁/Gap锁/Next-key锁/插入意向锁)

    Record锁/Gap锁/Next-key锁/插入意向锁 文章总共分为五个部分: InnoDB的锁机制浅析(一)-基本概念/兼容矩阵 InnoDB的锁机制浅析(二)-探索InnoDB中的锁(Recor ...

  8. MySql InnoDB中的锁研究

    # MySql InnoDB中的锁研究 ## 1.InnoDB中有哪些锁### 1. 共享和排他(独占)锁(Shared and Exclusive Locks) InnoDB实现标准的行级锁定,其中 ...

  9. MySQL/InnoDB中,乐观锁、悲观锁、共享锁、排它锁、行锁、表锁、死锁概念的理解

    文章出处:https://www.souyunku.com/2018/07/30/mysql/?utm_source=tuicool&utm_medium=referral MySQL/Inn ...

随机推荐

  1. shell之实战应用一(查找xml文档中的关键字段)

      前几天同事问我一个问题,说如下的文档中,如何把name后面的字段(红色框中的字段)单独打印出来?

  2. Xamarin.Android 关于so包报错问题

    问题描述:使用so包时报错. 解决方法: 1.保证 libs > armeabi 和 armeabi-v7a 中的so包一致. 2.去掉 x86,x86_64,arm64-v8a. 3. so的 ...

  3. 深入理解v-model

    原文链接:http://www.geeee.top/2019/04/03/vue-v-model/ 转载请注明出处 v-model v-model 是vue的一个语法糖,用于在表单控件或者在组件上创建 ...

  4. Java并发编程笔记之LinkedBlockingQueue源码探究

    JDK 中基于链表的阻塞队列 LinkedBlockingQueue 原理剖析,LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线 ...

  5. Java并发编程笔记之CopyOnWriteArrayList源码分析

    并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...

  6. PostgreSQL查询优化逻辑优化之其他

    上一节我们介绍了PostgreSQL的子查询优化,子查询优化把一部分可以优化的子查询上拉到主查询成为join. preprocess_expression 将表达式(目标列,where,join,ha ...

  7. 深度学习论文翻译解析(一):YOLOv3: An Incremental Improvement

    论文标题: YOLOv3: An Incremental Improvement 论文作者: Joseph Redmon Ali Farhadi YOLO官网:YOLO: Real-Time Obje ...

  8. MVC 【ASPX视图引擎】

    新建项目----ASP.NET MVC 4 Web 应用程序------选择模板(空).视图引擎(ASPX) 1.认识控制器Controller using System; using System. ...

  9. Is the “*apply” family really not vectorized?

    Question: So we are used to say to every R new user that "apply isn't vectorized, check out the ...

  10. 纯css3实现的动画导航菜单

    测试咯 css3 前端特效代码 网页模板 图片素材 css3 前端特效代码 网页模板 图片素材 css3 前端特效代码 网页模板 图片素材 css3 前端特效代码 网页模板 图片素材 css3 前端特 ...