从一个案例深入剖析InnoDB隐式锁和可见性判断(4)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 从一个案例深入剖析InnoDB隐式锁和可见性判断

五、关于锁的判定

5.1 lock_sec_rec_read_check_and_lock函数

主要用于二级索引数据查找段阶段加显示锁,,对于update/delete而言,首先是需要找到需要修改的数据,加锁前需要判断本记录是否存在隐式锁,由于二级索引行数据不包含trx id,因此先用page的max trx id和当前活跃的最小读写事务进行比对判断,如果大于等于则可能存在显示锁,然后需要回表通过主键进行精细化判断。而精细化回表判断行是否存在隐式锁,那么代价就比较大了,因此这需要一个判断流程如下

lock_sec_rec_read_check_and_lock:
                                
 if ((page_get_max_trx_id(block->frame) >= trx_rw_min_trx_id()
      || recv_recovery_is_on())
     && !page_rec_is_supremum(rec)) {

  lock_rec_convert_impl_to_expl(block, rec, index, offsets);//如果符合前面的条件才调入 lock_rec_convert_impl_to_expl
 } 

如下调入:

->lock_rec_convert_impl_to_expl

->lock_sec_rec_some_has_impl
->row_vers_impl_x_locked 此处会进行聚集索引的回表,同样是通过二级索引进行定位返回btr_cur_search_to_nth_level
->row_vers_impl_x_locked_low 最后会调入 row_vers_impl_x_locked_low函数进行核心判断

栈如下:

#0  row_vers_impl_x_locked_low (clust_rec=0x7fff39a21226 "\200", clust_index=0x7ffeb5092680, rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730, mtr=0x7fffe8460e90)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0vers.cc:101
#1 0x0000000001b2c84e in row_vers_impl_x_locked (rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0vers.cc:390
#2 0x00000000019e8448 in lock_sec_rec_some_has_impl (rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:1276
#3 0x00000000019f339a in lock_rec_convert_impl_to_expl (block=0x7fff38d94ca0, rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:6124
#4 0x00000000019f3dd2 in lock_sec_rec_read_check_and_lock (flags=0, block=0x7fff38d94ca0, rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730, mode=LOCK_X,
gap_mode=1024, thr=0x7ffeb4c89358) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:6357
#5 0x0000000001af7271 in sel_set_rec_lock (pcur=0x7ffeb4c887d8, rec=0x7fff39a2ac30 "\200", index=0x7ffeb5093610, offsets=0x7fffe8461730, mode=3, type=1024, thr=0x7ffeb4c89358,
mtr=0x7fffe8461a50) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:1278
#6 0x0000000001b00049 in row_search_mvcc (buf=0x7ffeb4977070 "\370\211\037", mode=PAGE_CUR_GE, prebuilt=0x7ffeb4c885b0, match_mode=1, direction=1)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:5710

但是需要注意的是,max trx id只会在二级索引上更新,并且每次更新一行都会更新掉,那么引起的一个问题就是如果连续多次删除同一个二级索引上的记录(delete from testimp4 where b=7700;),除第一次以外都会调入row_vers_impl_x_locked_low这个函数,因为这是查询一行加锁一行修改一行(每行都会修改page的max trx id)的。但是update却不同,update如果修改本二级索引的值一般会进入(如:update testimp4 set b=1500 where b=1800;)Searching rows for update状态,先建立一个临时文件来先存储需要更改的行记录,然后进行批量更改进入updating状态,那么则不会出现这种问题,因为这是在数据查找阶段进行的判断,而不是数据修改阶段。又比如(如:update testimp2 set c='a' where b=1800)这样的语句也不会触发,这是因为b索引的行记录一直没有改变,因此不会修改b索引page的max trx id。因此update很好的规避了这个问题不会频繁的进入函数row_vers_impl_x_locked_low进行判定,但是delete却不行。

关于row_vers_impl_x_locked_low函数对于二级索引是否存在隐式锁的判定,比较复杂分为好多种情况,不再描述。因此最开始我们看到的问题,这个过程已经进入了row_vers_impl_x_locked_low函数,那么可以判断这个delete语句可能更新了多行(但是从代码行数上判断不是这种情况),或者有可能本语句事务做过修改本语句修改记录的其他语句,需要进行精细化判断。

5.2 lock_sec_rec_modify_check_and_lock

主要用于数据修改阶段加隐式锁,二级索引由于行数据的修改(update修改了本二级索引字包含段值或者尾部的主键)而被动维护的加锁。注意如果是select for update where条件是主键则不会加判断二级索引是否包含隐含锁,如果出现冲突会堵塞在主键上。

5.3 lock_clust_rec_read_check_and_lock

数据查找阶段加显示锁,主要用于主键查找数据加显示锁或者二级索引访问后的回表主键加显示锁,加锁前需要判断是否存在隐含锁。由于主键行中包含了trx id伪列,因此可以简单的用本行trx id的事务是否还活跃进行判定了,这个过程代价很小,因此每行加锁总是会有这个过程,也就是每次都会调用lock_rec_convert_impl_to_expl函数进行判断,如下:

lock_clust_rec_read_check_and_lock
->lock_rec_convert_impl_to_expl
->lock_clust_rec_some_has_impl (主键判断非常简单)

栈如下:

#0  lock_clust_rec_some_has_impl (rec=0x7fff05ad40db "\200", index=0x7ffe8802ce70, offsets=0x7fffe8461660)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/include/lock0priv.ic:69
#1 0x00000000019f3333 in lock_rec_convert_impl_to_expl (block=0x7fff050a0950, rec=0x7fff05ad40db "\200", index=0x7ffe8802ce70, offsets=0x7fffe8461660)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:6118
#2 0x00000000019f418d in lock_clust_rec_read_check_and_lock (flags=0, block=0x7fff050a0950, rec=0x7fff05ad40db "\200", index=0x7ffe8802ce70, offsets=0x7fffe8461660, mode=LOCK_X,
gap_mode=1024, thr=0x7ffeb49903c8) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/lock/lock0lock.cc:6430
#3 0x0000000001af7193 in sel_set_rec_lock (pcur=0x7ffeb498fe38, rec=0x7fff05ad40db "\200", index=0x7ffe8802ce70, offsets=0x7fffe8461660, mode=3, type=1024, thr=0x7ffeb49903c8,
mtr=0x7fffe8461980) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:1263
#4 0x0000000001b00049 in row_search_mvcc (buf=0x7ffeb498f380 "\371\005", mode=PAGE_CUR_GE, prebuilt=0x7ffeb498fc10, match_mode=1, direction=0)
at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/row/row0sel.cc:5710
5.4 lock_clust_rec_modify_check_and_lock

主键数据修改阶段加隐式锁,当前发现为在直接update主键值或者delete操作的时候,但是这种情况下实际上主键已经在数据查询阶段加了显示锁。

六、update不完全等同于delete&&insert

直接区分如下:

  • 主键更新,接口row_upd_clust_step
row_upd_changes_ord_field_binary 判断是否更新了聚集索引的值
如果更新了
-> row_upd_clust_rec_by_insert 进行主键删除插入(设置del flag)
如果没有更新
->row_upd_clust_rec
->btr_cur_optimistic_update 只考虑乐观update
->row_upd_changes_field_size_or_external 判断新记录是否超过本行现有大小
如果否
->btr_cur_update_in_place 原地更新
如果是
->page_cur_delete_rec 则需要进行主键删除(实际删除非设置del falg)
->btr_cur_insert_if_possible 插入
  • 二级索引更新,接口row_upd_sec_step 始终为删除插入(设置del flag)

七、关于History list length 的单位

实际上History list length 就是当一个update undo log (非insert)的计数器,一个事务只有一个undo log 。来源为trx_sys->rseg_history_len,这个值会在事务提交的时候更新,无论事务大小。但是由于很多内部事务的存在,这个值会远大于可观测的事务个数。栈如下:

#0  trx_purge_add_update_undo_to_history (trx=0x7fffeac7df50, undo_ptr=0x7fffeac7e370, undo_page=0x7fff2837c000 "\373\252\223T", update_rseg_history_len=true, n_added_logs=1, 
mtr=0x7fffe8399830) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/trx/trx0purge.cc:354
#1 0x0000000001b9c064 in trx_undo_update_cleanup (trx=0x7fffeac7df50, undo_ptr=0x7fffeac7e370, undo_page=0x7fff2837c000 "\373\252\223T", update_rseg_history_len=true, n_added_logs=1,
mtr=0x7fffe8399830) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/trx/trx0undo.cc:1970
#2 0x0000000001b8b639 in trx_write_serialisation_history (trx=0x7fffeac7df50, mtr=0x7fffe8399830) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/trx/trx0trx.cc:1684
#3 0x0000000001b8c9b0 in trx_commit_low (trx=0x7fffeac7df50, mtr=0x7fffe8399830) at /home/mysql/soft/percona-server-5.7.29-32/storage/innobase/trx/trx0trx.cc:2184
到这里,杂七杂八记录了一大堆,记录于此以备后用。

附录1函数接口

1、read view
  • MVCC::view_open:建立read view
  • ReadView::prepare:准备read view中的值
  • ReadView::complete:写入read view中的值
  • MVCC::view_close:释放read view
2、可见性判断
  • lock_clust_rec_cons_read_sees:主键可见性判断
  • lock_sec_rec_cons_read_sees:二级索引可见性判断

附录 2具体函数

1、read view
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id;

/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
trx_id_t m_up_limit_id;

/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id;

/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;

/** The view does not need to see the undo logs for transactions
whose transaction number is strictly smaller (<) than this value:
they can be removed in purge if not needed by other views */
trx_id_t m_low_limit_no;
void
ReadView::prepare(trx_id_t id)
{
ut_ad(!m_cloned);
ut_ad(mutex_own(&trx_sys->mutex));

m_creator_trx_id = id;

m_low_limit_no = m_low_limit_id = trx_sys->max_trx_id;

if (!trx_sys->rw_trx_ids.empty()) {
copy_trx_ids(trx_sys->rw_trx_ids);
} else {
m_ids.clear();
}

if (UT_LIST_GET_LEN(trx_sys->serialisation_list) > 0) {
const trx_t* trx;

trx = UT_LIST_GET_FIRST(trx_sys->serialisation_list);

if (trx->no < m_low_limit_no) {
m_low_limit_no = trx->no;
}
}
}

void
ReadView::complete()
{
ut_ad(!m_cloned);
/ The first active transaction has the smallest id. /
m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;

ut_ad(m_up_limit_id <= m_low_limit_id);

m_closed = false;
}

2、可见性判断

二级索引回表判断可见性

Row_sel_get_clust_rec_for_mysql::operator()
->lock_clust_rec_cons_read_sees (回表后根据主键判断其可见性)
->row_sel_build_prev_vers_for_mysql(构建前版本)
->row_vers_build_for_consistent_read
本函数循环构建,直到条件满足,或者前版本为NULL
if (view->changes_visible(trx_id, index->table->name)) {

/* The view already sees this version: we can copy
it to in_heap and return */

buf = static_cast<byte*>(
mem_heap_alloc(
in_heap, rec_offs_size(*offsets)));

old_vers = rec_copy(buf, prev_version, offsets);
rec_offs_make_valid(old_vers, index, offsets);

if (vrow && *vrow) {
vrow = dtuple_copy(vrow, in_heap);
dtuple_dup_v_fld(*vrow, in_heap);
}
break;

最终会将前版本的主键值根据需求取字段返回给MySQL层

关于using index 也需要回表流程

row_search_mvcc:
if (!srv_read_only_mode
&& !lock_sec_rec_cons_read_sees( // 如果二级索引记录判断为不可见
rec, index, trx->read_view)) {
/* We should look at the clustered index.
However, as this is a non-locking read,
we can skip the clustered index lookup if
the condition does not match the secondary
index entry. */
switch (row_search_idx_cond_check(
buf, prebuilt, rec, offsets)) {
case ICP_NO_MATCH:
goto next_rec;
case ICP_OUT_OF_RANGE:
err = DB_RECORD_NOT_FOUND;
goto idx_cond_failed;
case ICP_MATCH:
goto requires_clust_rec; //走这里就进入了回表判断流程
}


lock_sec_rec_cons_read_sees:
trx_id_t max_trx_id = page_get_max_trx_id(page_align(rec));//获取页的max trx id
ut_ad(max_trx_id > 0);
return(view->sees(max_trx_id));

全文完。

Enjoy MySQL :)



            </div>
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
SQL 移动开发 算法
MySQL 8.0.23 Hypergraph Join Optimizer代码详解
MySQL Join MySQL本身没有常规意义上的执行计划,一般情况就是通过JOIN和QEP_TAB这两个结构组成。QEP_TAB 的全称是Query Execution Plan Table,这个“Table“可以是物理表、内存表、常量表、子查询的结果表等等。作为整个单独JOIN执行计划载体之前还承担着整个执行路径的调用和流转,但是从8.0.20后,全面的生成了独立的
1640 0
MySQL 8.0.23 Hypergraph Join Optimizer代码详解
|
存储 关系型数据库 MySQL
从一个案例深入剖析InnoDB隐式锁和可见性判断(4)
从一个案例深入剖析InnoDB隐式锁和可见性判断
690 0
|
SQL 关系型数据库 MySQL
从一个案例深入剖析InnoDB隐式锁和可见性判断(2)
从一个案例深入剖析InnoDB隐式锁和可见性判断
146 0
从一个案例深入剖析InnoDB隐式锁和可见性判断(2)
|
NoSQL 关系型数据库 索引
从一个案例深入剖析InnoDB隐式锁和可见性判断(1)
从一个案例深入剖析InnoDB隐式锁和可见性判断
148 0
从一个案例深入剖析InnoDB隐式锁和可见性判断(1)
|
6月前
|
Ubuntu 应用服务中间件 Shell
Docker入门
Docker入门
63 0
|
关系型数据库 MySQL 索引
从一个案例深入剖析InnoDB隐式锁和可见性判断(3)
从一个案例深入剖析InnoDB隐式锁和可见性判断
|
AliSQL 关系型数据库 MySQL
RDS AliSQL 面向 Binlog 的性能优化大揭密(下)——强效瓶颈消除
本篇将继续揭秘AliSQL在binlog高并发性能上做的企业级优化。
RDS AliSQL 面向 Binlog 的性能优化大揭密(下)——强效瓶颈消除
|
关系型数据库 MySQL 数据库
[图解MySQL]MySQL组提交(group commit)
[图解MySQL]MySQL组提交(group commit)
17346 0
|
缓存 NoSQL
TiDB的分布式事务原理探究
TiDB分布式事务原理探究,步骤分为 事务开启,事务写,事务回滚,或者事务提交。事务提交中由包括操作映射,Prewrite 和 Commit
632 0
|
SQL 存储 安全
MySQL 8.0 MVCC 源码解析
本文在此基础上,对 MVCC 展开详细的分析,同时修改了之前的一些不太准确的说法,希望可以助你在面试中更好的发(zhuang)挥(bi)。
617 0
MySQL 8.0 MVCC 源码解析