You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 6 Web API结合System.Data.SQLite使用TransactionScope遇阻

问题描述

ASP.NET Core 6 Web API项目,使用SQLite数据库,未来计划迁移到SQL Server,因此严格划分数据库层。项目特殊点是需要两个SQLite数据库:一个内容固定的系统库,一个配置可能在API调用间变化的当前“事件”库。

数据库层通过_databaseService创建连接:

public DbConnection CreateConnection()
{
    var connection = DbFactory.CreateConnection();

    if (connection != null)
    {
        connection.ConnectionString = ConnectionString;
        connection.Open();
        return connection;
    }

    return null;
}

数据库层使用该连接的方式如下:

public int UpdateInvoiceItem(int priceListItemId, int qty, int invoiceItemId)
{
    const string sql = "update INVOICE_ITEM set PRICE_LIST_ITEM_ID = @priceListItemId, QTY = @qty where INVOICE_ITEM_ID =  @invoiceItemId";

    using var conn = _databaseService.GetEventDbHelper().CreateConnection();

    return conn.Execute(sql, new { priceListItemId, qty, invoiceItemId });
}

业务层使用TransactionScope批量操作时出现问题:第一次调用UpdateInvoiceItem正常,第二次无响应,推测是事务导致的。

public void UpdateInvoice(Invoice invoice)
{
    using var transactionScope = new TransactionScope();

    foreach (var invoiceItem in invoice.InvoiceItems)
    {
        if (invoiceItem.Qty == 0)
        {
            _invoiceDs.RemoveInvoiceItem(invoiceItem.InvoiceItemId);
        }
        else if (invoiceItem.OrgQty != invoiceItem.Qty ||
                 invoiceItem.OrgPriceListItemId != invoiceItem.PriceListItemId)
        {
            _invoiceDs.UpdateInvoiceItem(invoiceItem.PriceListItemId, invoiceItem.Qty,
                invoiceItem.PriceListItemId);
        }
    }

    transactionScope.Complete();
}

需求:如何让每次获取的连接都加入TransactionScope?是否应该让数据库层(UpdateInvoiceItem)持有连接至类生命周期(已设置为Scoped)?或是有其他方案?


解决方案

核心问题分析

SQLite本身对多连接事务支持有限,加上你现在每次操作都创建新连接,在TransactionScope下,第一个连接开启事务后,第二个新连接会因为SQLite的单连接事务特性被阻塞,这就是第二次调用无响应的原因。同时,TransactionScope默认会尝试提升为分布式事务,但SQLite不支持分布式事务,这也会加剧问题。

可行方案

1. 复用Scoped连接(推荐,适配未来SQL Server迁移)

既然你的数据库层类已经是Scoped生命周期,直接在类级别持有一个打开的连接,而不是每次方法调用都创建新连接:

  • 在数据库层类的构造函数中,通过_databaseService.GetEventDbHelper().CreateConnection()获取连接并保存为私有字段
  • 确保连接在类释放时关闭(实现IDisposable接口,在Dispose方法中关闭连接)
  • 所有数据库操作复用这个连接

修改后的数据库层类示例:

public class InvoiceDs : IDisposable
{
    private readonly DbConnection _conn;
    private bool _disposed = false;

    public InvoiceDs(DatabaseService databaseService)
    {
        _conn = databaseService.GetEventDbHelper().CreateConnection();
    }

    public int UpdateInvoiceItem(int priceListItemId, int qty, int invoiceItemId)
    {
        const string sql = "update INVOICE_ITEM set PRICE_LIST_ITEM_ID = @priceListItemId, QTY = @qty where INVOICE_ITEM_ID =  @invoiceItemId";
        return _conn.Execute(sql, new { priceListItemId, qty, invoiceItemId });
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing)
        {
            _conn?.Close();
            _conn?.Dispose();
        }
        _disposed = true;
    }
}

这样在同一个请求(Scoped)内,所有数据库操作都会复用同一个连接,TransactionScope能正确将这个连接纳入事务管理,同时也符合未来SQL Server的使用习惯。

2. 手动管理事务(替代方案,适合临时调整)

如果不想修改连接生命周期,可以在业务层手动创建事务,然后传递给数据库层的方法:

  • 业务层先获取连接,开启事务
  • 将事务对象传递给每个数据库操作方法
  • 最后统一提交或回滚

业务层修改示例:

public void UpdateInvoice(Invoice invoice)
{
    using var conn = _databaseService.GetEventDbHelper().CreateConnection();
    using var transaction = conn.BeginTransaction();
    try
    {
        foreach (var invoiceItem in invoice.InvoiceItems)
        {
            if (invoiceItem.Qty == 0)
            {
                _invoiceDs.RemoveInvoiceItem(transaction, invoiceItem.InvoiceItemId);
            }
            else if (invoiceItem.OrgQty != invoiceItem.Qty ||
                     invoiceItem.OrgPriceListItemId != invoiceItem.PriceListItemId)
            {
                _invoiceDs.UpdateInvoiceItem(transaction, invoiceItem.PriceListItemId, invoiceItem.Qty,
                    invoiceItem.InvoiceItemId);
            }
        }
        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

数据库层方法修改为接收事务参数:

public int UpdateInvoiceItem(DbTransaction transaction, int priceListItemId, int qty, int invoiceItemId)
{
    const string sql = "update INVOICE_ITEM set PRICE_LIST_ITEM_ID = @priceListItemId, QTY = @qty where INVOICE_ITEM_ID =  @invoiceItemId";
    return transaction.Connection.Execute(sql, new { priceListItemId, qty, invoiceItemId }, transaction: transaction);
}

这个方案不需要依赖TransactionScope,但需要修改多个方法签名,未来迁移到SQL Server时也能兼容,但代码侵入性更强。

3. 配置TransactionScope避免分布式事务提升(不推荐)

如果坚持使用TransactionScope和每次创建新连接,需要配置TransactionScopeOption.Required并设置TransactionScopeAsyncFlowOption.Enabled,同时关闭分布式事务提升,但SQLite本身不支持分布式事务,这个方案无法解决核心的单连接阻塞问题,仅作为辅助配置:

using var transactionScope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
    TransactionScopeAsyncFlowOption.Enabled);

总结

优先选择复用Scoped连接的方案,既解决当前SQLite的事务问题,又能和未来SQL Server的使用方式对齐,代码改动也相对可控。

内容的提问来源于stack exchange,提问作者Sam Carleton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 21:20:17