数据库决定事务隔离级别,C# 仅传递设置;Snapshot 需 DBA 启用;ReadUncommitted 有脏读等风险;缩短事务时间比调隔离级更关键。

事务隔离级别由数据库决定,C# 代码只是传递设置
你写的 SqlConnection + SqlTransaction 代码本身不实现隔离逻辑,它只是把 IsolationLevel 枚举值(比如 IsolationLevel.ReadCommitted)通过 T-SQL 的 SET TRANSACTION ISOLATION LEVEL 命令发给 SQL Server。最终的锁行为、版本控制、阻塞与否,全由数据库引擎按该级别执行。
这意味着:C# 并发控制不能只靠改隔离级别来解决。比如你用 IsolationLevel.Serializable,看似“最安全”,但实际会极大增加锁范围和死锁概率——这不是 C# 层能缓解的,而是数据库在运行时做出的资源调度决策。
-
IsolationLevel.Unspecified不代表“无隔离”,而是使用数据库默认级别(SQL Server 默认是ReadCommitted) - 若连接字符串启用了
MultipleActiveResultSets=True(MARS),某些隔离行为可能与预期不同,尤其在异步操作中 - .NET 的
async/await不改变事务边界:一个SqlTransaction实例不能跨await续用,否则抛InvalidOperationException:“The transaction is no longer available.”
C# 并发编程 常见误用:把 lock 和数据库事务混为一谈
很多开发者在多线程写数据库时,下意识加 lock 语句块保护数据库操作,这是典型错位:
-
lock只锁住当前进程内的某段代码,对其他应用、其他服务器、甚至同一应用的不同进程完全无效 - 数据库事务的并发控制(如行锁、意向锁、快照版本)才是跨进程、跨机器的真实协调机制
- 滥用
lock(this)或静态锁可能导致线程饥饿,而数据库锁超时(CommandTimeout)才是更合理的失败兜底
正确做法是:用最小必要隔离级别 + 明确的事务范围 + 合理的重试策略(如 SqlException.Number == 1205 表示死锁,应重试)。
Snapshot 隔离需要数据库端显式启用,C# 无法自动开启
IsolationLevel.Snapshot 在 C# 中合法,但若数据库未启用快照隔离(ALLOW_SNAPSHOT_ISOLATION ON)或读已提交快照(READ_COMMITTED_SNAPSHOT ON),运行时会直接报错:
System.Data.SqlClient.SqlException: Snapshot isolation transaction failed……
这不是 C# 配置问题,而是 DBA 必须提前执行的 T-SQL:
ALTER DATABASE YourDB SET ALLOW_SNAPSHOT_ISOLATION ON; ALTER DATABASE YourDB SET READ_COMMITTED_SNAPSHOT ON;
注意:READ_COMMITTED_SNAPSHOT 改变的是默认 ReadCommitted 的行为(从锁变版本),而 ALLOW_SNAPSHOT_ISOLATION 才允许你在 C# 中显式指定 IsolationLevel.Snapshot。
高并发场景下,IsolationLevel.ReadUncommitted 的真实代价
用 IsolationLevel.ReadUncommitted(或 NOLOCK 提示)确实能避免阻塞,但它带来的数据风险常被低估:
- 可能读到未提交的脏数据(
SqlTransaction.Rollback()后你还拿它做了业务判断) - 可能跳过行(phantom reads)、重复读行(duplicate reads),尤其在分页查询中导致漏数据或重复处理
- 对索引视图、XML 列、空间数据等,
NOLOCK可能直接报错或返回不一致结果
它适合报表类只读、容忍误差的场景;但绝不该用于订单扣减、库存校验、资金流水等核心路径——这时候宁可调低超时、加重试,也不该用 ReadUncommitted 换吞吐。
真正影响并发性能的,往往不是隔离级别本身,而是事务持续时间(比如在事务里调用 HTTP API 或做大量计算)。缩短事务生命周期,比在 Serializable 和 ReadUncommitted 之间反复横跳更有意义。