9.27 事务和工作单元
📝 模块更新日志
-
问题修复
-
Scoped.CreateUowAsync
作用域工作单元异常无法回滚问题 4.8.8.44 ⏱️2023.09.23 #I833I9
-
9.27.1 事务
事务指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。 而一个逻辑工作单元要成为事务,就必须满足 ACID
属性。
A
:原子性(Atomicity):事务中的操作要么都不做,要么就全做C
:一致性(Consistency):事务执行的结果必须是从数据库从一个一致性状态转换到另一个一致性状态I
:隔离性(Isolation):一个事务的执行不能被其他事务干扰D
:持久性(Durability):一个事务一旦提交,它对数据库中数据的改变就应该是永久性的
9.27.2 工作单元
简单来说,就是为了保证一次完整的功能操作所产生的一系列提交数据的完整性,起着事务的作用。在计算机领域中,工作单元通常用 UnitOfWork
名称表示。
通常我们保证用户的每一次请求都是处于在一个功能单元中,也就是工作单元。
9.27.3 如何使用
9.27.3.1 [UnitOfWork]
自动管理
在 Furion
框架中,我们只需要在控制器 Action 中贴 [UnitOfWork]
特性即可开启工作单元模式,保证了每一次请求都是一个 工作单元
,要么同时成功,要么同时失败。
- 单库操作
下面方式支持所有关系型数据库类型
[UnitOfWork] // 由于出现错误,所以所有数据库变更都会自动回滚
public async Task 测试环境事务(int id)
{
// 各种奇葩数据库操作
await _personRepository.DeleteNowAsync(id);
// 其他数据库操作。。
// 故意出错
var d = await _personRepository.SqlQueriesAsync("select * from persion2 d");
}
- 多库操作
支持各种奇葩的 ORM
,包括 ADO.NET
,EFCore
等第三方,支持所有关系型数据库类型但不支持 Sqlite
[UnitOfWork(UseAmbientTransaction = true)] // 由于出现错误,所以所有数据库变更都会自动回滚
public async Task 测试环境事务(int id)
{
// 各种奇葩数据库操作
await _personRepository.DeleteNowAsync(id);
// 其他数据库操作。。
// 故意出错
var d = await _personRepository.SqlQueriesAsync("select * from persion2 d");
}
UnitOfWork
内置配置:UseAmbientTransaction
:是否开启分布式环境事务,bool
类型,默认false
,不支持Sqlite
TransactionScope
:配置分布式环境事务范围,TransactionScopeOption
类型,当UseAmbientTransaction
为true
有效TransactionIsolationLevel
:配置分布式环境事务隔离级别,IsolationLevel
类型,当UseAmbientTransaction
为true
有效TransactionTimeout
:配置分布式环境事务执行超时时间,int
类型,当UseAmbientTransaction
为true
有效TransactionScopeAsyncFlow
:配置分布式环境事务异步流支持,TransactionScopeAsyncFlowOption
类型,当UseAmbientTransaction
为true
有效EnsureTransaction
:强制使字符串sql
拓展事务有效,bool
类型,默认false
以下内容仅限 Furion 3.7.3 +
版本使用。
如使用非 EFCore
ORM 框架,可实现 IUnitOfWork
接口之后调用 services.AddUnitOfWork<TUnitOfWork>()
注册即可,如示例代码:
using Microsoft.AspNetCore.Mvc.Filters;
namespace Furion.DatabaseAccessor;
/// <summary>
/// SqlSugar 工作单元实现
/// </summary>
public sealed class SqlSugarUnitOfWork : IUnitOfWork
{
/// <summary>
/// SqlSugar 对象
/// </summary>
private readonly ISqlSugarClient _sqlSugarClient;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="sqlSugarClient"></param>
public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient)
{
_sqlSugarClient = sqlSugarClient;
}
/// <summary>
/// 开启工作单元处理
/// </summary>
/// <param name="context"></param>
/// <param name="unitOfWork"></param>
/// <exception cref="NotImplementedException"></exception>
public void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork)
{
_sqlSugarClient.AsTenant().BeginTran();
}
/// <summary>
/// 提交工作单元处理
/// </summary>
/// <param name="resultContext"></param>
/// <param name="unitOfWork"></param>
/// <exception cref="NotImplementedException"></exception>
public void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)
{
_sqlSugarClient.AsTenant().CommitTran();
}
/// <summary>
/// 回滚工作单元处理
/// </summary>
/// <param name="resultContext"></param>
/// <param name="unitOfWork"></param>
/// <exception cref="NotImplementedException"></exception>
public void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork)
{
_sqlSugarClient.AsTenant().RollbackTran();
}
/// <summary>
/// 执行完毕(无论成功失败)
/// </summary>
/// <param name="context"></param>
/// <param name="resultContext"></param>
/// <exception cref="NotImplementedException"></exception>
public void OnCompleted(FilterContext context, FilterContext resultContext)
{
_sqlSugarClient.Dispose();
}
}
之后注册即可:
services.AddUnitOfWork<SqlSugarUnitOfWork>();
有时候我们自定义了工作单元之后,个别 ORM
不支持分布式环境事务,那么就会出现执行错误,我们可以通过 System.Transactions.Transaction.Current != null
来判断是否启用了分布式环境事务,不等于 null
则为启用,否则未启用。
9.27.3.2 EnsureTransaction()
方法 ✨
有些时候我们通过静态类或者其他方式不小心创建了新的 DbContext
实例,这时候贴了 [UnitOfWork]
也不见起效,这时候可以通过以下方法来确认事务是否有效:
repository.EnsureTransaction();
如果不喜欢手动方式也可以通过 [UnitOfWork(true)]
开启此功能。
该方法会将当前仓储添加到数据库上下文池中,并确保事务可用。
9.27.3.2 手动管理
- 示例一
- 示例二
- 示例三(分布式)
// 开启事务
using (var transaction = _testRepository.Database.BeginTransaction())
{
try
{
_testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });
_testRepository.SaveNow();
_testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
_testRepository.SaveNow();
var blogs = _testRepository.Entity
.OrderBy(b => b.Url)
.ToList();
// 提交事务
transaction.Commit();
}
catch (Exception)
{
// 回滚事务
// transaction.RollBack(); // 新版本自动回滚了
}
}
var options = new DbContextOptionsBuilder<DefaultDbContext>()
.UseSqlServer(new SqlConnection(connectionString))
.Options;
// 创建连接字符串
using (var context1 = new DefaultDbContext(options))
{
// 开启事务
using (var transaction = context1.Database.BeginTransaction())
{
try
{
_testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });
_testRepository.SaveNow();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context1.SaveChanges();
// 创建新的连接对象
using (var context2 = new DefaultDbContext(options))
{
// 共享连接事务
context2.Database.UseTransaction(transaction.GetDbTransaction());
var blogs = context2.Blogs
.OrderBy(b => b.Url)
.ToList();
}
// 提交事务
transaction.Commit();
}
catch (Exception)
{
// 回滚事务
// transaction.RollBack(); // 新版本自动回滚了
}
}
}
// 开启分布式事务
// 如果事务包裹的代码中包含异步 async/await,那么需要设置 TransactionScopeAsyncFlowOption.Enabled = true
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
try
{
// 这里是 Ado.NET 操作
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM dbo.Blogs";
command.ExecuteNonQuery();
// 创建EF Core 数据库上下文
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.Options;
using (var context = new BloggingContext(options))
{
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
}
// 框架封装的仓储
_testRepository.Insert(new Blog { Url = "http://blogs.msdn.com/dotnet" });
_testRepository.SaveChanges();
// 提交事务
scope.Complete();
}
catch (System.Exception)
{
// 自动回滚
}
}
}
9.27.3.3 EnableRetryOnFailure
错误处理
如果使用的是 EFCore
数据库且启用了 EnableRetryOnFailure()
功能,那么 [UnitOfWork]
将抛出以下错误:
InvalidOperationException: The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions.
Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.
这时候需手动事务,并创建执行策略:CreateExecutionStrategy
,详情请参考官方说明:https://learn.microsoft.com/zh-cn/ef/core/miscellaneous/connection-resiliency
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
strategy.Execute(
() =>
{
using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
context.SaveChanges();
transaction.Commit();
});
9.27.4 工作单元特性说明
9.27.4.1 [UnitOfWork]
特性
[UnitOfWork]
特性只能用于控制器的 Action
中,一旦贴了 [UnitOfWork]
特性后,那么该请求自动启用工作单元模式,要么成功,要么失败。
9.27.4.2 [ManualCommit]
特性
默认情况下,Furion
框架会在一次成功请求之后自动调用 SaveChanges()
方法,如果选择手动调用 SaveChanges()
方法,可以在控制器 Action
中贴 [ManualCommit]
特性即可。
9.27.5 反馈与建议
给 Furion 提 Issue。
想了解更多 事务
知识可查阅 EF Core - 使用事务 章节。