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 自增列的约束

([email protected]) [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
([email protected]) [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
([email protected]) [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 自增的参数

([email protected]) [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:

([email protected]localhost) [test]> create table t_ai_l(a int auto_increment, b int, primary key(a));
Query OK, 0 rows affected (0.02 sec)

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> insert into t_ai_l values(NULL, 10);
Query OK, 1 row affected (0.00 sec)

事务不提交

session2:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> insert into t_ai_l values(NULL, 20);
Query OK, 1 row affected (0.00 sec)

咦?没等待耶,amazing!

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

session1&2:

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

session1:

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> insert into t_ai_l values(NULL, 30);
Query OK, 1 row affected (0.00 sec)

([email protected]) [test]> commit;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [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:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

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

session2:

([email protected]) [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:

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

session3:

([email protected]) [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 如何提升自增并发度

([email protected]) [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 实力分析一波

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

([email protected]localhost) [(none)]> set global innodb_status_output_locks=1;
Query OK, 0 rows affected (0.00 sec)
([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> delete from l where a = 2;
Query OK, 1 row affected (0.00 sec)

([email protected]) [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

([email protected]) [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:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> delete from l where a = 2;
Query OK, 1 row affected (0.00 sec)

session2:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]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上看下便知
    ([email protected]) [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
    ([email protected]) [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这条记录
([email protected]) [(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库弄进去

([email protected]) [(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是直接杀连接

Ⅲ、锁超时

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

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

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

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

([email protected]) [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 锁定一个范围,并且锁定记录本身

Ⅱ、模拟加锁场景

([email protected]) [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)

([email protected]) [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)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [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 对主键加锁

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where a <=2 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 2 |    4 |    6 |    8 |
+---+------+------+------+
1 row in set (0.01 sec)

([email protected]) [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的话就是只锁住记录本身,如下:

([email protected]) [(none)]> show variables like ‘tx_isolation‘;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)

([email protected]) [(none)]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where a <=2 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 2 |    4 |    6 |    8 |
+---+------+------+------+
1 row in set (0.00 sec)

([email protected]) [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事务隔离级别

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where b = 6 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 4 |    6 |    8 |   10 |
+---+------+------+------+
1 row in set (0.02 sec

([email protected]) [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隔离级别下的情况,如下:

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where b = 6 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 4 |    6 |    8 |   10 |
+---+------+------+------+
1 row in set (0.01 sec)

([email protected]) [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:

([email protected]) [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)

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.01 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [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:

([email protected]localhost) [(test)]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> insert into l values (3,4,14,20);

hang~~~

session1:

([email protected]) [(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事务隔离级别

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where b = 12 for update;
Empty set (0.00 sec)

([email protected]host) [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:

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where b = 7 for update;
Empty set (0.00 sec)

([email protected]) [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:

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [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事务隔离级别

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where b = 12 for update;
Empty set (0.00 sec)

([email protected]) [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事务隔离级别

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.01 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where d = 10 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 4 |    6 |    8 |   10 |
+---+------+------+------+
1 row in set (0.00 sec)

([email protected]) [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事务隔离级别

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> select * from l where d = 10 for update;
+---+------+------+------+
| a | b    | c    | d    |
+---+------+------+------+
| 4 |    6 |    8 |   10 |
+---+------+------+------+
1 row in set (0.00 sec)

([email protected]) [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:

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [test]> insert into l values (16,18,20,22);
Query OK, 1 row affected (0.00 sec)

([email protected]) [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:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> select * from l where a = 16 for update;
hang~~~  ???

session1:

([email protected]) [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:

([email protected]) [test]> show variables like ‘tx_isolation‘;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

([email protected]) [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)

([email protected]) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]) [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:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]localhost) [test]> insert into l values (14 ,16, 18, 20);
~~~

session3:

([email protected]) [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:

([email protected]) [test]> commit;
Query OK, 0 rows affected (0.01 sec)

([email protected]) [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:

([email protected]localhost) [test]> begin;
Query OK, 0 rows affected (0.00 sec)

([email protected]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是可以直接插的

原文地址:https://www.cnblogs.com/DataArt/p/10236653.html

时间: 2024-11-01 20:25:21

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

Innodb中的事务隔离级别和锁的关系

前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. #一次封锁or两段锁? 因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道

mysql中InnoDB存储引擎的行锁和表锁

Mysql的InnoDB存储引擎支持事务,默认是行锁.因为这个特性,所以数据库支持高并发,但是如果InnoDB更新数据的时候不是行锁,而是表锁的话,那么其并发性会大打折扣,而且也可能导致你的程序出错. 而导致行锁变为表锁的情况之一就是: SQL的更新(update)或者删除(delete)语句中未使用到索引,导致在InnoDB在对数据进行相应操作的时候必须把整个表锁起来进行检索(表锁).而如果使用了索引的话,InnoDB只会通过索引条件检索数据,而只锁住索引对应的行(行锁). 下面记录一下我遇到

Innodb中的事务隔离级别和锁的关系(转)

原文:http://tech.meituan.com/innodb-lock.html 前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. 一次封锁or两段锁? 因为有大量的并

MySQL InnoDB中的事务隔离级别和锁的关系

前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. 一.一次封锁or两段锁因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会

Innodb 中的事务隔离级别和锁的关系

转自:https://tech.meituan.com/innodb-lock.html 前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力.所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在.这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么. #一次封锁or两段锁?因为有大量的

MySQL Innodb 中的锁

MySQL Innodb 中的锁 锁是用来解决并发冲突的必要手段,MySQL 中的并发主要是指多个线程同时对同一个数据库进行操作,其中不同线程可能代表不同的事务,本质上也就是对共享资源的不同事务的同时访问. Innodb 支持行级锁和意向锁.行级锁就是对行记录进行加锁,行级锁也分为两种类型,一种是共享锁( S 锁),一种是排他锁( X 锁).意向锁为表级别的锁,也就是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,一般来说,意向锁是从上往下(数据库->表->页->记录

Innodb中的锁

虽然比较擅长的是Oracle,但是公司使用的是MySQL数据库,所以不得不对MySQL数据库多研究一下.今天就谈一下MySQL中的锁. 谈锁之前,要明白为什么会有锁这种东西.之所以有锁,大部分情况下是为了实现事务(transaction)之间的隔离,那么事务之间有几种隔离方式,各种隔离方式又是为了达到什么效果呢?先来说一下各种读现象. 脏读:即一个事务A读取了事务B还没有提交过的数据: 不可重复读:即事务A在第一次按照某条SQL语句读取后跟第二次按照相同的SQL语句读取数据库,这段时间内,事务B

Innodb中的行锁与表锁

在Innodb引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候或只锁住一行呢? InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的.InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁! 在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能. 行级锁都是基于索引的,如果一条SQL语句用不到索

详解 MySql InnoDB 中的三种行锁(记录锁、间隙锁与临键锁)

详解 MySql InnoDB 中的三种行锁(记录锁.间隙锁与临键锁) 前言 InnoDB 通过 MVCC 和 NEXT-KEY Locks,解决了在可重复读的事务隔离级别下出现幻读的问题.MVCC 我先挖个坑,日后再细讲,这篇文章我们主要来谈谈那些可爱的锁. 什么是幻读? 幻读是在可重复读的事务隔离级别下会出现的一种问题,简单来说,可重复读保证了当前事务不会读取到其他事务已提交的 UPDATE 操作.但同时,也会导致当前事务无法感知到来自其他事务中的 INSERT 或 DELETE 操作,这就