SqlSugar(及数据库 ORM)并发冲突
深入理解 SqlSugar(及数据库 ORM)并发冲突:“A command is already in progress” 错误原理与解决方案
1. 错误现象
在使用 SqlSugar 或其他 .NET 数据库驱动(如 Npgsql、EF Core、Dapper)时,多线程并发操作可能会抛出异常:
A command is already in progress
该错误常见于:
- MQTT 消息并发处理
- Web API 多请求共享同一个
SqlSugarClient实例 - 后台服务多个异步任务共用数据库操作对象
2. 根本原因:数据库连接是“单工”通道
2.1 顺序式单命令通道
PostgreSQL(以及绝大多数关系型数据库)的 连接协议 是顺序式的,一个物理数据库连接(TCP 连接)在同一个时间 只能处理一个 SQL 命令。
- 即使使用异步代码
await db.InsertAsync(...),底层仍然需要按顺序发送命令、等待响应。 - 若前一个命令尚未完成,又在同一连接上发起第二个命令(例如来自另一线程),驱动会拒绝并抛出上述异常。
类比:这就像打电话给客服,你还没说完第一件事,又急着说第二件事——对方会要求你“等我说完”再继续。
2.2 SqlSugarClient 的本质
SqlSugarClient 封装了一个数据库连接(DbConnection,如 NpgsqlConnection),并在内部持有该连接:
var db = new SqlSugarClient(config);
- 若
IsAutoCloseConnection = false(默认),连接会一直保持打开,所有操作共用同一个物理连接。 - 如果多个线程共享这一个
db实例,它们会尝试在同一个连接上并发执行 SQL → 违反“单命令”规则 → 报错。
示例(错误写法):
// 假设单例或静态共享实例
SqlSugarClient db = new SqlSugarClient(config);
// 线程 A
await db.InsertAsync(data1); // 执行 INSERT
// 线程 B 几乎同时
await db.InsertAsync(data2); // ❌ 同一个连接再次发起命令 → 冲突!
3. 解决方案:每次操作新建 SqlSugarClient 实例
3.1 原理
每次创建新的 SqlSugarClient 并设置 IsAutoCloseConnection = true 时:
- 内部会从 连接池 获取一个空闲的物理连接(或新建)。
- 执行完 SQL 后自动调用
connection.Close(),将连接归还池中,不会长时间占用。 - 每个操作拥有独立的“逻辑连接”,底层由连接池高效复用物理连接,实现线程隔离。
正确写法:
using var db = new SqlSugarClient(config);
db.Ado.IsAutoCloseConnection = true;
await db.Insertable(data).ExecuteCommandAsync();
3.2 流程步骤
| 步骤 | 说明 |
|---|---|
1. 创建 SqlSugarClient |
内部新建一个 NpgsqlConnection |
| 2. 执行 SQL | 从连接池获取空闲物理连接(或新建) |
3. IsAutoCloseConnection = true |
执行完后自动调用 connection.Close() |
4. using 释放 |
SqlSugarClient 被 Dispose,资源清理 |
| 5. 连接归还池 | 物理连接不会真正断开,归还连接池供下次复用 |
✅ 结果:每个操作都有自己的数据库上下文,天然线程安全,无并发冲突。
4. 连接池(Connection Pooling)的作用
Npgsql(及其他 ADO.NET 驱动)默认 启用连接池。
- 调用
connection.Close()时,物理 TCP 连接 并不会真正断开,而是放回池中。 - 下次新建连接时,直接从池中拿一个现成的,开销极小(纳秒~微秒级)。
- 因此,频繁创建/销毁
SqlSugarClient几乎无性能损失,反而是最佳实践。
5. 为什么不建议共享 SqlSugarClient 实例?
| 场景 | 是否安全 | 原因 |
|---|---|---|
| 单线程顺序执行 | ✅ 安全 | 命令串行,无并发 |
| 多线程/异步并发 | ❌ 不安全 | 多个任务争用同一个连接 |
| ASP.NET Core 每个请求用同一个实例 | ❌ 危险 | 框架多线程处理请求,多个请求共享同一实例会导致冲突 |
⚠️ SqlSugar 官方文档明确说明:
SqlSugarClient不是线程安全的,不应跨线程共享。
6. EF Core 的类比
EF Core 的 DbContext 同样不是线程安全的,官方推荐:
- Web 请求:每个请求一个
DbContext(注册为 Scoped)。 - 后台服务:每次操作用
IServiceScope创建新上下文。
原理完全一致:隔离连接上下文,避免并发冲突。
7. 实战案例分析:TelemetryBuffer 遥测缓冲区
7.1 问题代码
public class TelemetryBuffer
{
private readonly SqlSugarClient _db; // ❌ 成员变量,整个生命周期内复用
public TelemetryBuffer(...)
{
_db = new SqlSugarClient(SqlSugarFactory.CollectionDbConfig());
}
public void Add(CdTelemetryData telemetry)
{
_queue.Enqueue(telemetry);
if (_queue.Count >= _bufferSize) Flush();
}
public void Flush()
{
// 多个线程可能同时进入这里
_db.Insertable(telemetries).ExecuteCommand(); // ❌ 使用共享的 _db
}
}
问题根源:
TelemetryBuffer可能被注册为Singleton或Scoped,MQTT 消息异步并发到达。- 多个线程同时调用
Add→Flush,它们共用同一个_db实例,底层同一个连接无法并发执行命令。
7.2 修复后的安全版本
public class TelemetryBuffer : IDisposable
{
private readonly int _bufferSize;
private readonly int _pageSize = 500;
private readonly ILogger<TelemetryBuffer> _logger;
private readonly ConcurrentQueue<CdTelemetryData> _queue = new();
// ✅ 不再持有 _db 成员变量
public TelemetryBuffer(int bufferSize, ILogger<TelemetryBuffer> logger)
: this(bufferSize, 500, logger) { }
public TelemetryBuffer(int bufferSize, int pageSize, ILogger<TelemetryBuffer> logger)
{
_bufferSize = bufferSize;
_pageSize = pageSize;
_logger = logger;
}
public void Add(CdTelemetryData telemetry)
{
_queue.Enqueue(telemetry);
if (_queue.Count >= _bufferSize) Flush();
}
public void Flush()
{
if (_queue.IsEmpty) return;
var telemetries = new List<CdTelemetryData>();
while (_queue.TryDequeue(out var telemetry))
{
telemetry.CreateTime = DateTime.Now;
telemetries.Add(telemetry);
}
// ✅ 每次 Flush 创建新 SqlSugarClient,用完立即释放
using var db = new SqlSugarClient(SqlSugarFactory.CollectionDbConfig())
{
Ado = { IsAutoCloseConnection = true }
};
try
{
int resultCount = db.Insertable(telemetries)
.PageSize(_pageSize)
.ExecuteCommand();
if (resultCount == 0)
{
_logger.LogError("批量插入遥测数据失败: {Data}", JsonConvert.SerializeObject(telemetries));
}
else
{
_logger.LogInformation("批量插入遥测数据成功,插入数量: {Count}", resultCount);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "批量插入遥测数据异常: {Message}", ex.Message);
// 可根据业务需要将失败数据重新入队或存入死信队列
}
}
public void Dispose()
{
Flush();
}
}
关键修改:
- 移除成员变量
_db。 - 在
Flush()方法内部使用using创建临时SqlSugarClient。 - 设置
IsAutoCloseConnection = true确保连接归还池。 - 每个
Flush()调用完全隔离,线程安全。
7.3 进一步优化建议
- 配置源头保证自动关闭:在
SqlSugarFactory.CollectionDbConfig()中直接设置IsAutoCloseConnection = true。 - 异步化(可选):将
Flush()改为异步版本FlushAsync(),使用ExecuteCommandAsync(),但需注意避免在 MQTT 回调中直接await,可结合后台任务定期批量刷新。 - 连接池监控:在 Npgsql 连接字符串中添加
Maximum Pool Size等参数,防止连接耗尽。
8. 总结与最佳实践
| 实践 | 原理/效果 |
|---|---|
每次操作新建 SqlSugarClient |
为每个操作分配独立逻辑连接,线程安全 |
设置 IsAutoCloseConnection = true |
确保连接用完归还池,避免泄漏 |
使用 using 语句 |
保证资源及时释放 |
| 绝不跨线程共享 ORM 实例 | 避免多线程竞争同一连接 |
| 充分利用连接池 | 高频创建/销毁连接几乎无性能损耗 |
💡 核心思想:在 .NET 中,几乎所有数据库客户端(EF Core、Dapper、SqlSugar、原生 ADO.NET)都要求“每个操作使用独立连接上下文”以保证线程安全。这不是 SqlSugar 的缺陷,而是所有基于连接的数据库驱动的通用约束。
掌握这个原理后,你可以举一反三地处理任何 ORM 的并发问题。

浙公网安备 33010602011771号