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

