ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

C#连接MySQL从入门到排坑:驱动选择、连接串与典型问题解析

C#连接MySQL从入门到排坑:驱动选择、连接串与典型问题解析 写 C# 连 MySQL 这事儿看起来不就是“装个驱动、写个连接串、创建连接、执行SQL”四步嘛但真到了项目里你会发现坑全藏在细节里。连接串里多一个分号、少一个参数程序可能就直接崩MySQL 8 默认的认证插件和旧版驱动不兼容报错能让你怀疑人生好不容易连上了中文乱码、连接池超时又接踵而来。这篇东西我想把 C# 连接 MySQL 从零到一的完整过程拆开讲一遍包括驱动怎么选、连接串每个参数什么意思、增删改查怎么写不容易出问题、以及我这些年踩过的典型坑适合刚接触 MySQL 的 .NET 开发者也适合被各种连接问题折磨到想骂人的老手。先交代一下背景我在实际项目里用过几种不同的 MySQL 驱动访问方式也帮同事排查过不少“昨天还能跑、今天连不上”的诡异问题。下面所有内容没有用到什么高深理论全部是工程里反复验证过的东西照着做基本能连通出了问题也知道该往哪儿查。1. 准备阶段驱动选型与项目配置1.1 驱动怎么选官方 Connector/NET 还是第三方C# 访问 MySQL 的第一步不是写代码而是选驱动。很多人直接在网上搜“C# MySQL”会看到两个主流包一个叫 MySql.Data官方 Connector/NET另一个叫 MySqlConnector还有一个是配合 EF Core 使用的 Pomelo.EntityFrameworkCore.MySql。它们的关系容易让人糊涂我简单梳理一下。MySql.Data 是官方提供的驱动历史最久网上能搜到的资料和案例绝大多数都是基于它。它支持 .NET Framework 和 .NET Core/.NET 5API 设计比较传统用起来中规中矩。MySqlConnector 是社区维护的高性能驱动异步接口实现得更彻底还修复了官方驱动一些长期存在的问题比如某些场景下的连接池行为、异步阻塞隐患。Pomelo 不是单独的驱动它是 EF Core 的数据库提供器底层依赖 MySqlConnector。如果不是团队已经有明确的技术选型我建议普通项目直接用 MySql.Data。原因很简单官方驱动兼容性最好遇到问题能找到的解决方案最多。如果你的项目对数据库访问性能特别敏感比如大量并发读、消息队列类场景可以考虑 MySqlConnector但没必要上来就挑战冷门选项。一个项目里别同时引用两个驱动否则因为命名空间都有MySql前缀代码里会出现各种摸不着头脑的编译冲突。1.2 安装驱动一条命令搞定确定驱动后安装其实很简单。以 Visual Studio 为例打开 NuGet 包管理器搜索“MySql.Data”选择稳定版本点击安装即可。命令行方式更快dotnet add package MySql.Data如果你需要指定版本可以在包名后追加版本号dotnet add package MySql.Data --version 8.0.33安装完成后项目 csproj 文件里会出现类似这样的引用PackageReference IncludeMySql.Data Version8.0.33 /老一代 .NET Framework 项目ASP.NET Web Forms 那种用 NuGet 可视化界面安装也一样。需要注意的是NuGet 包版本和 .NET 运行时版本有对应关系新版驱动对老框架支持会收缩。比如较新的 9.x 版本已经不再支持旧的 .NET Framework 4.x 那一套如果项目还卡在旧框架上老老实实用 8.x 或更老的版本不要盲目追新。1.3 确认版本和兼容性我见过不少诡异问题最后定位出来都跟驱动和数据库版本错配有关。比如 Server 是 MySQL 8.0驱动还停留在 5.6.x 时代最典型的表现就是连接时报错“Authentication method caching_sha2_password not supported”。这句报错是因为 MySQL 8 默认使用 caching_sha2_password 认证插件而老驱动没有实现对应的客户端认证协议。反过来如果数据库是老的 MySQL 5.6/5.7驱动版本太新一般问题不大但也可能碰到 TLS 版本协商、密码哈希方式不匹配的情况。所以安装前最好先确认数据库版本。在服务端命令行执行select version();就能看到。然后对照驱动的 release notes 选择合适的版本。工程上最稳妥的做法是用当前时间往前推 1-2 年的稳定版本不要用刚发布没多久的首个大版本也不要坚持用已经停止维护的老版本。2. 连接字符串里的学问连接字符串是 C# 连 MySQL 里最容易踩坑的地方也是排查问题时第一个要怀疑的环节。很多人一遇到“连不上”就直接怀疑代码其实八成是连接串写错了。2.1 基础参数逐项解析先看一个最常见的连接字符串示例Serverlocalhost;Port3306;Databasetestdb;Uidroot;Pwd123456;CharSetutf8mb4;这里每个参数都有具体作用Server数据库服务器的 IP 或主机名。localhost 表示本机写 127.0.0.1 也可以但两者在某些网络环境下行为并不完全一样。服务器如果开了 IPv6localhost 可能被解析成 ::1而 MySQL 只监听了 127.0.0.1这种情况就连接不上。PortMySQL 服务端口默认 3306。如果你的 MySQL 改了端口比如为了安全改成 13306这里必须同步改否则客户端默认连 3306会直接超时。Database要连接的库名。不写也可以连接成功但后续执行的 SQL 都需要手动指定数据库名容易出错建议显式写上。Uid 和 Pwd登录用户名和密码。注意用户名和密码里如果包含特殊字符比如分号、百分号处理起来比较麻烦后面会专门说。CharSet客户端使用的字符集。中文项目推荐直接写 utf8mb4不要写 utf8。原因我后面乱码那一节详细解释。2.2 高级参数和它们的作用基础参数能连上但想稳定、可控地运行还要知道几个高级参数SslMode控制是否使用 SSL 加密连接。取值有 None、Preferred、Required 等。开发环境省事可以设成 None生产环境我建议 Required强制对传输层加密避免密码在网络上明文传递。MySQL 8 默认开启了 SSL 支持如果连接串没设置 SslMode驱动默认行为可能是 Preferred会尝试协商加密一旦服务端配置了必须 SSL 校验而你还要求无效证书就可能报证书验证失败。AllowPublicKeyRetrieval这个参数和 MySQL 8 的 caching_sha2_password 认证插件有关。当使用 RSA 公钥加密传输密码时客户端默认不允许自动从服务器获取公钥所以要手动设置为 True。很多新手遇到Authentication method caching_sha2_password not supported就是忘了设置这个参数。但要提醒一下开启它存在中间人攻击风险只能在保证网络可信的前提下用。ConnectionTimeout建立连接的超时时间单位秒默认 15。很多人在本地调试时觉得超时太慢就调成 300这是非常坏的习惯。超时是保护机制不是越慢越好。生产环境建议保持 5-10 秒连接不上就该快速失败让服务发现异常而不是卡住所有线程。Pooling连接池开关默认 True。C# 连接数据库默认启用连接池连接池能显著提高性能。如果设成 False每次 Open 都会创建全新物理连接并发一高很容易撑爆服务端的最大连接数。Max Pool Size连接池最大连接数默认 100。如果达到上限会抛异常提示连接池耗尽。要确认这个值和 MySQL 服务端 max_connections 配置谁更小否则连接池认为还能再开连接服务端其实已经拒绝了。ConnectionLifeTime连接在池中的最长存活时间默认 0 表示不限。对老 MySQL 网络设备有断空闲连接问题的环境可以设置一个值比如 300 秒让超时连接自动重建。2.3 连接字符串放哪、怎么保护在控制台程序里测试时把连接字符串直接写死在代码里没问题但正式项目千万别这么干。连接字符串里有数据库密码随着代码提交到仓库等于把钥匙送给了任何能读到代码的人。ASP.NET Core 项目建议放到配置文件里通过配置系统读取{ ConnectionStrings: { Default: Serverlocalhost;Port3306;Databasetestdb;Uidroot;Pwd123456;CharSetutf8mb4;SslModeNone;AllowPublicKeyRetrievalTrue; } }代码里这样取var connectionString builder.Configuration.GetConnectionString(Default);再讲究一点不要把真实密码放在 appsettings.json 里。开发环境可以用环境变量或者用 User Secrets 工具部署时再用正式密码覆盖。密码里如果包含特殊字符比如分号连接串会解析错乱这时需要用引号包裹密码值转义规则让人头大。我最后的建议是密码字段尽量只使用大小写字母和数字降低解析出错概率。3. 核心代码从连接到事务代码这部分并不难但有几个习惯如果从一开始就养成后面能少踩很多坑。3.1 建立连接别小看 using 那行代码C# 里最标准、最短的连接写法是这样var connStr Serverlocalhost;Port3306;Databasetestdb;Uidroot;Pwd123456;; using (var conn new MySqlConnection(connStr)) { conn.Open(); // 执行操作 }新版 C# 还可以简化成using var conn new MySqlConnection(connStr); await conn.OpenAsync();为什么要用 using因为 MySqlConnection 实现了 IDisposable它的 Dispose 方法不仅会关闭连接还会把连接对象归还给连接池。如果你不用 using就得手动写 try/catch/finally 保证 conn.Close() 或 conn.Dispose() 一定会执行。任何一条代码路径漏了释放连接都不会真正关闭最终效果就是连接池被占满。这里还有一个很多人理解反的概念conn.Close()并不会真的断开数据库连接它只是把连接放回连接池物理连接还活着。下次再Open()时客户端从池里取出一个已存在的连接直接复用省去了 TCP 握手和认证的开销。所以不必担心循环里频繁 Open/Close 会拖垮数据库连接池会帮你把物理连接管好。3.2 查询数据的几种姿势执行 SQL 的入口是 MySqlCommand。根据返回结果的不同有三种常见用法。第一种读取单行单列的值比如查询用户数量用 ExecuteScalarusing var cmd new MySqlCommand(SELECT COUNT(*) FROM t_user, conn); var count Convert.ToInt32(await cmd.ExecuteScalarAsync());第二种执行 INSERT、UPDATE、DELETE 这类不返回结果集的语句用 ExecuteNonQueryusing var cmd new MySqlCommand(UPDATE t_user SET statusstatus WHERE idid, conn); cmd.Parameters.AddWithValue(status, 1); cmd.Parameters.AddWithValue(id, 1001); var affected await cmd.ExecuteNonQueryAsync();受影响行数可以帮助判断更新是否真的命中数据如果 affected 0说明条件没匹配到任何行。第三种查询返回多行结果集用 ExecuteReaderusing var cmd new MySqlCommand(SELECT id, name, created_at FROM t_user WHERE statusstatus, conn); cmd.Parameters.AddWithValue(status, 1); using var reader await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var id reader.GetInt32(id); var name reader.GetString(name); var time reader.GetDateTime(created_at); }用 ExecuteReader 时有一个非常致命的细节DataReader 是流式读取的它占用着连接期间不能关闭连接也不能断开连接必须把 DataReader 读完或 Dispose 掉之后连接才能执行其他 SQL 操作。很多人报错“connection already open, and already in use”就是因为在同一个连接上先开了一个 DataReader没有关闭就接着执行下一条命令。3.3 参数化查询防 SQL 注入的第一道防线新手最常见的错误写法是拼接字符串var sql SELECT * FROM t_user WHERE name name ;如果 name 的值是 OR 11这条 SQL 就变成了查询所有数据严重的还可能被删库。必须使用参数化查询把值作为参数传给 SQL 语句而不是写进 SQL 文本里。var cmd new MySqlCommand(SELECT * FROM t_user WHERE namename, conn); cmd.Parameters.AddWithValue(name, name);注意 MySQL 的参数占位符是开头。如果你是从 SqlServer 项目转过来的习惯可能还是这个倒是相同但在操作 SQLite 用$参数的地方就会踩坑。参数化还有一个好处传入的值会自动做类型转换和转义日期、布尔、空值都处理得比较稳妥。但我还要提一句AddWithValue 虽然写着“用起来方便”它对所有类型都会做隐式推断在某些场景下可能推断错误导致查询条件失效或者索引失效。更严谨的做法是创建参数时指定类型cmd.Parameters.Add(new MySqlParameter(createdFrom, MySqlDbType.DateTime)); cmd.Parameters[createdFrom].Value fromDate;3.4 事务处理多条语句一起成功或一起失败如果业务逻辑要做两步操作比如订单创建成功后扣减库存这两步要么都成功要么都失败必须使用事务。MySQL 默认每条 SQL 都是自动提交的不包事务的话第一步成功、第二步失败数据就脏了。事务写法如下using var conn new MySqlConnection(connStr); await conn.OpenAsync(); using var tx await conn.BeginTransactionAsync(); try { using var cmd1 new MySqlCommand(UPDATE t_stock SET countcount-1 WHERE idid, conn, tx); cmd1.Parameters.AddWithValue(id, 1); await cmd1.ExecuteNonQueryAsync(); using var cmd2 new MySqlCommand(INSERT INTO t_order(user_id, product_id) VALUES(uid,pid), conn, tx); cmd2.Parameters.AddWithValue(uid, 10); cmd2.Parameters.AddWithValue(pid, 100); await cmd2.ExecuteNonQueryAsync(); await tx.CommitAsync(); } catch { await tx.RollbackAsync(); throw; }关键点在于创建 MySqlCommand 时要把事务对象作为第三个参数传进去否则这条命令就在连接自己的隐式事务里执行了并不会归属到 tx 这个事务下。很多人明明写了事务却发现回滚不生效就是漏了这个参数。事务开启后要注意别在里面做太耗时的操作比如调用外部接口、等用户输入、执行大批量计算。长事务会一直持有行锁其他会话更新同一行数据时会阻塞等待积压多了容易造成连接耗尽和死锁。如果数据库默认隔离级别是 REPEATABLE READ长事务还会导致数据快照问题读到的一直是事务开始时的版本。这类问题的排查信号是业务上明明更新了数据同一个事务里再查结果还是老值。这个现象非常隐蔽建议事务体尽可能短、快速提交。4. 常见问题排查实录这一部分是我最想分享的。C# 连 MySQL 的代码本身很简单但运行起来之后各种诡异问题基本都是环境或配置导致的。4.1 连接不上的排查链路遇到Unable to connect to any of the specified MySQL hosts先别急着改代码。我一般按这个顺序排查确认 MySQL 服务真的在运行。服务端执行systemctl status mysql或 Windows 服务列表里看 MySQL 服务状态。确认端口是否正确。连接串写了 3306服务端实际监听 3036肯定连不上。确认防火墙是否放行。云服务器和本地服务器最容易因为安全组规则没放行 3306 导致外网连不上直接 telnet 测试一下telnet 192.168.1.10 3306如果提示能连通但连接后马上断开那多半是服务端拒绝认证如果一直卡着不动那是网络不通。检查 MySQL 的 bind-address 配置。如果只监听 127.0.0.1那任何外部 IP 都没法直接连需要改成0.0.0.0或指定内网网卡地址。还有一个我踩过的细节Server 参数填写 localhost客户端可能会优先尝试走 socket 连接而不是 TCP。某些官方驱动的早期版本在 Linux 上对 localhost 的处理和预期不一致导致连不上。遇到莫名问题先把 localhost 改成 127.0.0.1 试试这个改动经常能救急。4.2 MySQL 8 认证插件错误这个坑太经典了我要单独拿出来讲。报错信息通常是Authentication method caching_sha2_password not supportedMySQL 8 默认新建的用户认证插件是 caching_sha2_password旧版本驱动没实现这个协议所以连不上。有三种解决办法第一种升级驱动。MySql.Data 8.x 以上版本已经支持 caching_sha2_password升级后问题自然消失。这是最推荐的方案。dotnet add package MySql.Data --version 8.0.33第二种连接串里显式设置加密相关参数SslModeNone;AllowPublicKeyRetrievalTrue;这样客户端会在需要时向服务器请求 RSA 公钥完成密码加密传输。这种方式可行但前面说过有安全风险生产环境如果开着 SSL 就不要再用这个组合。第三种把 MySQL 用户改回旧的认证插件ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY password; FLUSH PRIVILEGES;这种做法的本质是降低数据库的认证安全性我不建议在新项目里这么干。它确实能解决驱动兼容问题适合维护老项目、无法升级场景下的临时救急。4.3 乱码问题挨着试不如懂原理插入数据后查询出来是问号或者查询条件带中文查不到结果这种问题几乎可以确定是字符集不一致。MySQL 里有一整套字符集体系牵扯到服务端默认字符集、数据库字符集、表字符集、列字符集、客户端连接字符集。最简单的排查思路把连接串里的 CharSet 明确写成 utf8mb4Serverlocalhost;Databasetestdb;Uidroot;Pwd123456;CharSetutf8mb4;同时检查数据库和表的字符集SHOW CREATE DATABASE testdb; SHOW CREATE TABLE t_user;如果建库建表时没指定默认可能继承服务端的 latin1 或其他字符集这时需要在客户端之外改库表结构ALTER DATABASE testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;我特别强调一点MySQL 的“utf8”不是真正的全量 UTF-8它其实是 utf8mb3只能存基本多语言平面的字符像 emoji、部分生僻字就存不进去会直接报错或存成乱码。从 5.5 之后官方推荐使用 utf8mb4所以 C# 连接 MySQL 时CharSet 不要写 utf8直接写 utf8mb4。4.4 连接池耗尽与超时报错信息出现Pool exhausted以及旧版驱动里的Timeout expired是并发场景下最常见的问题。登录上去看数据库进程列表你会看到大量 Sleep 状态的连接数量接近服务端 max_connections。出现这种情况我把排查重心放在三个地方第一检查代码里是不是有连接未释放。最常见的是 DataReader 没有 Dispose或者事务抛异常后没有执行 Rollback连接一直被占用。最简单的自测方法把所有数据库操作都改成 using 包裹运行一段时间再查连接数。第二检查是不是有人把 Pooling 设为 False。如果项目里有这样的代码Poolingfalse;那每次 Open 都会新建物理连接并发上来很容易打满数据库连接数。正常情况应该让连接池工作。第三评估连接池上限有没有调低或者被误改成很小的值比如 Max Pool Size10而应用并发是几百那基本上一秒不到就池耗尽了。在无法快速优化代码的临时场景下可以适度调大 Max Pool Size比如 200但这是治标不治本真正的根因还是连接没释放或并发模型不合理。这个问题的另一个隐蔽来源是异步方法里没有使用 await而是用.Result或.Wait()导致线程阻塞在异步操作上连接被占用却不归还。只要你发现线程数飙升、连接池很快耗尽就要去检查是不是有人在异步代码里用了同步等待。最后提一句show processlist;是排查连接问题的神器。看到几百个 Sleep 连接就知道有人在退出前没关连接看到一堆Query状态且执行时间很长就要怀疑有慢 SQL 或者锁等待。先看进程列表再回代码里找原因比瞎猜高效得多。我自己在实际项目里最深的体会是连接字符串的每一个参数都不是摆设连不上或者连上了不稳定十有八九是参数配置出了问题而连接资源的管理是保证稳定性的基本功。如果这篇文章只能留下一句话我希望是这句话先确认连接串再看进程列表最后才怀疑驱动和代码。这套排查顺序帮我解决了很多看起来“莫名其妙”的问题。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表