数据库死锁频发?这9种锁你必须掌握!

  |   0 评论   |   21 浏览

业务场景

最近证书签发系统频频出现超时,排查日志发现大量数据库死锁:

ERROR: Deadlock found when trying to get lock; try restarting transaction

订单表并发量大,多个事务同时读写同一数据,导致锁冲突不断。作为后端开发,你是否也遇到过类似问题?

其实,死锁的本质是锁使用不当。理解数据库的锁机制,才能有效避免并发问题。

今天这张图整理了 9种数据库锁类型,帮你彻底搞懂什么时候该用什么锁!

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,说明版本冲突
  1. 添加重试机制:死锁后自动重试 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. 是否可以通过调整隔离级别或索引优化?

理解锁机制,你的数据库性能将提升一个台阶!


如果你觉得这篇文章有帮助,欢迎点赞、收藏、分享~

有问题欢迎在评论区讨论,我看到后会第一时间回复!

善忘技术夹-公众号

评论

发表评论

validate