【华为云MySQL技术专栏】InnoDB锁视图及行锁结构介绍
1、背景
INNODB_LOCKS和INNODB_LOCK_WAITS两张系统表用来展示InnoDB中的行锁等待关系,但是这两张表在8.0中被移到了Performance Schema库下并改名为data_locks和data_lock_waits,且需要客户启用Performance Schema性能监控插件后才能采集相关信息。启用Performance Schema后会采集大量事件来监视MySQL的运行状态,进而对MySQL实例产生性能影响。2、使用方式
-
特性开关:innodb_rds_data_lock_info_enabled默认关闭,支持Session和Global级别动态修改,在使用时可以Session级别开启
-
RDS_INNODB_DATA_LOCK_INFO:展示行锁相关信息的表
mysql> desc information_schema.RDS_INNODB_DATA_LOCK_INFO;
+------------------------+-----------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------------------+-----------------+------+-----+---------+-------+
| lock_id | varchar(128) | NO | | | |
| lock_trx_id | bigint unsigned | NO | | | |
| lock_mode | varchar(32) | NO | | | |
| lock_type | varchar(32) | NO | | | |
| lock_status | varchar(32) | NO | | | |
| lock_object_schema | varchar(64) | NO | | | |
| lock_object_name | varchar(64) | NO | | | |
| lock_partition_name | varchar(64) | YES | | | |
| lock_subpartition_name | varchar(64) | YES | | | |
| lock_index | varchar(64) | YES | | | |
| lock_space | bigint unsigned | YES | | | |
| lock_page | bigint unsigned | YES | | | |
| lock_rec | bigint unsigned | YES | | | |
| object_instance_begin | bigint | NO | | | |
| lock_data | varchar(8192) | YES | | | |
+------------------------+-----------------+------+-----+---------+-------+
15 rows in set (0.01 sec)

RDS_INNODB_DATA_LOCK_WAIT_INFO:展示行锁之间的等待关系
mysql> desc information_schema.RDS_INNODB_DATA_LOCK_WAIT_INFO;
+----------------------------------+-----------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------------------------+-----------------+------+-----+---------+-------+
| requesting_trx_id | bigint unsigned | NO | | | |
| requesting_lock_id | varchar(128) | NO | | | |
| requesting_object_instance_begin | bigint | NO | | | |
| blocking_trx_id | bigint unsigned | NO | | | |
| blocking_lock_id | varchar(128) | NO | | | |
| blocking_object_instance_begin | bigint | NO | | | |
+----------------------------------+-----------------+------+-----+---------+-------+
6 rows in set (0.02 sec)

- 使用示例
假设我们创建了一张表,其主键为ID。在会话1中我们开启了一个事务,不进行提交,此时该会话会一直持有表t1中行id=1的行的S类型的行锁。当会话2执行SELECT * FROM t1 WHERE id = 1 FOR UPDATE的操作时,需要获取X级别的行锁,但由于行锁S和X类型的互斥而被阻塞。此时我们可以通过查询锁视图的表,找到阻塞的会话,将对应的事务提交或这回滚,来让会话2继续运行。

我们可以通过查询RDS_INNODB_DATA_LOCK_WAIT_INFO这张表来获得系统中锁相关的信息,表中第一行我们可以看到事务2325对test库t1表中第四个页面的第二条记录加X类型的行锁(非间隙锁)时产生阻塞。第二行表示事务421565470607752已经持有了test库t1表中第四个页面的第二条记录S类型的行锁。这样我们就能基本判断出行锁的阻塞关系了。

但是对于一个高并发的系统,即事务数量很多时,我们很难通过这种方式直接找到两个事务的等待关系,因此在更多的时候,我们可以直接使用RDS_INNODB_DATA_LOCK_WAIT_INFO这张表来快速找到会话之间的等待关系。这里我们发现ID为2324的事务在被会话421565470607752阻塞。然后再根据事务ID在INFORMATION_SCHEMA.INNODB_TRX这张表中找到对应的会话ID进行提交或者KILL的操作,来释放锁进而恢复业务。

根据上面的原理,我们可以这两个操作拼接成如下SQL语句方便直接查询到阻塞的会话ID,然后进行提交或回滚。
SELECT trx.trx_mysql_thread_id AS THREAD_ID
FROM INFORMATION_SCHEMA.INNODB_TRX trx
JOIN INFORMATION_SCHEMA.RDS_INNODB_DATA_LOCK_WAIT_INFO lockinfo
ON trx.trx_id = lockinfo.blocking_trx_id;
3、源码实现
为了实现这两个锁视图,我们首先需要了解InnoDB是如何实现行锁的。所以接下来我们首先会简单介绍InnoDB行锁的实现,然后再介绍如何实现行锁视图。
3.1 InnoDB锁基本概念
下图展示了InnoDB中锁的维护方式。InnoDB中每一个锁对象由lock_t的对象来表示。对于行锁来说,只要确定了这一行所在的表的space_id,在表空间的页面号,在页面内的行号,我们就可以确定这行记录的位置。因此每个行锁的lock_t会维护space_id, page_no,和n_bits来标识对应的行。如果是表锁,只需要引用对应表的dict_table_t的对象即可。相反dict_table_t的对象中的locks链表也包含了该表上所有表锁的信息。最终这些锁对象lock_t结构体会存储在哈希桶中,由lock_sys_t来维护。

- lock_sys_t: 一个全局唯一的对象,用来维护InnoDB中所有的锁对象,核心成员变量如下
struct lock_sys_t {
/** 主要是针对老版本lock_sys_t->mutex锁的优化,现在拆分为三个维度global,page,table,
1. 事务提交或回滚时,释放所有行锁和表锁会用到 global_latch。
2. 事务加行锁时,会用到page_shards的分片的锁。
3. 事务加表锁时,会用到table_shards的分片的锁。
*/
locksys::Latches latches;
/** 记录锁全局hash,里面是lock_t的对象*/
hash_table_t *rec_hash;
/** 互斥锁,用于保护waiting_threads */
Lock_mutex wait_mutex;
/** 阻塞线程等待队列. */
srv_slot_t *waiting_threads;
/** 阻塞线程等待队列的链尾. */
srv_slot_t *last_slot;
/** 等锁最长时间. */
std::chrono::steady_clock::duration n_lock_max_wait_time;
};
-
lock_t:每个行锁或表锁的内存对象,核心成员变量如下
struct lock_t {
/** 锁所属的事务对象 */
trx_t *trx;
/** 这个事务所有相关的锁对象 */
UT_LIST_NODE_T(lock_t) trx_locks;
/** 锁对象所在的索引 */
dict_index_t *index;
/** 锁对象链表,相同数据页的锁对象单链表进行管理,每个数据页的锁对象链表是全局lock_sys->rec_hash的一个桶 */
lock_t *hash;
/* 具体的一个锁对象,是union类型,表示要么是行锁 要么是表锁 */
union {
/** Table lock,主要记录的是dict_table_t的对象*/
lock_table_t tab_lock;
/** Record lock,主要记录page_id和heap no*/
lock_rec_t rec_lock;
};
/* 存储行锁模式、行锁类型、行锁状态等信息 */
uint32_t type_mode;
}
-
type_mode:
1-4位表示锁模式,包括意向共享锁、意向排它锁、共享锁、排它锁还是自增锁;
5和6位表示锁是表类型,表锁或行锁其中一个;
9位表示是否锁等待;
10到32位表示是记录锁类型,LOCK_GAP表示间隙锁,LOCK_REC_NOT_GAP表示行锁,临键锁是LOCK_ORDINARY,此值为0,也就是当不设置LOCK_GAP和LOCK_REC_NOT_GAP时,锁的类型就为临键锁。

- 除了全局锁维度,innodb在事务维度也会维护锁相关的信息,每个事务对应的内存结构体trx_t中有一个trx_lock_t的结构体,trx_lock_t中wait_lock和trx_locks会分别记录当前事务等待和已经授权的锁的信息。这也是我们实现锁视图时所用到的重要的结构体。
struct trx_lock_t {
ulint n_active_thrs; /*!< number of active query threads */
/*事务的状态*/
trx_que_t que_state; /*!< valid when trx->state
== TRX_STATE_ACTIVE: TRX_QUE_RUNNING,
TRX_QUE_LOCK_WAIT, ... */
/*事务正在请求的锁*/
lock_t* wait_lock; /*
/*事务发生了死锁,在死锁检测中被选为进行回滚的事务*/
bool was_chosen_as_deadlock_victim;
/*开始等待时间 */
time_t wait_started;
/** 预分配的记录锁的内存池. 在事务创建的时候预分配,默认预创建REC_LOCK_CACHE 8个lock_t对象,如果使用完再进行扩展 */
lock_pool_t rec_pool;
/** 预分配的表锁的内存池. */
lock_pool_t table_pool;
/* 指向所有已经获取的锁,一个事务可以持有多个锁 */
trx_lock_list_t trx_locks;
/* 持有锁的数量 */
ulint n_rec_locks;
};
3.2 InnoDB锁视图实现方式
InnoDB行锁视图是实现在Information_schema库下,类似其他IS表的视图在i_s.cc下定义了两个系统表
-
RDS_INNODB_DATA_LOCK_INFO
struct st_mysql_plugin i_s_innodb_data_lock_info = {
STRUCT_FLD(name, "RDS_INNODB_DATA_LOCK_INFO"),
STRUCT_FLD(init, innodb_locks_init),
}
static int innodb_locks_init(void* p) /*!< in/out: table schema object */
{
ST_SCHEMA_TABLE* schema;
DBUG_ENTER("innodb_locks_init");
schema = (ST_SCHEMA_TABLE*) p;
schema->fields_info = innodb_data_lock_fields_info;
schema->fill_table = trx_i_s_common_fill_table;
DBUG_RETURN(0);
}
-
RDS_INNODB_DATA_LOCK_WAIT_INFO
struct st_mysql_plugin i_s_innodb_data_lock_wait_info = {
STRUCT_FLD(name, "RDS_INNODB_DATA_LOCK_WAIT_INFO"),
STRUCT_FLD(init, data_lock_wait_info_init),
}
static int data_lock_wait_info_init(void* p)
{
ST_SCHEMA_TABLE* schema;
DBUG_ENTER("data_lock_wait_info_init");
schema = (ST_SCHEMA_TABLE*) p;
schema->fields_info = innodb_lock_wait_fields_info;
schema->fill_table = trx_i_s_common_fill_table;
DBUG_RETURN(0);
}
-
两个系统表填充数据都是调用的trx_i_s_common_fill_table的函数,复用的是查询innodb_trx表时调用的函数,整体流程如下
trx_i_s_common_fill_table
/* Step 1. 检查权限有PROCESS_ACL */
if (check_global_access(thd, PROCESS_ACL)) {
return 0;
}
/* Step 2. cache加写锁 */
trx_i_s_cache_start_write(cache); //
/* 并查询事务链表,将锁信息添加到cache中 */
trx_i_s_possibly_fetch_data_into_cache(cache);
/* Step 3. cache解锁 */
trx_i_s_cache_end_write(cache); //解锁
/* Step 4. cache加读锁 */
trx_i_s_cache_start_read(cache);
/* Step 5. 根据cache的内容填充i_s的系统表,查询的是哪张就会填哪张表,下面的函数3选1 */
fill_innodb_trx_from_cache() // innodb_trx
fill_data_lock_info_from_cache() // rds_innodb_data_lock_info
fill_data_lock_wait_info_from_cache() // rds_innodb_data_lock_wait_info
/* Step 6. cache解锁 */
trx_i_s_cache_end_read(cache)
-
check_global_access检查权限,需要调用者拥有PROCESS_ACL的权限
-
trx_i_s_cache_start_write(cache);通过cache中的rw lock给cache加写锁
cache为trx_i_s_cache,是一个全局变量,目前包含三个类型为i_s_table_cache_t的子模块innodb_trx, innodb_locks和innodb_lock_waits,用来在三张视图查询时,将查询结果进行缓存
-
trx_i_s_possibly_fetch_data_into_cache:这个函数是构造出锁视图的核心函数,数据添加到对应的cache中,这个过程需要获取全局锁的内存管理系统lock_sys和事务内存管理系统trx_sys的锁,因此可能会短暂阻塞用户的读写业务。
int trx_i_s_possibly_fetch_data_into_cache(
trx_i_s_cache_t *cache) /*!< in/out: cache */
{
// 与上一次读间隔100毫秒才能查询
if (!can_cache_be_updated(cache)) {
return (1);
}
// 获取lock_sys和trx_sys的锁
{
locksys::Global_exclusive_latch_guard guard{UT_LOCATION_HERE};
trx_sys_mutex_enter();
fetch_data_into_cache(cache);
trx_sys_mutex_exit();
}
return (0);
}
fetch_data_into_cache_low()会遍历InnoDB中trx_sys的读写事务链表rw_trx_list和只读事务链表mysql_trx_list。对于每一个处于等待状态的事务,调用add_trx_relevant_locks_to_cache函数来将其正在等待的锁添加到Cache中,然后去lock_sys中找到阻塞它的锁也添加到cache中。
static bool add_trx_relevant_locks_to_cache(trx_i_s_cache_t *cache, const trx_t *trx, i_s_locks_row_t **requested_lock_row)
{
/** 如果事务处于等待状态 */
if (trx->lock.que_state == TRX_QUE_LOCK_WAIT) {
i_s_locks_row_t *blocking_lock_row;
const lock_t *wait_lock = trx->lock.wait_lock;
wait_lock_heap_no = wait_lock_get_heap_no(wait_lock);
/* 将等待的锁放入到cache中 */
*requested_lock_row =
add_lock_to_cache(cache, wait_lock, wait_lock_heap_no,
lock_status_str);
/* 这里做了一个参数rds_skip_i_s_trx_blocking_locks,防止遍历时间过长影响业务,可以及时中断查询 */
if (current_thd != nullptr &&
current_thd->variables.rds_skip_i_s_trx_blocking_locks) {
return (true);
}
/* 找到阻塞的锁,并将他添加到cache中 */
locksys::find_blockers(*wait_lock, [&](const lock_t &curr_lock) {
const trx_t *trx = lock_get_trx(&curr_lock);
if (!trx->lock.wait_lock) {
lock_status_str = "GRANTED";
} else {
lock_status_str = "WAITING";
}
/* 添加阻塞的锁到rds_lock_data_info的cache中 */
blocking_lock_row = add_lock_to_cache(cache, &curr_lock,
/* heap_no is the same
for the wait and waited
locks */
wait_lock_heap_no,
lock_status_str);
/* 将他们的关联关系加入到data_lock_wait_info表对应的cache中 */
add_lock_wait_to_cache(cache, *requested_lock_row, blocking_lock_row)
}
}
找到阻塞的锁的方法
const lock_t *find_blockers(const lock_t &wait_lock,
std::function<bool(const lock_t &)> visitor) {
locksys::Trx_locks_cache wait_lock_cache{};
// 行锁,从lock_sys中的rec_hash桶中根据space_id, page_no, heap_no哈希值找到对应行上已经持有的锁
if (lock_get_type_low(&wait_lock) == LOCK_REC) {
const uint16_t heap_no = lock_rec_find_set_bit(&wait_lock);
const auto found = wait_lock.hash_table().find_on_record(
RecID{&wait_lock, heap_no}, [&](lock_t *lock) {
if (lock == &wait_lock) {
return true;
}
if (locksys::has_to_wait(&wait_lock, lock, wait_lock_cache)) {
return visitor(*lock);
}
return false;
});
return found == &wait_lock ? nullptr : found;
}
// 如果是表锁,会遍历dict_table_t中的locks,找到该表上已经持有的锁
for (auto lock : wait_lock.tab_lock.table->locks) {
if (lock == &wait_lock) {
return nullptr;
}
if (locksys::has_to_wait(&wait_lock, lock, wait_lock_cache)) {
if (visitor(*lock)) {
return lock;
}
}
}
return nullptr;
}
-
如果是表锁,先去当前lock对应的dict_table_t,dict_table_t中会记录所有表持有的锁,进行遍历,然后判断兼容性。
-
如果是行锁,根据page_id + heap no计算出的哈希值,从lock_sys->rec_hash的哈希桶中找到对应的桶,然后对哈希链表进行遍历,也就是找到对同一行记录加过的锁,进行兼容性判断,兼容性判断过程在rec_lock_check_conflict()函数中。
-
最后我们会从cache中读取刚刚构造出的锁信息,然后构造出每个表的字段。
4、总结
锁等待时间通常是影响数据库响应时间的核心因素之一,本文简单介绍了InnoDB中锁的维护方式,以及RDS 8.0在Information_schema库下实现的锁视图RDS_INNODB_DATA_LOCK_INFO和RDS_INNODB_DATA_LOCK_WAIT_INFO的实现原理和使用方式。锁视图提供了深入查看数据库内部锁等待的重要手段。熟练使用这些视图,将极大提升定位数据库锁相关问题的效率。
- 点赞
- 收藏
- 关注作者
评论(0)