SQL Server 2019 事务和锁 --- 语法知识点及使用方法详解
一、事务管理
1. 事务的原理
事务(Transaction) 是数据库操作的逻辑单元,具有 ACID 特性:
- 原子性(Atomicity):事务内所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency):事务执行前后,数据库从一个一致状态转移到另一个一致状态。
- 隔离性(Isolation):并发事务互不干扰。
- 持久性(Durability):事务提交后,结果永久保存。
SQL Server 使用 事务日志(Transaction Log) 记录所有修改,用于崩溃恢复和回滚。
二、事务管理的常用语句
1. 基本语法结构
sql
BEGIN TRANSACTION [transaction_name | @tran_name_variable]
-- SQL 语句组
COMMIT TRANSACTION [transaction_name | @tran_name_variable]
-- 或
ROLLBACK TRANSACTION [transaction_name | @tran_name_variable]
2. 保存点(Savepoint)
sql
SAVE TRANSACTION savepoint_name
ROLLBACK TRANSACTION savepoint_name
案例1:基本事务操作 --- 银行转账
sql
-- 创建测试表
CREATE TABLE dbo.Accounts (
AccountID INT PRIMARY KEY,
AccountName NVARCHAR(50),
Balance DECIMAL(18,2)
);
GO
INSERT INTO dbo.Accounts VALUES
(1, '张三', 1000.00),
(2, '李四', 500.00);
GO
-- 开始事务:从张三账户转300元到李四账户
BEGIN TRY
BEGIN TRANSACTION TransferMoney;
-- 1. 减少张三余额
UPDATE dbo.Accounts
SET Balance = Balance - 300.00
WHERE AccountID = 1;
-- 模拟异常:除零错误
-- SELECT 1/0; -- 取消注释可测试回滚
-- 2. 增加李四余额
UPDATE dbo.Accounts
SET Balance = Balance + 300.00
WHERE AccountID = 2;
-- 提交事务
COMMIT TRANSACTION TransferMoney;
PRINT '转账成功!';
END TRY
BEGIN CATCH
-- 发生错误则回滚
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION TransferMoney;
PRINT '转账失败,已回滚!错误信息:' + ERROR_MESSAGE();
END CATCH
GO
-- 查询结果
SELECT * FROM dbo.Accounts;
GO
注释:
BEGIN TRANSACTION标记事务开始。COMMIT提交事务,使修改永久生效。ROLLBACK回滚事务,撤销所有修改。TRY...CATCH捕获异常确保事务完整性。@@TRANCOUNT返回当前连接中活动事务数,用于判断是否需回滚。
案例2:使用保存点(部分回滚)
sql
BEGIN TRANSACTION OrderProcess;
-- 步骤1:插入订单头
INSERT INTO dbo.Orders (OrderID, CustomerID, OrderDate)
VALUES (1001, 201, GETDATE());
PRINT '订单头插入成功';
-- 设置保存点
SAVE TRANSACTION AfterHeader;
-- 步骤2:插入订单明细(可能失败)
BEGIN TRY
INSERT INTO dbo.OrderDetails (OrderID, ProductID, Quantity, Price)
VALUES (1001, 501, 2, 99.99);
-- 模拟错误:插入不存在的产品ID(外键约束失败)
INSERT INTO dbo.OrderDetails (OrderID, ProductID, Quantity, Price)
VALUES (1001, 999, 1, 199.99); -- 假设 ProductID=999 不存在
COMMIT TRANSACTION OrderProcess;
PRINT '订单完整提交';
END TRY
BEGIN CATCH
-- 仅回滚到保存点,保留订单头
ROLLBACK TRANSACTION AfterHeader;
PRINT '订单明细失败,回滚到保存点。订单头保留。错误:' + ERROR_MESSAGE();
-- 可选择提交剩余部分或整体回滚
COMMIT TRANSACTION OrderProcess; -- 提交订单头
-- 或 ROLLBACK TRANSACTION OrderProcess; -- 完全回滚
END CATCH
GO
注释:
SAVE TRANSACTION创建保存点,允许部分回滚。- 适用于"部分操作可接受失败"的场景。
- 最终仍需
COMMIT或完全ROLLBACK。
三、事务的隔离级别
SQL Server 支持 5 种隔离级别,控制事务间可见性:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 最高 |
| READ COMMITTED(默认) | ❌ | ✅ | ✅ | 高 |
| REPEATABLE READ | ❌ | ❌ | ✅ | 中 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 最低 |
| SNAPSHOT | ❌ | ❌ | ❌ | 高(行版本控制) |
✅ 表示可能发生,❌ 表示被阻止
设置隔离级别语法:
sql
SET TRANSACTION ISOLATION LEVEL {
READ UNCOMMITTED
| READ COMMITTED
| REPEATABLE READ
| SNAPSHOT
| SERIALIZABLE
}
案例3:演示不同隔离级别的行为
sql
-- 准备测试数据
CREATE TABLE dbo.Inventory (
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(50),
Stock INT
);
GO
INSERT INTO dbo.Inventory VALUES (1, '手机', 100);
GO
-- 会话1:设置为 READ UNCOMMITTED(允许脏读)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;
-- 更新库存但不提交
UPDATE dbo.Inventory SET Stock = Stock - 10 WHERE ProductID = 1;
PRINT '会话1:库存已减10,未提交。当前库存:' + CAST((SELECT Stock FROM dbo.Inventory WHERE ProductID=1) AS VARCHAR);
-- 此时切换到会话2执行查询(见下方)
-- 等待5秒模拟并发
WAITFOR DELAY '00:00:05';
-- 回滚事务(模拟取消操作)
ROLLBACK TRANSACTION;
PRINT '会话1:事务已回滚,库存恢复。';
GO
-- ===== 在另一个查询窗口执行(会话2) =====
-- 会话2:读取未提交数据(脏读)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT Stock AS '脏读到的库存' FROM dbo.Inventory WHERE ProductID = 1;
-- 结果可能为 90(即使会话1未提交且最终回滚)
GO
-- 会话2:使用默认隔离级别(READ COMMITTED)则阻塞直到会话1提交或回滚
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT Stock AS '正常读取' FROM dbo.Inventory WHERE ProductID = 1;
-- 此查询会等待会话1结束
GO
注释:
READ UNCOMMITTED允许读取未提交数据(脏读),性能高但数据可能不一致。READ COMMITTED(默认)确保只读已提交数据,避免脏读。- 实际开发中应根据业务需求选择隔离级别。
案例4:使用 SNAPSHOT 隔离级别(行版本控制)
sql
-- 启用数据库快照隔离(需先设置数据库选项)
ALTER DATABASE YourDatabaseName SET ALLOW_SNAPSHOT_ISOLATION ON;
GO
-- 在会话1中:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
SELECT Stock AS '快照读取库存' FROM dbo.Inventory WHERE ProductID = 1;
-- 假设此时读到100
-- 在会话2中(同时执行):
BEGIN TRANSACTION;
UPDATE dbo.Inventory SET Stock = 80 WHERE ProductID = 1;
COMMIT TRANSACTION;
-- 更新为80
-- 回到会话1,再次读取:
SELECT Stock AS '再次快照读取' FROM dbo.Inventory WHERE ProductID = 1;
-- 仍然读到100!因为基于事务开始时的快照
COMMIT TRANSACTION; -- 提交后下次读取才会看到80
GO
注释:
SNAPSHOT隔离级别使用行版本控制,读操作不阻塞写,写操作不阻塞读。- 避免了脏读、不可重复读、幻读。
- 需要
tempdb存储版本数据,可能增加资源消耗。
四、锁(Locking)
1. 锁的内涵与作用
锁 是 SQL Server 用于实现事务隔离性和一致性的机制,防止多个事务同时修改同一数据导致冲突。
- 作用:保证数据一致性、实现事务隔离级别。
- 自动管理:SQL Server 自动加锁和释放锁(提交或回滚时)。
- 粒度:从细到粗:RID(行ID)→ KEY(索引键)→ PAGE(页)→ EXTENT(区)→ TABLE(表)→ DATABASE(库)
2. 可锁定资源与锁的类型
常见锁类型:
| 锁类型 | 缩写 | 说明 |
|---|---|---|
| 共享锁 | S | 读操作持有,允许多个事务同时读 |
| 排他锁 | X | 写操作持有,阻止其他事务读写 |
| 更新锁 | U | 防止死锁的中间状态,读取后准备更新 |
| 意向锁 | IS/IX/UIX | 表示下层资源有对应锁(如表上有IX,表示某行有X锁) |
| 架构锁 | SCH-M/SCH-S | 表结构变更时使用 |
| 大容量更新锁 | BU | BULK INSERT 时使用 |
3. 死锁(Deadlock)
死锁:两个或多个事务互相等待对方释放锁,形成循环等待。
SQL Server 会自动检测死锁,并选择一个"牺牲者"(Deadlock Victim)回滚其事务,解除死锁。
案例5:手动加锁(LOCK HINTS)与死锁演示
sql
-- 准备数据
CREATE TABLE dbo.TableA (ID INT PRIMARY KEY, Value INT);
CREATE TABLE dbo.TableB (ID INT PRIMARY KEY, Value INT);
GO
INSERT INTO dbo.TableA VALUES (1, 100), (2, 200);
INSERT INTO dbo.TableB VALUES (1, 1000), (2, 2000);
GO
-- 会话1:先锁 TableA,再锁 TableB
BEGIN TRANSACTION;
-- 对 TableA 加排他锁
SELECT * FROM dbo.TableA WITH (XLOCK, ROWLOCK) WHERE ID = 1;
PRINT '会话1:已锁定 TableA.ID=1';
-- 等待会话2锁定 TableB
WAITFOR DELAY '00:00:05';
-- 尝试锁定 TableB
UPDATE dbo.TableB SET Value = Value + 1 WHERE ID = 1;
PRINT '会话1:更新 TableB 成功';
COMMIT TRANSACTION;
GO
-- ===== 在另一个窗口执行会话2 =====
-- 会话2:先锁 TableB,再锁 TableA(与会话1顺序相反 → 死锁风险)
BEGIN TRANSACTION;
-- 对 TableB 加排他锁
SELECT * FROM dbo.TableB WITH (XLOCK, ROWLOCK) WHERE ID = 1;
PRINT '会话2:已锁定 TableB.ID=1';
-- 等待会话1锁定 TableA
WAITFOR DELAY '00:00:05';
-- 尝试锁定 TableA → 此时可能被死锁检测器选为牺牲者
UPDATE dbo.TableA SET Value = Value + 1 WHERE ID = 1;
PRINT '会话2:更新 TableA 成功'; -- 可能不会执行到此
COMMIT TRANSACTION;
GO
注释:
WITH (XLOCK, ROWLOCK)手动指定加排他锁、行级锁。- 死锁发生时,SQL Server 返回错误 1205,回滚牺牲者事务。
- 避免死锁的最佳实践:所有事务按相同顺序访问资源。
案例6:查看当前锁信息(使用 DMV)
sql
-- 查询当前数据库中的锁信息
SELECT
request_session_id AS SessionID,
resource_type AS ResourceType,
resource_database_id AS DBID,
DB_NAME(resource_database_id) AS DatabaseName,
resource_description AS ResourceDesc,
resource_associated_entity_id AS EntityID,
request_mode AS LockMode, -- S, X, U, IX, IS etc.
request_status AS Status, -- GRANT, WAIT
t.text AS SQLText
FROM sys.dm_tran_locks l
LEFT JOIN sys.dm_exec_requests r ON l.request_session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE resource_database_id = DB_ID()
ORDER BY request_session_id, resource_type;
GO
注释:
sys.dm_tran_locks动态管理视图显示当前锁。request_status = 'WAIT'表示正在等待锁(可能阻塞)。- 结合
sys.dm_exec_sql_text查看阻塞的SQL语句。
五、综合性案例
综合案例1:高并发库存扣减(避免超卖)
sql
-- 场景:电商秒杀,多个用户同时抢购同一商品
-- 要求:库存不能为负,避免超卖
-- 创建商品表
CREATE TABLE dbo.Products (
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(100),
Stock INT CHECK (Stock >= 0) -- 约束防止负库存
);
GO
INSERT INTO dbo.Products VALUES (1, '限量版手机', 10);
GO
-- 存储过程:安全扣减库存
CREATE PROCEDURE dbo.usp_DecreaseStock
@ProductID INT,
@Quantity INT,
@Result INT OUTPUT -- 0=成功, 1=库存不足
AS
BEGIN
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRY
BEGIN TRANSACTION;
-- 使用 UPDLOCK, ROWLOCK 防止并发读取旧值
-- 使用 HOLDLOCK 保持锁到事务结束(等价于 SERIALIZABLE)
DECLARE @CurrentStock INT;
SELECT @CurrentStock = Stock
FROM dbo.Products WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
WHERE ProductID = @ProductID;
IF @CurrentStock >= @Quantity
BEGIN
UPDATE dbo.Products
SET Stock = Stock - @Quantity
WHERE ProductID = @ProductID;
SET @Result = 0; -- 成功
PRINT '扣减成功,剩余库存:' + CAST((@CurrentStock - @Quantity) AS VARCHAR);
END
ELSE
BEGIN
SET @Result = 1; -- 库存不足
PRINT '库存不足!当前库存:' + CAST(@CurrentStock AS VARCHAR) + ',需求数量:' + CAST(@Quantity AS VARCHAR);
END
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
SET @Result = -1; -- 系统错误
PRINT '系统错误:' + ERROR_MESSAGE();
END CATCH
END
GO
-- 测试:模拟并发调用
DECLARE @Result INT;
EXEC dbo.usp_DecreaseStock @ProductID = 1, @Quantity = 3, @Result = @Result OUTPUT;
SELECT @Result AS ResultCode;
GO
-- 可同时在多个窗口运行以上测试,观察库存变化和错误处理
注释:
WITH (UPDLOCK, ROWLOCK, HOLDLOCK)确保读取时加更新锁,并保持到事务结束,避免其他事务读取或修改。CHECK约束作为最后防线。- 输出参数
@Result用于应用程序判断结果。
综合案例2:死锁监控与处理脚本
sql
-- 创建死锁事件监控表
CREATE TABLE dbo.DeadlockLog (
LogID INT IDENTITY(1,1) PRIMARY KEY,
LogTime DATETIME2 DEFAULT GETDATE(),
DeadlockGraph XML,
VictimSessionID INT,
VictimSQLText NVARCHAR(MAX)
);
GO
-- 创建死锁事件通知(需启用跟踪标志 1222 或使用扩展事件)
-- 此处使用 SQL Server Agent + WMI Event Alert 或扩展事件更佳
-- 简化版:定期查询错误日志(不推荐生产环境)
-- 替代方案:使用扩展事件捕获死锁(推荐)
CREATE EVENT SESSION [DeadlockMonitor] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(SET filename=N'DeadlockMonitor')
WITH (STARTUP_STATE=ON);
GO
-- 启动会话
ALTER EVENT SESSION [DeadlockMonitor] ON SERVER STATE = START;
GO
-- 查询死锁报告
SELECT
XEvent.value('(@timestamp)[1]', 'datetime2') AS DeadlockTime,
XEvent.query('.') AS DeadlockGraph
FROM
(SELECT CAST(event_data AS XML) AS XEvent
FROM sys.fn_xe_file_target_read_file('DeadlockMonitor*.xel', NULL, NULL, NULL)
) AS EventData;
GO
-- 自动记录到表(需配合 SQL Agent 定期执行)
INSERT INTO dbo.DeadlockLog (DeadlockGraph, VictimSessionID, VictimSQLText)
SELECT
XEvent.query('.') AS DeadlockGraph,
XEvent.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'varchar(100)') AS VictimSessionID,
XEvent.value('(//deadlock/process-list/process/inputbuf)[1]', 'nvarchar(max)') AS VictimSQLText
FROM
(SELECT CAST(event_data AS XML) AS XEvent
FROM sys.fn_xe_file_target_read_file('DeadlockMonitor*.xel', NULL, NULL, NULL)
) AS EventData
WHERE XEvent.value('(@name)[1]', 'varchar(100)') = 'xml_deadlock_report';
GO
注释:
- 扩展事件(Extended Events)是监控死锁的推荐方式。
xml_deadlock_report事件包含完整的死锁图。- 可分析死锁图找出根本原因(如访问顺序不一致、缺少索引等)。
综合案例3:事务与锁的完整电商订单示例
sql
-- 创建订单相关表
CREATE TABLE dbo.Customers (
CustomerID INT PRIMARY KEY,
Name NVARCHAR(50),
CreditLimit DECIMAL(18,2)
);
GO
CREATE TABLE dbo.Orders (
OrderID INT IDENTITY(1000,1) PRIMARY KEY,
CustomerID INT,
OrderDate DATETIME2 DEFAULT GETDATE(),
TotalAmount DECIMAL(18,2),
Status NVARCHAR(20) DEFAULT 'Pending'
);
GO
CREATE TABLE dbo.OrderItems (
OrderItemID INT IDENTITY(1,1) PRIMARY KEY,
OrderID INT,
ProductID INT,
Quantity INT,
Price DECIMAL(18,2)
);
GO
CREATE TABLE dbo.Products (
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(100),
Price DECIMAL(18,2),
Stock INT
);
GO
-- 插入测试数据
INSERT INTO dbo.Customers VALUES (1, 'VIP客户', 5000.00);
INSERT INTO dbo.Products VALUES (1, '笔记本电脑', 4500.00, 5);
GO
-- 创建下单存储过程(包含事务、锁、错误处理)
CREATE PROCEDURE dbo.usp_CreateOrder
@CustomerID INT,
@ProductID INT,
@Quantity INT,
@OrderID INT OUTPUT,
@ErrorMessage NVARCHAR(4000) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- 严重错误时自动回滚
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 最高隔离,避免幻读
DECLARE @TotalPrice DECIMAL(18,2);
DECLARE @CustomerCredit DECIMAL(18,2);
DECLARE @ProductPrice DECIMAL(18,2);
DECLARE @AvailableStock INT;
BEGIN TRY
BEGIN TRANSACTION;
-- 1. 检查客户信用额度(加共享锁)
SELECT @CustomerCredit = CreditLimit
FROM dbo.Customers WITH (HOLDLOCK)
WHERE CustomerID = @CustomerID;
-- 2. 检查产品价格和库存(加更新锁)
SELECT
@ProductPrice = Price,
@AvailableStock = Stock
FROM dbo.Products WITH (UPDLOCK, HOLDLOCK)
WHERE ProductID = @ProductID;
-- 3. 验证业务规则
IF @CustomerCredit IS NULL
BEGIN
SET @ErrorMessage = '客户不存在';
THROW 50001, @ErrorMessage, 1;
END
IF @ProductPrice IS NULL
BEGIN
SET @ErrorMessage = '产品不存在';
THROW 50002, @ErrorMessage, 1;
END
IF @AvailableStock < @Quantity
BEGIN
SET @ErrorMessage = '库存不足,当前库存:' + CAST(@AvailableStock AS NVARCHAR);
THROW 50003, @ErrorMessage, 1;
END
SET @TotalPrice = @ProductPrice * @Quantity;
IF @TotalPrice > @CustomerCredit
BEGIN
SET @ErrorMessage = '超出信用额度,订单金额:' + CAST(@TotalPrice AS NVARCHAR) + ',信用额度:' + CAST(@CustomerCredit AS NVARCHAR);
THROW 50004, @ErrorMessage, 1;
END
-- 4. 创建订单
INSERT INTO dbo.Orders (CustomerID, TotalAmount, Status)
VALUES (@CustomerID, @TotalPrice, 'Confirmed');
SET @OrderID = SCOPE_IDENTITY();
-- 5. 创建订单明细
INSERT INTO dbo.OrderItems (OrderID, ProductID, Quantity, Price)
VALUES (@OrderID, @ProductID, @Quantity, @ProductPrice);
-- 6. 扣减库存
UPDATE dbo.Products
SET Stock = Stock - @Quantity
WHERE ProductID = @ProductID;
-- 7. 扣减客户信用额度(模拟)
UPDATE dbo.Customers
SET CreditLimit = CreditLimit - @TotalPrice
WHERE CustomerID = @CustomerID;
COMMIT TRANSACTION;
SET @ErrorMessage = NULL; -- 成功
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
SET @OrderID = NULL;
SET @ErrorMessage = ERROR_MESSAGE();
-- 可在此记录错误日志
END CATCH
END
GO
-- 测试下单
DECLARE @NewOrderID INT, @ErrorMsg NVARCHAR(4000);
EXEC dbo.usp_CreateOrder
@CustomerID = 1,
@ProductID = 1,
@Quantity = 2,
@OrderID = @NewOrderID OUTPUT,
@ErrorMessage = @ErrorMsg OUTPUT;
SELECT @NewOrderID AS CreatedOrderID, @ErrorMsg AS ErrorMessage;
GO
-- 查询结果
SELECT * FROM dbo.Orders;
SELECT * FROM dbo.OrderItems;
SELECT * FROM dbo.Products;
SELECT * FROM dbo.Customers;
GO
注释:
- 使用
SERIALIZABLE隔离级别确保整个事务期间数据不被其他事务修改。WITH (UPDLOCK, HOLDLOCK)防止并发冲突。SET XACT_ABORT ON确保严重错误自动回滚。- 完整的错误处理和业务规则验证。
- 适用于高一致性要求的核心业务。
六、最佳实践总结
- 事务要短小精悍:减少锁持有时间,提高并发。
- 按固定顺序访问资源:避免死锁。
- 合理选择隔离级别:平衡一致性与并发性。
- 使用锁提示谨慎 :仅在必要时手动加锁(如
UPDLOCK,HOLDLOCK)。 - 监控死锁和阻塞:使用扩展事件或 DMV。
- 错误处理必须包含回滚:确保事务完整性。
- 避免在事务中执行用户交互或耗时操作。
- 测试高并发场景:确保系统稳定。
✅ 以上内容涵盖 SQL Server 2019 事务与锁的核心语法、管理操作及实战案例,可直接用于学习、开发与调优。