【华为云MySQL技术专栏】InnoDB锁视图及行锁结构介绍

举报
GaussDB 数据库 发表于 2025/12/18 17:54:14 2025/12/18
【摘要】 1、背景相比于MyISAM等存储引擎,InnoDB支持了行级别的锁,这极大地提升了数据库的并发性能。但是在业务高峰期或者由于一些使用问题,例如事务未提交等,SQL语句依然会长时间等待行锁,进而影响业务。为了帮助用户快速定位行锁等待的相关问题,MySQL 5.7在Information_Schema库下提供了INNODB_LOCKS和INNODB_LOCK_WAITS两张系统表用来展示Inno...

1、背景

相比于MyISAM等存储引擎,InnoDB支持了行级别的锁,这极大地提升了数据库的并发性能。但是在业务高峰期或者由于一些使用问题,例如事务未提交等,SQL语句依然会长时间等待行锁,进而影响业务。
为了帮助用户快速定位行锁等待的相关问题,MySQL 5.7在Information_Schema库下提供INNODB_LOCKSINNODB_LOCK_WAITS两张系统表用来展示InnoDB中的行锁等待关系,但是这两张表在8.0中被移到了Performance Schema库下并改名为data_locksdata_lock_waits,且需要客户启用Performance Schema性能监控插件后才能采集相关信息。启用Performance Schema后会采集大量事件来监视MySQL的运行状态,进而对MySQL实例产生性能影响。
因此,为了方便现网定位行锁相关问题,我们在8.0版本重新将这张锁视图在Information_Schema库下进行实现。本文我们会主要介绍华为云RDS自研锁视图的使用方式和InnoDB行锁的相关概念。

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)

1.png

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)

2.png

  • 使用示例

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

3.png

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

5.png

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

6.png

根据上面的原理,我们可以这两个操作拼接成如下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来维护。

7.png

  • 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时,锁的类型就为临键锁。

8.png

  • 除了全局锁维度,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的实现原理和使用方式。锁视图提供了深入查看数据库内部锁等待的重要手段。熟练使用这些视图,将极大提升定位数据库锁相关问题的效率。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。