.NET 8 + SqlSugar 实现分布式事务完整教程
1. 前言
在复杂的业务系统中,我们经常需要操作多个数据库,并保证数据的一致性。例如,一个订单操作需要同时更新订单库和库存库,如果其中一个失败,另一个也必须回滚。这就是分布式事务要解决的问题。
SqlSugar 作为一款优秀的 ORM,提供了多租户模式,可以轻松实现跨多个数据库的事务(基于数据库本身的分布式事务支持)。本文将从一个空白项目开始,逐步搭建一个基于 .NET 8 + SqlSugar 的 Web API,并演示如何通过工作单元(UnitOfWork)模式实现单库事务和多库分布式事务。
2. 环境准备
- Visual Studio 2022
- .NET 8 SDK
- SQL Server
- 基本的 C# 和 ASP.NET Core 知识
3. 创建项目
打开 Visual Studio 2022,选择“创建新项目” -> “ASP.NET Core Web API”,项目名称设为 DistributedTransactionDemo,框架选择 .NET 8.0,取消勾选“使用控制器”(可选),点击创建。
4. 安装 NuGet 包
在解决方案资源管理器中右键项目 -> 管理 NuGet 程序包,安装以下包:
SqlSugarCore(最新版,例如 5.1.4.143)Swashbuckle.AspNetCore(用于 Swagger 文档,开发调试用)
5. 配置数据库连接(多库)
打开 appsettings.json,添加两个数据库的连接字符串,分别对应订单库和库存库(这里以 SQL Server 和 MySQL 为例,你可以根据实际环境调整):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"OrderDb": "Server=.;Database=OrderDb;User Id=sa;Password=123456;TrustServerCertificate=true;",
"StockDb": "Server=localhost;Database=StockDb;User=root;Password=123456;Charset=utf8;"
},
"AllowedHosts": "*"
}
注意:MySQL 连接字符串格式可能与 SQL Server 不同,请根据实际情况修改。
6. 创建实体类
在项目中新建 Models 文件夹,添加两个实体类,分别对应订单和库存。
Order.cs(放在 Models/Order.cs)
using SqlSugar;
namespace DistributedTransactionDemo.Models;
[SugarTable("Orders")]
public class Order
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public DateTime CreateTime { get; set; }
}
Stock.cs(放在 Models/Stock.cs)
using SqlSugar;
namespace DistributedTransactionDemo.Models;
[SugarTable("Stocks")]
public class Stock
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string ProductName { get; set; }
public int AvailableQuantity { get; set; }
public DateTime LastUpdateTime { get; set; }
}
7. 实现工作单元核心组件
工作单元模式用于将一组数据库操作封装在同一个事务中。我们将通过 AOP 过滤器自动管理事务的开启、提交和回滚。
7.1 定义工作单元接口 IUnitOfWork
新建文件夹 Core,在其中创建 IUnitOfWork.cs:
using Microsoft.AspNetCore.Mvc.Filters;
namespace DistributedTransactionDemo.Core;
/// <summary>
/// 工作单元接口
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// 开启事务
/// </summary>
void BeginTransaction(ActionExecutingContext context);
/// <summary>
/// 提交事务
/// </summary>
void CommitTransaction(ActionExecutedContext resultContext);
/// <summary>
/// 回滚事务
/// </summary>
void RollbackTransaction(ActionExecutedContext resultContext);
/// <summary>
/// 清理资源
/// </summary>
void OnCompleted(ActionExecutingContext context, ActionExecutedContext resultContext);
}
7.2 实现 SqlSugar 的工作单元 SqlSugarUnitOfWork
在 Core 文件夹下创建 SqlSugarUnitOfWork.cs:
using Microsoft.AspNetCore.Mvc.Filters;
using SqlSugar;
namespace DistributedTransactionDemo.Core;
public class SqlSugarUnitOfWork : IUnitOfWork
{
private readonly ISqlSugarClient _sqlSugarClient;
public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient)
{
_sqlSugarClient = sqlSugarClient;
}
public void BeginTransaction(ActionExecutingContext context)
{
// 开启多租户事务(如果只配置了一个数据库,效果等同于单库事务)
_sqlSugarClient.AsTenant().BeginTran();
}
public void CommitTransaction(ActionExecutedContext resultContext)
{
_sqlSugarClient.AsTenant().CommitTran();
}
public void RollbackTransaction(ActionExecutedContext resultContext)
{
_sqlSugarClient.AsTenant().RollbackTran();
}
public void OnCompleted(ActionExecutingContext context, ActionExecutedContext resultContext)
{
// 释放资源
_sqlSugarClient.Dispose();
}
}
7.3 创建工作单元特性 UnitOfWorkAttribute
在 Core 文件夹下创建 UnitOfWorkAttribute.cs,用于标记需要启用事务的 Action:
namespace DistributedTransactionDemo.Core;
[AttributeUsage(AttributeTargets.Method)]
public class UnitOfWorkAttribute : Attribute
{
}
7.4 创建工作单元过滤器 UnitOfWorkFilter
在 Core 文件夹下创建 UnitOfWorkFilter.cs:
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
namespace DistributedTransactionDemo.Core;
public class UnitOfWorkFilter : IAsyncActionFilter, IOrderedFilter
{
private readonly ILogger<UnitOfWorkFilter> _logger;
public UnitOfWorkFilter(ILogger<UnitOfWorkFilter> logger)
{
_logger = logger;
}
public int Order => 999; // 尽量靠后执行,包裹整个 Action
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
var method = actionDescriptor?.MethodInfo;
// 检查是否标记了 UnitOfWorkAttribute
if (method == null || !method.IsDefined(typeof(UnitOfWorkAttribute), true))
{
await next();
return;
}
_logger.LogInformation("开始工作单元事务");
var unitOfWork = context.HttpContext.RequestServices.GetRequiredService<IUnitOfWork>();
unitOfWork.BeginTransaction(context);
var resultContext = await next();
if (resultContext.Exception == null || resultContext.ExceptionHandled)
{
unitOfWork.CommitTransaction(resultContext);
_logger.LogInformation("事务提交成功");
}
else
{
unitOfWork.RollbackTransaction(resultContext);
_logger.LogError(resultContext.Exception, "事务回滚");
}
unitOfWork.OnCompleted(context, resultContext);
_logger.LogInformation("工作单元结束");
}
}
8. 注册服务(Program.cs)
打开 Program.cs,配置服务容器和中间件。
using SqlSugar;
using DistributedTransactionDemo.Core;
using DistributedTransactionDemo.Models;
using DistributedTransactionDemo.Repositories;
var builder = WebApplication.CreateBuilder(args);
// 添加控制器并全局注册工作单元过滤器
builder.Services.AddControllers(options =>
{
options.Filters.Add<UnitOfWorkFilter>();
});
// 添加 Swagger 生成器(用于开发调试)
builder.Services.AddSwaggerGen();
// 注册 SqlSugarClient 为 Scoped,配置多个数据库连接
builder.Services.AddScoped<ISqlSugarClient>(sp =>
{
// 从配置文件读取连接字符串
var orderDbConn = builder.Configuration.GetConnectionString("OrderDb");
var stockDbConn = builder.Configuration.GetConnectionString("StockDb");
var configs = new List<ConnectionConfig>
{
new ConnectionConfig
{
ConfigId = "OrderDb", // 数据库标识,后续通过该标识获取连接
ConnectionString = orderDbConn,
DbType = DbType.SqlServer,
IsAutoCloseConnection = true
},
new ConnectionConfig
{
ConfigId = "StockDb",
ConnectionString = stockDbConn,
DbType = DbType.MySql, // 根据实际数据库类型修改
IsAutoCloseConnection = true
}
};
return new SqlSugarClient(configs);
});
// 注册工作单元服务
builder.Services.AddTransient<IUnitOfWork, SqlSugarUnitOfWork>();
// 注册仓储(稍后创建)
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IStockRepository, StockRepository>();
var app = builder.Build();
// 自动建表(仅开发环境,便于测试)
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
// 为两个数据库分别建表
db.GetConnection("OrderDb").CodeFirst.InitTables<Order>();
db.GetConnection("StockDb").CodeFirst.InitTables<Stock>();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 注释掉 HTTPS 重定向,开发时使用 HTTP 方便调试
// app.UseHttpsRedirection();
app.MapControllers();
app.Run();
9. 创建仓储和业务服务
为了演示跨库操作,我们创建两个仓储,分别操作订单库和库存库。
9.1 订单仓储
新建文件夹 Repositories,创建接口和实现。
IOrderRepository.cs
using DistributedTransactionDemo.Models;
namespace DistributedTransactionDemo.Repositories;
public interface IOrderRepository
{
Task<int> InsertAsync(Order order);
}
OrderRepository.cs
using SqlSugar;
using DistributedTransactionDemo.Models;
namespace DistributedTransactionDemo.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly ISqlSugarClient _db;
public OrderRepository(ISqlSugarClient db)
{
_db = db;
}
public async Task<int> InsertAsync(Order order)
{
// 获取订单库的连接进行操作
return await _db.GetConnection("OrderDb").Insertable(order).ExecuteReturnIdentityAsync();
}
}
9.2 库存仓储
IStockRepository.cs
using DistributedTransactionDemo.Models;
namespace DistributedTransactionDemo.Repositories;
public interface IStockRepository
{
Task<int> UpdateStockAsync(string productName, int quantity);
}
StockRepository.cs
using SqlSugar;
using DistributedTransactionDemo.Models;
namespace DistributedTransactionDemo.Repositories;
public class StockRepository : IStockRepository
{
private readonly ISqlSugarClient _db;
public StockRepository(ISqlSugarClient db)
{
_db = db;
}
public async Task<int> UpdateStockAsync(string productName, int quantity)
{
// 获取库存库的连接
var stockDb = _db.GetConnection("StockDb");
// 查询现有库存
var stock = await stockDb.Queryable<Stock>()
.Where(s => s.ProductName == productName)
.FirstAsync();
if (stock == null)
{
// 如果没有记录,则插入新库存记录(数量为扣减后的值)
stock = new Stock
{
ProductName = productName,
AvailableQuantity = -quantity, // 假设库存扣减后可能为负数,这里仅演示
LastUpdateTime = DateTime.Now
};
return await stockDb.Insertable(stock).ExecuteCommandAsync();
}
else
{
// 更新库存
stock.AvailableQuantity -= quantity;
stock.LastUpdateTime = DateTime.Now;
return await stockDb.Updateable(stock).ExecuteCommandAsync();
}
}
}
10. 单库事务示例(回顾)
在控制器中添加一个仅操作订单库的示例,验证单库事务。新建控制器 TestController.cs:
using Microsoft.AspNetCore.Mvc;
using DistributedTransactionDemo.Core;
using DistributedTransactionDemo.Models;
using DistributedTransactionDemo.Repositories;
namespace DistributedTransactionDemo.Controllers;
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
private readonly IOrderRepository _orderRepository;
public TestController(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
/// <summary>
/// 单库事务成功示例
/// </summary>
[HttpGet("single-success")]
[UnitOfWork]
public async Task<IActionResult> SingleSuccess()
{
var order = new Order
{
ProductName = "笔记本电脑",
Quantity = 1,
Price = 5999,
CreateTime = DateTime.Now
};
await _orderRepository.InsertAsync(order);
return Ok("订单创建成功");
}
}
11. 分布式事务实现(多库)
现在我们来演示跨订单库和库存库的分布式事务。
11.1 修改 IUnitOfWork 接口(可选)
目前 SqlSugarUnitOfWork 中使用的 AsTenant().BeginTran() 已经支持多库事务,因此接口无需修改。但为了更清晰,可以保留原样。
11.2 配置多数据库连接(已在第 5 步完成)
确保 Program.cs 中注册了多个 ConnectionConfig。
11.3 创建业务服务(可选)
为了演示,我们可以创建一个 OrderService 来组合两个仓储的操作。
IOrderService.cs
namespace DistributedTransactionDemo.Services;
public interface IOrderService
{
Task<bool> CreateOrderWithStock(string productName, int quantity, decimal price);
}
OrderService.cs
using DistributedTransactionDemo.Repositories;
using DistributedTransactionDemo.Models;
namespace DistributedTransactionDemo.Services;
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IStockRepository _stockRepository;
public OrderService(IOrderRepository orderRepository, IStockRepository stockRepository)
{
_orderRepository = orderRepository;
_stockRepository = stockRepository;
}
public async Task<bool> CreateOrderWithStock(string productName, int quantity, decimal price)
{
var order = new Order
{
ProductName = productName,
Quantity = quantity,
Price = price,
CreateTime = DateTime.Now
};
await _orderRepository.InsertAsync(order);
// 扣减库存
await _stockRepository.UpdateStockAsync(productName, quantity);
return true;
}
}
然后在 Program.cs 中注册该服务:
builder.Services.AddScoped<IOrderService, OrderService>();
11.4 控制器中调用
在 TestController 中添加新的 Action:
private readonly IOrderService _orderService;
public TestController(IOrderRepository orderRepository, IOrderService orderService)
{
_orderRepository = orderRepository;
_orderService = orderService;
}
/// <summary>
/// 跨库分布式事务成功示例
/// </summary>
[HttpGet("distributed-success")]
[UnitOfWork]
public async Task<IActionResult> DistributedSuccess()
{
await _orderService.CreateOrderWithStock("笔记本电脑", 1, 5999);
return Ok("订单和库存更新成功");
}
/// <summary>
/// 跨库分布式事务失败示例(模拟异常,事务回滚)
/// </summary>
[HttpGet("distributed-fail")]
[UnitOfWork]
public async Task<IActionResult> DistributedFail()
{
await _orderService.CreateOrderWithStock("手机", 2, 3999);
// 模拟异常
throw new InvalidOperationException("模拟错误,事务应回滚");
// 注意:异常后的代码不会执行,但过滤器会捕获异常并回滚事务
}
12. 测试验证
- 启动项目,确保两个数据库已创建(或自动建表成功)。
- 使用 Swagger 或 Postman 调用以下接口:
GET /api/Test/distributed-success预期返回成功,检查两个数据库:订单库新增一条订单记录,库存库中“笔记本电脑”的库存减少1(如果不存在则新增一条记录)。GET /api/Test/distributed-fail预期返回 500 错误,检查两个数据库:订单库和库存库均无变化(事务回滚)。
13. 总结与注意事项
13.1 原理总结
- SqlSugar 的多租户模式通过
AsTenant()提供跨库事务支持,底层依赖于数据库的分布式事务协调能力(如 SQL Server 的 MSDTC、MySQL 的 XA 事务)。 - 工作单元过滤器自动管理事务生命周期,开发者只需在 Action 上标记
[UnitOfWork],无需手动处理事务提交/回滚。
13.2 注意事项
- 数据库支持:确保所有参与分布式事务的数据库支持分布式事务(XA 或 MSDTC)。例如,MySQL 需要启用 XA,SQL Server 需要启用 MSDTC 服务并配置防火墙。
- 性能考虑:分布式事务性能低于本地事务,应避免在频繁调用的接口中使用。对于要求不高的场景,可考虑最终一致性方案(如 Saga、本地消息表)。
- 连接生命周期:
ISqlSugarClient必须注册为 Scoped,确保同一请求内使用同一个连接实例。 - 事务超时:分布式事务可能涉及网络延迟,可根据业务调整事务超时时间。
- 错误处理:过滤器中的异常处理确保了事务的正确回滚,但业务代码中应尽量避免捕获异常后不抛出,否则可能导致事务误提交。
13.3 扩展阅读
- SqlSugar 官方文档:多租户事务
- 分布式事务模式:Saga、TCC、本地消息表
通过本教程,你已经学会了如何在 .NET 8 中使用 SqlSugar 结合工作单元模式实现单库和跨库分布式事务。你可以将此模式应用到实际项目中,确保关键业务的数据一致性。