暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

insert 加锁分析

辣肉面加蛋加素鸡 2021-09-05
2537

前言一个 insert 出现幻读的假设三种加锁情况1. 先 select 后 insert2. 先 insert 后 select3. 先 insert 后 select - 特殊隐式锁Latchmini transaction 作用insert 里的 mini transactionmini transaction 总结总结

前言

上一篇举了一个并发 insert 产生死锁的例子,为了更好地理解死锁产生的原因,必须得先了解 insert 的上锁逻辑,先从一个固有的理念开始说起:

在之前一段时间的测试过程中,有一个固化的理念:insert 插入成功后会加 X 锁。但实际测试会发现以下现象:

mysql> desc insert_test;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
|
 id    | int(11)     | NO   | PRI | NULL    |       |
| name  | varchar(30| YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

mysql> select * from insert_test;
Empty set (0.00 sec)

mysql> begin;insert into insert_test values (1,'jack');
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

复制

此时的 innodb engine 日志:

# 这部分 RW-LATCH 后文会分析
-------------
RW-LATCH INFO
-------------
Total number of rw-locks 49283
OS WAIT ARRAY INFO: reservation count 2831
OS WAIT ARRAY INFO: signal count 2560
RW-shared spins 0, rounds 1317, OS waits 644
RW-excl spins 0, rounds 1356, OS waits 43
RW-sx spins 8, rounds 191, OS waits 5
Spin rounds per wait: 1317.00 RW-shared, 1356.00 RW-excl, 23.88 RW-sx

# 主要看事务状态,没有 X 锁
------------
TRANSACTIONS
------------
Trx id counter 403327
Purge done for trx's n:o < 403322 undo n:o < 0 state: running but idle
History list length 11
Total number of lock structs in row lock hash table 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 328558144322824not started
0 lock struct(s), heap size 11600 row lock(s)
---TRANSACTION 403326, ACTIVE 3 sec
1 lock struct(s), heap size 11600 row lock(s), undo log entries 1
MySQL thread id 111, OS thread handle 47083759875840, query id 1500532 localhost root

复制

(1,'jack') 这条记录已成功插入,但是日志里显示没有锁,其实这时候加的是隐式锁。

附之前画的加锁流程,之前分析都是基于这张图的:


一个 insert 出现幻读的假设

在分析什么是隐式锁前,我们先看个网上的问题:

加了插入意向锁后,插入数据之前,此时执行了 select…lock in share mode 语句(没有取到待插入的值),然后插入了数据,下一次再执行 select…lock in share mode(不会跟插入意向锁冲突),发现多了一条数据,于是又产生了幻读。会出现这种情况吗?

就语句执行的顺序,可能会出现3种不同的情况。

三种加锁情况

1. 先 select 后 insert

事务1:

mysql> select * from insert_test;
+----+------+
| id | name |
+----+------+
|  1 | jack |
|  5 | tom  |
+----+------+
2 rows in set (0.00 sec)

mysql> begin
;select * from insert_test where id=3 lock in share mode;
Query OK, 0 rows affected (0.00 sec)

Empty set (0.01 sec)

复制

事务2:

mysql> begin;insert into insert_test values (2,'peter');
Query OK, 0 rows affected (0.00 sec)

复制

事务日志:

------------
TRANSACTIONS
------------
Trx id counter 403333
Purge done for trx's n:o < 403332 undo n:o < 0 state: running but idle
History list length 13
Total number of lock structs in row lock hash table 2
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 328558144323920not started
0 lock struct(s), heap size 11600 row lock(s)
---TRANSACTION 403332, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 11601 row lock(s)
MySQL thread id 114, OS thread handle 47083760146176, query id 1500541 localhost root update
insert into insert_test values (2,'peter')
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 41 page no 3 n bits 72 index PRIMARY of table `gdb`.`insert_test` trx id 403332 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0

复制

顺序图:

这个顺序逻辑比较好理解, select...lock in share mode
 先执行,会在记录间隙上 GAP 锁,然后事务2 insert 时会请求插入意向锁,插入意向锁和 GAP 锁冲突,事务2会处于等待状态,不会出现幻读。

2. 先 insert 后 select

事务1:

mysql> begin;insert into insert_test values (3,'peter');
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

复制

事务2:

mysql> begin;select * from insert_test where id=3 lock in share mode;
Query OK, 0 rows affected (0.00 sec)

复制

顺序图:

先执行 insert
 ,成功插入记录之后,会对记录加 X 锁, 会阻塞事务2 的 select...lock in share mode
 请求 S 锁的操作,也不会出现幻读。

3. 先 insert 后 select - 特殊

它的特殊之处是,如果 select...lock in share mode
 的请求发生在 insert
 申请完插入意向锁之后,写数据之前,这时候 GAP 锁和插入意向锁是不冲突的,所以当 insert
 成功插入后,把事务1提交了,再次执行 select...lock in share mode
 ,如果查到 id=3 的数据,是不是意味着出现了幻读。

顺序图:

隐式锁

前言里说到,insert 其实加的是隐式锁,所以 show engine innodb status
 命令里不会展示任何的锁信息。

隐式锁之所以能组织其他事务加锁,靠的是隐式锁的转换。

一个事务1插入一条记录且未提交,此时事务2要对这条记录加锁,会先判断记录上保存的事务id是否活跃,如果是活跃的,就会去帮事务1建立一个锁对象,然后自身进入等待事务1的阶段,这个步骤就是隐式锁转化为显示锁的步骤。

看下 select * from insert_test where id=3 lock in share mode;
 语句重要的流程,核心代码如下:


先判断 trx_state_eq
 事务是否活跃,再根据 lock_rec_has_expl
 判断是否存在排他记录锁,如果事务活跃且没有锁 if (!trx_state_eq && !lock_rec_has_expl)
 ,就为该事务加上排他记录锁。

上文例子的流程如下:

  1. insert
     先判断没有没和插入意向锁冲突的锁,如果有(如GAP锁),加插入意向锁且处于锁等待状态;如果没有,直接写数据,不加任何锁;

  2. select * from insert_test where id=3 lock in share mode;
     语句执行后,先判断记录上有没有存在的事务,如果有,为 insert
     事务加一把排它记录锁,并将自己加入到锁等待队列 (函数语句 lock_rec_add_to_queue
    );

如果情况再极端点,检查是否有锁冲突与写入数据之间还是有时间差的。select * from insert_test where id=3 lock in share mode;
 语句会由于记录不存在,也不是活跃事务,从而不触发锁的隐式转换,那幻读的问题还是存在?

Latch

这里还有个概念 Latch 。Latch 也是一种锁,但和我们平时说的表锁和行锁还是有区别的。Latch 是一种轻量级的锁,锁定时间一般非常短,其主要目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制。


insert
 会在检查锁冲突和写数据之前,会对记录所在的页加一个 RW-X-LATCH 锁,执行完写数据之后再释放该锁。

这个锁的释放非常快,但是这个锁足以保证在插入数据的过程中其他事务无法访问记录所在的页。

这个加锁过程实际上是在 mini transaction 里完成的。

mini transaction 作用

mini transation 主要用于innodb redo log 和 undo log写入,保证两种日志的ACID特性。

mini transation 可以参考这篇博文,写的非常详细:https://xnerv.wang/what-innodb-mini-transation/

insert 里的 mini transaction

每个 mtr_t 对象变量名都叫 mtr,为了区分不同 mtr,给不同的对象加编号。第一个 mtr 从 row_ins_clust_index_entry_low 开始:

  • insert 语句:

    mtr_start(mtr_1) // mtr_1 贯穿整条insert语句
    row_ins_clust_index_entry_low

    mtr_s_lock(dict_index_get_lock(index), mtr_1) // 对index加s锁
    btr_cur_search_to_nth_level
    row_ins_clust_index_entry_low

    mtr_memo_push(mtr_1) // buffer RW_NO_LATCH 入栈
    buf_page_get_gen
    btr_cur_search_to_nth_level
    row_ins_clust_index_entry_low

    mtr_memo_push(mtr_1) // page RW_X_LATCH 入栈
    buf_page_get_gen
    btr_block_get_func
    btr_cur_latch_leaves
    btr_cur_search_to_nth_level
    row_ins_clust_index_entry_low

    mtr_start(mtr_2) // mtr_2 用于记录 undo log
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    mtr_start(mtr_3) // mtr_3 分配或复用一个 undo log
    trx_undo_assign_undo
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    mtr_memo_push(mtr_3) // 对复用(也可能是分配)的 undo log page加RW_X_LATCH 入栈
    buf_page_get_gen
    trx_undo_page_get
    trx_undo_reuse_cached // 这里先尝试复用,如果复用失败,则分配新的undolog
    trx_undo_assign_undo
    trx_undo_report_row_operation

     trx_undo_insert_header_reuse(mtr_3) // 写 undo log header
    trx_undo_reuse_cached
    trx_undo_assign_undo
    trx_undo_report_row_operation

    trx_undo_header_add_space_for_xid(mtr_3) // 在 undo header 中预留XID空间
    trx_undo_reuse_cached
    trx_undo_assign_undo
    trx_undo_report_row_operation

    mtr_commit(mtr_3) // 提交 mtr_3
    trx_undo_assign_undo
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    mtr_memo_push(mtr_2) // 即将写入的 undo log page 加 RW_X_LATCH 入栈
    buf_page_get_gen
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    trx_undo_page_report_insert(mtr_2) // undo log 记录 insert 操作
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    mtr_commit(mtr_2) // 提交 mtr_2
    trx_undo_report_row_operation
    btr_cur_ins_lock_and_undo
    btr_cur_optimistic_insert
    row_ins_clust_index_entry_low

    /*
    mtr_2 提交后开始执行 insert 操作
    page_cur_insert_rec_low 具体执行 insert 操作
    在该函数末尾调用 page_cur_insert_rec_write_log 写 redo log
    */


    page_cur_insert_rec_write_log(mtr_1) // insert 操作写 redo log
    page_cur_insert_rec_lowpage_cur_tuple_insert
    btr_cur_optimistic_insert

    mtr_commit(mtr_1) // 提交 mtr_1
    row_ins_clust_index_entry_low

    复制
  • 事务提交:

    mtr_start(mtr_4) // mtr_4 用于 prepare transaction
    trx_prepare
    trx_prepare_for_mysql
    innobase_xa_prepare
    ha_prepare_low
    MYSQL_BIN_LOG::prepare
    ha_commit_trans
    trans_commit_stmt
    mysql_execute_command

    mtr_memo_push(mtr_4) // undo page 加 RW_X_LATCH 入栈
    buf_page_get_gen
    trx_undo_page_get
    trx_undo_set_state_at_prepare
    trx_prepare

    mlog_write_ulint(seg_hdr + TRX_UNDO_STATE, undo->state, MLOG_2BYTES, mtr_4) 写入TRX_UNDO_STATE
    trx_undo_set_state_at_prepare
    trx_prepare

    mlog_write_ulint(undo_header + TRX_UNDO_XID_EXISTS, TRUE, MLOG_1BYTE, mtr_4) 写入 TRX_UNDO_XID_EXISTS
    trx_undo_set_state_at_prepare
    trx_prepare

    trx_undo_write_xid(undo_header, &undo->xid, mtr_4) undo 写入 xid
    trx_undo_set_state_at_prepare
    trx_prepare

    mtr_commit(mtr_4) // 提交 mtr_4
    trx_prepare

    mtr_start(mtr_5) // mtr_5 用于 commit transaction
    trx_commit
    trx_commit_for_mysql
    innobase_commit_low
    innobase_commit
    ha_commit_low
    MYSQL_BIN_LOG::process_commit_stage_queue
    MYSQL_BIN_LOG::ordered_commit
    MYSQL_BIN_LOG::commit
    ha_commit_trans
    trans_commit_stmt
    mysql_execute_command

    mtr_memo_push(mtr_5) // undo page 加 RW_X_LATCH 入栈
    buf_page_get_gen
    trx_undo_page_get
    trx_undo_set_state_at_finish
    trx_write_serialisation_history
    trx_commit_low
    trx_commit

    trx_undo_set_state_at_finish(mtr_5) // set undo state, 这里是 TRX_UNDO_CACHED
    trx_write_serialisation_history
    trx_commit_low
    trx_commit

    mtr_memo_push(mtr_5) // 系统表空间 transaction system header page 加 RW_X_LATCH 入栈
    buf_page_get_gen
    trx_sysf_get
    trx_sys_update_mysql_binlog_offset
    trx_write_serialisation_history
    trx_commit_low
    trx_commit

    trx_sys_update_mysql_binlog_offset // 更新偏移量信息到系统表空间
    trx_write_serialisation_history
    trx_commit_low
    trx_commit

    mtr_commit(mtr_5) // 提交 mtr_5
    trx_commit_low
    trx_commit

    复制

mini transaction 总结

每个 mini-transaction 会遵守下面的几个规则:

  • 修改一个页需要获得该页的 X-LATCH;

  • 访问一个页需要获得该页的 S-LATCH 或 X-LATCH;

  • 持有该页的 LATCH 直到修改或者访问该页的操作完成;

总结

至此总结一下,两个事物的执行逻辑如下:


  1. insert
     语句,对要操作的页加 RW-X-LATCH,然后判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁,结束后释放 RW-X-LATCH;

  2. select … lock in share mode
     语句,对要操作的页加 RW-S-LATCH,如果页面上存在 RW-X-LATCH 会被阻塞,没有的话则判断记录上是否存在活跃的事务,如果存在,则为 insert 事务创建一个排他记录锁,并将自己加入到锁等待队列,最后也会释放 RW-S-LATCH;


文章转载自辣肉面加蛋加素鸡,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论