T-SQL 事务(Transaction)
核心考点:事务的概念与特性、事务的开启/提交/回滚、事务解决的问题(数据一致性)、@@ERROR全局变量的使用、check约束的作用
适配表结构:CardNew(StdudentId, StudentName, CurrentMoney)(StdudentId自增主键,CurrentMoney有余额大于1的约束)
一、案例背景:转账场景的问题(无事务)
转账操作涉及"转出"和"转入"两个更新操作,若不使用事务,其中一个操作失败、另一个操作成功,会导致数据不一致(如钱转出失败但转入成功,出现"无中生有")。
1.1 创建表并插入测试数据
-- 创建CardNew表(银行卡表)
create table CardNew
(
StdudentId int identity(1000,1) primary key not null, -- 自增主键,起始值1000,步长1
StudentName varchar(10) not null, -- 学生姓名,非空约束
CurrentMoney money check(CurrentMoney>1) -- 余额,check约束:余额必须大于1,否则插入/更新报错
);
-- 插入4条测试数据(初始余额分别为1400、1500、1600、1700)
insert into CardNew(StudentName,CurrentMoney) values('赵云',1400);
insert into CardNew(StudentName,CurrentMoney) values('马超',1500);
insert into CardNew(StudentName,CurrentMoney) values('张飞',1600);
insert into CardNew(StudentName,CurrentMoney) values('关羽',1700);
-- 查询表中数据,验证插入结果
select * from CardNew;
关键说明:check(CurrentMoney>1) 约束确保余额始终大于1,若更新后余额≤1,会触发约束报错,操作失败。
1.2 无事务的转账操作(存在数据一致性问题)
-- 需求:从赵云(StdudentId=1000)转账1000元给马超(StdudentId=1001)
-- 步骤1:赵云账户扣除1000元(1400-1000=400,符合check约束,操作成功)
update CardNew set CurrentMoney = CurrentMoney - 1000 where StdudentId=1000;
-- 步骤2:马超账户增加1000元(1500+1000=2500,操作成功)
update CardNew set CurrentMoney = CurrentMoney + 1000 where StdudentId=1001;
-- 查询转账后数据(此时数据正常,无问题)
select * from CardNew;
-- 模拟异常场景:从马超(StdudentId=1001)转账1500元给不存在的账户(StdudentId=1004)
-- 步骤1:马超账户扣除1500元(2500-1500=1000,符合约束,操作成功)
update CardNew set CurrentMoney = CurrentMoney - 1500 where StdudentId=1001;
-- 步骤2:向不存在的账户转入1500元(无匹配记录,操作成功但无实际效果)
update CardNew set CurrentMoney = CurrentMoney + 1500 where StdudentId=1004;
-- 查询结果:马超账户少了1500元,但1004账户未增加,数据不一致
select * from CardNew;
问题分析:无事务时,两个更新操作相互独立,其中一个操作异常(如转入不存在的账户、转出后余额违反约束),另一个操作仍会执行,导致数据不一致,违背业务逻辑。
二、事务的核心概念与特性(必背)
事务(Transaction)是一个不可分割的工作单元,包含一系列SQL操作,这些操作要么全部执行成功并提交,要么全部失败并回滚,确保数据的一致性和完整性。
2.1 事务的四大特性(ACID)
-
原子性(Atomicity):事务中的所有操作要么全部执行,要么全部不执行,不存在部分执行的情况(核心特性,适配转账场景)。
-
一致性(Consistency):事务执行前后,数据库数据的完整性约束(如check、主键、外键)保持一致(如转账后总金额不变)。
-
隔离性(Isolation):多个事务同时执行时,彼此之间相互隔离,互不影响(避免并发操作导致的数据混乱)。
-
持久性(Durability):事务提交后,对数据库的修改是永久的,即使数据库崩溃,数据也不会丢失。
2.2 事务的核心语法(必背)
-
begin transaction:开启事务,标记事务的开始。 -
commit transaction:提交事务,将事务中所有操作的结果永久保存到数据库,事务结束。 -
rollback transaction:回滚事务,撤销事务中所有未提交的操作,恢复到事务开启前的状态,事务结束。 -
@@ERROR:全局变量,返回上一条SQL语句的错误码(0表示无错误,非0表示有错误),用于判断事务中是否出现异常。
三、使用事务解决转账一致性问题(核心案例)
通过事务包裹转账的两个更新操作,结合@@ERROR判断是否出现异常,异常则回滚,无异常则提交,确保两个操作要么都成功,要么都失败。
-- 需求:从马超(StdudentId=1001)转账1000元给张飞(StdudentId=1002),使用事务保证数据一致性
declare @errorNum int; -- 定义变量,统计事务中出现的错误个数
set @errorNum = 0; -- 初始化错误个数为0
begin transaction; -- 开启事务,标记事务开始
begin
-- 步骤1:转出逻辑(马超账户扣除1000元)
update CardNew set CurrentMoney = CurrentMoney - 1000 where StdudentId=1001;
set @errorNum = @errorNum + @@ERROR; -- 记录上一条操作的错误码(0无错,非0有错)
-- 步骤2:转入逻辑(张飞账户增加1000元)
update CardNew set CurrentMoney = CurrentMoney + 1000 where StdudentId=1002;
set @errorNum = @errorNum + @@ERROR; -- 累计错误个数
-- 判断是否出现错误:错误个数>0,说明至少有一个操作失败
if(@errorNum > 0)
begin
rollback transaction; -- 回滚事务:撤销所有操作,恢复到事务开启前的状态
print '转账失败,事务已回滚';
end
else
begin
commit transaction; -- 提交事务:将所有操作结果永久保存
print '转账成功,事务已提交';
end
end
-- 查询转账后数据,验证事务效果
select * from CardNew;
3.1 事务执行效果验证(分两种场景)
场景1:操作无异常(正常转账)
马超账户(1001)扣除1000元后余额为1000(符合check约束),张飞账户(1002)增加1000元后余额为2600,@@ERROR始终为0,@errorNum=0,执行commit transaction,转账成功,数据一致。
场景2:操作有异常(违反check约束)
-- 模拟异常:从赵云(1000)转账1399元给马超(1001)(赵云余额1400-1399=1,违反check(CurrentMoney>1)约束)
declare @errorNum int;
set @errorNum = 0;
begin transaction;
begin
-- 转出逻辑:赵云账户扣除1399元,余额变为1,违反check约束,操作失败,@@ERROR≠0
update CardNew set CurrentMoney = CurrentMoney - 1399 where StdudentId=1000;
set @errorNum = @errorNum + @@ERROR;
-- 转入逻辑:马超账户增加1399元
update CardNew set CurrentMoney = CurrentMoney + 1399 where StdudentId=1001;
set @errorNum = @errorNum + @@ERROR;
if(@errorNum > 0)
begin
rollback transaction; -- 回滚事务,两个操作都撤销
print '转账失败(违反余额约束),事务已回滚';
end
else
commit transaction;
end
select * from CardNew; -- 查询结果:赵云和马超余额均未变化,数据一致
关键说明:转出操作违反check约束,@@ERROR返回非0值,@errorNum>0,执行回滚,两个更新操作都被撤销,数据恢复到事务开启前的状态,避免了数据不一致。
四、核心考点汇总(必背)
-
事务的作用:解决多步SQL操作的数据一致性问题(如转账、批量修改),确保操作要么全成功,要么全失败。
-
事务的核心语法:开启(begin transaction)、提交(commit transaction)、回滚(rollback transaction)。
-
@@ERROR的用法:返回上一条SQL语句的错误码,0表示无错误,非0表示有错误,用于判断事务中是否出现异常。
-
check约束:用于限制字段的取值范围(如余额>1),违反约束时,对应的SQL操作失败。
-
事务的ACID特性:原子性(核心)、一致性、隔离性、持久性,重点掌握原子性和一致性。
五、易错踩坑点
-
开启事务后,未判断错误就直接提交,导致异常场景下数据不一致。
-
混淆@@ERROR的作用,@@ERROR仅返回上一条SQL的错误码,需每次操作后立即累加,否则会被后续操作覆盖。
-
事务中包含DDL语句(如create table、drop table),DDL语句会自动提交事务,导致事务失效(事务仅适用于DML语句:insert、update、delete)。
-
回滚或提交事务后,未结束事务,或重复提交/回滚,导致语法报错。
-
忽略check约束的影响,在事务中执行违反约束的操作,未处理异常,导致事务回滚但未提示原因。
六、事务语法速记模板(转账场景)
-- 事务模板(适用于多步DML操作,确保数据一致性)
declare @errorNum int; -- 统计错误个数
set @errorNum = 0;
begin transaction; -- 开启事务
begin
-- 操作1:转出/修改/插入
操作1的SQL语句;
set @errorNum += @@ERROR; -- 累加错误码
-- 操作2:转入/修改/删除
操作2的SQL语句;
set @errorNum += @@ERROR; -- 累加错误码
-- 可选:更多操作,依次累加错误码
-- 判断是否有错误
if(@errorNum > 0)
begin
rollback transaction; -- 回滚事务
print '操作失败,事务回滚';
end
else
begin
commit transaction; -- 提交事务
print '操作成功,事务提交';
end
end
-- 查询结果,验证事务效果
select * from 表名;
T-SQL 事务拓展:并发、嵌套事务、进阶考点(承接上面转账案例)
一、事务四大特性 ACID 复习(必考简答)
- 原子性 A:事务内所有操作不可分割,全成或全败(转账核心)
- 一致性 C:事务前后数据库约束、业务规则不变(余额不能小于 1、转账总金额不变)
- 隔离性 I:多个事务同时运行,互相隔离,互不干扰(解决并发脏读、不可重复读、幻读)
- 持久性 D:commit 提交后数据永久保存,断电、宕机不丢失
二、并发事务三大问题(选择 / 简答高频)
多个人同时操作同一张银行卡(并发转账)会出现三类数据错乱:
1. 脏读
一个事务读到另一个事务未提交、中途回滚的数据。 举例: 事务 A 扣钱 1000(未 commit),事务 B 读取 A 余额;A 随后 rollback 撤销扣款。B 读到了不存在的扣款数据,就是脏读。
2. 不可重复读
同一事务内,两次读取同一行数据,中间被其他事务 update 修改并提交,两次结果不一样。 举例: 事务 B 第一次查赵云余额 1400;事务 A 转账扣掉 1000 并提交;事务 B 再次查询,余额变成 400,同一事务两次读取结果不同。
3. 幻读
同一事务内,两次相同条件查询,中间其他事务 insert/delete 提交,两次返回行数不一样。 举例: 事务 B 查询余额 > 1000 的人,查出 4 人;事务 A 新增一张银行卡并提交;事务 B 再查,变成 5 人,像多出来一行 "幻影数据"。
三、SQL Server 5 种隔离级别(解决并发问题)
隔离级别从低到高,锁越来越严格,并发性能越来越差:
- Read Uncommitted 读未提交:允许脏读、不可重复读、幻读
- Read Committed(系统默认级别):禁止脏读,允许不可重复读、幻读
- Repeatable Read:禁止脏读、不可重复读,允许幻读
- Serializable 可序列化:三类问题全部禁止,并发最低
- Snapshot 快照:行版本隔离,不加共享锁,无阻塞
修改当前会话隔离级别语法
sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
四、嵌套事务(重点易错点)
核心结论:
SQL Server 没有真正的嵌套事务,内层 begin transaction 只是计数器累加,只有最外层 commit 才会真正写入数据库;任意一层 rollback 直接回滚全部事务。
示例演示
sql
declare @err int=0
begin transaction --外层 计数=1
update CardNew set CurrentMoney=CurrentMoney-1000 where StdudentId=1000
set @err+=@@ERROR
begin transaction --内层嵌套 计数=2
update CardNew set CurrentMoney=CurrentMoney+1000 where StdudentId=1001
set @err+=@@ERROR
if(@err>0)
rollback transaction; --只要一次回滚,全部撤销
else
commit transaction; --内层commit 计数=1,数据不落地
--必须外层再commit,事务才真正提交写入硬盘
commit transaction;
易错坑
- 内层 commit 只是事务计数器 - 1,不会永久保存数据
- 任意一层执行 rollback,整个大事务全部回滚,不分内外层
五、事务进阶:TRY-CATCH 异常捕获(比 @@ERROR 更推荐)
原来只用@@ERROR有缺陷:每一句执行后必须立刻累加,否则会被下一句覆盖。 使用begin try...begin catch自动捕获所有异常,不用手动累加错误码,考试、开发优先写这种。
转账事务完整版(TRY-CATCH 标准写法)
sql
begin transaction
begin try
--转出
update CardNew set CurrentMoney = CurrentMoney - 1000 where StdudentId=1000
--转入
update CardNew set CurrentMoney = CurrentMoney + 1000 where StdudentId=1001
--无任何异常,提交
commit transaction
print '转账成功'
end try
begin catch
--任意一句报错,直接回滚全部操作
rollback transaction
print '转账失败,已回滚'
--打印错误信息
print '错误编号:'+cast(ERROR_NUMBER() as varchar)
print '错误描述:'+ERROR_MESSAGE()
end catch
select * from CardNew
内置错误函数(catch 块专用)
- ERROR_NUMBER ():错误编号
- ERROR_MESSAGE ():完整错误文字
- ERROR_SEVERITY ():错误严重等级
六、事务禁用场景(不能用事务)
DDL 语句执行时会自动提交当前事务,导致事务失效,不要放在事务中: create table /drop table /alter table /create proc /drop view 等
错误示例(事务直接失效):
sql
begin transaction
create table test(id int) --DDL自动提交事务
update CardNew set CurrentMoney=1000 where StdudentId=1000
rollback --此时事务早已提交,回滚无效
七、高频简答题背诵总结
-
事务原子性是什么? 答:事务是不可分割单元,内部 DML 操作要么全部提交生效,要么全部回滚撤销,不会出现一半成功一半失败的数据不一致情况。
-
不加事务转账会出现什么问题? 答:转出扣款失败(余额违反约束报错),但转入加钱语句正常执行,总金额对不上,数据错乱。
-
脏读、不可重复读、幻读三者区别?
- 脏读:读到未提交的修改
- 不可重复读:同一事务两次读同一行,中间被别人 update 提交
- 幻读:同一事务两次范围查询,中间被别人 insert/delete 提交
-
@@ERROR 和 TRY-CATCH 哪个更好? TRY-CATCH,自动捕获所有异常,无需每一行手动记录错误码,不会丢失错误信息。
-
嵌套事务为什么内层 commit 没用? SQL Server 事务靠计数器控制,内层 commit 只减少计数,只有最外层 commit 才会持久化数据;任意 rollback 直接全部回滚。