SqlConnection.BeginTransaction() 仅开启事务但不自动生效,必须将 SqlCommand.Transaction 显式设为返回的 SqlTransaction 对象,否则仍走自动提交;事务期间不可关闭连接,异常时须手动 Rollback,推荐 using + try/catch 确保资源释放与回滚。

SqlConnection.BeginTransaction() 是事务起点,但必须配对使用 Commit/Rollback
事务不是开启就自动生效的——BeginTransaction() 返回一个 SqlTransaction 对象,后续所有命令都得显式绑定它,否则仍走默认自动提交模式。常见错误是只调用了 BeginTransaction 却忘了把 SqlCommand.Transaction 属性设为该对象。
实操要点:
-
SqlCommand必须在同一个SqlConnection上创建,且在调用BeginTransaction()之后、Commit()或Rollback()之前设置Command.Transaction = transaction - 事务期间不能关闭连接,也不能在事务未结束时调用
Connection.Close()(会隐式 Rollback) - 若中途抛异常,必须手动调用
transaction.Rollback();不写 catch 或漏掉 Rollback,连接可能挂起,资源无法释放
用 using 块 + try/catch 管理事务生命周期最稳妥
手动管理 Commit() 和 Rollback() 容易遗漏分支。推荐用 using 包裹连接和事务,并在 catch 中明确 Rollback —— 这比依赖 finally 更直观,也避免因异常跳过关键步骤。
示例关键片段:
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var trans = conn.BeginTransaction())
{
try
{
var cmd1 = new SqlCommand("INSERT INTO Orders (...) VALUES (...)", conn, trans);
cmd1.ExecuteNonQuery();
<pre class="brush:php;toolbar:false;"> var cmd2 = new SqlCommand("UPDATE Inventory SET Stock = Stock - @qty WHERE Id = @id", conn, trans);
cmd2.Parameters.AddWithValue("@qty", qty);
cmd2.Parameters.AddWithValue("@id", productId);
cmd2.ExecuteNonQuery();
trans.Commit(); // 只有到这里才真正提交
}
catch
{
trans.Rollback(); // 出错立刻回滚
throw; // 重新抛出,不吞异常
}
}}
事务隔离级别影响并发行为,别默认用 ReadCommitted
BeginTransaction() 默认用 IsolationLevel.ReadCommitted,适合多数场景,但如果你遇到“幻读”或需要更严格一致性(比如金融类扣款+记账),就得显式指定级别。注意:高隔离级别(如 Serializable)会加更重锁,可能引发阻塞甚至死锁。
选型参考:
- 需要防止脏读、不可重复读 →
RepeatableRead(比默认更强,但不防幻读) - 要求绝对一致、可预测结果(如库存校验+扣减原子执行)→
Serializable,但务必配合超时和重试逻辑 - 纯读多写少报表类操作 →
ReadUncommitted(允许脏读,极少用,慎选) - 设置方式:
conn.BeginTransaction(IsolationLevel.Serializable)
跨多个 DbContext(EF Core)时,不能靠 SqlConnection.Transaction 混用
如果你混用原生 ADO.NET 和 EF Core,别试图把 SqlTransaction 直接赋给 DbContext.Database.CurrentTransaction —— EF Core 有自己的事务抽象层,底层可能已封装了连接池复用逻辑。强行桥接容易触发 InvalidOperationException: The connection does not support MultipleActiveResultSets 或事务不生效。
正确做法:
- 纯 EF Core 场景:用
context.Database.BeginTransaction(),再传入transaction到ExecuteSqlRaw()等方法 - 混合场景(必须同时用 EF 和 SqlCommand):统一用
context.Database.GetDbConnection()获取底层连接,再在其上开事务,并确保所有命令共用该连接实例 - 避免在事务中调用
context.SaveChanges()后又执行原生 SQL,除非你确认它们共享同一事务上下文
事务真正的难点不在语法,而在边界判断:哪些操作必须捆进同一个事务?连接是否被连接池回收?异常路径是否每条都覆盖了 Rollback?这些地方一疏忽,数据不一致就悄无声息发生了。


















