SQL Server数据库锁机制详解:锁的产生原因、锁兼容性与业务阻塞
一、为什么数据库需要锁?
数据库锁(Lock)是数据库管理系统实现并发控制的重要机制。
在企业 ERP、进销存、供应链、电商等业务系统中,通常存在大量用户同时操作数据库的情况:
- 用户 A 正在审核采购订单。
- 用户 B 正在修改同一张采购订单。
- 用户 C 正在查询采购订单。
- 用户 D 正在更新库存。
- 用户 E 正在生成销售单据。
如果数据库没有并发控制机制,多个事务可能同时修改同一条数据,导致数据覆盖、库存错误、订单状态不一致等问题。
例如:
假设某商品当前库存为 100。
两个用户同时执行:
UPDATE goods SET stock = stock - 10 WHERE id = 1001;
如果没有正确的并发控制,可能出现更新丢失等问题。
因此,数据库需要通过锁机制协调不同事务对数据的访问。
锁的核心目的不是阻止用户操作,而是保证多个事务并发执行时的数据正确性。
二、数据库锁是如何产生的?
SQL Server 会根据 SQL 语句、事务隔离级别、执行计划和访问方式,自动申请相应的锁。
开发人员通常不需要手动创建锁。
1. SELECT 查询产生的锁
例如:
SELECT * FROM SPJHD WHERE DJBH = '10001';
SQL Server 默认隔离级别是 READ COMMITTED。
在默认情况下,普通 SELECT 通常会获取共享锁(S),用于保证读取的数据符合当前隔离级别的一致性要求。
常见锁:
- IS:意向共享锁。
- S:共享锁。
- PAGE S:页面共享锁,具体取决于执行计划和锁策略。
在 READ COMMITTED 下,普通 SELECT 的共享锁通常在语句执行期间持有,语句结束后释放。
因此,普通 SELECT 不意味着锁会一直持有到事务提交。
2. UPDATE 更新产生的锁
例如:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
UPDATE 需要保证修改过程不会被其他事务同时破坏。
SQL Server 通常会涉及:
- IX:表级意向排他锁。
- U:更新锁。
- X:排他锁。
实际使用的锁粒度取决于执行计划和锁策略。
UPDATE 修改数据后,排他锁通常会一直持有到事务提交或回滚。
例如:
BEGIN TRANSACTION; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001'; -- 后续还有大量业务逻辑 EXEC p_CreateSpJhJsd_ERP; COMMIT TRANSACTION;
即使 UPDATE 本身只执行了几毫秒,只要事务没有提交,相关排他锁就可能一直存在。
这也是 ERP 系统出现长时间阻塞的常见原因。
3. INSERT 插入产生的锁
例如:
INSERT INTO SPJHD(DJBH, RQ)
VALUES ('10002', GETDATE());
INSERT 可能产生:
- IX:意向排他锁。
- X:新记录对应的排他锁。
- 页面级锁:与插入目标页面及索引维护有关。
如果多个事务同时向相同索引区域插入数据,还可能发生页面竞争或索引热点。
4. DELETE 删除产生的锁
例如:
DELETE FROM SPJHD WHERE DJBH = '10001';
DELETE 需要获取排他锁,保护被删除的记录及相关索引修改。
如果删除大量数据,还可能产生大量锁,并增加锁升级的风险。
三、SQL Server 常见锁类型
SQL Server 的锁可以从锁模式和锁粒度两个角度理解。
1. 按锁模式分类
| 锁类型 | 名称 | 主要作用 |
|---|---|---|
| S | Shared | 共享锁,用于读取 |
| X | Exclusive | 排他锁,用于修改 |
| U | Update | 更新锁,降低更新过程中的死锁风险 |
| IS | Intent Shared | 意向共享锁 |
| IX | Intent Exclusive | 意向排他锁 |
| SIX | Shared with Intent Exclusive | 共享意向排他锁 |
| Sch-S | Schema Stability | 架构稳定锁 |
| Sch-M | Schema Modification | 架构修改锁 |
需要注意:
U 锁不是 X 锁。U 锁用于更新准备阶段,实际修改时通常需要转换为 X 锁。
IS、IX、SIX 属于意向锁,用于表达更低层级资源的锁定意图,并不意味着整张表已经被锁定。
2. 按锁粒度分类
| 锁粒度 | 名称 | 锁定范围 |
|---|---|---|
| RID | 行标识锁 | 堆表中的单条记录 |
| KEY | 键锁 | 索引中的一个键 |
| PAGE | 页面锁 | 一个 8KB 数据页 |
| HoBT | 堆或 B 树锁定单元 | 索引或堆结构 |
| OBJECT | 对象锁 | 通常是整张表等数据库对象 |
| DATABASE | 数据库锁 | 数据库级资源 |
需要注意:
KEY 锁作用于索引键,并不完全等同于直接锁定数据表中的物理记录。
PAGE 锁也不一定意味着锁升级,它可能是 SQL Server 正常选择的锁粒度。
四、锁是否会阻塞其他功能?
这是理解数据库并发问题最重要的部分。
锁是否造成阻塞,不取决于是否存在锁,而取决于两个事务申请的锁是否兼容,以及它们是否访问了相同或重叠的数据库资源。
1. S 锁与 S 锁:通常不会阻塞
用户 A:
SELECT * FROM SPJHD WHERE DJBH = '10001';
用户 B:
SELECT * FROM SPJHD WHERE DJBH = '10001';
两个用户同时查询同一条记录。
双方获取 S 锁。
S 锁与 S 锁兼容,因此通常可以同时执行。
这意味着:
普通 SELECT 不会因为另一个普通 SELECT 获取了 S 锁而被阻塞。
2. S 锁与 X 锁:会发生阻塞
用户 A:
BEGIN TRANSACTION; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001'; -- 不提交
用户 B:
SELECT * FROM SPJHD WHERE DJBH = '10001';
用户 A 持有 X 锁。
用户 B 需要获取 S 锁。
由于 S 与 X 不兼容,用户 B 必须等待。
这说明:
即使用户 B 只是查询数据,也可能被另一个用户的 UPDATE 阻塞。
3. X 锁与 X 锁:会发生阻塞
用户 A:
BEGIN TRANSACTION; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
用户 B:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
两个用户同时更新同一条记录。
用户 B 必须等待用户 A 提交或回滚。
4. 不同记录之间通常可以并行
用户 A:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
用户 B:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10002';
如果两个订单对应不同的锁资源,且不存在页面锁、表锁、索引热点或其他公共资源竞争,那么两个事务可以并行执行。
这也是数据库设计和索引设计需要关注的重点。
五、锁兼容性矩阵
下面是常见锁模式之间的兼容关系。
Y 表示兼容,N 表示不兼容。
| 已持有锁 | IS | S | U | IX | SIX | X |
|---|---|---|---|---|---|---|
| IS | Y | Y | Y | Y | Y | N |
| S | Y | Y | N | N | N | N |
| U | Y | N | N | N | N | N |
| IX | Y | N | N | Y | N | N |
| SIX | Y | N | N | N | N | N |
| X | N | N | N | N | N | N |
这里需要特别注意:
- S 与 S 兼容,所以普通查询通常不会互相阻塞。
- S 与 X 不兼容,所以查询可能被更新阻塞。
- IX 与 IX 兼容,所以不同事务可以分别准备修改不同记录。
- X 与其他常见数据锁模式都不兼容。
- 意向锁不能直接理解为表锁。
六、PAGE 锁为什么可能影响其他用户?
SQL Server 数据库默认采用 8KB 数据页。
一个页面可能存储多条记录。
例如:
SPJHD
│
├── PAGE 100
│ ├── 记录 A
│ ├── 记录 B
│ ├── 记录 C
│ └── 记录 D
│
├── PAGE 101
│ ├── 记录 E
│ ├── 记录 F
│ └── 记录 G
│
└── PAGE 102
├── 记录 H
└── 记录 I
如果事务 A 获取 PAGE 100 的 X 锁,那么事务 B 即使访问 PAGE 100 中另一条记录,也可能发生阻塞。
这就是页面锁与键锁的重要区别。
例如:
用户 A:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
用户 B:
SELECT * FROM SPJHD WHERE DJBH = '10002';
如果两个订单恰好位于同一个页面,并且事务 A 持有该页面的 X 锁,事务 B 可能被阻塞。
但是,如果用户 A 只是持有 PAGE S 锁,用户 B 的 PAGE S 读取通常仍然兼容。
因此,判断 PAGE 锁是否导致阻塞,必须同时看锁模式。
七、什么情况下会发生锁升级?
SQL Server 为了控制锁管理开销,支持锁升级(Lock Escalation)。
例如:
一个事务更新大量记录:
UPDATE SPJHD SET RQ = GETDATE() WHERE RQ < '2025-01-01';
假设需要修改几十万条数据。
SQL Server 可能需要维护大量 KEY 锁或 RID 锁。
当满足锁升级条件时,SQL Server 可能将大量较低粒度锁升级为更高粒度的锁,通常是表级锁。
常见触发因素:
- 单个语句或事务产生大量锁。
- 锁管理器达到锁内存压力条件。
- 执行计划涉及大量记录扫描或修改。
- 锁升级检查达到相应阈值。
需要注意:
锁升级不是每次达到固定数量就必然发生,SQL Server 还会考虑锁管理内存压力等因素。
锁升级可能扩大阻塞范围:
原来:
事务 A
├── KEY 100
├── KEY 101
└── KEY 102
事务 B
└── 更新 KEY 200
可以并行
升级后:
事务 A
└── OBJECT X
事务 B
└── 更新同一张表
发生等待
因此,大范围 UPDATE、DELETE 是 ERP 系统需要重点关注的操作。
八、为什么一个业务操作会导致整个模块卡死?
在传统 ERP 系统中,多个业务模块可能共享相同的基础业务表。
例如:
| 业务模块 | 可能涉及的表 |
|---|---|
| 采购 | SPJHD、SPJHDMX |
| 结算 | JHJSDMP、SPJHD |
| 库存 | SPJHD、库存明细 |
| 销售 | 销售单、库存明细 |
| 单据编号 | DJBHWH_ERP |
假设采购审核过程中执行了以下操作:
BEGIN TRANSACTION; UPDATE SPJHD SET ... WHERE DJBH = @DJBH; UPDATE DJBHWH_ERP SET DJH = DJH + 1 WHERE ...; EXEC p_CreateSpJhJsd_ERP; COMMIT TRANSACTION;
如果事务持续时间很长,期间获取的排他锁就可能一直保留。
其他用户如果也需要访问相同资源,就可能等待。
尤其是单据编号表、库存汇总表等公共资源,一旦成为竞争热点,就可能影响多个业务模块。
这里有一个非常重要的结论:
业务模块卡死不一定是模块内部所有表都被锁住,而可能是多个模块共同依赖的一张公共表或一条公共记录被锁住。
因此,排查模块级阻塞时,应重点检查公共表、事务范围和阻塞链。
九、死锁与阻塞有什么区别?
两者经常被混淆,但本质不同。
1. 阻塞(Blocking)
事务 A:
BEGIN TRANSACTION; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
事务 B:
UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
事务 B 等待事务 A。
只要 A 不提交,B 就可能一直等待。
这是阻塞。
2. 死锁(Deadlock)
事务 A:
BEGIN TRANSACTION; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001'; UPDATE JHJSDMP SET ... WHERE LXDJ = '20001';
事务 B:
BEGIN TRANSACTION; UPDATE JHJSDMP SET ... WHERE LXDJ = '20001'; UPDATE SPJHD SET RQ = GETDATE() WHERE DJBH = '10001';
事务 A 和事务 B 获取锁的顺序不同。
最终:
- A 持有 SPJHD,等待 JHJSDMP。
- B 持有 JHJSDMP,等待 SPJHD。
形成循环等待。
SQL Server 检测到死锁后,会选择一个事务作为牺牲者,回滚该事务,通常返回错误 1205。
区别:
| 比较项 | 阻塞 | 死锁 |
|---|---|---|
| 是否循环等待 | 不一定 | 是 |
| 是否自动解决 | 通常不会 | SQL Server 自动选择牺牲者 |
| 是否自动回滚 | 通常不会 | 牺牲者事务回滚 |
| 主要原因 | 锁不兼容 | 锁获取顺序冲突等 |
| 主要治理方向 | 缩短事务、减少竞争 | 统一加锁顺序、减少交叉访问 |
十、如何减少数据库锁造成的阻塞?
对于传统 ERP 系统,建议从以下几个方向治理。
1. 缩短事务范围
不建议:
BEGIN TRANSACTION; UPDATE SPJHD SET ...; -- 大量业务计算 -- 调用多个存储过程 -- 执行复杂查询 -- 等待外部接口 COMMIT TRANSACTION;
建议:
- 事务中只保留必要的数据库操作。
- 不要在事务中调用耗时的外部接口。
- 避免在事务中执行大量不必要的查询。
- 尽可能缩短锁的持有时间。
2. 建立合理的索引
索引能够帮助 SQL Server 更精准地定位记录,减少不必要的数据扫描。
例如:
UPDATE JORDER SET SP = '1' WHERE DJBH = @LXDJ;
如果 DJBH 没有索引,可能扫描大量记录。
但如果 DJBH 是有效的聚集主键,通常可以精准定位目标记录。
对于 EXISTS:
IF EXISTS (
SELECT 1
FROM JHJSDMP
WHERE LXDJ = @LXDJ
)
应该检查 LXDJ 是否存在合适的索引。
索引不是越多越好,过多索引会增加 INSERT、UPDATE、DELETE 的维护成本。
3. 避免大批量事务
不要一次更新几十万条记录并长时间持有事务。
可以根据业务一致性要求分批处理。
例如:
WHILE 1 = 1
BEGIN
UPDATE TOP (1000) SPJHD
SET RQ = GETDATE()
WHERE RQ < '2025-01-01';
IF @@ROWCOUNT = 0
BREAK;
END;
注意:分批提交需要结合业务一致性设计,不能简单拆分原子性要求较高的业务事务。
4. 统一事务访问顺序
例如多个业务都需要更新:
SPJHD JHJSDMP DJBHWH_ERP
尽量统一访问顺序,减少交叉锁定和死锁。
5. 对公共资源进行并发治理
对于单据编号、库存汇总等热点资源:
- 优化索引。
- 缩短更新事务。
- 避免不必要的串行化。
- 必要时使用应用层并发控制。
- 对死锁进行有限次数的完整事务重试。
6. 评估 RCSI
SQL Server 支持 READ_COMMITTED_SNAPSHOT(RCSI)。
开启后,普通 READ COMMITTED 查询使用行版本读取,减少读取与更新之间的 S/X 锁竞争。
ALTER DATABASE ERP3 SET READ_COMMITTED_SNAPSHOT ON;
主要影响:
| 操作组合 | 普通 READ COMMITTED | 开启 RCSI |
|---|---|---|
| SELECT + SELECT | 通常不阻塞 | 通常不阻塞 |
| SELECT + UPDATE | 可能阻塞 | 通常不因数据锁阻塞 |
| UPDATE + SELECT | 可能阻塞 | 通常不因数据锁阻塞 |
| UPDATE + UPDATE | 可能阻塞 | 仍然可能阻塞 |
RCSI 不会消除 UPDATE 之间的锁竞争,也不会消除所有类型的锁等待。
启用前需要评估 TempDB 版本存储、事务语义、应用兼容性,并在测试环境验证。
十一、如何定位到底是谁造成了阻塞?
SQL Server 2012 可以通过系统动态管理视图进行排查。
1. 查询当前阻塞链
SELECT
r.session_id,
r.blocking_session_id,
r.status,
r.wait_type,
r.wait_time / 1000 AS wait_seconds,
r.wait_resource,
s.host_name,
s.program_name,
s.login_name,
t.text AS sql_text
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s
ON r.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id > 50
AND (
r.blocking_session_id <> 0
OR r.wait_type LIKE 'LCK%'
)
ORDER BY r.wait_time DESC;
2. 查询当前锁
SELECT
request_session_id,
resource_type,
request_mode,
request_status,
resource_associated_entity_id,
resource_description
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID()
ORDER BY request_session_id;
3. 查询事务持续时间
SELECT
st.session_id,
st.transaction_id,
at.transaction_begin_time,
DATEDIFF(
SECOND,
at.transaction_begin_time,
GETDATE()
) AS transaction_seconds,
at.transaction_state
FROM sys.dm_tran_session_transactions st
JOIN sys.dm_tran_active_transactions at
ON st.transaction_id = at.transaction_id
ORDER BY at.transaction_begin_time;
4. 捕获死锁
建议通过 SQL Server Extended Events 捕获:
xml_deadlock_report
重点分析:
- 死锁涉及哪些表。
- 哪些存储过程参与。
- 哪些 SQL 持有锁。
- 哪些 SQL 正在等待。
- 是否存在不同的锁获取顺序。
注意:阻塞和死锁需要分别监控,死锁图无法替代完整的阻塞链分析。
十二、总结:锁不等于阻塞,阻塞不等于系统故障
理解数据库锁,需要建立三个层次的认识。
第一层:锁是正常机制。
SELECT、INSERT、UPDATE、DELETE 都可能产生锁。锁本身不是问题,而是数据库保证数据一致性的必要机制。
第二层:锁是否阻塞取决于兼容性和资源范围。
S 与 S 通常兼容,S 与 X 不兼容,X 与 X 不兼容。
即使两个用户操作不同的订单,如果涉及相同的数据页、索引资源或公共业务记录,也可能产生阻塞。
第三层:真正需要治理的是不合理的锁竞争。
尤其是:
- 长事务。
- 缺失索引导致的大范围扫描。
- 锁升级。
- 公共业务表热点。
- 存储过程交叉调用。
- 不一致的事务访问顺序。
对于传统 ERP 系统,不能简单要求所有业务都修改 SQL,而应该从数据库监控、事务治理、索引优化、并发控制和隔离级别等方面逐步改善。
最终目标是:
保证数据一致性的同时,让不同用户、不同订单、不同业务模块尽可能并行执行,避免一个用户的操作影响其他用户的正常工作。
以上就是SQL Server数据库锁机制详解:锁的产生原因、锁兼容性与业务阻塞的详细内容,更多关于SQL Server数据库锁机制详解的资料请关注脚本之家其它相关文章!


最新评论