数据库死锁频发?这9种锁你必须掌握!
- 业务场景
- 锁的三大分类维度
- 按照锁的粒度
- 按照锁的模式
- 按照锁的对象
- 1️⃣ Shared Lock(共享锁)- 读多写少的神器
- 作用
- 使用场景
- SQL 示例
- 实战建议
- 2️⃣ Exclusive Lock(排他锁)- 写操作的保障
- 作用
- 使用场景
- SQL 示例
- 并发冲突示例
- 实战建议
- 3️⃣ Update Lock(更新锁)- 预防空死锁
- 作用
- 为什么需要它?
- SQL 示例
- 实战建议
- 4️⃣ Schema Lock(Schema 锁)- 表结构的守护者
- 作用
- 类型
- SQL 示例
- 并发冲突示例
- 实战建议
- 5️⃣ Bulk Update Lock(批量更新锁)- 大数据导入的加速器
- 作用
- SQL 示例
- 性能对比
- 实战建议
- 6️⃣ Key Range Lock(键范围锁)- 幻读的克星
- 作用
- 什么是幻读?
- SQL 示例
- 实战建议
- 7️⃣ Row-Level Lock(行级锁)- 并发的最佳选择
- 作用
- SQL 示例
- 并发示例
- 实战建议
- 8️⃣ Page-Level Lock(页级锁)- 行锁与表锁的折中
- 作用
- SQL 示例
- 锁定范围示例
- 实战建议
- 9️⃣ Table-Level Lock(表级锁)- 简单但粗暴
- 作用
- SQL 示例
- 实战建议
- 锁的兼容性矩阵
- 最佳实践总结
- 1️⃣ 选择合适的锁粒度
- 2️⃣ 减少锁持有时间
- 3️⃣ 避免长事务
- 4️⃣ 监控死锁并优化
- 5️⃣ 选择合适的隔离级别
- 常见问题排查
- Q1:为什么我的查询很慢?
- Q2:如何减少死锁?
- Q3:行锁为什么会退化为表锁?
- 总结
业务场景
最近证书签发系统频频出现超时,排查日志发现大量数据库死锁:
ERROR: Deadlock found when trying to get lock; try restarting transaction
订单表并发量大,多个事务同时读写同一数据,导致锁冲突不断。作为后端开发,你是否也遇到过类似问题?
其实,死锁的本质是锁使用不当。理解数据库的锁机制,才能有效避免并发问题。
今天这张图整理了 9种数据库锁类型,帮你彻底搞懂什么时候该用什么锁!

锁的三大分类维度
在深入具体锁类型之前,先理解锁的分类逻辑:
按照锁的粒度
- 行级锁(Row-Level):锁定单行数据,并发度最高
- 页级锁(Page-Level):锁定一个数据页(通常包含多行)
- 表级锁(Table-Level):锁定整张表,并发度最低
按照锁的模式
- 共享锁(Shared):读操作使用,允许多个事务同时读取
- 排他锁(Exclusive):写操作使用,只允许一个事务修改
- 更新锁(Update):预防死锁的特殊锁
按照锁的对象
- 数据锁:锁定实际数据
- Schema 锁:锁定表结构
- Key Range 锁:锁定索引范围
下面逐一详解这 9 种锁的使用场景和注意事项。
1️⃣ Shared Lock(共享锁)- 读多写少的神器
作用
允许多个事务同时读取同一资源,但都不允许修改。这就是我们常说的 “读锁”。
使用场景
报表查询、数据统计、库存查看等只读场景。
SQL 示例
-- MySQL 使用共享锁
START TRANSACTION;
SELECT * FROM Orders WHERE OrderID = 123 LOCK IN SHARE MODE;
-- SQL Server 使用共享锁
SELECT * FROM Orders WITH (HOLDLOCK) WHERE OrderID = 123;
-- 提交后释放锁
COMMIT;
实战建议
✅ 适合场景:
- 数据仓库的报表查询
- 财务系统的对账操作
- 高并发读取且数据不常变化的场景
⚠️ 注意事项:
- 持有共享锁时,其他事务无法修改数据
- 长事务持有共享锁会阻塞写操作,影响性能
2️⃣ Exclusive Lock(排他锁)- 写操作的保障
作用
当一个事务持有排他锁时,其他事务既不能读也不能写。这就是我们常说的 “写锁”。
使用场景
数据插入、更新、删除等写操作。
SQL 示例
-- MySQL 更新操作自动加排他锁
START TRANSACTION;
UPDATE Orders SET Price = 99.0 WHERE OrderID = 123;
-- 其他事务此时无法读取或修改 OrderID=123 的行
COMMIT;
并发冲突示例
-- 事务 1
BEGIN;
UPDATE Orders SET Price = 99.0 WHERE OrderID = 123;
-- 事务 1 未提交...
-- 事务 2(会被阻塞)
BEGIN;
SELECT * FROM Orders WHERE OrderID = 123; -- 等待事务1释放锁
实战建议
✅ 适合场景:
- 所有写操作(INSERT、UPDATE、DELETE)
- 需要保证数据强一致性的场景
⚠️ 注意事项:
- 排他锁会完全阻塞其他事务,慎用长事务
- 尽量减少锁的持有时间,快速提交
3️⃣ Update Lock(更新锁)- 预防空死锁
作用
防止死锁的"中间态"锁。事务先获取更新锁,准备修改时再升级为排他锁。其他事务可以获取共享锁,但无法获取更新锁。
为什么需要它?
假设没有更新锁:
- 事务 A:读取数据(共享锁)
- 事务 B:读取数据(共享锁)
- 事务 A:尝试升级为排他锁(等待 B 释放共享锁)
- 事务 B:尝试升级为排他锁(等待 A 释放共享锁)
- 结果:死锁!
使用更新锁可以避免这个问题。
SQL 示例
-- SQL Server 使用更新锁
SELECT * FROM Orders WITH (UPDLOCK) WHERE OrderID = 123;
-- 此时其他事务可以读,但不能加更新锁
UPDATE Orders SET Price = 99.0 WHERE OrderID = 123;
实战建议
✅ 适合场景:
- 读后写(Read-then-Write)的场景
- 需要防止死锁的高并发系统
⚠️ 注意事项:
- 更新锁仅在 SQL Server 中显式可用
- MySQL/PostgreSQL 通过其他机制处理死锁
4️⃣ Schema Lock(Schema 锁)- 表结构的守护者
作用
锁定表的结构定义,防止在表结构变更期间有其他事务操作表。
类型
- Schema Modification Lock (Sch-M):修改表结构时使用
- Schema Stability Lock (Sch-S):查询或编译时使用
SQL 示例
-- 添加字段(持有 Sch-M 锁)
ALTER TABLE Orders ADD COLUMN NewColumn INT;
-- 期间其他事务无法操作 Orders 表
并发冲突示例
-- 事务 1:修改表结构
BEGIN;
ALTER TABLE Orders ADD COLUMN Status VARCHAR(20);
-- 事务 1 未提交...
-- 事务 2:查询表(会被阻塞)
SELECT * FROM Orders WHERE OrderID = 123;
实战建议
✅ 适合场景:
- DDL 操作(CREATE、ALTER、DROP)
- 生产环境变更(建议在低峰期执行)
⚠️ 注意事项:
- Schema 锁会阻塞所有对该表的访问
- 大表 ALTER 操作可能耗时很长,务必做好评估
5️⃣ Bulk Update Lock(批量更新锁)- 大数据导入的加速器
作用
批量导入数据时使用,减少锁开销,提升性能。其他事务无法修改表,但可以读取(取决于数据库实现)。
SQL 示例
-- SQL Server 批量导入
BULK INSERT Orders FROM 'orders.csv'
WITH (
TABLOCK, -- 表级锁
ROWS_PER_BATCH = 1000
);
-- MySQL LOAD DATA
LOAD DATA INFILE 'orders.csv'
INTO TABLE Orders
SET Price = 99.0;
性能对比
| 方式 | 100万行数据耗时 | 锁冲突 |
|---|---|---|
| 逐行 INSERT | 120秒 | 高 |
| BULK INSERT | 15秒 | 低 |
实战建议
✅ 适合场景:
- 大数据导入、数据迁移
- ETL 流程中的批量加载
⚠️ 注意事项:
- 批量导入期间表可能无法正常服务
- 建议在业务低峰期执行
6️⃣ Key Range Lock(键范围锁)- 幻读的克星
作用
锁定一个索引范围,防止其他事务在该范围内插入新数据,解决 “幻读” 问题。
什么是幻读?
-- 事务 1:查询价格在 100-200 的订单
SELECT * FROM Orders WHERE Price BETWEEN 100 AND 200;
-- 结果:5 行
-- 事务 2:插入一条新订单(价格 150)
INSERT INTO Orders VALUES (456, 'Tom', 150);
COMMIT;
-- 事务 1:再次查询
SELECT * FROM Orders WHERE Price BETWEEN 100 AND 200;
-- 结果:6 行(多了一行,这就是"幻读")
SQL 示例
-- SQL Server 使用范围锁防止幻读
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM Orders
WHERE OrderID BETWEEN 100 AND 200
WITH (HOLDLOCK, ROWLOCK);
-- 此时其他事务无法插入 OrderID 在 100-200 的新行
实战建议
✅ 适合场景:
- 金融交易(需要严格隔离)
- 报表统计(需要精确数据)
⚠️ 注意事项:
- 范围锁会大幅降低并发度
- 权衡业务需求,不一定所有场景都需要防幻读
7️⃣ Row-Level Lock(行级锁)- 并发的最佳选择
作用
只锁定被操作的那一行数据,其他行可以被其他事务并发访问。
SQL 示例
-- MySQL 行级锁
START TRANSACTION;
UPDATE Orders SET Price = 99.0
WHERE OrderID = 123; -- 只锁定这一行
-- SQL Server 显式使用行级锁
UPDATE Orders SET Price = 99.0
WHERE OrderID = 123
WITH (ROWLOCK);
并发示例
表数据:
OrderID 123 | Price 100.1 ← 事务 A 更新(锁定)
OrderID 124 | Price 200.5 ← 事务 B 可以并发更新(不同行)
OrderID 125 | Price 300.8 ← 事务 C 可以并发更新(不同行)
实战建议
✅ 适合场景:
- 高并发 OLTP 系统(订单、支付、库存)
- 大多数业务场景的默认选择
⚠️ 注意事项:
- 行级锁需要索引支持,全表扫描会退化为表锁
- 锁定大量行时,锁管理开销增加
8️⃣ Page-Level Lock(页级锁)- 行锁与表锁的折中
作用
锁定一个数据页(通常 8KB - 16KB),页内的多行数据会被一起锁定。
SQL 示例
-- SQL Server 使用页级锁
UPDATE Orders SET Price = 99.0
WHERE OrderID = 123
WITH (PAGLOCK); -- 锁定 OrderID 123 所在的页
锁定范围示例
数据页 1:OrderID 1-50
数据页 2:OrderID 51-100 ← 事务 A 锁定这页(OrderID 80 在这里)
数据页 3:OrderID 101-150
如果事务 A 锁定了数据页 2,那么 OrderID 51-100 的所有行都无法被其他事务修改。
实战建议
✅ 适合场景:
- 行级锁开销过大时的折中方案
- 连续访问多行数据的场景
⚠️ 注意事项:
- 可能误伤不需要锁定的行
- 大多数数据库默认会动态调整锁粒度
9️⃣ Table-Level Lock(表级锁)- 简单但粗暴
作用
锁定整张表,所有事务都无法访问(或只能读取)。
SQL 示例
-- MySQL 表级读锁
LOCK TABLES Orders READ;
SELECT * FROM Orders; -- 当前事务可以读
-- 其他事务也可以读,但不能写
UNLOCK TABLES;
-- MySQL 表级写锁
LOCK TABLES Orders WRITE;
UPDATE Orders SET Price = 99.0; -- 只有当前事务可以操作
UNLOCK TABLES;
-- SQL Server 表级锁
SELECT * FROM Orders WITH (TABLOCK);
实战建议
✅ 适合场景:
- 批量数据更新(如每晚的定时任务)
- DDL 操作(ALTER TABLE、DROP TABLE)
- 表级数据迁移
⚠️ 注意事项:
- 并发度最低,生产环境慎用
- 长时间持有表锁会导致系统"假死"
锁的兼容性矩阵
快速判断哪些锁可以共存:
| 锁类型 | IS(意向共享) | S(共享) | IX(意向排他) | U(更新) | X(排他) |
|---|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ✅ | ❌ |
| S | ✅ | ✅ | ❌ | ❌ | ❌ |
| IX | ✅ | ❌ | ✅ | ❌ | ❌ |
| U | ✅ | ❌ | ❌ | ❌ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ | ❌ |
理解要点:
- 共享锁(S)与共享锁(S)兼容
- 排他锁(X)与任何锁都不兼容
- 意向锁(IS/IX)用于快速判断表级锁冲突
最佳实践总结
1️⃣ 选择合适的锁粒度
-- ❌ 过于粗暴
LOCK TABLES Orders WRITE;
-- ✅ 精确锁定
UPDATE Orders SET Price = 99.0
WHERE OrderID = 123;
2️⃣ 减少锁持有时间
-- ❌ 锁持有时间过长
BEGIN;
SELECT * FROM Orders WHERE OrderID = 123; -- 加共享锁
-- 执行耗时的业务逻辑(5秒)
UPDATE Orders SET Price = 99.0 WHERE OrderID = 123; -- 升级为排他锁
COMMIT;
-- ✅ 先查询再锁定
-- 步骤1:不加锁查询
SELECT * FROM Orders WHERE OrderID = 123;
-- 步骤2:快速加锁并更新
BEGIN;
UPDATE Orders SET Price = 99.0 WHERE OrderID = 123;
COMMIT;
3️⃣ 避免长事务
-- ❌ 长事务
BEGIN;
-- 查询订单
SELECT * FROM Orders WHERE Status = 'PENDING';
-- 调用第三方支付接口(可能超时)
-- 更新订单状态
UPDATE Orders SET Status = 'PAID' WHERE OrderID = 123;
COMMIT;
-- ✅ 短事务 + 业务补偿
-- 步骤1:查询
SELECT * FROM Orders WHERE Status = 'PENDING' AND OrderID = 123;
-- 步骤2:调用支付接口(不加锁)
-- 步骤3:快速更新
BEGIN;
UPDATE Orders SET Status = 'PAID' WHERE OrderID = 123 AND Status = 'PENDING';
COMMIT;
-- 如果支付失败,单独补偿
4️⃣ 监控死锁并优化
-- MySQL 查看死锁日志
SHOW ENGINE INNODB STATUS;
-- SQL Server 查看死锁
SELECT * FROM sys.dm_tran_locks;
-- 优化建议:
-- 1. 按相同顺序访问表
-- 2. 使用乐观锁(版本号)
-- 3. 添加合理的索引
5️⃣ 选择合适的隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 几乎不用 |
| READ COMMITTED | ❌ | ✅ | ✅ | ⭐⭐⭐⭐ | 大多数场景(MySQL默认) |
| REPEATABLE READ | ❌ | ❌ | ✅ | ⭐⭐⭐ | 需要一致性读(PostgreSQL默认) |
| SERIALIZABLE | ❌ | ❌ | ❌ | ⭐ | 金融交易 |
-- MySQL 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- PostgreSQL 设置隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
常见问题排查
Q1:为什么我的查询很慢?
排查步骤:
-- 1. 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
-- 2. 查看锁等待事务
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 3. 查看长时间运行的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
Q2:如何减少死锁?
解决方案:
1. 按固定顺序访问表:例如先操作 Orders,再操作 Users
2. 使用乐观锁:
UPDATE Orders
SET Price = 99.0, Version = Version + 1
WHERE OrderID = 123 AND Version = 5;
-- 如果 affected_rows = 0,说明版本冲突
- 添加重试机制:死锁后自动重试 2-3 次
Q3:行锁为什么会退化为表锁?
原因:
- 没有使用索引,导致全表扫描
- 锁定的行数过多(超过阈值)
优化:
-- 创建合适的索引
CREATE INDEX idx_order_id ON Orders(OrderID);
-- 使用覆盖索引避免回表
SELECT Price FROM Orders WHERE OrderID = 123;
总结
掌握数据库锁机制,是后端开发者的必修课。快速回顾这 9 种锁:
| 锁类型 | 用途 | 使用场景 |
|---|---|---|
| Shared Lock | 读锁 | 报表查询、数据统计 |
| Exclusive Lock | 写锁 | INSERT/UPDATE/DELETE |
| Update Lock | 防死锁 | 读后写场景 |
| Schema Lock | 锁表结构 | DDL 操作 |
| Bulk Update Lock | 批量导入 | 大数据迁移 |
| Key Range Lock | 防幻读 | 金融交易 |
| Row-Level Lock | 行级锁 | 高并发 OLTP |
| Page-Level Lock | 页级锁 | 折中方案 |
| Table-Level Lock | 表级锁 | 批量更新 |
记住这 3 条黄金法则:
1. ✅ 尽量用行锁,提升并发度
2. ✅ 减少锁持有时间,快速提交
3. ✅ 避免长事务,降低死锁风险
实战建议:下次遇到数据库并发问题时,先问自己三个问题:
1. 当前使用的是哪种锁?
2. 锁的持有时间是否过长?
3. 是否可以通过调整隔离级别或索引优化?
理解锁机制,你的数据库性能将提升一个台阶!
如果你觉得这篇文章有帮助,欢迎点赞、收藏、分享~
有问题欢迎在评论区讨论,我看到后会第一时间回复!
评论
发表评论
|
|
|