不同于Oracle:MySQL的insert会阻塞update

  • GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。
  • GreatSQL是MySQL的国产分支版本,使用上与MySQL一致。
  • 作者: 王庆勋
  • 文章来源:GreatSQL社区原创

某银行客户在从Oracle迁移到MySQL的开发中,MySQL在READ-COMMITTED隔离级别下,出现了insert阻塞update的情况,但同样的情况下,Oracle的insert则不会阻塞update。本文通过复现该问题,分析MySQL的锁信息,确认是MySQL与Oracle在并发事务处理上的差异,在进行数据库迁移改造的程序开发应予以关注。

1.问题复现

1.1.环境准备

MySQL版本的8.0.26,隔离级别是READ-COMMITTED ,测试表t的字段a为主键。

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.26    |
+-----------+
1 row in set (0.02 sec)

mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set (0.00 sec)

mysql> desc t;
+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| a     | int  | NO   | PRI | NULL    |       |
| b     | int  | YES  |     | NULL    |       |
+-------+------+------+-----+---------+-------+
2 rows in set (0.01 sec)

mysql> select * from t;
+---+------+
| a | b    |
+---+------+
| 7 |    7 |
+---+------+
1 row in set (0.00 sec)

1.2. insert阻塞update的操作步骤

insert语句未提交时,update同样主键的数据会被阻塞。

session1session2
插入一条数据(a=8)后未提交。 mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into t values(8,8); Query OK, 1 row affected (0.01 sec)
更改数据,条件是a=8,将会被阻塞 mysql> update t set b=0 where a=8; <<挂起,等待innodb_lock_wait_timeout超时

2.分析原因

2.1.检查事务锁信息

mysql> select * from information_schema.innodb_trx\G
*************************** 1. row ***************************
                    trx_id: 3795
                 trx_state: LOCK WAIT
               trx_started: 2022-10-11 16:03:38
     trx_requested_lock_id: 139727275779216:52:4:3:139724882995456
          trx_wait_started: 2022-10-11 16:03:38
                trx_weight: 2
       trx_mysql_thread_id: 9346
                 trx_query: update t set b=0 where a=8
       trx_operation_state: starting index read
         trx_tables_in_use: 1
         trx_tables_locked: 1
          trx_lock_structs: 2
     trx_lock_memory_bytes: 1128
           trx_rows_locked: 1
         trx_rows_modified: 0
   trx_concurrency_tickets: 0
       trx_isolation_level: READ COMMITTED
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
       trx_schedule_weight: 1
*************************** 2. row ***************************
                    trx_id: 3790
                 trx_state: RUNNING
               trx_started: 2022-10-11 16:03:29
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 9320
                 trx_query: NULL
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 1
          trx_lock_structs: 2
     trx_lock_memory_bytes: 1128
           trx_rows_locked: 1
         trx_rows_modified: 1
   trx_concurrency_tickets: 0
       trx_isolation_level: READ COMMITTED
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
       trx_schedule_weight: NULL
2 rows in set (0.00 sec)

说明:通过InnoDB的事务表innodb_trx查询到thread_id=9346的事务3795正在等待锁(trx_state: LOCK WAIT),
thread_id=9320的事务3790正在执行(trx_state: RUNNING)。

mysql> select * from performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139727275779216:1113:139724882998560
ENGINE_TRANSACTION_ID: 3795
            THREAD_ID: 9441
             EVENT_ID: 5000
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: t
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139724882998560
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139727275779216:52:4:3:139724882995456
ENGINE_TRANSACTION_ID: 3795
            THREAD_ID: 9441
             EVENT_ID: 5012
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: t
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139724882995456
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: WAITING
            LOCK_DATA: 8
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139727275781640:1113:139724883017072
ENGINE_TRANSACTION_ID: 3790
            THREAD_ID: 9415
             EVENT_ID: 15467
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: t
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 139724883017072
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 139727275781640:52:4:3:139724883013968
ENGINE_TRANSACTION_ID: 3790
            THREAD_ID: 9441
             EVENT_ID: 5007
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: t
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 139724883013968
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 8
4 rows in set (0.00 sec)
说明:事务3795正在等待LOCK_TYPE: RECORD,LOCK_MODE:X,REC_NOT_GAP,等待的主键值为8;
事务3790已获取主键值为8的LOCK_TYPE: RECORD,LOCK_MODE:X,REC_NOT_GAP锁。

mysql> select * from sys.innodb_lock_waits\G
*************************** 1. row ***************************
                wait_started: 2022-10-11 16:03:38
                    wait_age: 00:02:50
               wait_age_secs: 170
                locked_table: `testdb`.`t`
         locked_table_schema: testdb
           locked_table_name: t
      locked_table_partition: NULL
   locked_table_subpartition: NULL
                locked_index: PRIMARY
                 locked_type: RECORD
              waiting_trx_id: 3795
         waiting_trx_started: 2022-10-11 16:03:38
             waiting_trx_age: 00:02:50
     waiting_trx_rows_locked: 1
   waiting_trx_rows_modified: 0
                 waiting_pid: 9346
               waiting_query: update t set b=0 where a=8
             waiting_lock_id: 139727275779216:52:4:3:139724882995456
           waiting_lock_mode: X,REC_NOT_GAP
             blocking_trx_id: 3790
                blocking_pid: 9320
              blocking_query: NULL
            blocking_lock_id: 139727275781640:52:4:3:139724883013968
          blocking_lock_mode: X,REC_NOT_GAP
        blocking_trx_started: 2022-10-11 16:03:29
            blocking_trx_age: 00:02:59
    blocking_trx_rows_locked: 1
  blocking_trx_rows_modified: 1
     sql_kill_blocking_query: KILL QUERY 9320
sql_kill_blocking_connection: KILL 9320
1 row in set (0.01 sec)
说明:事务3795等待testdb.t上的rec_not_gap独占锁,事务3790持有该独占锁。
mysql> select distinct,* from sys.processlist where conn_id in (select trx_mysql_thread_id from information_schema.innodb_trx)\G
*************************** 1. row ***************************
                thd_id: 9441
               conn_id: 9346
                  user: admin@172.17.128.73
                    db: testdb
               command: Query
                 state: updating
                  time: 141
     current_statement: update t set b=0 where a=8
     statement_latency: 2.37 min
              progress: NULL
          lock_latency: 431.00 us
         rows_examined: 0
             rows_sent: 0
         rows_affected: 0
            tmp_tables: 0
       tmp_disk_tables: 0
             full_scan: NO
        last_statement: NULL
last_statement_latency: NULL
        current_memory: 140.15 KiB
             last_wait: wait/io/table/sql/handler
     last_wait_latency: Still Waiting
                source: handler.cc:3250
           trx_latency: 13.30 min
             trx_state: ACTIVE
        trx_autocommit: NO
                   pid: 9632
          program_name: mysql
*************************** 2. row ***************************
                thd_id: 9415
               conn_id: 9320
                  user: admin@172.17.128.73
                    db: testdb
               command: Sleep
                 state: NULL
                  time: 801
     current_statement: NULL
     statement_latency: NULL
              progress: NULL
          lock_latency: 288.00 us
         rows_examined: 0
             rows_sent: 0
         rows_affected: 1
            tmp_tables: 0
       tmp_disk_tables: 0
             full_scan: NO
        last_statement: insert into t values(8,8)
last_statement_latency: 765.23 us
        current_memory: 218.19 KiB
             last_wait: wait/io/socket/sql/client_connection
     last_wait_latency: Still Waiting
                source: viosocket.cc:146
           trx_latency: 13.52 min
             trx_state: ACTIVE
        trx_autocommit: NO
                   pid: 9600
          program_name: mysql
说明:被阻塞事务执行的sql语句update t set b=0 where a=8,
阻塞事务执行的sql语句是insert into t values(8,8)。

说明:

MySQL的隔离级别是通过索引上的锁实现并发事务控制的。在READ-COMMITTED隔离级别下,session1在执行insert语句时,在主键索引上获取了a=8的行记录独占锁,以禁止插入相同主键的数据;session2如果同时插入相同的主键数据被阻塞,容易理解(Oracle也同样阻塞)。出于同样的原因session2执行update时,由于无法获取a=8的行记录独占锁,同样也会被阻塞。

2.2.验证MySQL事务未提交时已写入数据文件

验证事务未提交时,insert语句已将数据写入数据文件,索引数据也已生成。

测试表test1
mysql> CREATE TABLE `test1` (
    ->   `id` int NOT NULL AUTO_INCREMENT,
    ->   `k` int NOT NULL DEFAULT '0',
    ->   `c` char(120) NOT NULL DEFAULT '',
    ->   `pad` char(60) NOT NULL DEFAULT '',
    ->   PRIMARY KEY (`id`),
    ->   KEY `k_1` (`k`)
    -> ) ;
Query OK, 0 rows affected (0.07 sec)

开启一个事务,插入10万条数据。
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test1 select * from sbtest1;
Query OK, 100000 rows affected (1.44 sec)
Records: 100000  Duplicates: 0  Warnings: 0

检查表的data_length和index_length
mysql> show table status where name like 'test1'\G
*************************** 1. row ***************************
           Name: test1
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 98712
 Avg_row_length: 228
    Data_length: 22593536
Max_data_length: 0
   Index_length: 2637824
      Data_free: 4194304
 Auto_increment: 100001
    Create_time: 2022-10-11 22:14:50
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 rows in set (0.01 sec)

回滚insert操作
mysql> rollback;
Query OK, 0 rows affected (1.35 sec)

更新统计信息
mysql> analyze table test1;

再次检查表的data_length和index_length
mysql> show table status where name like 'test1'\G 
*************************** 1. row ***************************
           Name: test1
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 0
 Avg_row_length: 0
    Data_length: 16384
Max_data_length: 0
   Index_length: 16384
      Data_free: 29360128
 Auto_increment: 100001
    Create_time: 2022-10-11 22:22:36
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 row in set (0.00 sec)

检查数据文件的大小
[root@host73 testdb]# ll *test1.ibd
-rw-r----- 1 greatdb greatdb 30408704 Oct 11 15:12 sbtest1.ibd
-rw-r----- 1 greatdb greatdb 33554432 Oct 11 22:24 test1.ibd

说明:

MySQL在执行insert 语句进行数据插入,未提交时,数据也已写入表的聚集索引,辅助索引也已生成。MySQL可以使用未提交数据的索引,通过锁机制实现事务的并发控制。

3.Oracle中insert没有阻塞update

在Oracle中,创建同样的测试表t,执行同样的insert和update,但insert不会阻塞update。

CREATE TABLE t (
  a int NOT NULL PRIMARY KEY ,
  b int DEFAULT NULL
);
insert into t values(7,7);
commit;

执行相同的insert和update语句。

session1session2
SQL> insert into t values(8,8);
1 row created.SQL> update t set b=0 where a=8;0 rows updated.
---

Enjoy GreatSQL :)

关于 GreatSQL

GreatSQL是由万里数据库维护的MySQL分支,专注于提升MGR可靠性及性能,支持InnoDB并行查询特性,是适用于金融级应用的MySQL分支版本。

相关链接: GreatSQL社区 Gitee GitHub Bilibili

GreatSQL社区:

image

社区有奖建议反馈: https://greatsql.cn/thread-54-1-1.html

社区博客有奖征稿详情: https://greatsql.cn/thread-100-1-1.html

社区2022年度勋章获奖名单: https://greatsql.cn/thread-184-1-1.html

(对文章有疑问或者有独到见解都可以去社区官网提出或分享哦~)

技术交流群:

微信&QQ群:

QQ群:533341697

微信群:添加GreatSQL社区助手(微信号:wanlidbc )好友,待社区助手拉您进群。

<think>首先,用户的问题是关于SQL中实现"insert or update"操作的方法。他们提供了几个引用,我应该参考这些引用来构建我的回答。 回顾用户提供的引用: - 引用[1]: 讨论了MySQL中的批量插入操作,提到了insert into values和insert into select,以及replace into和insert into on duplicate key update。这些用于插入或更新数据。 - 引用[2]: 提到了SQL命令INSERT OR UPDATE,但似乎是示例性的,可能不是标准SQL。 - 引用[3]: 解释了如何在MySQL中实现insertOrUpdate,提到了INSERT INTO ... ON DUPLICATE KEY UPDATE, REPLACE INTO, 和 INSERT IGNORE INTO。 - 引用[4]: 涵盖了SQL语句中的insertupdate操作,包括基本的语法示例。 - 引用[5]: 讨论了insert or update语句中的一些问题,比如在设置值时遇到的困难。 用户的核心问题是:"我想查找SQL中实现insert or update操作的方法 请问SQL实现insert or update操作的方法"。所以,我需要总结SQL中实现这种操作的方法。 现在,系统指令要求: - 所有行内数学表达式必须使用$...$格式(但这里可能不涉及数学表达式,所以可能不需要)。 - 独立公式使用$$...$$(同样,不相关)。 - LaTeX语法正确(在代码块中)。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如[^1]。 - 结构清晰,逐步解决问题。 结构我的回答: 1. **引言**:简要解释什么是"insert or update"操作,即当记录存在时更新,不存在时插入。 2. **主要方法**: - 基于引用,提到SQL中的几种方式: - **INSERT INTO ... ON DUPLICATE KEY UPDATE**(MySQL特有) - **REPLACE INTO**(MySQL特有) - **INSERT IGNORE INTO**(MySQL特有) - 其他数据库可能有类似功能,如SQLite的INSERT OR REPLACE、INSERT OR IGNORE。 - 标准SQL方式:使用MERGE或UPSERT语句(取决于数据库)。 - 在引用中提到了INSERT OR UPDATE,但引用[2]可能指的是自定义或特定实现。 - 解释每个方法的工作原理、优缺点。 3. **示例代码**:提供一些SQL示例代码块来展示用法。 4. **注意事项**:引用中提到的问题,如加锁问题(INSERT INTO SELECT)、REPLACE INTO与ON DUPLICATE UPDATE的区别(REPLACE是delete-insert,后者是update)、INSERT IGNORE的忽略冲突。 5. **推荐或总结**:或许提到引用[1]中的图书推荐(但不要过度推销)。 6. **相关问题**:生成2-3个相关问题。 使用中文回答。 添加引用标识:当引用特定段落时,在末尾添加[^n]。 步骤: - 介绍概念。 - 描述方法: - INSERT INTO ... ON DUPLICATE KEY UPDATE - REPLACE INTO - INSERT IGNORE INTO - MERGE语句(如SQL Server的MERGE) - 其他数据库的UPSERT - 给出示例。 - 讨论常见问题。 - 生成相关问题。 确保回答真实可靠:基于提供的引用和一般知识。 相关问题生成:参考示例,生成2-3个问题。 </think>以下是针对您问题的完整解答:SQL中实现"insert or update"操作(也称为UPSERT操作)的方法,是指当目标表中已存在指定唯一键(如主键或唯一索引)的记录时执行更新(UPDATE),否则执行插入(INSERT)。这种操作在数据处理中非常常见,例如处理批量导入或同步数据时。基于您提供的引用内容(特别是引用[1],[3]和[5]),我将逐步解释SQL中实现这一操作的主流方法、语法示例、注意事项及相关问题。 ### 1. SQL中实现insert or update的主要方法 SQL标准中并没有统一的UPSERT命令,但不同数据库系统提供了类似功能。以下是常见的几种方式,参考了MySQL的实现(如引用[1]和[3]所述),并附带其他数据库的注意事项。 #### (1) **INSERT INTO ... ON DUPLICATE KEY UPDATE (MySQL特有)** - **工作原理**:当插入的记录与表中唯一键冲突时,自动执行更新操作;否则,执行插入操作。这不是删除后插入,而是直接更新现有记录,因此性能更高,避免数据丢失。 - **语法示例**: ```sql INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...) ON DUPLICATE KEY UPDATE column1 = VALUES(column1), column2 = VALUES(column2); ``` - 示例:如果表`goods`的主键`id`冲突,则更新价格和数量;否则插入新记录。 ```sql INSERT INTO goods (id, goodsName, price, num) VALUES (1, 'apple', 12.5, 200) ON DUPLICATE KEY UPDATE price = VALUES(price), num = VALUES(num); ``` - **优点**:高效,只在冲突时更新特定字段,不影响其他列。 - **注意点**:该方法依赖于唯一键(如主键或唯一索引)。如果表中无唯一键,操作会失败(引用[3])。 #### (2) **REPLACE INTO (MySQL特有)** - **工作原理**:如果唯一键冲突,先删除现有记录,再插入新记录;如果没有冲突,则直接插入。这相当于先执行DELETE后INSERT,可能导致数据完整性问题和性能开销。 - **语法示例**: ```sql REPLACE INTO table_name (column1, column2, ...) VALUES (value1, value2, ...); ``` - 示例:在`goods`表中,如果`id=1`的记录存在,则删除旧记录并插入新值。 ```sql REPLACE INTO goods (id, goodsName, price) VALUES (1, 'pear', 13); ``` - **优点**:语法简单。 - **缺点**:删除-插入操作可能触发级联删除或影响自增ID,不适合高并发场景(引用[1])。 #### (3) **INSERT IGNORE INTO (MySQL特有)** - **工作原理**:当唯一键冲突时,忽略冲突行,不执行插入或更新;没有冲突时正常插入。这仅适用于插入场景,本质上是“插入或跳过”。 - **语法示例**: ```sql INSERT IGNORE INTO table_name (column1, column2, ...) VALUES (value1, value2, ...); ``` - 示例:向`goods`表插入记录,如果`id`冲突,则忽略该行。 ```sql INSERT IGNORE INTO goods (id, goodsName) VALUES (1, 'straws'); ``` - **优点**:避免报错,适合容忍数据丢失的场景。 - **缺点**:不能执行更新操作,只能跳过冲突,用途有限(引用[3])。 #### (4) **其他数据库的UPSERT实现** - **SQLite**:使用`INSERT OR REPLACE`或`INSERT OR IGNORE`,类似于MySQL的REPLACE和IGNORE(引用[2]提到类似功能)。 - **PostgreSQL**:使用`INSERT ... ON CONFLICT DO UPDATE`语句,语法与MySQL的ON DUPLICATE KEY UPDATE相似。 - **SQL Server**:使用`MERGE`语句(标准SQL支持),它允许在一条命令中处理插入、更新和删除。 - 示例: ```sql MERGE INTO target_table AS t USING source_table AS s ON t.key = s.key WHEN MATCHED THEN UPDATE SET t.column1 = s.column1 WHEN NOT MATCHED THEN INSERT (column1, column2) VALUES (s.column1, s.column2); ``` - **Oracle**:同样支持`MERGE`语句。 - **注意**:标准SQL的MERGE是最通用的方式,但语法较复杂,适合跨数据库兼容(引用[2]和[4])。 ### 2. 注意事项和常见问题 - **性能与加锁问题**:在批量操作时(如INSERT INTO SELECT),需注意加锁行为可能导致阻塞。例如,INSERT INTO SELECT可能锁定整表,影响并发性能(引用[1])。建议在事务中使用,并添加索引优化。 - **原子性与一致性**:所有方法在事务中保证原子性(要么全部成功,要么全部回滚)(引用[1])。但REPLACE INTO可能破坏事务完整性,因为它涉及删除操作。 - **值设置问题**:在ON DUPLICATE KEY UPDATE中,使用`VALUES(column)`引用插入值。如果值计算复杂(如递增计数器),可能需要先查询数据库再处理,避免语法错误(引用[5]中提到的问题)。 - **约束处理**:UPSERT操作依赖唯一键。如果表无主键或唯一索引,操作可能失败或行为不一致(引用[3])。 - **推荐方法**:优先使用ON DUPLICATE KEY UPDATE或MERGE,它们性能更好且避免数据丢失风险。REPLACE INTO和INSERT IGNORE应谨慎使用。 ### 3. 总结 SQL中实现insert or update操作的核心是通过数据库特有的语法(如MySQL的ON DUPLICATE KEY UPDATE)或标准MERGE语句来处理唯一键冲突。选择方法时需考虑数据库类型、性能需求和数据完整性。例如,在MySQL中,ON DUPLICATE KEY UPDATE是最佳实践,因为它高效且直接更新(引用[1])。 如果您使用的是MySQL,推荐参考《MySQL技术大全:开发、优化与运维实战》一书,它详细介绍了这些操作的优化和实战案例(引用[1])。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值