insert时调用本身字段_MySQL RC级别下并发insert锁超时问题 - 源码分析

作者:网易数据库团队

DDB(网易杭研自研的MySQL数据库中间件产品)团队小伙伴发现了一个问题,觉得比较奇怪。于是找到我们,希望解释下。过程中除解释了问题的现象,也通过代码了解了更多的InnoDB DML执行逻辑,还发现了MySQL/InnoDB官方在二级唯一索引冲突检查时加锁行为的反复。本系列打算用三四篇文章来聊聊这个事情。这是第二篇,从源码层面来论证所做的假设。第一篇链接:

MySQL RC级别下并发insert锁超时问题 - 现象分析和解释


基于第一篇的假设

对于主键索引,最多存在一条主键相同的记录,该记录或者是delete-marked状态,或者是普通状态。因为对记录进行不涉及主键字段的update时总是inplace的,不存在delete+insert情况;insert时如果发现了主键相同的delete-marked记录,则直接复用该记录,即insert转为inplace update。而对于二级索引,update时总是执行delete+insert,insert时也不会复用delete-marked状态的记录。

看看代码层面是否支持。

update场景分析

update一条记录时,先更新主键索引,再判断是否需要修改了其他二级索引的字段,将被动了索引字段的索引也更新掉。

主键索引

主键update的函数入口是row_upd_clust_step,在该函数中会判断本次update是否更新了主键中的字段,若是,则调用row_upd_clust_rec_by_insert:

7cca5eda98fac834058f360c312b520a.png

row_upd_changes_ord_field_binary_func函数的实现逻辑是判断该索引的n_uniq列的值是否被改变,如其中的列值被改变,则返回true,否则返回false。也就是说如果update语句改变了主键索引定义中指明的那些列的值,那么走delete+insert。本文关注的是未更新这些排序字段的场景,但从row_upd_clust_rec_by_insert函数的介绍可以反推出如果没有更新排序字段,那么都是采用直接更新现有记录或delete-marked记录,而不是通过delete+insert方式。为了证明这个推断,可以通过是否调用了函数btr_cur_del_mark_set_clust_rec来进行判断。

二级索引

二级索引update的函数入口是row_upd_sec_step,先通过下面的语句判断是否需要更新某个二级索引:

if (node->state == UPD_NODE_UPDATE_ALL_SEC
	    || row_upd_changes_ord_field_binary(node->index, node->update,
thr, node->row, node->ext)) {
return(row_upd_sec_index_entry(node, thr));
}
#define UPD_NODE_UPDATE_ALL_SEC    5 /* an ordering field of the clustered
index record was changed, or this is
a delete operation: should update
all the secondary index records */

UPD_NODE_UPDATE_ALL_SEC标志位对应update操作更改了主键索引的排序字段(对应row_upd_clust_rec_by_insert)或删除了主键。row_upd_changes_ord_field_binary上面已经介绍过,是修改了该索引的排序字段(n_uniq)。所以只有在主键的n_uniq或本索引的n_uniq被修改的时候才需要更新二级索引。

我们接下来看,若确实需要更新二级索引,则调用row_upd_sec_index_entry函数来执行更新逻辑。

/***********************************************************//**
Updates a secondary index entry of a row.
@return DB_SUCCESS if operation successfully completed, else error
code or DB_LOCK_WAIT */
static MY_ATTRIBUTE((warn_unused_result))
dberr_t
row_upd_sec_index_entry(

在该函数中,会调用btr_cur_del_mark_set_sec_rec来将旧记录标记为delete-marked

c2dd49cb2765c4ad37635b911187d421.png

并调用row_build_index_entry向该二级索引中插入一条新的记录

5fe97381c315ca98b8c7aadf8aa4dd59.png

所以,从二级索引更新逻辑可以看到,都是采用delete+insert的流程。当然,像主键索引一样,可以通过判断是否调用了btr_cur_del_mark_set_sec_rec函数来确认。

综合主键索引和二级索引的情况,可以说跟我们的假设是没有冲突的。

insert场景分析

接下来看看insert一条记录是如何执行的,由于InnoDB是索引组织表,insert操作当然都是发生在每个索引上。入口函数为row_ins,其会为每个索引循环调用row_ins_index_entry_step->row_ins_index_entry来处理具体的索引插入行为。

/***************************************************************//**
Inserts an index entry to index. Tries first optimistic, then pessimistic
descent down the tree. If the entry matches enough to a delete marked record,
performs the insert by updating or delete unmarking the delete marked
record.
@return DB_SUCCESS, DB_LOCK_WAIT, DB_DUPLICATE_KEY, or some other error code */
static
dberr_t
row_ins_index_entry(

该函数根据是否为主键索引又分别调用row_ins_clust_index_entry和row_ins_sec_index_entry。我们分别看看主键索引和二级索引的处理函数。

主键索引

row_ins_clust_index_entry进一步调用row_ins_clust_index_entry_low:

/***************************************************************//**
Tries to insert an entry into a clustered index, ignoring foreign key
constraints. If a record with the same unique key is found, the other
record is necessarily marked deleted by a committed transaction, or a
unique key violation error occurs. The delete marked record is then
updated to an existing record, and we must write an undo log record on
the delete marked record.
@retval DB_SUCCESS on success
@retval DB_LOCK_WAIT on lock wait when !(flags & BTR_NO_LOCKING_FLAG)
@retval DB_FAIL if retry with BTR_MODIFY_TREE is needed
@return error code */
dberr_t
row_ins_clust_index_entry_low(

从row_ins_clust_index_entry_low函数描述知道,在主键索引插入时,如果发现有delete-marked的记录,该记录的唯一性字段跟要插入的记录一样,那么直接调用row_ins_clust_index_entry_by_modify函数进行复用。

二级索引

row_ins_sec_index_entry进一步调用row_ins_sec_index_entry_low:

/***************************************************************//**
Tries to insert an entry into a secondary index. If a record with exactly the
same fields is found, the other record is necessarily marked deleted.
It is then unmarked. Otherwise, the entry is just inserted to the index.
@retval DB_SUCCESS on success
@retval DB_LOCK_WAIT on lock wait when !(flags & BTR_NO_LOCKING_FLAG)
@retval DB_FAIL if retry with BTR_MODIFY_TREE is needed
@return error code */
dberr_t
row_ins_sec_index_entry_low(

从row_ins_sec_index_entry_low函数描述可知,不同于主键索引,只有找到该索引的所有字段都相同的delete-marked索引时才会复用,此时调用的函数为row_ins_sec_index_entry_by_modify,而其他情况都是执行插入新记录的逻辑。

从上面的分析可以知道,对于插入场景,都会在某些情况下复用delete-marked状态的老记录。而这是通过函数row_ins_must_modify_rec来判断的。

/***************************************************************//**
Checks if an index entry has long enough common prefix with an
existing record so that the intended insert of the entry must be
changed to a modify of the existing record. In the case of a clustered
index, the prefix must be n_unique fields long. In the case of a
secondary index, all fields must be equal.  InnoDB never updates
secondary index records in place, other than clearing or setting the
delete-mark flag. We could be able to update the non-unique fields
of a unique secondary index record by checking the cursor->up_match,
but we do not do so, because it could have some locking implications.
@return TRUE if the existing record should be updated; FALSE if not */
UNIV_INLINE
ibool
row_ins_must_modify_rec(
/*====================*/
const btr_cur_t* cursor) /*!< in: B-tree cursor */
{
/* NOTE: (compare to the note in row_ins_duplicate_error_in_clust)
Because node pointers on upper levels of the B-tree may match more
to entry than to actual user records on the leaf level, we
have to check if the candidate record is actually a user record.
A clustered index node pointer contains index->n_unique first fields,
and a secondary index node pointer contains all index fields. */

return(cursor->low_match
	       >= dict_index_get_n_unique_in_tree(cursor->index)
&& !page_rec_is_infimum(btr_cur_get_rec(cursor)));
}

该函数的说明很清楚得介绍了什么时候会复用delete-marked记录:如果主键索引搜索到一条记录,只要delete-marked的记录与说要插入记录的前缀匹配长度等于主键索引的n_unique长度,那么就复用。但对于二级唯一索引,则需要该索引的每个字段(n_fields)的值都相同才会复用。

我们进一步分析该函数的实现。其中cursor->low_match的解释为:

ulint low_match; /*!< if search mode was PAGE_CUR_LE,
the number of matched fields to the
first user record AT THE CURSOR or
to the left of it after
btr_cur_search_to_nth_level;
NOT defined for PAGE_CUR_GE or any
other search modes; see also the NOTE
in up_match! */

函数dict_index_get_n_unique_in_tree的定义为:

dict_index_get_n_unique_in_tree(
/*============================*/
const dict_index_t* index) /*!< in: an internal representation
of index (in the dictionary cache) */
{
ut_ad(index);
ut_ad(index->magic_n == DICT_INDEX_MAGIC_N);
ut_ad(index->cached);

if (dict_index_is_clust(index)) {
return(dict_index_get_n_unique(index));
}
return(dict_index_get_n_fields(index));
}

如果是主键索引,则返回dict_index_get_n_unique,否则返回dict_index_get_n_fields,这两个函数分别获取的是索引对象index的n_uniq和n_fields字段,我们看看这两个字段代表什么意思:

unsigned n_uniq:10;/*!< number of fields from the beginning
which are enough to determine an index
entry uniquely */
unsigned n_def:10;/*!< number of fields defined so far */
unsigned n_fields:10;/*!< number of fields in the index */

对于主键索引n_uniq就是定义的主键定义包含的列个数,n_fields是表的用户列加系统列的个数;对于二级唯一索引n_uniq是索引定义包含的列个数,n_felds是索引定义列数加主键包含的列。而在进行唯一性约束检查时,判断的是n_uniq。

这也支持了我们之前做的假设:在进行唯一性约束检查时,对于主键索引,不管找到的是普通的还是delete-marked记录,都意味着不可能存在主键相同的其他记录了,包括delete-marked,所以不需要继续加共享读锁判断游标的下一条记录。但是如果是二级唯一索引,由于insert和update方式的不同,就有可能存在唯一键前缀相同的其他记录。所以必须继续查下一条。


本篇我们从代码层来分析所做的假设是否正确。从分析结果看,似乎是正确的。下一篇希望构造实际的场景来验证这些代码的行为。

原文链接:MySQL RC级别下并发insert锁超时问题 - 源码分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值