SQL Server数据库锁机制详解:锁的产生原因、锁兼容性与业务阻塞

 更新时间:2026年11月05日 09:08:08   作者:nbsaas-boot  
还在为数据库卡顿、事务阻塞头疼吗?本文深入浅出讲解SQL Server锁机制,从锁的产生、类型、兼容性到死锁与阻塞的区别,帮你快速定位ERP系统并发问题,掌握锁升级、索引优化和事务治理技巧,让你的系统不再因锁等待而卡死,需要的朋友可以参考下

一、为什么数据库需要锁?

数据库锁(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. 按锁模式分类

锁类型名称主要作用
SShared共享锁,用于读取
XExclusive排他锁,用于修改
UUpdate更新锁,降低更新过程中的死锁风险
ISIntent Shared意向共享锁
IXIntent Exclusive意向排他锁
SIXShared with Intent Exclusive共享意向排他锁
Sch-SSchema Stability架构稳定锁
Sch-MSchema 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 表示不兼容。

已持有锁ISSUIXSIXX
ISYYYYYN
SYYNNNN
UYNNNNN
IXYNNYNN
SIXYNNNNN
XNNNNNN

这里需要特别注意:

  1. S 与 S 兼容,所以普通查询通常不会互相阻塞。
  2. S 与 X 不兼容,所以查询可能被更新阻塞。
  3. IX 与 IX 兼容,所以不同事务可以分别准备修改不同记录。
  4. X 与其他常见数据锁模式都不兼容。
  5. 意向锁不能直接理解为表锁。

六、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 可能将大量较低粒度锁升级为更高粒度的锁,通常是表级锁。

常见触发因素:

  1. 单个语句或事务产生大量锁。
  2. 锁管理器达到锁内存压力条件。
  3. 执行计划涉及大量记录扫描或修改。
  4. 锁升级检查达到相应阈值。

需要注意:

锁升级不是每次达到固定数量就必然发生,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数据库锁机制详解的资料请关注脚本之家其它相关文章!

相关文章

  • 除MSSQL数据库text字段中恶意脚本的删方法

    除MSSQL数据库text字段中恶意脚本的删方法

    删除MSSQL数据库text字段的替换处理示例--全表替换,看到有人提问,所以整理了一个好久以前的处理方法,以供大家参考
    2009-07-07
  • SQL Server删除重复数据只保留一条的实现步骤

    SQL Server删除重复数据只保留一条的实现步骤

    在进行数据处理和数据分析的过程中,我们经常会遇到重复记录的问题,重复记录可能会导致数据不准确,也会增加数据处理的工作量,这篇文章主要给大家介绍了关于SQL Server删除重复数据只保留一条的实现步骤,需要的朋友可以参考下
    2024-01-01
  • SQLServer 参数化查询经验分享

    SQLServer 参数化查询经验分享

    本篇文章将介绍参数化查询。我将讨论如果一个查询可以被参数化,那么SQL Server优化器怎样尝试将其参数化,以及你可以怎样建立你自己的参数化查询。
    2010-05-05
  • SQL Server修改数据的几种语句详解

    SQL Server修改数据的几种语句详解

    在SQL中的基本操作就是增删改查,查询语句不会对数据库中的数据进行修改,下面这篇文章主要给大家介绍了关于SQL Server修改数据的几种语句,需要的朋友可以参考下
    2022-10-10
  • SQL实现模糊查询的四种方法总结

    SQL实现模糊查询的四种方法总结

    本文主要介绍了SQL实现模糊查询的四种方法总结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • .Net下执行sqlcmd的方法

    .Net下执行sqlcmd的方法

    遇到这样一个问题:程序实现创建sqlserver 端点,添加webmethod,删除webmethod,如果直接用ado.net 的sqlhelp 实现,总是报错,后来想到了用sqlcmd
    2010-06-06
  • SQL学习笔记五去重,给新加字段赋值的方法

    SQL学习笔记五去重,给新加字段赋值的方法

    SQL学习笔记五去重,给新加字段赋值的方法,需要的朋友可以参考下。
    2011-08-08
  • SQL Server数据库自动备份的实现步骤

    SQL Server数据库自动备份的实现步骤

    要编写一个自动备份 SQL Server 数据库的脚本,可以使用 SQL Server Management Studio (SSMS) 或者 Transact-SQL (T-SQL) 脚本,本文给大家介绍了一个一个简单的 T-SQL 脚本示例,需要的朋友可以参考下
    2023-11-11
  • sql server2016里面的json功能浅析

    sql server2016里面的json功能浅析

    这篇文章主要介绍了sql server2016里面的json功能,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-02-02
  • sqlserver 数据库学习笔记

    sqlserver 数据库学习笔记

    sqlserver 数据库学习笔记,学习sqlserver的朋友可以参考下。
    2011-11-11

最新评论