方案概述

1. 现象

 当实际生产中,明明查询只有一条数据,没有其它用户修改这一笔数据,当我们需要修改这一笔数据时,往往会卡住,到底是怎么回事呢?

 

2. 模拟

mysql的事务级别是RC

master [localhost:21829] {root} (test) > show variables like '%iso%';
+-----------------------+----------------+
| Variable_name         | Value          |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
1 row in set (0.01 sec)

 Session 1:

Session 1:
 master [localhost:21829] {root} (test) > begin;
Query OK, 0 rows affected (0.00 sec)
 
master [localhost:21829] {root} (test) > select * from test_idx where age=30;
+----+----------+------+------+
| id | name     | age  | dept |
+----+----------+------+------+
|  1 | zhangsan |   30 | hhh  |
|  2 | zhang    |   30 | it   |
|  5 | lisi     |   30 | it   |
+----+----------+------+------+
3 rows in set (0.00 sec)

master [localhost:21829] {root} (test) > select * from  test_idx where name='zhang' and age=30 for update;
+----+-------+------+------+
| id | name  | age  | dept |
+----+-------+------+------+
|  2 | zhang |   30 | it   |
+----+-------+------+------+
1 row in set (0.01 sec)

 Session 2:

Session 2:
master [localhost:21829] {root} (test) > begin;
Query OK, 0 rows affected (0.01 sec)
 
master [localhost:21829] {root} (test) > select * from test_idx where name='zhangsan' and age=30;
+----+----------+------+------+
| id | name     | age  | dept |
+----+----------+------+------+
|  1 | zhangsan |   30 | hhh  |
+----+----------+------+------+
1 row in set (0.00 sec)
 
master [localhost:21829] {root} (test) > update test_idx set dept='ab' where name='zhangsan' and age=30;
。。。。。。
 这里卡住了。

 Session 3:

Session 3:
  查锁
 
master [localhost:21829] {root} ((none)) > select locked_table,locked_table_name,locked_index,locked_type,waiting_trx_id,waiting_pid,blocking_trx_id,blocking_trx_started,sql_kill_blocking_query,sql_kill_blocking_connection,blocking_lock_mode from sys.innodb_lock_waits \G
*************************** 1. row ***************************
                locked_table: `test`.`test_idx`
           locked_table_name: test_idx
                locked_index: idx_age
                 locked_type: RECORD
              waiting_trx_id: 23098
                 waiting_pid: 11
             blocking_trx_id: 23094
        blocking_trx_started: 2024-05-06 19:05:32
     sql_kill_blocking_query: KILL QUERY 8
sql_kill_blocking_connection: KILL 8
          blocking_lock_mode: X,REC_NOT_GAP
1 row in set (0.00 sec)

 可以看到锁类型是记录锁,同时锁在索引idx_age,并且是X,REC_NOT_GAP。

 先看一下表结构:

master [localhost:21829] {root} (test) > show create table test_idx;
+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                                                                    |
+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| test_idx | CREATE TABLE `test_idx` (
  `id` int NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  `age` int DEFAULT NULL,
  `dept` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci


可以看出第二索引idx_age是单列age的索引。mysql查找数据先是通过二级索引,找到主键的键值,再通过主键键值找到主键的物理行索引到数据的。从上面的例上可以看出,索引idx_age值为30并不知道能索引到多少笔数据,mysql在修改数据时,就把这个idx_age值为30的都上锁,idx_age对应的主键键值有id为1,2,5,这三行数据也需要对应的加锁,所以sql  select * from  test_idx where name='zhang' and age=30 for update;虽然只索引到1笔数据,但是索引加锁是锁了3笔。那这个问题要如何解决呢?

其实很简单,把单列改索改为复合索引就可以 idx_age(age,name)。


3. 小结

  • mysql锁数据是通过索引来加锁数据。(表没有索引的是通过主键来锁数据,如果表没主键,mysql会自动创建隐藏的主键)
  • mysql建索引是不能随便建的,需要根据实际的应用SQL建索引。