Skip to content
清晨的一缕阳光
返回

MySQL 锁机制详解

核心概念

锁是数据库实现并发控制的核心机制,用于保证多个事务并发访问数据时的正确性和一致性。MySQL 中的锁可以从不同维度进行分类:

锁的粒度分类

锁类型锁定范围并发度适用场景
全局锁整个实例最低全库逻辑备份
表级锁整张表较低DDL 操作、MyISAM 引擎
行级锁单行记录最高InnoDB 引擎、高并发场景

锁的行为分类

锁的状态分类

锁机制底层原理

InnoDB 锁的实现方式

InnoDB 的行锁是通过索引项实现的,而不是锁住实际的物理行。这意味着:

-- 如果 where 条件没有使用索引,InnoDB 会退化为表锁
SELECT * FROM users WHERE name = 'john';  -- name 无索引,锁全表

-- 使用主键查询,只锁单行
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 只锁 id=1 的行

锁的存储结构

InnoDB 将锁信息存储在内存的锁结构中,主要包括:

lock_t (锁结构)
├── lock_heap_t (堆锁信息)
├── lock_rec_t (记录锁)
└── lock_gap_t (间隙锁)

每个锁记录包含:

各种锁类型详解

1. 记录锁(Record Lock)

记录锁是最基本的行锁,锁定索引记录本身。

-- 示例:锁定单条记录
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 此时其他事务无法修改 id=1 的记录
COMMIT;

特点:

2. 间隙锁(Gap Lock)

间隙锁锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录。

-- 示例:范围查询产生间隙锁
BEGIN;
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 锁定 (10, 20) 区间,其他事务无法插入 id=15 的记录
COMMIT;

锁定范围:

3. 临键锁(Next-Key Lock)

临键锁 = 记录锁 + 间隙锁,是 InnoDB 的默认锁机制。

Next-Key Lock = Record Lock + Gap Lock

锁定范围:

-- 可重复隔离级别下,范围查询使用临键锁
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM users WHERE id > 10 AND id < 20 FOR UPDATE;
-- 锁定区间:(10, 20],包含 20 但不包含 10
COMMIT;

4. 意向锁(Intention Lock)

意向锁是表级锁,用于表明事务希望在行上获取什么类型的锁。

意向锁类型含义兼容的表锁
意向共享锁(IS)事务打算获取行的 S 锁IS, IX
意向排他锁(IX)事务打算获取行的 X 锁IX

作用:

-- 执行以下语句会自动添加意向锁
SELECT ... FOR SHARE;   -- 添加 IS 锁
SELECT ... FOR UPDATE;  -- 添加 IX 锁

5. 自增锁(AUTO-INC Lock)

自增锁是表级锁,用于保证自增列的连续性和唯一性。

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10, 2)
);

-- 插入时会自动获取自增锁
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);

配置参数:

-- 控制自增锁的分配策略
innodb_autoinc_lock_mode = 0;  -- 传统模式(语句级锁)
innodb_autoinc_lock_mode = 1;  -- 连续模式(默认)
innodb_autoinc_lock_mode = 2;  -- 交错模式(最高并发)

锁的兼容性矩阵

行锁兼容性

当前锁\请求锁S 锁X 锁
S 锁✅ 兼容❌ 冲突
X 锁❌ 冲突❌ 冲突

意向锁兼容性

当前锁\请求锁ISIXS 表锁X 表锁
IS
IX
S 表锁
X 表锁

锁的使用场景与示例

场景 1:防止丢失更新

-- 错误示例:不使用锁,可能导致丢失更新
-- 事务 A
SELECT balance FROM accounts WHERE id = 1;  -- 余额 1000
-- 事务 B
SELECT balance FROM accounts WHERE id = 1;  -- 余额 1000
-- 事务 A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 900
-- 事务 B
UPDATE accounts SET balance = balance - 200 WHERE id = 1;  -- 800(错误!)

-- 正确示例:使用 FOR UPDATE
-- 事务 A
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- 1000
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 900
COMMIT;
-- 事务 B(等待 A 提交后)
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;  -- 900
UPDATE accounts SET balance = balance - 200 WHERE id = 1;  -- 700
COMMIT;

场景 2:批量更新防止死锁

-- 错误示例:不同顺序更新可能导致死锁
-- 事务 A
UPDATE users SET status = 1 WHERE id IN (1, 2, 3);
-- 事务 B
UPDATE users SET status = 1 WHERE id IN (3, 2, 1);  -- 可能死锁

-- 正确示例:固定顺序更新
-- 所有事务都按 id 升序更新
UPDATE users SET status = 1 WHERE id IN (1, 2, 3);

场景 3:范围查询的间隙锁

-- 创建测试表
CREATE TABLE test_locks (
    id INT PRIMARY KEY,
    value INT
);

INSERT INTO test_locks VALUES (1, 10), (5, 50), (10, 100);

-- 事务 A:范围查询
BEGIN;
SELECT * FROM test_locks WHERE id BETWEEN 5 AND 10 FOR UPDATE;
-- 锁定范围:(5, 10],包含 5 和 10

-- 事务 B:尝试插入(会被阻塞)
INSERT INTO test_locks VALUES (7, 70);  -- 等待事务 A 提交

-- 事务 B:尝试更新边界(会被阻塞)
UPDATE test_locks SET value = 55 WHERE id = 5;  -- 等待

-- 事务 B:更新范围外(可以执行)
UPDATE test_locks SET value = 15 WHERE id = 1;  -- 成功

场景 4:唯一索引与普通索引的锁差异

-- 唯一索引查询(等值):只锁记录
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 只锁 id=1

-- 普通索引查询(等值):锁记录 + 间隙
SELECT * FROM users WHERE name = 'john' FOR UPDATE;  
-- 锁定所有 name='john' 的记录 + 间隙(防止插入)

-- 范围查询:临键锁
SELECT * FROM users WHERE id > 10 FOR UPDATE;  
-- 锁定 (10, +∞) 区间

锁的查看与诊断

查看锁信息

-- 查看锁等待情况(MySQL 5.7+)
SELECT 
    request_trx_id,
    requested_lock_id,
    blocking_trx_id,
    blocking_lock_id
FROM performance_schema.data_lock_waits;

-- 查看当前持有的锁
SELECT 
    engine_transaction_id,
    lock_table,
    lock_index,
    lock_type,
    lock_mode,
    lock_data
FROM performance_schema.data_locks;

-- 查看锁等待(MySQL 8.0+)
SELECT * FROM sys.innodb_lock_waits;

查看事务信息

-- 查看所有事务
SELECT * FROM information_schema.INNODB_TRX;

-- 查看事务详细信息
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_requested_lock_id,
    trx_wait_started,
    trx_mysql_thread_id,
    trx_query
FROM information_schema.INNODB_TRX
WHERE trx_state = 'LOCK WAIT';

查看锁等待链

-- 完整的锁等待分析
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;

锁优化策略

1. 减少锁粒度

-- 优化前:范围大,锁多
UPDATE orders SET status = 1 WHERE create_time > '2024-01-01';

-- 优化后:分批处理,减少锁持有时间
UPDATE orders SET status = 1 
WHERE create_time > '2024-01-01' AND id BETWEEN 1 AND 1000;
UPDATE orders SET status = 1 
WHERE create_time > '2024-01-01' AND id BETWEEN 1001 AND 2000;

2. 缩短事务时间

-- 错误示例:长事务持有锁时间长
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 执行大量业务逻辑...
-- 发送 HTTP 请求...
-- 写日志...
COMMIT;

-- 正确示例:快速提交
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 再执行其他业务逻辑

3. 避免死锁的技巧

-- 技巧 1:固定访问顺序
-- 所有事务都按 id 升序访问
UPDATE accounts SET balance = balance - 100 WHERE id IN (1, 2, 3);

-- 技巧 2:使用较低隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 技巧 3:添加超时机制
SET innodb_lock_wait_timeout = 10;  -- 10 秒超时

4. 合理使用索引

-- 无索引:全表锁
UPDATE users SET status = 1 WHERE email = 'test@example.com';

-- 添加索引:行锁
CREATE INDEX idx_email ON users(email);
UPDATE users SET status = 1 WHERE email = 'test@example.com';

常见陷阱与注意事项

陷阱 1:隐式类型转换导致锁升级

-- phone 是 VARCHAR 类型
-- 错误:数字不加引号,导致类型转换,锁全表
SELECT * FROM users WHERE phone = 13800138000 FOR UPDATE;

-- 正确:加引号,使用索引
SELECT * FROM users WHERE phone = '13800138000' FOR UPDATE;

陷阱 2:函数导致索引失效

-- 错误:对索引列使用函数,锁全表
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01' FOR UPDATE;

-- 正确:使用范围查询
SELECT * FROM orders 
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02' FOR UPDATE;

陷阱 3:间隙锁导致并发度降低

-- 在可重复读隔离级别下
-- 范围查询会锁住整个区间,影响并发
SELECT * FROM users WHERE id > 100 FOR UPDATE;

-- 解决方案 1:使用读已提交隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 解决方案 2:优化为等值查询
SELECT * FROM users WHERE id = 101 FOR UPDATE;

陷阱 4:自增锁导致的性能问题

-- 批量插入时,自增锁会影响并发
INSERT INTO orders (user_id, amount) 
SELECT user_id, amount FROM temp_orders;

-- 优化:使用 innodb_autoinc_lock_mode = 2
SET GLOBAL innodb_autoinc_lock_mode = 2;

总结

本文详细介绍了 MySQL 中的各种锁机制:

核心要点

  1. 锁的粒度:行锁 > 表锁 > 全局锁(并发度从高到低)
  2. InnoDB 锁实现:基于索引,无索引会退化为表锁
  3. 锁类型
    • 记录锁:锁定索引记录
    • 间隙锁:锁定索引间隙
    • 临键锁:记录锁 + 间隙锁(默认)
    • 意向锁:表级锁,表示锁定意图
  4. 锁兼容性:S 锁与 S 锁兼容,其他组合基本冲突
  5. 死锁预防:固定访问顺序、缩短事务、合理索引

最佳实践

参考资料


分享这篇文章到:

上一篇文章
HashMap 完整系列详解
下一篇文章
Java 包装类型详解