第一次在生产环境遇到SqlSugarClient线程安全问题是在一个高并发的订单处理系统中。凌晨三点,监控系统突然报警显示数据库连接池耗尽,紧接着就是一连串的"对象当前正在其他地方使用"的异常。这个惨痛教训让我意识到,即使像SqlSugar这样优秀的ORM框架,在多线程场景下也藏着不少暗礁。
SqlSugar作为.NET生态中轻量高效的ORM工具,以其简单的API和出色的性能赢得了大量开发者的青睐。但在多线程环境下,一个看似无害的SqlSugarClient实例共享就可能引发灾难性后果——从诡异的查询结果丢失到致命的数据库连接泄漏,这些问题往往在压力测试时才会暴露,而到那时可能为时已晚。
翻看SqlSugar的官方文档,会发现明确警示:"SqlSugarClient不是线程安全的"。这并非框架设计缺陷,而是有意为之的权衡结果。SqlSugarClient内部维护着以下关键状态:
这些状态在单线程中能完美工作,但多线程并发时就会产生竞态条件。比如两个线程同时操作同一个SqlSugarClient实例:
csharp复制// 危险示例!
var db = new SqlSugarClient(connectionConfig);
Parallel.For(0, 100, i => {
var data = db.Queryable<Order>().First(); // 可能抛出"连接正在使用"异常
});
在实际项目中,线程不安全通常表现为:
每个线程创建独立实例是最稳妥的方案,配合依赖注入容器更佳:
csharp复制// 正确做法:每个线程独立实例
Parallel.For(0, 100, i => {
using var db = new SqlSugarClient(connectionConfig);
var data = db.Queryable<Order>().First(); // 安全
});
// 在ASP.NET Core中的DI配置
services.AddScoped<ISqlSugarClient>(_ => {
return new SqlSugarClient(connectionConfig);
});
对于实例创建成本敏感的场景,可以使用ObjectPool:
csharp复制var pool = new DefaultObjectPool<SqlSugarClient>(
new SqlSugarPooledObjectPolicy(connectionConfig),
maximumRetained: 20);
Parallel.For(0, 100, i => {
var db = pool.Get();
try {
var data = db.Queryable<Order>().First();
} finally {
pool.Return(db); // 必须确保归还
}
});
SqlSugar 5.0+提供了内置线程安全方案:
csharp复制// 程序启动时配置
SqlSugarScope scope = new SqlSugarScope(connectionConfig, db => {
db.Aop.OnLogExecuting = (sql, pars) => {
Console.WriteLine(sql);
};
});
// 多线程安全使用
Parallel.For(0, 100, i => {
var data = scope.Queryable<Order>().First(); // 内部自动管理连接
});
当使用连接池时,务必注意:
csharp复制var connectionConfig = new ConnectionConfig() {
ConnectionString = "Server=.;Database=test;Uid=sa;Pwd=123",
IsAutoCloseConnection = true, // 必须设置为true
DbType = DbType.SqlServer,
InitKeyType = InitKeyType.Attribute
};
警告:IsAutoCloseConnection=false时,必须手动CloseConnection,否则会造成连接泄漏
跨线程事务必须使用独立实例:
csharp复制// 错误示例(事务污染):
using var db = new SqlSugarClient(connectionConfig);
db.BeginTran();
Parallel.For(0, 10, i => {
db.Insertable(new Order()).ExecuteCommand(); // 灾难!
});
db.CommitTran();
// 正确做法:
Parallel.For(0, 10, i => {
using var db = new SqlSugarClient(connectionConfig);
db.BeginTran();
db.Insertable(new Order()).ExecuteCommand();
db.CommitTran();
});
以下是在4核CPU服务器上的基准测试(10000次查询):
| 方案 | 耗时(ms) | 内存占用(MB) | 连接数峰值 |
|---|---|---|---|
| 单实例共享 | 异常 | - | - |
| 线程独享模式 | 1,200 | 85 | 8 |
| ObjectPool(20) | 950 | 45 | 8 |
| SqlSugarScope | 880 | 60 | 4 |
现象:
code复制System.InvalidOperationException: The connection is already open
解决方案:
现象:
code复制SqlSugar.SqlSugarException: Object is currently in use elsewhere
根因:
修复方案:
csharp复制// 错误示例:
db.Queryable<Order>().Where(it => db.Queryable<Detail>().Any(d => d.OrderId == it.Id)).ToList();
// 正确写法:
var orderIds = db.Queryable<Detail>().Select(d => d.OrderId).ToList();
db.Queryable<Order>().Where(it => orderIds.Contains(it.Id)).ToList();
监控指标:
应急处理:
powershell复制# 快速回收应用池
Restart-WebAppPool -Name "YourAppPool"
多线程下配置读写分离需要特别注意:
csharp复制var db = new SqlSugarScope(
new List<ConnectionConfig> {
new ConnectionConfig() { ConfigId = "write", /*主库配置*/ },
new ConnectionConfig() { ConfigId = "read", /*从库配置*/ }
},
db => {
db.GetConnection("read").IsAutoCloseConnection = true;
});
// 查询自动走从库
var data = db.Queryable<Order>().ToList();
// 写入走主库
db.GetConnection("write").Insertable(new Order()).ExecuteCommand();
对于全局操作需要引入分布式锁:
csharp复制using var redLock = await redlockFactory.CreateLockAsync("order_lock", TimeSpan.FromSeconds(30));
if (redLock.IsAcquired) {
using var db = new SqlSugarClient(connectionConfig);
var balance = db.Queryable<Account>().First(it => it.UserId == userId);
// 扣减余额操作
}
当需要混用ORM时:
csharp复制using var db = new SqlSugarClient(connectionConfig);
using var conn = db.Ado.Connection; // 获取底层IDbConnection
// Dapper操作必须放在SqlSugar操作外层
var dapperResult = conn.Query("SELECT * FROM Orders");
// 后续SqlSugar操作需要新建命令
var sugarResult = db.Queryable<Order>().ToList();
在经历了多次生产环境的事故后,我的血泪经验是:永远假设你的代码会被100个线程同时执行。对于SqlSugarClient的使用,要么严格保持线程隔离,要么使用官方推荐的Scope模式。那些看似能提升性能的"小聪明"优化,往往会在系统压力测试时变成最脆弱的环节。