资讯动态

C#连接SQL Server实战指南:从SqlClient到批量写入与性能优化

发布时间:2026/10/17 9:20:45 来源:尧图企业网站定制
说实话C#连SQL Server这件事网上教程多到爆炸但大部分是复制粘贴级别的“能用就行”真正到生产环境一跑就各种掉链子。我早年做某制造业管理系统时大量业务逻辑都压在数据库交互这一层从连接串配置到参数化查询再到批量写入能踩的坑基本都踩过一遍。这篇文章就把我这几年的实战经验整理出来不讲空话只聊怎么用、为什么这么用、以及哪些坑千万别踩。先说清楚这篇文章适合谁看刚把C#语法啃完想上手数据库的小白能用框架写CRUD但没搞懂底层原理的初级开发者以及想系统梳理SqlClient最佳实践的中级程序员。看完之后你能独立完成从连接数据库、执行增删改查、调用存储过程、处理事务到批量写入的完整链路顺带把SQL注入、连接泄漏、并发超时这些常见毛病一并治了。1. 先想明白C#和SQL Server之间的通信管道是怎么搭起来的在很多新手眼里C#和SQL Server交互就是拖个控件、写几行代码的事。但如果你不清楚底层到底发生了什么出问题的时候就只能靠猜。我习惯把这套机制拆成三层理解。1.1 从架构角度看C#访问SQL Server的完整通路数据从SQL Server数据库传到你的C#程序界面中间要经过四个环节客户端代码、ADO.NET数据提供程序、网络协议、数据库引擎。客户端代码就是你写的C#类和方法负责发起查询意图、接收结果并处理逻辑。数据提供程序以SqlClient为核心它实现了ADO.NET规范里的Connection、Command、DataReader、DataAdapter等对象职责是把C#的调用翻译成SQL Server能懂的请求。网络协议SqlClient默认使用TDS协议Tabular Data Stream与SQL Server通信。如果你没特意配置它走的是TCP 1433端口。数据库引擎SQL Server收到请求后执行语句把结果集或影响行数沿原路返回。理解这层之后很多诡异问题就豁然开朗了比如连接串里明明写了服务器名却连不上很可能就是TDS协议或端口没走通再比如某个查询首次执行特别慢很大概率是查询优化器正在为你的语句生成执行计划。如果你只想做本地小工具这套链路怎么搭都行一旦涉及局域网或云服务器部署网络连通性、端口放行、DNS解析这些外部因素就成了排查的第一站。1.2 .NET版本选型会直接决定你的代码写法和组件来源现在的.NET生态已经分裂成两个主要分支.NET FrameworkWindows专用常见4.6.1、4.7.2、4.8和.NET Core/.NET 5跨平台目前主线是.NET 6/8。后者是微软长期投入的方向新项目我基本都推荐直接用.NET 8因为它的长期支持周期长、性能优化到位SqlClient相关的异步API也更成熟。如果用.NET FrameworkSystem.Data.SqlClient是内置的直接在项目里引用就行。如果用.NET 6/8官方推荐改用Microsoft.Data.SqlClient这个包它是SqlClient的继任者修复了老包不少安全问题和加密连接相关的缺陷。在NuGet包管理器中安装Microsoft.Data.SqlClient即可和安装普通第三方库没区别dotnet add package Microsoft.Data.SqlClient安装完成后代码里的命名空间是Microsoft.Data.SqlClient而不是老旧的System.Data.SqlClient。别小看这个区别很多老教程的代码你直接复制过来在.NET 8下编译会报错就是命名空间对不上。1.3 连接字符串里的每个参数都是经验值不能随便写连接字符串是数据库交互的入口配置常见的写法长这样string connStr Server127.0.0.1,1433;DatabaseMyAppDb;User Idsa;Password你的密码;TrustServerCertificatetrue;EncryptOptional;我来逐个拆解这些参数在生产环境里真的一个都不能省Server可以是机器名、IP地址也可以带上端口号逗号分隔不是冒号。默认端口是1433但很多DBA会改掉所以显式写出来是最稳的。Database目标数据库名。如果这里有你没有权限访问的库名连接时可能直接报错。User Id / PasswordSQL Server登录账号。不建议在生产环境用sa这种超级权限账号。TrustServerCertificate / Encrypt这俩是安全相关的。默认情况下新版SqlClient会对连接做加密但如果服务器证书不被信任就会抛出证书链验证失败。设成TrustServerCertificatetrue表示开发环境跳过证书校验公网或生产环境不建议开。Pooling默认开启连接池。你可以设置Max Pool Size和Min Pool Size来控制连接池容量。还有一种连接方式叫集成安全认证适用于Windows域环境或本机开发string connStr Server127.0.0.1;DatabaseMyAppDb;Trusted_ConnectionTrue;TrustServerCertificatetrue;这种方式的原理是利用当前Windows身份访问SQL Server好处是密码不会出现在代码里坏处是部署到服务器时需要给应用池账号授权数据库权限配起来比SQL账号麻烦。我自己的习惯是开发环境用Windows认证或独立低权限SQL账号生产环境一定用独立账号并严格控制权限密码放到配置中心或环境变量绝不写进代码仓库。2. 核心操作实战从连接数据库到完成增删改查这一章是高频操作区也是很多初学者最容易写出“能跑但隐患重重”代码的地方。我会按正常开发流程把连接、执行、读取、关闭资源这些完整写一遍并指出哪些写法是绝对该避免的。2.1 用using语句管理连接生命周期避免连接泄漏凡是实现了IDisposable接口的对象都应该用using包裹或手动Dispose。Connection、Command、DataReader、Transaction这些全是非托管资源它们持有的是数据库连接、网络会话等底层句柄不释放就会造成连接池被占满。我的基操模板是这样的using Microsoft.Data.SqlClient; public class UserRepository { private readonly string _connectionString; public UserRepository(string connectionString) { _connectionString connectionString; } public User GetUserById(int id) { const string sql SELECT UserId, UserName, Email FROM dbo.Users WHERE UserId UserId;; using var connection new SqlConnection(_connectionString); using var command new SqlCommand(sql, connection) { CommandType CommandType.Text, CommandTimeout 30 }; command.Parameters.AddWithValue(UserId, id); connection.Open(); using var reader command.ExecuteReader(); if (reader.Read()) { return new User { Id reader.GetInt32(0), Name reader.GetString(1), Email reader.GetString(2) }; } return null; } }这段代码有几个细节值得说两个using嵌套确保Connection和Command都被释放。CommandType显式指定为Text虽然默认值也是Text但写出来更清晰。CommandTimeout设为了30秒防止某条脏SQL卡住整个应用。用parameters传值不拼接字符串。千万别这么写public DataTable GetUsers(string keyword) { var conn new SqlConnection(_connectionString); var cmd new SqlCommand($SELECT * FROM Users WHERE Name LIKE %{keyword}%, conn); var adapter new SqlDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); return dt; }这段代码有四个致命伤连接没释放、SQL拼字符串SQL注入风险、SELECT *无谓传输无用列、方法没有异常处理。如果公司代码评审遇到这种写法基本都会被要求重写。2.2 为什么强烈推荐参数化查询而不是拼接字符串这个问题我面试别人的时候几乎每次都会问。参数化查询的本质是把SQL语句的逻辑部分和数据部分分开传输。SQL Server收到的请求是编译好的模板加参数值比如“WHERE UserId UserId参数UserId100”而不是直接拼好的“WHERE UserId 100”。这样做最直接的好处是堵死SQL注入漏洞。再者参数化查询能提高执行计划复用率。同样的SQL模板哪怕参数值不同SQL Server也能直接命中缓存过的执行计划省去重新编译的开销。那么用AddWithValue好还是new SqlParameter好老实说AddWithValue在绝大多数场景下够用但它有个坑它会根据传入值的.NET类型推断SQL类型比如传C#的string可能被推断为nvarchar而表里字段是varchar一旦长度不对就可能导致索引失效或隐式转换。稳一点的做法是显式指定参数类型和长度command.Parameters.Add(new SqlParameter(UserName, SqlDbType.NVarChar, 50) { Value userName });对于日期类型也同理SqlDbType.DateTime应该明确指定不然容易踩到日期格式转换的边界问题。2.3 DataReader、DataAdapter、DataSet到底怎么选择这是新手最容易问混淆的问题。我平时会根据场景选不搞一刀切。SqlDataReader只进式游标连接保持打开状态只能向前逐行读取适合单次遍历、不需要缓存结果的场景。比如查用户列表直接绑定到ListView或者把结果流式写入文件。SqlDataAdapter DataTableDataAdapter在后台会自动帮你管理连接Fill时连接可以只开一会儿数据被填充到内存中的DataTable后连接就释放了。适合把结果缓存到内存或者需要离线操作数据的场景。DataSet可以把它理解成内存中的一个小型数据库可以包含多张表、表之间有Relation关系。适合非常复杂的离线操作但代价是内存占用高、性能差Web开发中绝大多数情况用DataTable就足够了。举个DataAdapter的典型用法public async TaskListUser SearchUsersAsync(string keyword) { const string sql SELECT UserId, UserName, Email FROM dbo.Users WHERE UserName LIKE Keyword %;; var result new ListUser(); await using var connection new SqlConnection(_connectionString); await using var command new SqlCommand(sql, connection); command.Parameters.Add(new SqlParameter(Keyword, SqlDbType.NVarChar, 50) { Value keyword }); await connection.OpenAsync(); await using var reader await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { result.Add(new User { Id reader.GetInt32(UserId), Name reader.GetString(UserName), Email reader.GetString(Email) }); } return result; }细心的读者可能看到了LIKE写法Keyword %。这样写的意义是让前缀匹配也能用上索引如果写成% Keyword %那查询优化器基本放弃使用普通索引因为前导通配符会让索引失效。2.4 写操作和受影响行数ExecuteNonQuery的正确打开方式INSERT、UPDATE、DELETE这类语句用ExecuteNonQuery执行它不返回结果集只返回受影响的行数。返回值可以用来判断操作是否真正生效。我给你一个带事务保护的DELETE示例这是实务里最常见的场景之一public bool DeleteUser(int userId) { const string sql DELETE FROM dbo.Users WHERE UserId UserId;; using var connection new SqlConnection(_connectionString); using var transaction connection.BeginTransaction(); try { using var command new SqlCommand(sql, connection, transaction); command.Parameters.Add(new SqlParameter(UserId, SqlDbType.Int) { Value userId }); connection.Open(); int affected command.ExecuteNonQuery(); transaction.Commit(); return affected 0; } catch { transaction.Rollback(); throw; } }注意这个细节BeginTransaction必须放在connection.Open()之前不对实际上BeginTransaction既可以在Open前调用也可以在Open后调用。但如果你执行Open之后再BeginTransaction中途要是Open失败处理的逻辑链会变复杂。我这里把connection.Open()放进了try里就无所谓。事务的存在是为了保证多条语句要么一起成功、要么一起回滚。如果你只有一条DELETE事务的意义看起来不明显但一旦后续扩展成先删主表再删子表事务的价值立竿见影。事务里别做耗时的外部API调用或Thread.Sleep那会把事务的锁持有时间拖长造成锁升级和死锁概率飙升。3. 进阶玩法存储过程、事务隔离级别与批量写入CRUD只是基本盘真正决定项目工程质量的是进阶场景的处理方式。这一章聊三个高频点存储过程的调用方式、事务隔离级别的选择、以及大数据量写入的效率方案。3.1 调用存储过程并获取输出参数和返回值存储过程在复杂企业系统里还有一席之地因为数据库端封装逻辑可以减少网络往返、便于DBA做性能调优也方便权限控制只给执行权限而不给底层表权限。假设数据库里已有这样一个存储过程CREATE PROCEDURE dbo.GetUserSummary UserId INT, TotalOrders INT OUTPUT AS BEGIN SET NOCOUNT ON; SELECT UserId, UserName, Email FROM dbo.Users WHERE UserId UserId; SELECT TotalOrders COUNT(*) FROM dbo.Orders WHERE UserId UserId AND OrderStatus Completed; ENDC#端调用方式public UserSummary GetUserSummary(int userId) { using var connection new SqlConnection(_connectionString); using var command new SqlCommand(dbo.GetUserSummary, connection) { CommandType CommandType.StoredProcedure, CommandTimeout 60 }; command.Parameters.Add(new SqlParameter(UserId, SqlDbType.Int) { Value userId }); var totalOrdersParam new SqlParameter(TotalOrders, SqlDbType.Int) { Direction ParameterDirection.Output }; command.Parameters.Add(totalOrdersParam); connection.Open(); using var reader command.ExecuteReader(); // 先处理第一个结果集 UserSummary summary null; if (reader.Read()) { summary new UserSummary { UserId reader.GetInt32(0), UserName reader.GetString(1), Email reader.GetString(2) }; } // 关键一步读完第一个结果集后必须让reader前进到下一个结果集输出参数才会真正被填充 reader.NextResult(); return summary; }这里有个容易踩的坑输出参数必须在结果集全部读取并关闭reader之后才能取到值。上例中即使调用两次NextResult也没问题但如果漏掉了这一步直接通过totalOrdersParam.Value取值大概率会拿到null或者DBNull。如果要获取返回值RETURN需要额外加一个Direction ParameterDirection.ReturnValue的参数。RETURN一般用于表示成功或失败例如RETURN 0表示成功、RETURN 1表示失败。3.2 事务隔离级别别把默认级别当圣旨SQL Server的默认隔离级别是Read Committed它已经能防止脏读。但如果你在事务里先查询某条记录然后再更新它中间被别的事务改了就会发生不可重复读。更极端的还有幻读即查询条件范围内的数据行数在事务执行期间被其他事务插入或删除。我在某个订单对账功能里就踩过这个坑A事务先把订单状态改成“处理中”然后去读关联表数据等它读完之后再回头看发现订单状态已经被B事务改成“已完成”了导致业务逻辑判断错乱。解决办法是给事务设置隔离级别比如Snapshot或ReadCommitted加锁提示。一种常用做法是启用Read Committed SnapshotRCSI数据库级别设置后读操作使用行版本控制写操作不加共享锁读写互不阻塞ALTER DATABASE MyAppDb SET READ_COMMITTED_SNAPSHOT ON;在C#里也可以显式开启事务隔离级别var transaction connection.BeginTransaction(IsolationLevel.Snapshot);或者用Serializable追求最大一致性但代价是并发能力和吞吐量会下降。我的建议是默认业务用Read Committed就够处理金额或库存这类高竞争数据时再考虑Snapshot不要一上来就Serializable把性能拖垮。3.3 批量插入别用循环单条INSERTSqlBulkCopy才是正确姿势如果系统里需要导入Excel、同步接口数据每秒几百条记录的写入用普通循环逐个ExecuteNonQuery会慢到让你怀疑人生。SqlBulkCopy是专门为批量写入设计的组件它底层用的是BCP协议能把一大把数据直接灌进SQL Server速度能提升一个数量级。一个简化版的批量写入实现public async Task BulkInsertUsersAsync(IEnumerableUser users) { var dataTable new DataTable(); dataTable.Columns.Add(UserName, typeof(string)); dataTable.Columns.Add(Email, typeof(string)); dataTable.Columns.Add(CreatedAt, typeof(DateTime)); foreach (var user in users) { dataTable.Rows.Add(user.UserName, user.Email, DateTime.UtcNow); } await using var connection new SqlConnection(_connectionString); await connection.OpenAsync(); using var bulkCopy new SqlBulkCopy(connection) { DestinationTableName dbo.Users, BatchSize 1000, BulkCopyTimeout 300 }; bulkCopy.ColumnMappings.Add(UserName, UserName); bulkCopy.ColumnMappings.Add(Email, Email); bulkCopy.ColumnMappings.Add(CreatedAt, CreatedAt); await bulkCopy.WriteToServerAsync(dataTable); }SqlBulkCopy的坑在于它默认不做数据完整性检查比如字符串超长、非空字段为null这些错误可能会在中途抛出异常。另外如果目标表有触发器或外键建议先确认触发器的逻辑因为SqlBulkCopy默认不触发某些约束必要时需要设置CheckConstraints属性。如果不想用DataTable中转也可以用IDataReader直接作为数据源减少DataTable的内存占用。总体来说凡是超过几百条的一次性插入我都建议上SqlBulkCopy。4. 从SqlClient到ORMDapper和EF Core应该怎么取舍聊到C#数据访问绕不开ORM。很多新人一上来就被推荐EF Core结果查询性能慢到怀疑人生然后开始黑ORM。你用不用ORM其实是项目规模决定的但底层一定要先掌握SqlClient。4.1 为什么我推荐先用Dapper这种轻量级微ORM项目里如果只是几十张表、查询以原生SQL为主Dapper几乎是完美的选择。它不生成SQL而是让你手写SQL然后帮你做结果集到实体类的映射。DataReader执行一遍的性能损耗非常低适合对SQL掌控力强、追求性能的团队。Dapper的连接管理和SqlClient没区别只是查询方式变了using Dapper; public async TaskIEnumerableUser GetUsersByStatusAsync(string status) { const string sql SELECT UserId, UserName, Email, Status FROM dbo.Users WHERE Status Status;; await using var connection new SqlConnection(_connectionString); return await connection.QueryAsyncUser(sql, new { Status status }); }上面这段代码用到了Dapper的匿名对象参数映射。Dapper会把new { Status status }里的属性名与SQL中的Status自动匹配。这里有几个隐性约定参数名大小写不敏感但拼写必须一致如果你的实体属性有下划线比如user_nameDapper不会自动映射到UserName需要配置Column Mapping或起别名。Dapper还有一个很顺手的功能多结果集查询。用QueryMultiple一次从数据库拿多张表数据省去多次网络往返const string sql SELECT UserId, UserName FROM dbo.Users WHERE Status Active; SELECT OrderId, UserId, Amount FROM dbo.Orders WHERE OrderDate Since;; using var multi await connection.QueryMultipleAsync(sql, new { Since startDate }); var users (await multi.ReadAsyncUser()).ToList(); var orders (await multi.ReadAsyncOrder()).ToList();一次数据库往返解决两个查询效率立竿见影。4.2 EF Core适合什么场景、需要注意哪些坑EF Core作为重量级ORM核心价值是Code First迁移、LINQ查询和状态跟踪机制。如果你的项目是领域模型驱动、表结构经常演进、团队里没有专职DBA用EF Core能显著提高开发效率。但EF Core的坑也非常集中Include/Navigation Property处理不好会产生N1查询。查询主表10条记录每条记录再单独查一次关联表数据库就要执行11条查询。默认的跟踪查询如果只读不修改应该改成AsNoTracking省去状态管理开销。复杂的聚合查询比如多表连接后组内排序、开窗函数LINQ写起来很别扭不如直接ExecuteSqlRaw或FromSqlRaw原生SQL。所以我的项目选型建议很简单简单工具类项目、报表类项目、性能敏感项目直接用SqlClient或Dapper。业务逻辑复杂、实体模型丰富、团队规模大、希望快速交付的项目用EF Core。同一个项目里两者共存也完全正常我见过不少主流系统重查询用Dapper、写模型用EF这样既保留灵活性又提升性能。4.3 异步编程模式从ExecuteReader到Mapper全部Async化高并发应用里数据库交互是最大的IO瓶颈。如果用同步方法执行数据库查询线程池线程会被阻塞在等待数据库返回结果的状态白白占着资源。改成async/await后线程可以立刻返回线程池处理其他请求数据库返回结果后再续跑。SqlClient的异步方法基本都齐全了我建议养成以下习惯connection.OpenAsync()而不是Open()command.ExecuteReaderAsync()而不是ExecuteReader()command.ExecuteNonQueryAsync()而不是ExecuteNonQuery()dataAdapter.FillAsync()而不是Fill()但异步不是绝对银弹。如果一个页面内部有多次独立查询你需要在并发执行它们之前控制好Connection的复用问题。多个Command不能同时在一个Connection上跑要么同时开多个Connection要么串行执行。如果开多个连接记得连接字符串里Max Pool Size够大。var userTask GetUserAsync(id); var orderTask GetOrdersAsync(userId); await Task.WhenAll(userTask, orderTask); var user await userTask; var orders await orderTask;听起来很顺理成章但数据库负载高的时候这种并发查询会同时占用多个连接连接池不够用就会排队。所以并发度也得有个度是压榨性能还是控制连接池水位需要根据实际监控数据做平衡。5. 常见问题排查与避坑指南数据库交互的报错花样繁多但翻来覆去其实就那么几类。我把高频问题整理成速查表附带排查思路就算你是第一次遇到也能按图索骥。5.1 连接超时与连接池耗尽的定位思路这个报错应该是SqlClient生态里最常见、也最让人头疼的“Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.”说白了就是连接池里没有可用连接了。原因通常有三个第一连接泄漏。代码里的Connection对象没有使用using或Dispose连接未归还池中。 第二连接池的最大容量设置过小。默认Max Pool Size是100高并发下不够用。 第三执行时间过长导致连接被占满。排查方式分几步走在连接字符串里增加Poolingtrue;Max Pool Size200治标不治本但可以缓解燃眉之急。打开性能计数器SQL Server和进程层面都能看到连接池的当前活动连接数、空闲连接数。代码审查重点排查每个分支是否都有Dispose。特别注意DataReader使用完后是否及时关闭因为DataReader打开期间连接一直被占用。用SQL Server的视图sp_who2或sys.dm_exec_sessions查当前连接Session看看哪些IP、哪个程序占了大量连接。这个报错90%的情况是代码问题不是数据库问题。先修代码再调参数。5.2 SQL注入、乱码与参数类型不一致的高发区SQL注入问题我前面已经提过这里补一个参数类型不一致的经典场景用户在界面输入“00123”你把它当成int传入SQL数据库那边字段是VARCHAR。这种情况下隐式转换可能会让SQL Server放弃索引。更典型的是把字符串传入int字段SQL Server直接报“Conversion failed when converting the nvarchar value”。平时写参数时我建议养成一个习惯先想清楚数据库字段是什么类型再显式指定SqlDbType不依赖AddWithValue的推测。乱码问题往往出现在中文场景。核心要点是SQL Server中NVARCHAR/NCHAR是Unicode存储VARCHAR/CHAR是非Unicode。C#字符串在内存里是Unicode传入NVarChar字段没问题。但是如果你的表字段是VARCHAR而参数是NVarCharSQL Server可能因为数据类型优先级不同做隐式转换严重时会导致索引失效甚至个别字符出现问号。经验法则C#侧和数据库侧都用NVARCHAR保持统一中文场景别混用。5.3 连接串泄露与权限管控的底线写进代码里的连接字符串就像把家门钥匙放在脚垫下面一样危险。常见泄露途径五花八门代码仓库提交、日志打印、异常消息堆栈输出、前端打包文件包含config等。我的底线是开发环境用低权限SQL账号只给所必需数据库的读写权限。生产环境密码放到环境变量、配置中心或密钥管理服务里部署时注入。日志系统里统一脱敏连接字符串、SQL参数值禁止打印。定期轮换密码很多安全事件都是因为长期不改密码导致的。权限控制方面SQL Server提供了最小权限原则。表级权限、Schema级权限、存储过程执行权限都可以分开控制。普通应用账号能不给DDL权限就不给防止SQL注入被利用时攻击者直接建表删库。6. 性能调优与安全加固的实战建议基础功能跑通只是开始性能和安全才是把代码从Demo变成生产级高可用服务的分界线。这一章聊聊我在SQL Server交互上沉淀下来的调优手段和安全加固步骤。6.1 从执行计划中定位慢查询每当我遇到“接口慢”的反馈第一反应不是改C#代码而是先抓数据库端慢查询和执行计划。SQL Server提供了动态管理视图可以直接查到哪些SQL执行耗时最久SELECT TOP 20 qs.total_worker_time AS TotalWorkerTime, qs.execution_count AS ExecCount, qs.total_elapsed_time / qs.execution_count AS AvgElapsedTime, SUBSTRING(st.text, (qs.statement_start_offset/2)1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) 1) AS SqlText, qp.query_plan FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp ORDER BY qs.total_worker_time DESC;这条查询能帮你快速勾勒出最耗资源的SQL长什么样。拿到SQL之后把它单独在SSMS里执行开启“显示实际执行计划”看有没有表扫描、关键步骤消耗占比、估计行数和实际行数偏差。这些信息直接决定是加索引还是改SQL写法。加索引不是无脑加复合索引的字段顺序有讲究最常用的等值条件放前面、范围条件放后面。另外索引不是越多越好写多的表索引太多会严重影响插入和更新性能。6.2 连接池参数调优与数据库端资源博弈前面提到连接池的Max Pool Size默认100但在一套高并发微服务系统里这个默认值很可能是瓶颈。调大Max Pool Size是最直观的手段string connStr Server...;Database...;User Id...;Password...;Max Pool Size200;Min Pool Size10;;但这里有一层隐藏博弈你调大连接池填充的其实是SQL Server那边的worker线程、内存和锁资源。数据库服务器的CPU和内存是固定应用层开再多连接数据库吃不消一样白搭。我更推荐反向调优先压测出数据库的基准吞吐量然后适当调小连接池大小让有限的数据库连接服务于更核心的请求而不是让闲散请求把连接池和数据库资源洪峰冲垮。配合上面的性能监控把连接池水位压在安全范围之内比一味调大更靠谱。6.3 安全的最后一道防线加密传输与审计数据库连接默认不加密的话网络抓包能直接看到SQL语句和返回数据这在跨机房、公网环境下风险不小。SqlClient支持Encrypt选项可以通过连接字符串强制加密传输string connStr Server...;Database...;EncryptStrict;TrustServerCertificatefalse;;EncryptStrict要求服务器证书必须有效适合生产环境强制加密开发环境可以用TrustServerCertificatetrue跳过证书校验但别把这套配置带到生产。最后再提一个很多团队会忽略的SQL Server的审计。登录日志、权限变更、DDL操作、异常大量的删除操作等都应该有审计追踪。C#应用里也可以在关键业务入口写操作日志跟数据库层的审计互为补充。万一出现数据被异常删改的情况至少能还原出“谁在什么时间做了什么”。我在实际做过的某制造业系统中就是因为早期没有数据库审计一次误操作把一张配置表清空了花了两天才从备份里恢复。后来我把应用操作日志和数据库审计补齐类似的事故再也没有机会造成了。7. 我的个人经验把数据访问层设计得更优雅一点最后分享一个不只是写代码更是架构层面的经验把数据访问逻辑从业务代码中剥离干净。很多团队代码里的数据访问和业务判断揉在一起改造起来非常痛苦比如更换ORM或加缓存时动辄几千行改动。我的做法是在数据访问层之上加仓库模式或服务接口。业务流程依赖的是UserRepository接口具体用SqlClient实现还是Dapper实现可以被替换。这样做的好处非常实际测试时可以用Mock对象模拟数据源不用依赖真实数据库运营中想加Redis缓存只需要在实现里包一层业务层完全无感。数据访问层设计还需要考虑一个容易被新人忽略的点数据库交互要尽量避免跨数据库事务。一个事务操作两个数据库听着很酷实际落地要么用到分布式事务协调器要么需要非常小心地处理多库一致性。最好在业务设计阶段就尽量保证一个事务只写一个库。我见过太多前期不规划数据层、后期为了扩展把代码改得面目全非的案例了。先花半天时间设计好接口边界后面能省下几天的重构时间这笔账怎么算都不亏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑