ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#连接MySQL实战:驱动选择、连接字符串与连接池排障指南

C#连接MySQL实战:驱动选择、连接字符串与连接池排障指南 如果你在 C# 里连接 MySQL 遇到过这样的场景——本机在 Visual Studio 里跑得好好的一部署到客户服务器上就报身份验证失败或者用一个经典连接字符串连本地却出现 SSL 握手错误又或者高并发一冲上来提示 timeout expired 而不是 SQL 报错——那你先别急着怪代码问题很可能出在驱动、连接串或者连接池上。这篇内容围绕 C# 连接 MySQL 这条主线把我实际项目中踩过的坑、做过的封装、验证过的参数配置都摊开来写一遍不管是做桌面程序、上位机还是 ASP.NET Core 后端遇到类似场景可以直接对照着排。1. 选驱动这一步走错后面全在填坑很多人第一次写 C# 连 MySQL直接去 NuGet 搜 MySql装完发现有两个长得差不多的包一个是MySql.Data另一个是MySqlConnector。如果没搞清楚两者的差异后面编译、部署、性能、甚至异步调用都会出问题。1.1 MySql.Data 和 MySqlConnector 到底怎么选MySql.Data是 Oracle 官方的 MySQL Connector/NET历史最悠久资料最多很多老项目都用它。MySqlConnector是社区开源的高性能驱动底层 API 和官方驱动几乎一模一样但异步实现更完善对 .NET Core / .NET 5 的适配更激进还修复了不少官方驱动在连接池和高并发场景下的 bug。对比项MySql.Data官方 Connector/NETMySqlConnector社区NuGet 包名MySql.DataMySqlConnector命名空间MySql.Data.MySqlClientMySqlConnector异步性能可用但高并发下偶有卡顿实测更稳异步接口完善老版本 MySQL 兼容5.6/5.7/8.x 均可5.5 以上的老版本也能兼容社区活跃度官方节奏较慢更新频繁issue 回复快如果你的项目要长期维护我建议优先选MySqlConnector。不用担心的迁移成本在于它同样提供了MySqlConnection、MySqlCommand、MySqlDataReader这些类方法签名和官方驱动基本一致老代码往往只需要改 using 和包引用就能跑起来。如果你的项目已经在用 Dapper 或 EF Core那更省事——Dapper 只需要传入一个IDbConnection实例底层驱动是谁影响不大。1.2 NuGet 安装与版本别搞混安装命令很简单在项目目录执行dotnet add package MySqlConnector # 或者官方驱动 dotnet add package MySql.Data如果是在 Visual Studio 里操作直接打开“管理 NuGet 程序包”搜索对应包名注意千万别把包名输错。曾经有个同事把 MySqlConnector 拼成了 MySqlConnecter结果装了一堆不相关的依赖编译直接起飞。还有一种情况是用 EF Core那常用的不是原生驱动而是Pomelo.EntityFrameworkCore.MySql这个 Provider。它是 EF Core 生态里对 MySQL 支持最完整的组件底层默认走 MySqlConnector性能和兼容性都比较有保障。用 Microsoft 官方的Mysql.EntityFrameworkCore也能跑但实际项目里我遇到过不少底层 SQL 生成和类型映射的坑所以更推荐 Pomelo。1.3 MySQL 8 的认证方式会反过来逼你升级驱动MySQL 5.7 时代默认的认证插件是mysql_native_password很多老驱动都能直接连。MySQL 8.0 开始把默认认证换成了caching_sha2_password如果你还在用 6.x 或者很老版本的 MySql.Data连接时会直接给你抛一个错误Authentication method caching_sha2_password not supported by any of the available plugins.这种报错不一定代表你的账号密码错了而是驱动根本不认识这个认证协议。解决办法有两个要么把驱动升到较新的版本MySql.Data 至少 8.0.26 以后MySqlConnector 一直支持问题不大要么把数据库用户降级回mysql_native_password。从安全角度看我不建议把生产环境数据库降级。caching_sha2_password在密码存储和传输上更安全旧的明文口令校验方式只适合那种再也升级不了的存量系统。换句话说遇到这个报错先去升级 NuGet 包而不是去改数据库用户。2. 连接字符串四个字段表象下藏着十几个真实参数连接字符串是 C# 连接 MySQL 出错率最高的环节。表面上就是几段keyvalue用分号拼起来实际上每个参数都可能影响你连接是否成功、查询是否乱码、高并发是否超时。2.1 最小可用连接串长什么样先给一个最精简的例子Server127.0.0.1;Port3306;Databaseshopdb;User IDapp_user;PasswordYourStrongPass;这个串在本地开发一般能通但真实项目里远远不够。Server不要写localhost尤其在 Linux 环境因为localhost可能被解析成 Unix Socket 而不是 TCP/IP后面我会专门讲这个坑。Port默认 3306如果改了端口必须写明。User ID和Password不用多说但要注意连接串字段名通常是User ID有些老的资料写成Uid或uid官方驱动也能识别不过统一写User ID最不容易误读。2.2 直接影响业务正确性的参数这部分我最看重下面几个参数作用推荐配置CharSet客户端字符集utf8mb4别用 utf8SslMode是否启用 SSL 加密通道开发 None生产 Required 或 VerifyCAConnection Timeout获取连接的超时时间15-30 秒Command Timeout单条 SQL 执行超时30 秒起根据业务调整AllowPublicKeyRetrieval非 SSL 下检索 RSA 公钥未启用 SSL 时设 truePooling是否启用连接池trueMax Pool Size连接池上限默认 100按并发量调整CharSet这个参数最容易出“中文乱码”问题。MySQL 的utf8其实不是真正的四字节 UTF-8只能存基本 BMP 字符遇到 emoji 或者生僻字就会变成问号。统一用utf8mb4才是正道而且数据库表结构、服务端 character_set 配置也要跟着一致。{ ConnectionStrings: { Default: Server127.0.0.1;Port3306;Databaseshopdb;User IDapp_user;PasswordYourStrongPass;CharSetutf8mb4;SslModePreferred;Connection Timeout30; } }2.3 ASP.NET Core 里配置连接串和依赖注入的正确姿势在 ASP.NET Core 项目里连接串最好放在appsettings.json的ConnectionStrings节点然后通过IConfiguration读取绝对不要硬编码在代码里。硬编码的后果不只是难维护还有一个常见的低级事故代码里写着测试库密码误发到生产环境连接串直接带崩线上服务。依赖注入的做法也很简单builder.Services.AddScopedIDbConnection(sp { var config sp.GetRequiredServiceIConfiguration(); return new MySqlConnection(config.GetConnectionString(Default)); });这样每个请求作用域都会拿到一个独立的MySqlConnection配合 Dapper 使用非常顺手。注意我这里选的是Scoped而不是Singleton因为MySqlConnection不是线程安全的多处共享同一个连接对象会造成不可预期的串数据问题。2.4 超时参数到底在卡什么Connection Timeout卡的是“从连接池拿连接 建立物理连接”的整个过程Command Timeout卡的是“SQL 执行到返回结果”的时间。这两种超时是完全独立的报错信息也不一样。如果高并发下出现Timeout expired但 MySQL 本身 CPU 和内存都不高多数情况是连接池耗尽而不是数据库执行慢。这种问题靠调大Command Timeout解决不了要从连接池和查询效率入手。3. 一套能上生产的数据库访问封装连接串搞定了接下来就是怎么写代码。很多人喜欢上来就用 EF Core 或者 Dapper但我觉得不管用不用 ORM都要先理解最底层的MySqlCommand是怎么工作的这样遇到问题才能知道它是 SQL 层的问题还是数据库层的问题。3.1 参数化查询必须养成习惯先看一个最基础的查询using var conn new MySqlConnection(connStr); await conn.OpenAsync(); using var cmd new MySqlCommand(); cmd.Connection conn; cmd.CommandText SELECT COUNT(*) FROM order_item WHERE status status; cmd.Parameters.AddWithValue(status, 1); var count Convert.ToInt32(await cmd.ExecuteScalarAsync());这里最大的注意事项是AddWithValue的隐式类型推断。如果你插入一个decimal类型的值AddWithValue可能会把它推断成字符串或 double导致数据库端报Incorrect decimal value这类错误。解决办法是显式指定参数类型cmd.Parameters.Add(amount, MySqlDbType.Decimal).Value amount;另一个原则是永远不要用字符串拼接的方式构造 SQL。网上很多老教程写的是SELECT * FROM users WHERE name name 这在有输入框的业务系统里就是裸奔随便一个 OR 11就能把你的表拖出来。参数化查询不是可选项是底线。3.2 事务、回滚与异常处理在涉及多张表写入的业务里事务必须包起来。C# 连接 MySQL 的最常用写法是这样using var conn new MySqlConnection(connStr); await conn.OpenAsync(); using var tx await conn.BeginTransactionAsync(); try { using var cmd1 new MySqlCommand(UPDATE account SET balance balance - amount WHERE id id, conn, tx as MySqlTransaction); cmd1.Parameters.AddWithValue(amount, amount); cmd1.Parameters.AddWithValue(id, fromAccountId); await cmd1.ExecuteNonQueryAsync(); using var cmd2 new MySqlCommand(UPDATE account SET balance balance amount WHERE id id, conn, tx as MySqlTransaction); cmd2.Parameters.AddWithValue(amount, amount); cmd2.Parameters.AddWithValue(id, toAccountId); await cmd2.ExecuteNonQueryAsync(); await tx.CommitAsync(); } catch { await tx.RollbackAsync(); throw; }有几个细节要注意事务必须在连接打开之后创建所有参与事务的命令都要显式挂上同一个事务对象CommitAsync成功后再释放连接不要在using块结束前就提交否则事务边界会被提前截断。有一种常见误操作是用TransactionScope包一切。在 .NET Framework 时代TransactionScope很常见但到了 .NET Core / .NET 5MySQL 驱动对分布式事务的支持并不好尤其在高性能场景下会引入额外的性能损耗。除非你的系统同时涉及多个异构数据库否则直接用BeginTransactionAsync反而更透明、更可控。3.3 存储过程调用Type 和 Parameter 别搞反我见过不少项目把存储过程调用写成普通 SQL然后怎么调都不出结果。存储过程必须显式声明CommandType CommandType.StoredProcedureusing var cmd new MySqlCommand(p_order_create, conn); cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(InOrderNo, orderNo); cmd.Parameters.AddWithValue(InCustomerId, customerId); cmd.Parameters.Add(OutOrderId, MySqlDbType.Int32); cmd.Parameters[OutOrderId].Direction ParameterDirection.Output; await cmd.ExecuteNonQueryAsync(); var newOrderId Convert.ToInt32(cmd.Parameters[OutOrderId].Value);存储过程的参数名要不要带前缀在 MySQL 驱动里是比较容易混淆的地方。我的经验是AddWithValue里的参数名不带前缀但读取输出参数时用带的名字访问这样最稳。如果存储过程执行特别慢先检查是不是COMMIT没写再查是不是 SQL 没有走索引。存储过程卡在客户端是没法用索引优化解决的只能去数据库侧排查。4. 连接池是怎么工作的以及连接耗尽事故的完整复盘这一节是我最想写的内容之一。C# 连接 MySQL 的初学者通常只关心“能不能连上”但真正跑到生产环境就会发现连接池用不好系统会在某个临界点瞬间雪崩。4.1 为什么不能每次请求都新建物理连接每次建立一个 MySQL 物理连接都需要经历 TCP 三次握手、协议协商、认证、字符集设置等过程即使在内网也要几十到上百毫秒。如果每个请求都新建连接再断开系统吞吐量会被网络和数据库端的握手开销拖垮。连接池的作用就是让这些物理连接保持复用。客户端从池里拿到的是一条“逻辑连接”底层实际连接并没有真正关闭而是在Dispose时归还到池里。在连接字符串里Poolingtrue是默认开启的但要注意只有你正确释放了连接池才会真正起来。4.2 一个连接泄漏导致的事故现场我之前维护过一个上位机项目服务每隔 500 毫秒轮询一批设备状态数据库是 MySQL。大概上线运行十分钟后数据库端开始报Too many connections然后所有业务请求全部超时。排查时先看了数据库状态SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SELECT * FROM information_schema.processlist WHERE command Sleep;结果发现大量Sleep状态的连接堆积这时候基本可以断定是客户端连接泄漏。再翻代码发现轮询逻辑里new MySqlConnection之后没有using也没有在finally里Close导致每轮循环都丢一个物理连接在外面。修复方式很简单把连接创建改成using var conn new MySqlConnection(connStr);。但这次事故给我的教训是写任何数据库访问代码都必须把“释放连接”当成和“打开连接”同样重要的一步。一个很实用的检查手段是在压测时观察Threads_connected的增量。正常稳定的连接池在压力下数值会先升到某个水位后保持平稳如果数值持续攀升说明代码里十有八九有连接泄漏。4.3 连接池参数怎么调才不翻车连接串里常见的池参数有参数作用我的建议Pooling是否启用池trueMin Pool Size最小空闲连接数0 或 2Max Pool Size最大连接数100 以内够用Connection Lifetime连接最大存活时间300 秒左右Connection Idle Timeout空闲后回收时间60 秒Max Pool Size并不是越大越好。MySQL 端能承载的连接数是有限的每一条连接都要吃内存和线程资源。我见过有人把Max Pool Size配到 1000结果 MySQL 连接数被打爆数据库 CPU 直接飙到 100%。合理的做法是先估算并发量再设置上限同时配合连接释放检查。如果连接池满了但确实都是活跃连接那就是数据库需要扩容或者 SQL 需要优化这时候调参没有意义。如果连接池满了但数据库 CPU 很低说明连接被长期占用没释放优先查代码而不是加参数。还有一个被很多人忽略的点MySQL 服务端默认wait_timeout是 8 小时连接池里的空闲连接如果超过这个时间会被服务端主动断开而客户端并不一定知道。结果就是你拿到的连接实际已经失效执行 SQL 时抛Connection is closed或底层 socket 异常。经验做法是把Connection Lifetime调成明显小于服务端超时时间比如 300 秒或者让连接池每次取出连接前做一次有效性检测。5. 高频异常排障从错误码反推问题根源这一节整理几个我真正在高频遇到过的异常。很多错误字符串一看就让人头大但只要理解了背后的机制排查起来其实非常快。5.1 Authentication method caching_sha2_password not supported这个错误在第 1 节里提过这里再展开一次。出现这个报错说明你的驱动版本太老不认 MySQL 8 的默认认证插件。我推荐的处理顺序是先升级驱动到最新版90% 的场景到这里就解决了。如果升级后仍然报错查看数据库用户的plugin字段SELECT user, host, plugin FROM mysql.user WHERE user app_user;如果确实需要兼容无法升级的老客户端才考虑临时修改用户认证方式ALTER USER app_user% IDENTIFIED WITH mysql_native_password BY YourStrongPass;但这里要谨慎改认证方式会影响该用户所有客户端的连接行为而且这个方案在新版 MySQL 里迟早要被废弃。能升驱动就升驱动能用新认证就用新认证。5.2 SSL 连接错误到底在错哪里MySQL 8 默认对连接安全性要求更高很多驱动在默认配置下会尝试先走 SSL 握手。如果数据库端没有正确配置 SSL或者客户端证书链不完整就会在连接阶段直接失败而不是等到执行 SQL 才报错。常见的错误信息长这样MySqlException: SSL Connection Error解决路径取决于你的环境。开发环境如果不需要加密传输可以在连接串里显式写SslModeNone;AllowPublicKeyRetrievalTrue跳过 SSL 握手同时允许驱动通过 RSA 公钥获取缓存认证的公钥。生产环境我建议至少用SslModeRequired也就是加密传输但暂不验证 CA。如果公司安全要求更高再升级到VerifyCA或VerifyFull同时配置好 CA 证书。不要一上来就抄网上的SslModeNone到生产环境万一数据库服务器配置了强制require_secure_transport你反而连接不上。还有一种比较隐蔽的情况数据库其实启用了 SSL但是证书过期了。排查时先到 MySQL 服务端查看 SSL 状态SHOW VARIABLES LIKE %ssl%; SHOW STATUS LIKE %ssl%;如果看到证书时间已经过期重新签发证书即可。5.3 Cant connect to local MySQL server through socket这个错误经典到几乎所有 MySQL 教程都会提但它经常会误导 C# 开发者。错误原文是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这个错误通常来自 MySQL 命令行客户端而不是 .NET 驱动。它表示客户端尝试通过 Unix Socket 文件连接但找不到 socket 文件。常见原因是 MySQL 服务没启动或者 socket 路径不在默认位置。在 C# 场景里我们多数是走 TCP/IP 的Serverlocalhost在某些环境下可能被驱动或操作系统解析成 Unix Socket 访问方式。处理方式很简单连接串里的Server写成127.0.0.1或者服务器实际内网 IP强制走 TCP。如果你在 Linux 上用 MySqlConnector 想故意走 Unix Socket 加速连接也可以直接把Server指向 socket 文件路径Server/var/run/mysqld/mysqld.sock;User IDroot;Passwordxxx;Databasexxx但跨机器的应用部署不要这么干因为 socket 文件只在本机有效远程客户端根本访问不到。5.4 远程主机强迫关闭了现有的连接这类报错在 .NET 里很常见完整文案是Unable to write data to the transport connection: 远程主机强迫关闭了现有的连接出现这个报错时第一反应不要只盯着 MySQL 服务端要分几个方向排查驱动和 SQL 版本不匹配SSL 协议协商失败时服务端会直接断连。试试升级驱动或调整 SslMode。单条 SQL 数据量过大MySQL 的max_allowed_packet默认值通常只有 4MB 或 64MB如果客户端写入超大数据包服务端会直接断开连接。SHOW GLOBAL VARIABLES LIKE max_allowed_packet;临时调大可以用SET GLOBAL max_allowed_packet 128 * 1024 * 1024;但生产环境需要同时改配置文件永久生效。网络中间层断连如果在负载均衡或代理后面连 MySQL设备经常会对空闲连接做定时回收客户端连接池里的连接表面还活着实际已经被中间层切断。这种情况下报错和 MySQL 本身无关需要调整中间层的空闲超时或者让客户端连接的生命周期短于中间层超时时间。这类问题最忌讳不看日志改配置。想清楚“哪一层断的”再看 MySQL error log、中间层日志、客户端驱动日志定位会快很多。6. 部署环境里绕不开的账号、时区与数据包边界最后这部分虽然不是纯连接代码但每次项目上线我都会先检查一遍不然要么连不上要么数据看着不对要么性能上不去。6.1 给应用单独建最小权限账号很多开发者在本地图省事直接用 root 连库写代码。这个习惯一旦带到生产环境等于把所有数据权限都暴露给应用进程。一旦应用被拖库或者代码泄露代价是灾难性的。合理做法是给应用单独建账号只授权需要的库和操作CREATE USER app_user192.168.% IDENTIFIED BY YourStrongPass; GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO app_user192.168.%; FLUSH PRIVILEGES;用户名的host字段也要仔细设。如果只允许应用服务器所在网段访问就写内网网段如果客户端来源很散再考虑用%。这里要注意MySQL 用户匹配是精确加模糊的app_user%和app_userlocalhost是两个不同用户别建了%却连不上本机。6.2 时区造成的八小时错位C# 的DateTime.Now和DateTime.UtcNow混用是新手最容易踩的坑。MySQL 也有自己的时区设置如果服务端使用了别的时区而你代码里传的是本地时间查询结果就可能看到八小时甚至十几个小时的偏移。我的建议是统一规范数据库时间列统一用DATETIME或TIMESTAMP存储 UTC 时间应用代码统一用DateTime.UtcNow写入只在展示层根据最终用户所在地转换成对应时区。如果在连接串里想直接控制时区有的驱动支持Connection Time Zone或DateTime相关参数但毕竟不同驱动写法有差异。最稳的办法还是代码层统一用 UTC避免把时区逻辑散落到数据库各处。6.3 数据包和批量写入的边界生产环境里批量导入或批量插入经常触发前面说的max_allowed_packet限制。C# 里做批量插入时不要暴力拼接一条几 MB 的 SQL应该分批提交。每条 SQL 控制在几千行以内既能避开数据包限制也能减少事务锁持有时间。如果数据量特别大可以优先考虑 MySQL 的批量加载机制比如MySqlBulkLoader或LOAD DATA LOCAL INFILE。这类工具加载速度远高于逐条INSERT但要注意受local_infile开关影响服务器端默认可能关闭需要单独配置。实测下来对几百万行数据批量加载要比逐条插入快几十倍以上。字符集边界也值得最后提一句连接串、数据库库表、以及 SQL 语句里如果用到中文排序或查询尽量统一utf8mb4。有时候连接库表都是对的但某个字段还是latin1就会导致查询出来是乱码或排序结果不对。这类问题靠连接串是救不回来的要回到表结构去改。说了这么多其实很多连接问题本身并不复杂只是链路太长容易让人来回试。我自己现在遇到连接异常一定是按这个顺序排查驱动版本是否匹配 MySQL 版本、连接串参数是否覆盖认证和 SSL、连接是否被正确释放、连接池参数是否合理、数据库账号权限是否到位。把这条链路走一遍大部分问题都能在几分钟内定位。如果你也在跟 MySQL 的连接问题较劲建议先别急着改代码把这几个前置条件全部过一遍可能一条连接就已经通了。
返回列表