基于SQL Server的C# Windows服务高并发SQL访问性能问题
解决高并发下SQL Server + C# Windows服务的查询性能瓶颈
嘿,咱们一步步拆解你的性能问题——在C# Windows服务里用500个并行任务访问SQL Server,本地单次查询要1.2秒,表是40列40万行的MyObject。高并发场景下这个耗时会被无限放大,下面是几个最能见效的优化方向,都是我踩过坑后总结的经验:
1. 先把连接池用对(最快见效的优化)
.NET的SqlConnection默认是开连接池的,但如果代码没做好连接管理,分分钟导致连接池耗尽、频繁创建销毁连接的额外开销。
- 必须用
using包裹连接和命令:这会自动把连接放回连接池,而不是真的关闭连接(连接池会复用空闲连接),绝对不能手动调用Close()后就不管了:using (var conn = new SqlConnection(yourConnString)) { await conn.OpenAsync(); // 用异步方法,别阻塞线程 using (var cmd = new SqlCommand(yourQuery, conn)) { // 一定要参数化!避免SQL注入还能让SQL Server缓存查询计划 cmd.Parameters.Add("@SerialNumber", SqlDbType.VarChar).Value = serialNumber; using (var reader = await cmd.ExecuteReaderAsync()) { // 读取数据逻辑 } } } - 调整连接池参数:默认连接池最大是100,500个并发的话肯定不够,在连接字符串里加
Max Pool Size=200(别超过SQL Server的最大连接数,默认32767,但实际要看服务器资源),再加Min Pool Size=30,避免低峰时连接被销毁,高峰时重新创建的开销。
2. 优化查询本身(从根源减少耗时)
单次查询1.2秒,说明你的查询大概率没走最优索引,或者返回了太多不必要的数据:
- 给查询条件加合适的索引:如果你的查询是按
PrimaryId和serialNumber过滤,那一定要建复合非聚集索引,如果需要返回的列多,就做覆盖索引(把需要的列都包含进去,不用回表查):CREATE NONCLUSTERED INDEX IX_MyObject_PrimaryId_SerialNumber ON MyObject (PrimaryId, SerialNumber) INCLUDE (Column1, Column2, ...) -- 只写你实际需要的列,别用*! - 别返回全表列:你的ORM是
ORM.GetObjecys<MyObject>,如果MyObject映射了40列,但业务只需要其中5列,一定要改查询,只取需要的列——少传数据就能少占带宽和内存,速度自然上去。 - 确认ORM用了参数化查询:如果是自定义ORM,别拼接SQL字符串!参数化不仅防注入,还能让SQL Server缓存执行计划,不用每次都重新编译。
3. 控制并发量(别把数据库打懵)
直接开500个并行任务,数据库根本扛不住,反而所有请求都变慢,建议限制并发数:
- 用
SemaphoreSlim限流:比如设置最大并发50,让请求排队有序执行,反而比一窝蜂快:var semaphore = new SemaphoreSlim(50); // 最多同时跑50个查询 for (int i = 0; i < 500; i++) { await semaphore.WaitAsync(); _ = Task.Run(async () => { try { Stopwatch sw = new Stopwatch(); sw.Start(); var serialNumber = "55292572"; var orderIdArr = await ORM.GetObjecys<MyObject>(t => t.PrimaryId == ... && t.SerialNumber == serialNumber); sw.Stop(); // 记录耗时逻辑 } finally { semaphore.Release(); // 一定要释放,不然会堵死 } }); } - 试试批量查询:如果这些查询的条件有规律,比如多个
serialNumber,能不能一次查多个,然后在内存里拆分结果?这样能减少数据库请求次数,降低开销。
4. 排查ORM的性能损耗
如果是自定义ORM,很可能在实体映射环节有额外开销:
- 缓存映射关系:别每次查询都用反射解析实体属性,提前把字段和属性的映射关系缓存起来,用表达式树代替反射。
- 关闭不必要的延迟加载:如果ORM默认开了延迟加载,会偷偷多查几次数据库,业务不需要的话一定要关掉。
- 用异步方法:你的代码里用的是同步
ExecuteReader,换成异步ExecuteReaderAsync,避免阻塞线程池线程,提升并发能力。
5. 服务器和数据库配置调优
- 给SQL Server足够内存:默认SQL Server会占大部分可用内存,别让它因为内存不足频繁读磁盘,这会大幅拖慢查询。
- 换SSD存储:如果数据库文件在机械硬盘上,赶紧换成SSD——大表查询的IO开销会骤降。
- 调整线程池参数:Windows服务的线程池默认线程数不多,可以用
ThreadPool.SetMinThreads适当调高,但别太夸张,不然线程上下文切换会变多。
最后,建议你用SQL Server的Extended Events或者Profiler抓一下慢查询,看看具体是查询编译、执行还是数据传输耗时最长,这样能精准定位问题,不用瞎试。
内容的提问来源于stack exchange,提问作者Raymond He




