连接未释放导致连接池耗尽是根本原因。需确保 sqlconnection 均用 using 包裹、禁用存储过程内嵌套连接、确认连接字符串含 pooling=true、优化长执行存储过程。

存储过程调用时连接没释放,导致池子被占满
常见现象是并发稍高就报 Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool,但数据库服务器本身压力不大。本质不是连接池太小,而是连接没归还——多数因为存储过程执行完没显式关闭 SqlConnection,或用了 using 却在其中抛了未捕获异常,导致 Dispose() 没走完。
- 确保所有
SqlConnection实例都包裹在using块里,哪怕只做一次查询 - 避免在
using外部保留对SqlConnection的引用(比如赋值给类字段) - 检查是否手动调用了
Open()但忘了Close();Close()和Dispose()在 ADO.NET 中等效,但依赖using更安全 - 如果用了 Dapper 或 EF Core,确认没绕过其连接管理机制(例如直接从
DbContext.Database.GetDbConnection()拿连接后自行操作)
存储过程中嵌套打开新连接(连接内开连接)
典型错误是在 SQL Server 存储过程里用 OPENROWSET、OPENDATASOURCE,或更隐蔽的:在 .NET 层调用一个存储过程,该过程内部又通过 SqlDataAdapter 或 SqlCommand 执行另一段 SQL —— 这会额外占用一个池连接,且不共享原连接上下文。
- 禁止在存储过程内发起外部数据库访问;跨库操作改用视图、同义词或应用层聚合
- 如果必须多数据源协作,把逻辑拆到应用层,用单个
SqlConnection分批处理,或启用 MARS(Multiple Active Result Sets),但仅限必要场景 - 检查 ORM 配置,如 Entity Framework 的
DbContext是否被重复构造(例如每次调用都 new 一个),造成隐式新开连接
连接字符串中 Pooling=true 被意外关掉
有些部署环境(尤其是容器化或 CI/CD 流水线)会动态拼接连接字符串,一不留神把 Pooling=false 写死进去,或者漏掉 Pooling=true(默认是 true,但显式声明更稳妥)。后果是每次调用都新建物理连接,TCP 握手+登录认证耗时陡增,吞吐直接腰斩。
- 在代码中打印或日志记录实际生效的连接字符串(脱敏后),确认含
Pooling=true - 避免在连接字符串里混用大小写参数名(如
poolingvsPooling),.NET Core 3.1+ 对大小写敏感 - 不要依赖“默认开启”,尤其在跨框架版本迁移时(.NET Framework 默认 true,.NET 5+ 同样 true,但显式写出来可防配置覆盖)
存储过程长时间运行,阻塞连接池周转
连接池大小是固定的(默认 100),如果某个存储过程平均执行 2 秒,QPS 到 50 就可能打满池子——不是因为慢,而是因为「连接被占着没法复用」。这时候加池大小只是掩耳盗铃,得治本。
- 用
SET STATISTICS IO ON和SET STATISTICS TIME ON定位存储过程里最耗时的语句,优先优化扫描行数多、缺少索引的WHERE或JOIN - 检查是否用了表变量(
@table)存大量数据,改用临时表(#temp)并建适当索引 - 避免在存储过程中循环调用其他存储过程(
WHILE+EXEC),改用集合操作或应用层分页 - 对读多写少场景,考虑用
WITH (NOLOCK)降低锁等待,但需评估脏读风险
真正卡住吞吐的,往往不是池子大小,而是单次调用占连接的时间。压测时盯住 sys.dm_exec_sessions 里的 last_request_end_time 和连接状态,比盲目调 Max Pool Size 管用得多。










