文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

MySQL insert t select s 导致 s 表锁表

2024-11-29 23:59

关注

现象

时间:20231124 09:58

数据库版本:MySQL 5.7.24

现象:insert select 备份表导致 update 锁等待

查看监控

其中:

测试

测试准备

mysql> show create table t3_bak \G
*************************** 1. row ***************************
       Table: t3_bak
Create Table: CREATE TABLE `t3_bak` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `a` int(10) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL,
  `b` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_name_a` (`name`,`a`)
) ENGINE=InnoDB AUTO_INCREMENT=10000000 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> select * from t3_bak limit 3;
+----+------+------+------+
| id | a    | name | b    |
+----+------+------+------+
| 11 |   11 | test |    0 |
| 12 |   12 | abc  |    0 |
| 13 |   13 | test |    0 |
+----+------+------+------+
3 rows in set (0.00 sec)

mysql> create table t3_bak_1124 like t3_bak;
Query OK, 0 rows affected (0.02 sec)

复现

操作流程,其中事务 1 备份全表,事务 2 update 其中一行数据。

时刻 2 查看锁信息

其中:

由于查询全表时加锁过多,为了查看事务 1 insert select 完整的锁信息,下面单独执行 insert select limit 语句。

SQL

mysql> insert into t3_bak_1124 select * from t3_bak limit 3;
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

查看锁等待信息

其中:

分析

执行流程

从 trace 中可以明确看到 insert select 的执行可以分两步:

详见下图。

select

insert

加锁函数

给 sel_set_rec_lock 函数设置断点,查看堆栈用于定位加锁操作。

其中:

其中:

if (prebuilt->select_lock_type != LOCK_NONE) {
  

  

  ulint lock_type;

  // 不加gap锁的场景
  if (!set_also_gap_locks
      || srv_locks_unsafe_for_binlog
      || trx->isolation_level <= TRX_ISO_READ_COMMITTED
      || (unique_search && !rec_get_deleted_flag(rec, comp))
      || dict_index_is_spatial(index)) {

   goto no_gap_lock;
  } else {
   lock_type = LOCK_ORDINARY;
  }
}

其中:

  1. 5(LOCK_NONE),如普通 select 快照读;
  2. 2(LOCK_S),如 select lock in share mode 当前读禁止写;
  3. 3(LOCK_X),如 select for update 当前读禁止读写。

如下所示,sel_set_rec_lock 函数中加锁时 lock_mode 同样使用 prebuilt->select_lock_type,个人判断行锁类型与表锁类型有关。

  err = sel_set_rec_lock(pcur,
             rec, index, offsets,
             prebuilt->select_lock_type,
             lock_type, thr, &mtr);

因此重点在于 prebuilt->select_lock_type 字段的赋值操作,定位到对应堆栈如下所示。

其中:

ha_innobase::store_lock 函数中根据 lock_type 与 sql_command 判断需要是否加 S 锁,相关代码如下所示。

// storge/innobase/handler/ha_innodb.cc


// 首先根据 lock_type 判断
} else if ((lock_type == TL_READ && in_lock_tables)
     || (lock_type == TL_READ_HIGH_PRIORITY && in_lock_tables)
     || lock_type == TL_READ_WITH_SHARED_LOCKS
     || lock_type == TL_READ_NO_INSERT
     || (lock_type != TL_IGNORE
         && sql_command != SQLCOM_SELECT)) {

  

  
 
  // 然后根据 sql_command 判断
  if (sql_command == SQLCOM_CHECKSUM
      || ((srv_locks_unsafe_for_binlog
    || trx->isolation_level <= TRX_ISO_READ_COMMITTED)
    && trx->isolation_level != TRX_ISO_SERIALIZABLE
    && (lock_type == TL_READ
        || lock_type == TL_READ_NO_INSERT)
    && (sql_command == SQLCOM_INSERT_SELECT // insert select 语句
        || sql_command == SQLCOM_REPLACE_SELECT
        || sql_command == SQLCOM_UPDATE
        || sql_command == SQLCOM_CREATE_TABLE))) {

    

    m_prebuilt->select_lock_type = LOCK_NONE;
    m_prebuilt->stored_select_lock_type = LOCK_NONE;
  } else {
    m_prebuilt->select_lock_type = LOCK_S;
    m_prebuilt->stored_select_lock_type = LOCK_S;
  }

其中:

  1. LOCK TABLES ... READ LOCAL
  2. SELECT ... IN SHARE MODE
  3. INSERT INTO ... SELECT / REPLACE INTO...SELECT / CREATE  ... SELECT

事务隔离级别不是 SERIALIZABLE,并开启 innodb_locks_unsafe_for_binlog

事务隔离级别是 RC

前面提到两个枚举类型,下面展示定义。

首先是 enum_sql_command,表示 SQL 的类型,比如 insert select = 6 = SQLCOM_INSERT_SELECT。

enum enum_sql_command {
  SQLCOM_SELECT,
  SQLCOM_CREATE_TABLE,
  SQLCOM_CREATE_INDEX,
  SQLCOM_ALTER_TABLE,
  SQLCOM_UPDATE,
  SQLCOM_INSERT,
  SQLCOM_INSERT_SELECT,
  ...
};

然后是 lock_mode,表示加锁的模式,比如 insert select = 2 = LOCK_S。


enum lock_mode {
 LOCK_IS = 0, 
 LOCK_IX, 
 LOCK_S,  
 LOCK_X,  
 LOCK_AUTO_INC, 
 LOCK_NONE, 
 LOCK_NUM = LOCK_NONE, 
 LOCK_NONE_UNSET = 255
};

加锁原因

下面分析 insert select 语句加 S 型 next-key lock 的原因。

首先参考官方文档。  

INSERT INTO T SELECT ... FROM S WHERE ... sets an exclusive index record lock (without a gap lock) on each row inserted into T. If the transaction isolation level is READ COMMITTED, or innodb_locks_unsafe_for_binlog is enabled and the transaction isolation level is not SERIALIZABLE, InnoDB does the search on S as a consistent read (no locks). Otherwise, InnoDB sets shared next-key locks on rows from S. InnoDB has to set locks in the latter case: During roll-forward recovery using a statement-based binary log, every SQL statement must be executed in exactly the same way it was done originally.

对于 insert t select s 语句,其中 t、s 分别表示表名。

执行过程中给 t 表加 record lock,具体是隐式锁,而给 s 表的加锁类型与事务隔离级别及参数配置有关:

然后参考 MySQL 45 讲。

创建测试表

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t;

在 RR 事务隔离级别下,binlog_format = statement 时执行以下语句时,为什么需要对 t 的所有行和间隙加锁呢?

insert into t2(c,d) select c,d from t;

原因是需要保证日志与数据的一致性,否则将导致主从不一致。

假设 insert select 时 t 表存在并发 insert,其中假设 session B 先执行。

其中:

insert into t values(-1,-1,-1);
insert into t2(c,d) select c,d from t;

因此从库执行时,会将 id=-1 的记录也写入 t2 表中,从而导致主从不一致。

参考 chatgpt,insert t select * from s 给 s 表加锁的原因如下所示,显示与 45 讲中一致。

在MySQL中,执行"insert ... select"语句时,会对选择的表S进行锁定以确保在整个选择和插入过程中的数据一致性。

理论上说,"insert ... select"操作包含两个步骤:第一步是从表S中选择数据;第二步是将选择的数据插入到目标表。在这两个步骤之间,如果表S的数据被其他事务或操作更改,那么从表S选择的数据可能就不再准确或一致,插入到目标表的数据也会出现问题。

因此,为了在整个选择和插入过程中保持数据的一致性,MySQL在执行"insert ... select"操作时会对表S进行锁定。这样在锁定期间,其他事务或操作就不能更改表S的数据,从而保证了数据的一致性。

参考文章 mysql- insert select带来的锁问题,由于复制的实现机制不同,针对 insert select 语句,oracle 中不需要锁定源表。

MySQL 中可以通过开启 innodb_locks_unsafe_for_binlog 来避免这个现象,显然可能导致主从不一致,因此不建议使用。

针对给源表加锁的问题,建议使用 select ... into outfile 和 load data file 的组合来代替 insert select 语句,从而避免操作期间锁表。

需要注意的是如果主从版本不一致,也有可能导致主从不一致,原因是不同版本的加锁规则不同。

官方文档显示 5.7 中 CREATE TABLE ... SELECT 语句与 INSERT ... SELECT 语句加锁规则相同,也就是给源表加锁。

CREATE TABLE ... SELECT ... performs the SELECT with shared next-key locks or as a consistent read, as for INSERT ... SELECT.

早期版本(个人理解比如 5.5,未验证)中不给源表加锁,因此假如主库是 5.5,从库是 5.6+,对于 binlog_format = statement,主库不加锁从库加锁,导致主从不一致。

针对该问题,有两种方案,使用 binlog_format = row 或将主库升级为 5.7。

MySQL 5.7 does not allow a CREATE TABLE ... SELECT statement to make any changes in tables other than the table that is created by the statement. Some older versions of MySQL permitted these statements to do so; this means that, when using replication between a MySQL 5.6 or later replica and a source running a previous version of MySQL, a CREATE TABLE ... SELECT statement causing changes in other tables on the source fails on the replica, causing replication to stop. To prevent this from happening, you should use row-based replication, rewrite the offending statement before running it on the source, or upgrade the source to MySQL 5.7. (If you choose to upgrade the source, keep in mind that such a CREATE TABLE ... SELECT statement fails following the upgrade unless it is rewritten to remove any side effects on other tables.)

执行计划

参考 MySQL 45 讲,对比以下三条语句的执行计划。

其中:

下面分别测试验证。

首先是 insert select 全表,显示 Innodb_rows_read 值的变化与慢查询中的扫描行数相等,且等于表的大小。

然后是 insert select limit,显示 Innodb_rows_read 值的变化与慢查询中的扫描行数相等,且等于 3。

最后是 insert 循环写入,显示 Innodb_rows_read 值的变化与慢查询中的扫描行数不相等,后者是前者的两倍。

原因是 insert 循环写入的执行流程为:

显然,insert select 相同表与不同表的主要区别是后者需要使用临时表,原因是如果读出来的数据直接写回原表,可能导致读取到新插入的记录,注意事务隔离级别为 RR 时,事务可以看到自己修改的数据。

注意这里的测试结果与 45 讲中不同,45 讲中 limit 失效, t 表全表扫描,limit 在从临时表插回原表时生效。

参考文章 关于MySQL insert into ... select 的锁情况,判断原因是 select 语句中使用主键排序与非主键排序时的加锁规则不同。其中:

如下所示,对比测试使用主键排序与非主键排序。

其中:

因此,使用 insert select 时需要重点关注是否使用主键排序,减少扫描行数与加锁行数

知识点

innodb_locks_unsafe_for_binlog

row_search_mvcc 函数中判断加锁类型时,如果开启 innodb_locks_unsafe_for_binlog 参数,只会对行加锁,而不会锁间隙。

innodb_locks_unsafe_for_binlog 参数用于控制查询与索引扫描时是否使用 gap lock。默认 0,表示使用 gap lock。

RR 开启 innodb_locks_unsafe_for_binlog 参数时相当于退化为 RC,但有两点不同:

开启 innodb_locks_unsafe_for_binlog 时,将导致幻读,原因是间隙没有加锁,因此其他事务可以插入。

注意与 RC 相同,开启 innodb_locks_unsafe_for_binlog 参数时,外键冲突检测与唯一性检查时依然需要使用 gap lock。

Enabling innodb_locks_unsafe_for_binlog does not disable the use of gap locking for foreign-key constraint checking or duplicate-key checking.

除了影响查询语句的加锁规则,开启 innodb_locks_unsafe_for_binlog 参数时也会影响更新操作,具体规则为:

由于开启 innodb_locks_unsafe_for_binlog 参数时可能导致主从数据不一致,因此官方不建议使用,8.0.0 中已删除该参数,如果需要使用,建议使用 RC。

那么,针对 insert select,RC 中会存在数据不一致的问题吗?

实际上不会,原因是 RC 不支持 binlog_format=statement。具体操作中 RC 虽然可以将 binlog_format 修改为 statement,但是写入时报错。

参考官方文档,RC 中 binlog_format 仅支持 ROW 格式。

Only row-based binary logging is supported with the READ COMMITTED isolation level. If you use READ COMMITTED with binlog_format=MIXED, the server automatically uses row-based logging.

thr_lock_type

thr_lock_type 是表锁的一种类型,从名称判断是多线程锁数据结构。

尽管 MySQL 对外展示出现的只有读锁与写锁两种类型,但实际上内部枚举类型中定义了 14 种多线程锁类型,详见下表。

其中:

具体不同类型的区别还不太清楚,待后续分析。

LOCK_AUTO_INC

前面关注的都是 insert select 中给源表的加锁规则,其实目标表的加锁规则也需要关注,比如自增锁 LOCK_AUTO_INC。

LOCK_AUTO_INC 也是表锁的一种类型,用于给自增计数器加锁,从而保证自增列(AUTO_INCREMENT)值的唯一性与连续性。

自增锁的锁定范围是 SQL 级别,但是锁的释放时间与自增锁模式有关,通过参数innodb_autoinc_lock_mode控制。

取值包括:

“bulk inserts” use the special AUTO-INC table-level lock and hold it until the end of the statement. This applies to all INSERT ... SELECT, REPLACE ... SELECT, and LOAD DATA statements.

LOCK_AUTO_INC 加锁函数是 ha_innobase::innobase_lock_autoinc,实现逻辑见下图,其中通过加锁模式与 SQL 类型选择加锁实现。

从 trace 中也可以看到,ha_innobase::write_row 函数中 row_ins 函数开始前后分别调用函数 handler::update_auto_increment 与 ha_innobase::innobase_lock_autoinc。

代码注释显示 ha_innobase::write_row 函数中在插入开始前获取当前自增值,并在插入结束后更新当前自增值。

// storge/innobase/handler/ha_innodb.cc
 
  
  // 内部调用 ha_innobase::innobase_lock_autoinc 函数
  update_auto_increment()
  
  
  
  build_template(true);
  
 
 // 内部调用 row_ins 函数
 error = row_insert_for_mysql((byte*) record, m_prebuilt);

 
  auto_inc = innobase_next_autoinc(
    auto_inc,
    1, increment, offset,
    col_max_value);
 
 // 内部调用 ha_innobase::innobase_lock_autoinc 函数
  err = innobase_set_max_autoinc(
    auto_inc);

结论

insert select 语句的执行分两步,先 select 后 insert,其中 select 阶段需要给源表加 S 型 next-key lock。

原因是数据查询阶段中判断加锁类型时:

其中 prebuilt->select_lock_type 对应 thr_lock_type,表示表锁的类型,其中对于 insert select,对应 S 型锁。

而在行锁加锁时 lock_mode 同样使用 prebuilt->select_lock_type,个人判断行锁类型与表锁类型有关。

关于加锁类型,有两个参数需要关注:

insert select 给源表加锁的原因是保证日志与数据的一致性,否则 binlog_format = statement 时可能导致主从数据不一致。

针对 insert select 给源表加锁的问题,有以下几个优化建议:

即使使用 insert select,也需要注意以下两点:

待办

来源:丹柿小院内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯