某银行客户在从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 inset(0.02 sec)mysql> show variables like'transaction_isolation';+-----------------------+----------------+| Variable_name         | Value          |+-----------------------+----------------+| transaction_isolation | READ-COMMITTED |+-----------------------+----------------+1 row inset(0.00 sec)mysql>desc t;+-------+------+------+-----+---------+-------+| Field | Type |Null| Key | Default | Extra |+-------+------+------+-----+---------+-------+| a     |int| NO   | PRI |NULL||| b     |int| YES  ||NULL||+-------+------+------+-----+---------+-------+2 rows inset(0.01 sec)mysql>select*from t;+---+------+| a | b    |+---+------+|7|7|+---+------+1 row inset(0.00 sec)

1.2. insert阻塞update的操作步骤

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

session1

session2

插入一条数据(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-1116:03:38     trx_requested_lock_id:139727275779216:52:4:3:139724882995456          trx_wait_started:2022-10-1116:03:38                trx_weight:2       trx_mysql_thread_id:9346                 trx_query:update t set b=0where 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:1trx_last_foreign_key_error:NULL trx_adaptive_hash_latched:0 trx_adaptive_hash_timeout:0          trx_is_read_only:0trx_autocommit_non_locking:0       trx_schedule_weight:1***************************2. row ***************************                    trx_id:3790                 trx_state: RUNNING               trx_started:2022-10-1116: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:1trx_last_foreign_key_error:NULL trx_adaptive_hash_latched:0 trx_adaptive_hash_timeout:0          trx_is_read_only:0trx_autocommit_non_locking:0       trx_schedule_weight:NULL2 rows inset(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:139724882998560ENGINE_TRANSACTION_ID:3795            THREAD_ID:9441             EVENT_ID:5000        OBJECT_SCHEMA: testdb          OBJECT_NAME: t       PARTITION_NAME:NULL    SUBPARTITION_NAME:NULL           INDEX_NAME:NULLOBJECT_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:139724882995456ENGINE_TRANSACTION_ID:3795            THREAD_ID:9441             EVENT_ID:5012        OBJECT_SCHEMA: testdb          OBJECT_NAME: t       PARTITION_NAME:NULL    SUBPARTITION_NAME:NULL           INDEX_NAME: PRIMARYOBJECT_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:139724883017072ENGINE_TRANSACTION_ID:3790            THREAD_ID:9415             EVENT_ID:15467        OBJECT_SCHEMA: testdb          OBJECT_NAME: t       PARTITION_NAME:NULL    SUBPARTITION_NAME:NULL           INDEX_NAME:NULLOBJECT_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:139724883013968ENGINE_TRANSACTION_ID:3790            THREAD_ID:9441             EVENT_ID:5007        OBJECT_SCHEMA: testdb          OBJECT_NAME: t       PARTITION_NAME:NULL    SUBPARTITION_NAME:NULL           INDEX_NAME: PRIMARYOBJECT_INSTANCE_BEGIN:139724883013968            LOCK_TYPE: RECORD            LOCK_MODE: X,REC_NOT_GAP          LOCK_STATUS: GRANTED            LOCK_DATA:84 rows inset(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-1116: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-1116: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=0where 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-1116:03:29            blocking_trx_age:00:02:59    blocking_trx_rows_locked:1  blocking_trx_rows_modified:1     sql_kill_blocking_query: KILL QUERY 9320sql_kill_blocking_connection: KILL 93201 row inset(0.01 sec)说明:事务3795等待testdb.t上的rec_not_gap独占锁,事务3790持有该独占锁。mysql>selectdistinct,*from sys.processlistwhere 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: updatingtime:141     current_statement:update t set b=0where 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:NULLlast_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:NULLtime: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:insertinto 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=0where 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语句已将数据写入数据文件,索引数据也已生成。

测试表test1mysql>CREATETABLE `test1` (->   `id` intNOTNULL AUTO_INCREMENT,->   `k` intNOTNULL DEFAULT '0',->   `c` char(120)NOTNULL DEFAULT '',->   `pad` char(60)NOTNULL 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>insertinto test1 select*from sbtest1;Query OK,100000 rows affected (1.44 sec)Records:100000  Duplicates:0  Warnings:0检查表的data_length和index_lengthmysql> 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:22593536Max_data_length:0   Index_length:2637824      Data_free:4194304 Auto_increment:100001    Create_time:2022-10-1122:14:50    Update_time:NULL     Check_time:NULL      Collation: utf8mb4_0900_ai_ci       Checksum:NULL Create_options:        Comment:1 rows inset(0.01 sec)回滚insert操作mysql> rollback;Query OK,0 rows affected (1.35 sec)更新统计信息mysql> analyze table test1;再次检查表的data_length和index_lengthmysql> 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:16384Max_data_length:0   Index_length:16384      Data_free:29360128 Auto_increment:100001    Create_time:2022-10-1122:22:36    Update_time:NULL     Check_time:NULL      Collation: utf8mb4_0900_ai_ci       Checksum:NULL Create_options:        Comment:1 row inset(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。

CREATETABLE t (  a intNOTNULL PRIMARY KEY ,  b int DEFAULT NULL);insertinto t values(7,7);commit;

执行相同的insert和update语句。

session1

session2

SQL> insert into t values(8,8);

1 row created.

SQL> update t set b=0 where a=8;0 rows updated.