
EntityFramework与TransactionScope事务和并发控制在开发企业级应用时数据的一致性和并发控制是绕不开的核心问题。Entity Framework (EF) 作为 .NET 生态中最流行的 ORM提供了丰富的事务支持而 TransactionScope 则为我们提供了一种更为灵活、跨资源的事务管理方式。本文将结合实战代码深入探讨 EF 与 TransactionScope 的结合使用以及如何处理并发冲突。### 1. 基础事务EF 自带的 SaveChanges 事务默认情况下EF 的SaveChanges()方法会在内部开启一个事务将所有增删改操作打包提交。如果其中任何一条语句失败整个事务会回滚确保数据的一致性。csharpusing (var context new MyDbContext()){ // 开启一个显式事务 using (var dbContextTransaction context.Database.BeginTransaction()) { try { // 执行多个数据操作 context.Orders.Add(new Order { CustomerName 张三, TotalAmount 100 }); context.Orders.Add(new Order { CustomerName 李四, TotalAmount 200 }); // 保存到数据库此时事务尚未提交 context.SaveChanges(); // 执行其他业务逻辑可能失败 if (SomeExternalService.Call() false) { throw new Exception(外部服务调用失败); } // 提交事务 dbContextTransaction.Commit(); Console.WriteLine(事务已成功提交); } catch (Exception ex) { // 事务自动回滚 Console.WriteLine($事务回滚: {ex.Message}); } }}关键点BeginTransaction()返回的事务对象控制着提交和回滚。如果代码在Commit()之前抛出异常事务会自动回滚前提是未调用Commit()。—### 2. TransactionScope跨上下文的多数据库事务当业务操作需要跨越多个 DbContext或者同时操作数据库和消息队列如 MSMQ时EF 自带的事务就不够用了。TransactionScope来自System.Transactions命名空间它能够协调多个资源管理器形成一个分布式事务。csharpusing System.Transactions;public void ProcessOrderWithTransactionScope(){ // 设置事务隔离级别为 ReadCommitted var options new TransactionOptions { IsolationLevel System.Transactions.IsolationLevel.ReadCommitted, Timeout TimeSpan.FromSeconds(30) }; using (var scope new TransactionScope(TransactionScopeOption.Required, options)) { // 操作第一个数据库上下文 using (var context1 new OrderDbContext()) { context1.Orders.Add(new Order { CustomerName 王五, TotalAmount 300 }); context1.SaveChanges(); } // 操作第二个数据库上下文可能是不同数据库 using (var context2 new InventoryDbContext()) { context2.Products.First(p p.Id 101).Stock - 1; context2.SaveChanges(); } // 还可以操作非数据库资源如文件、消息队列 // 例如MessageQueue.Send(订单已创建); // 完成事务 scope.Complete(); Console.WriteLine(TransactionScope 事务成功提交); } // 如果未调用 Complete()则所有操作自动回滚}重要说明- 默认情况下SQL Server 会提升为分布式事务需要 MSDTC 服务支持。- 如果只用单个 SQL Server 数据库可以设置TransactionScopeAsyncFlowOption.Enabled来支持异步操作。- 务必在using块的最后调用scope.Complete()否则事务回滚。—### 3. 并发控制乐观并发与悲观并发并发控制是事务的孪生兄弟。EF 主要支持乐观并发Optimistic Concurrency即假设冲突不常发生在更新时检查数据是否被他人修改过。#### 3.1 使用RowVersion或Timestamp实现乐观并发SQL Server 中的rowversion类型会自动更新非常适合做并发标记。csharppublic class Product{ public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } // 并发控制标记必须映射为 rowversion 类型 public byte[] RowVersion { get; set; }}// 配置Fluent APIprotected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.EntityProduct() .Property(p p.RowVersion) .IsRowVersion(); // 自动处理并发检查}// 模拟并发冲突public void UpdateProductPrice(int productId, decimal newPrice){ using (var context new MyDbContext()) { var product context.Products.Find(productId); product.Price newPrice; try { context.SaveChanges(); Console.WriteLine(更新成功); } catch (DbUpdateConcurrencyException ex) { // 处理并发冲突 foreach (var entry in ex.Entries) { var databaseValues entry.GetDatabaseValues(); if (databaseValues null) { Console.WriteLine(记录已被删除); } else { Console.WriteLine(记录已被他人修改当前数据库值); Console.WriteLine($价格: {databaseValues[Price]}); // 可选择刷新原始值然后重试 entry.OriginalValues.SetValues(databaseValues); // context.SaveChanges(); // 重试 } } } }}工作原理在SaveChanges时EF 会将RowVersion作为 WHERE 条件的一部分。如果数据库中的 RowVersion 已经改变则影响行数为 0EF 抛出DbUpdateConcurrencyException。#### 3.2 使用WHERE条件手动实现并发控制如果不想依赖数据库的rowversion可以自己添加版本号字段。csharppublic class Account{ public int Id { get; set; } public decimal Balance { get; set; } public int Version { get; set; } // 自定义版本号}// 更新余额并检查版本号public bool TransferMoney(int fromAccountId, int toAccountId, decimal amount){ using (var context new MyDbContext()) { using (var tx context.Database.BeginTransaction()) { try { // 读取并锁定行悲观并发 var fromAccount context.Accounts .Where(a a.Id fromAccountId) .FirstOrDefault(); if (fromAccount.Balance amount) throw new InvalidOperationException(余额不足); // 更新源账户并检查版本号 fromAccount.Balance - amount; fromAccount.Version; // 使用原生 SQL 执行条件更新 var rowsAffected context.Database.ExecuteSqlRaw( UPDATE Accounts SET Balance {0}, Version {1} WHERE Id {2} AND Version {3}, fromAccount.Balance, fromAccount.Version, fromAccountId, fromAccount.Version - 1); if (rowsAffected 0) { // 并发冲突回滚事务 tx.Rollback(); Console.WriteLine(检测到并发冲突转账失败); return false; } // 更新目标账户 var toAccount context.Accounts.Find(toAccountId); toAccount.Balance amount; context.SaveChanges(); tx.Commit(); Console.WriteLine(转账成功); return true; } catch (Exception ex) { tx.Rollback(); Console.WriteLine($转账失败: {ex.Message}); return false; } } }}这段代码展示了- 悲观并发使用BeginTransaction和数据库锁通过 SQL 语句。- 乐观并发通过Version字段和WHERE条件判断如果影响行数为 0 则说明有人抢先修改。—### 4. 事务与并发的最佳实践| 场景 | 推荐方案 | 说明 ||------|----------|------|| 单个 DbContext 的简单操作 |SaveChanges()| 默认自带事务 || 多个操作需要在同一事务中 |Database.BeginTransaction()| 轻量级不涉及 MSDTC || 跨上下文/跨数据库 |TransactionScope| 注意分布式事务性能开销 || 高并发环境 | 乐观并发RowVersion | 减少锁冲突提高吞吐量 || 金融类关键操作 | 悲观并发锁行 | 确保绝对安全但会阻塞其他事务 |重要注意事项1.TransactionScope 的默认隔离级别是Serializable这可能导致死锁。建议显式设置为ReadCommitted或更低。2.异步操作在异步代码中使用 TransactionScope 时必须启用TransactionScopeAsyncFlowOption.Enabled否则会报错。3.不要长时间持有事务事务会持有数据库锁影响并发性能。尽量短小精悍。4.避免在事务中调用远程服务这会导致事务时间过长容易被数据库超时中断。—### 5. 总结Entity Framework 提供了从简单到复杂的完整事务支持SaveChanges的隐式事务、Database.BeginTransaction的显式事务以及TransactionScope的分布式事务。在并发控制方面EF 的乐观并发RowVersion是最常用的方案而结合 SQL 条件更新可以实现更细粒度的控制。实战中我们应该- 优先使用 EF 自带事务避免不必要的分布式事务。- 在需要跨资源时谨慎使用 TransactionScope并注意性能损耗。- 根据业务场景选择合适的并发控制策略读多写少用乐观并发写多且冲突频繁用悲观并发。事务和并发控制是保证数据一致性的基石理解并灵活运用这些机制才能构建出可靠的企业级应用。希望本文的代码示例能为你提供实用的参考。