📊 SQL 入门 Day 20:锁机制

场景引入

小李的公司电商系统突然出现大量订单超时,排查发现是多个事务同时修改同一条热门商品库存时互相等待,导致系统几乎"卡死"。DBA 赶到后看了一眼监控,说:"锁竞争了。" 锁是数据库并发控制的基石,理解锁机制才能写出既安全又高效的高并发 SQL。

一、为什么需要锁?

数据库是共享资源,多个会话(Session / 连接)可能同时读写同一行数据。没有锁,就会出现:

问题 说明
丢失更新 两个事务同时读到库存=10,各自减1后都写回9,实际应剩8
脏读 读到另一个事务未提交的数据(可被回滚)
不可重复读 同一事务内两次读同一行,值变了
幻读 同一事务内两次范围查询,行数变了

锁的作用:协调并发访问,保证数据一致性与事务隔离

二、MySQL InnoDB 锁类型

InnoDB 实现了行级锁(Row-Level Lock),同时支持表级锁作为后备。

1. 共享锁(Shared Lock,S Lock)

  • 允许持有者读取该行
  • 多个事务可同时持有同一行的 S 锁
  • 阻止其他事务获取 X 锁(即阻止修改)
sql 复制代码
-- 显式加共享锁(SELECT ... LOCK IN SHARE MODE)
-- MySQL 8.0 起推荐使用 FOR SHARE
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR SHARE;
-- 此时其他会话可以读,但不能修改 id=1 的行
COMMIT;

2. 排他锁(Exclusive Lock,X Lock)

  • 允许持有者读取并修改该行
  • 同一行上 X 锁与任何其他锁(S 或 X)互斥
  • UPDATEDELETEINSERT 自动加 X 锁
sql 复制代码
-- 显式加排他锁
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- 此时其他会话无法读取或修改该行(FOR SHARE 或 FOR UPDATE 都会等待)
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

3. 意向锁(Intention Lock)

  • 表级别锁,表示"某个事务即将对表中的行加锁"
  • 意向共享锁(IS):准备加 S 锁
  • 意向排他锁(IX):准备加 X 锁
  • MySQL 自动管理,开发者通常无需关心

意义:当要加表锁时,不用遍历所有行检查是否有行锁,直接看意向锁即可快速判断。

4. 行锁 vs 表锁

存储引擎 默认锁粒度
InnoDB 行级锁(并发高)
MyISAM 表级锁(并发差,已过时)

即使使用 InnoDB,不走索引的查询会导致行锁升级为表锁

sql 复制代码
-- ❌ name 字段无索引,全表扫描 → 锁住整表
UPDATE employees SET salary = 10000 WHERE name = '张三';

-- ✅ 走主键/唯一索引 → 只锁 index=5 这一行
UPDATE employees SET salary = 10000 WHERE id = 5;

三、加锁规则与 MVCC 配合

InnoDB 默认使用 READ COMMITTEDREPEATABLE READ 隔离级别,MVCC(多版本并发控制) 负责"读不阻塞写,写不阻塞读":

  • 快照读(Snapshot Read) :普通 SELECT,不加锁,读取历史版本
  • 当前读(Current Read)SELECT ... FOR UPDATE / FOR SHARE 以及 UPDATE/DELETE,加锁,读最新已提交版本
sql 复制代码
-- 快照读 → 不加锁,读快照
SELECT * FROM orders WHERE id = 100;

-- 当前读 → 加行锁(X 锁)
UPDATE orders SET status = 'paid' WHERE id = 100;

四、死锁(Deadlock)

两个事务互相等待对方释放锁,形成循环等待。

sql 复制代码
事务 A:UPDATE t SET x=1 WHERE id=1;  -- 锁 id=1
事务 B:UPDATE t SET x=2 WHERE id=2;  -- 锁 id=2
事务 A:UPDATE t SET x=3 WHERE id=2;  -- 等待 B 释放 id=2
事务 B:UPDATE t SET x=4 WHERE id=1;  -- 等待 A 释放 id=1

MySQL 处理 :InnoDB 自动检测死锁,回滚其中一个事务 (代价较小的那个),并抛出 ER_LOCK_DEADLOCK 错误(代码 1213)。

sql 复制代码
-- 应用层需要重试
try:
    cursor.execute("UPDATE t SET x=1 WHERE id=1")
    cursor.execute("UPDATE t SET x=2 WHERE id=2")  -- 可能收到死锁错误
except pymysql.err.OperationalError as e:
    if e.args[0] == 1213:  # Deadlock
        connection.rollback()
        time.sleep(0.1)
        retry()

避免死锁的最佳实践

做法 说明
固定访问顺序 所有事务按相同顺序访问资源(如先 id=1 再 id=2)
缩短事务 尽量快提交,减少锁持有时间
合理使用索引 避免行锁升级为表锁
降低隔离级别 不需要 SERIALIZABLE 时尽量用 READ COMMITTED
限制单表更新量 大批量更新考虑分批处理

五、如何查看当前锁状态?

sql 复制代码
-- 查看当前正在等待锁的事务
SELECT * FROM performance_schema.data_lock_waits\G

-- 查看所有锁
SELECT * FROM performance_schema.data_locks\G

-- 查看 InnoDB 状态(含锁信息)
SHOW ENGINE INNODB STATUS\G

实用排查命令:

sql 复制代码
-- 找出阻塞其他事务的会话
SELECT 
    waiting.pid AS blocked_pid,
    blocking.pid AS blocking_pid,
    waiting.query AS blocked_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.processlist waiting ON w.requesting_thread_id = waiting.id
JOIN information_schema.processlist blocking ON w.blocking_thread_id = blocking.id;

六、锁与隔离级别的关系

隔离级别 脏读 不可重复读 幻读 加锁方式
READ UNCOMMITTED ✅ 可能 ✅ 可能 ✅ 可能 写加 X 锁,读不加锁
READ COMMITTED ❌ 不会 ✅ 可能 ✅ 可能 写加 X 锁,读不加锁(语句级快照)
REPEATABLE READ (默认) ❌ 可能 写加 X 锁 + Gap Lock / Next-Key Lock
SERIALIZABLE 所有 SELECT 隐式转 FOR SHARE

MySQL 的 RR 级别通过 Next-Key Lock(行锁 + Gap Lock) 可以一定程度上防止幻读,但并非 100%。如需完全防幻读,用 SERIALIZABLE

注意事项

  1. 索引是行锁的前提:不走索引 = 表锁,并发暴跌
  2. 事务务必提交:未提交的事务会一直持有锁,阻塞其他会话
  3. SELECT 默认不加锁 :需要加锁时显式使用 FOR UPDATEFOR SHARE
  4. 死锁不是 bug,是正常现象:应用层做好重试机制即可
  5. Gap Lock:RR 级别下,范围条件会锁住不存在的记录间隙,防止幻读,但也可能降低并发

总结

锁机制是数据库并发控制的基石。记住三条核心原则:

  • 行锁靠索引 --- 没索引就是表锁
  • 阅读过锁 --- 用 FOR UPDATE / FOR SHARE 显式加锁
  • 快提交、定顺序 --- 避免死锁和锁等待
相关推荐
Code额2 小时前
Python 连接 DeepSeek API,OpenAI 对话方式总结
后端·python·ai·ai编程
星火10242 小时前
【LangChain4j系列10】Guardrails 安全护栏
人工智能·后端
四千岁2 小时前
稀疏向量BM25Retriever不支持中文怎么办?jieba来帮忙
前端·javascript·后端
用户6919026813392 小时前
Docker基本概念
后端·docker·容器
颜进强2 小时前
14 - OpenSpec 老页面改造骨架:定位 + 增量 + 回归三件套
前端·后端·ai编程
唐青枫2 小时前
别只把 switch 当成多路 if:Zig 模式匹配、状态机与 Tagged Union 实战
后端
步行cgn2 小时前
MyBatis 错误 Result Maps collection does not contain value for ... 详解与解决方案
后端
用户250694921612 小时前
Cordis 从入门到实战:插件卸载后,别留下一地鸡毛
后端
SomeB1oody3 小时前
【RustyML入门】5.3. 聚类指标
开发语言·后端·机器学习·rust·教程