.NET 8 + SqlSugar 实现分布式事务完整教程

0 阅读8分钟

.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. 测试验证

  1. 启动项目,确保两个数据库已创建(或自动建表成功)。
  2. 使用 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 注意事项

  1. 数据库支持:确保所有参与分布式事务的数据库支持分布式事务(XA 或 MSDTC)。例如,MySQL 需要启用 XA,SQL Server 需要启用 MSDTC 服务并配置防火墙。
  2. 性能考虑:分布式事务性能低于本地事务,应避免在频繁调用的接口中使用。对于要求不高的场景,可考虑最终一致性方案(如 Saga、本地消息表)。
  3. 连接生命周期ISqlSugarClient 必须注册为 Scoped,确保同一请求内使用同一个连接实例。
  4. 事务超时:分布式事务可能涉及网络延迟,可根据业务调整事务超时时间。
  5. 错误处理:过滤器中的异常处理确保了事务的正确回滚,但业务代码中应尽量避免捕获异常后不抛出,否则可能导致事务误提交。

13.3 扩展阅读

  • SqlSugar 官方文档:多租户事务
  • 分布式事务模式:Saga、TCC、本地消息表

通过本教程,你已经学会了如何在 .NET 8 中使用 SqlSugar 结合工作单元模式实现单库和跨库分布式事务。你可以将此模式应用到实际项目中,确保关键业务的数据一致性。