直接开干。
搞C#开发,免不了要跟数据库打交道。前阵子有个项目要从SQL Server迁到MySQL,我顺手把整个流程完整捋了一遍,从装库到C#里增删改查,再到连接池、批量写入这些坑,全部实测过一遍。这篇东西就是把这些经验沉淀下来,按“安装-连接-操作-排坑”的顺序写清楚,照着做基本能少走两星期弯路。
不管你是刚接触C#的应届生,还是被公司派去维护老项目的“接盘侠”,或者是想把自己本地环境从SQL Server换成MySQL的独立开发者,这篇内容都适用。我会尽量把每一步为什么要这么做、底层发生了什么讲透,而不是只丢给你一串命令。
1. 环境准备:MySQL安装与初始化配置
很多新手倒在这一步,不是因为安装包不会下载,而是装完之后不知道哪一步才是真正“起步”的节点。我先给一个推荐组合:Windows + MySQL 8.0 Community Server + .NET 6/8 + MySqlConnector驱动,这套在目前的生产环境里兼容性最好。下面按步骤走。
1.1 在Windows下正确安装MySQL Community Server
先去MySQL官网下载页找到Community Server版本,注意选“Windows (x86, 32-bit), MSI Installer”或者ZIP Archive,我推荐MSI安装包,省心。下载的时候会要求登录Oracle账号,这一步可以直接点“No thanks, just start my download”跳过。
安装时选择Server only即可;如果你想用Workbench图形化管理,可以同时勾选。安装模式选“Developer Default”会自动装一堆组件,反而拖慢速度,所以只装Server就够了。
安装过程中最关键的步骤是配置root用户密码和Authentication Method。8.0默认推荐的是caching_sha2_password,这个后面C#连接时有个老驱动兼容性问题,我们下文会细说。建议在这里选“Use Strong Password Encryption”保持默认。
提示:root密码千万别在安装界面上点“随机生成”然后抄在小本本上,最后搞丢了。我见过太多同事这么干,最后不得不靠
--skip-grant-tables重置密码,费半天劲。直接用你自己管理的强密码,写完记到密码管理器里。
配置完密码后,可以顺手把MySQL注册成Windows服务,这样开机自动启动。安装向导最后会执行步骤清单,先等它跑完,然后再去下一步。
1.2 安装后必须做的三件配置
装好后不要急着写代码,先把这三件事做完,能避免后面90%的坑。
第一件事:确认服务状态和命令行路径。安装MySQL后默认路径一般在C:\Program Files\MySQL\MySQL Server 8.0\bin。打开cmd,执行:
cd "C:\Program Files\MySQL\MySQL Server 8.0\bin" mysql -u root -p输入密码后能进入mysql>提示符,说明服务正常。如果提示找不到命令,就是环境变量没配。右键“此电脑” -> 属性 -> 高级系统设置 -> 环境变量,把...\bin路径追加到Path变量里,这样以后在任何路径下都能直接敲mysql命令。
第二件事:创建一个专门的业务数据库和账号。不要什么事情都用root账号干。这是我从同事那学来的教训——生产库里如果有人用root误操作drop了表,连隔离的机会都没有。执行:
CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'demo_user'@'%' IDENTIFIED BY 'YourStrongPass123'; GRANT ALL PRIVILEGES ON demo_db.* TO 'demo_user'@'%'; FLUSH PRIVILEGES;注意字符集选utf8mb4而不是utf8,因为MySQL的utf8实际只支持最多3字节的字符,遇到表情符号或生僻字直接报错。utf8mb4才是完整版UTF-8。排序规则utf8mb4_general_ci性能好,足够应对绝大多数场景;真要追求严格Unicode规则可以选utf8mb4_unicode_ci,但一般不需要。
第三件事:调整连接数和等待超时参数。MySQL默认最大连接数是151,本地开发够用,但一旦C#程序开了连接池,一压测就飙到几百个连接,直接报Too many connections。打开my.ini(一般在C:\ProgramData\MySQL\MySQL Server 8.0\),在[mysqld]下加入:
max_connections = 500 wait_timeout = 600 interactive_timeout = 600改完保存,在cmd执行net stop mysql && net start mysql重启服务。
1.3 Linux服务器安装MySQL的差异点
如果你的目标环境是Linux,安装逻辑类似但细节不同。以Ubuntu 22.04为例,通过APT安装的是MySQL 8.0的发行版:
sudo apt update sudo apt install mysql-server -y sudo systemctl enable mysql sudo systemctl start mysql第一次装完,root默认通过auth_socket插件认证,即只能通过系统socket登录,不能远程密码登录。你需要先:
sudo mysql进入MySQL命令行后,执行:
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'NewRootPass'; FLUSH PRIVILEGES;Linux上另一个常见问题是bind-address默认绑定127.0.0.1,这意味着C#程序如果是跨机器访问这台MySQL,无论如何都连不上。改/etc/mysql/mysql.conf.d/mysqld.cnf:
bind-address = 0.0.0.0然后重启:
sudo systemctl restart mysql同时别忘了在云服务器安全组里放行3306端口。我踩过最蠢的坑:服务器上MySQL全配置好了,netstat显示监听正常,但安全组没放行,C#客户端连了半小时都超时。
2. C#连接MySQL的驱动选型与连接串配置
MySQL装好了,接下来是C#这边。这块我打算把驱动选型和连接串参数掰碎了讲,因为大多数人第一次连不上,问题不在代码,而在驱动或连接串。
2.1 MySqlConnector vs Oracle官方驱动,到底选谁
现在C#连MySQL,主流有两条路:一条是Oracle官方提供的MySql.Data(NuGet包名也叫这个),另一条是社区维护的MySqlConnector。
我可以明确告诉你结论:新项目直接用MySqlConnector。原因有三。
第一,MySqlConnector是异步优先设计的,对async/await的支持甩官方驱动好几条街。官方驱动虽然也能用ExecuteReaderAsync,但底层同步阻塞的问题老是被诟病,在高并发下容易把线程池拖垮。
第二,MySqlConnector对caching_sha2_password认证的支持更彻底。官方MySql.Data在8.0.26之前的版本连MySQL 8.0默认认证会有兼容问题,得专门改连接串,而MySqlConnector从设计第一天就支持了。
第三,MySqlConnector开源活跃,GitHub上issue响应快,性能横幅也好看。如果你还在维护老项目且用着官方驱动没出问题,那就别动;但新项目一律MySqlConnector。
在Visual Studio或Rider里,打开NuGet包管理器,搜索MySqlConnector,安装最新稳定版。写这篇内容时最新版本已经到了2.x系列,API稳定。
注意:如果你公司的项目还在用
.NET Framework 4.x,选MySqlConnector时要选低于2.0的版本,1.3.x系列还支持.NET Framework;2.0起转向.NET Standard 2.0/2.1,老的Framework项目可能不兼容。这就是“势利”的现实约束。
2.2 连接串的每个参数都是什么意思
连接串是C#连MySQL最容易出错的地方。一个典型的MySqlConnector连接串长这样:
Server=127.0.0.1;Port=3306;Database=demo_db;User Id=demo_user;Password=YourStrongPass123;CharSet=utf8mb4;SslMode=Preferred;我一个个讲:
- Server:填IP或主机名。
localhost和127.0.0.1在MySQL里是有区别的。localhost默认走socket连接(Linux上尤其明显),127.0.0.1走TCP。C#程序一律用127.0.0.1或服务器IP,别写localhost。 - Port:默认3306。如果你改了MySQL端口,这里要对应。
- Database:要连接的库名,可以留空,但建议直接指定。
- User Id / Password:之前创建的业务账号,别用root。
- CharSet:指定为
utf8mb4,这样查询结果里的中文和emoji不会乱码。这是很多人漏掉的坑,不填的话用默认utf8mb4也差不多,但显式声明更稳妥。 - SslMode:MySQL 8.0默认开启SSL。如果服务器没配置证书,用
Preferred(如果服务器支持就加密),或者干脆用None(本地开发强烈建议),省得握手阶段出幺蛾子。
还有几个进阶参数,按场景用:
ConnectionTimeout=15:默认15秒,连接超时会抛异常,可以调短一点。DefaultCommandTimeout=30:这是ExecuteNonQuery等命令的默认超时,大批量导入时要把这个值调大,否则跑很久的存储过程会被中途掐断。Pooling=true:默认就开启,连接池相关我们在后面单独讲。AllowBatch=true:MySqlConnector允许一个命令写多条SQL,用分号分隔,省一次网络往返。
2.3 建立第一个连接:最小可运行示例
装了包、写完连接串,先用最朴素的方式验证一下连通性:
using MySqlConnector; string connStr = "Server=127.0.0.1;Port=3306;Database=demo_db;User Id=demo_user;Password=YourStrongPass123;CharSet=utf8mb4;SslMode=None;"; await using var connection = new MySqlConnection(connStr); await connection.OpenAsync(); using var command = new MySqlCommand("SELECT 1", connection); var result = await command.ExecuteScalarAsync(); Console.WriteLine($"连接成功,返回结果: {result}");如果输出1,说明C#已经能跟MySQL正常通信。这里用await using是C# 8.0后的推荐写法,数据库连接实现了IAsyncDisposable,异步释放不会阻塞线程。
ExecuteScalarAsync拿的是结果集第一行第一列,适合做这种“探活”查询。实际业务中,我们更常用ExecuteReaderAsync、ExecuteNonQueryAsync,下面逐个讲。
3. C#操作MySQL:从增删改查到参数化查询
连接没问题,接下来就是实际的CRUD操作。这一节是重头戏,我会把性能和安全相关的细节都塞进去。很多人写数据库代码只用SqlCommand那一套,换成MySQL就不会举一反三了,其实套路完全一样。
3.1 创建表和插入数据:参数化查询是底线
先在MySQL里建张简单的用户表:
CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NULL, age INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;有几点说明一下:ENGINE=InnoDB是必须的,因为只有InnoDB支持事务、外键和行级锁,MyISAM性能虽高但数据安全差一截,新业务一律InnoDB。created_at用DEFAULT CURRENT_TIMESTAMP,插入时不需要赋值,MySQL自动填。
C#插入代码:
public static async Task<int> InsertUserAsync(string username, string email, int age) { var sql = "INSERT INTO users (username, email, age) VALUES (@username, @email, @age);"; await using var connection = new MySqlConnection(_connStr); await connection.OpenAsync(); await using var command = new MySqlCommand(sql, connection); command.Parameters.AddWithValue("@username", username); command.Parameters.AddWithValue("@email", (object?)email ?? DBNull.Value); command.Parameters.AddWithValue("@age", age); return await command.ExecuteNonQueryAsync(); }这里没有一行是多余的,每个细节都有讲究:
- 用
Parameters.AddWithValue而不是拼接字符串,是为了防SQL注入。字符串拼接在开发环境看不出来,但在公网部署后迟早被人用' OR 1=1 --这种手法打穿。你永远不要相信用户输入。 @email传了(object?)email ?? DBNull.Value,是为了把C#的null转成数据库的NULL。AddWithValue接收object类型,你直接传null,C#会把重载解析成字符串参数还是什么鬼,容易出歧义,所以显式转一下。ExecuteNonQueryAsync返回的是受影响行数。插一条返回1,可以用来成功判断。
你可能会说,每次操作都手动打开关闭连接不麻烦吗?对,所以实际项目我们通常会封装一个DatabaseHelper或者直接用ORM。但作为底层兜底,理解原生连接的生命周期是基本功。连接用完之后必须释放,await using就是为了保证这一点,否则连接池里的连接会被占满,后面我们排坑时会遇到。
3.2 查询数据:DataReader和DataTable两条路线
查询是业务里最频繁的操作。C#连MySQL查数据有几种方式,我分别说一下适用的场景。
第一种,SqlDataReader边读边处理,适合大数据量流式读取:
var sql = "SELECT id, username, email, age, created_at FROM users WHERE age > @minAge ORDER BY id DESC LIMIT 100;"; await using var connection = new MySqlConnection(_connStr); await connection.OpenAsync(); await using var command = new MySqlCommand(sql, connection); command.Parameters.AddWithValue("@minAge", 18); await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var id = reader.GetInt64("id"); var username = reader.GetString("username"); var age = reader.IsDBNull(reader.GetOrdinal("age")) ? 0 : reader.GetInt32("age"); Console.WriteLine($"{id} - {username} - {age}"); }这里有个坑:reader.GetInt32("age"),如果数据库里age是NULL,直接调用会抛异常。所以先用IsDBNull判断,代码里那一行就是这么来的。这属于“看着不起眼但早晚要踩”的经典报错之一。
第二种,SqlDataAdapter填充DataTable,适合把结果一次性加载到内存,然后绑定到控件(WinForms/DataGrid):
var sql = "SELECT * FROM users;"; using var adapter = new MySqlDataAdapter(sql, _connStr); var table = new DataTable(); await Task.Run(() => adapter.Fill(table));MySqlDataAdapter自己会管好连接生命周期,不需要我们手动Open/Close,这是DataAdapter和Command的主要区别。
3.3 更新和删除:事务保护不能少
更新和删除本身语法简单,我就不贴基础SQL了,重点讲事务。举个例子:你在做一个订单系统,要先扣库存,再生成订单记录。如果第一个操作成功、第二个失败,库存扣了但订单没生成,这数据就是脏的。这种场景必须用事务。
public static async Task TransferInventoryAsync(int productId, int quantity, string orderNo) { await using var connection = new MySqlConnection(_connStr); await connection.OpenAsync(); await using var transaction = await connection.BeginTransactionAsync(IsolationLevel.ReadCommitted); try { var sqlUpdate = "UPDATE products SET stock = stock - @qty WHERE id = @pid AND stock >= @qty;"; await using (var cmdUpdate = new MySqlCommand(sqlUpdate, connection, transaction)) { cmdUpdate.Parameters.AddWithValue("@qty", quantity); cmdUpdate.Parameters.AddWithValue("@pid", productId); int rows = await cmdUpdate.ExecuteNonQueryAsync(); if (rows == 0) throw new Exception("库存不足或商品不存在"); } var sqlInsert = "INSERT INTO orders (order_no, product_id, quantity) VALUES (@orderNo, @pid, @qty);"; await using (var cmdInsert = new MySqlCommand(sqlInsert, connection, transaction)) { cmdInsert.Parameters.AddWithValue("@orderNo", orderNo); cmdInsert.Parameters.AddWithValue("@pid", productId); cmdInsert.Parameters.AddWithValue("@qty", quantity); await cmdInsert.ExecuteNonQueryAsync(); } await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } }这个写法里有三个地方是新手上路最容易犯错的:
第一,MySqlCommand构造函数要传transaction对象,否则命令默认在无事务模式下执行。我见过有人明明建了事务,但命令没传transaction,结果更新执行了,事务回滚半点效果没有,还一脸不解。
第二,UPDATE语句里的AND stock >= @qty是防止超卖的核心。数据库在事务中会锁住这条记录,两个并发请求同时减库存时,只有满足条件的那个能成功。这个条件就是乐观锁在下游数据库的微观体现。
第三,IsolationLevel.ReadCommitted是MySQL默认的隔离级别(其实InnoDB默认是REPEATABLE READ,但我习惯显式指定),它比Serializable性能好,又比ReadUncommitted安全。事务里只处理自己关心的行时,隔离级别不需要过分拔高。
3.4 存储过程调用:从C#传参再拿返回值
有些公司习惯把复杂业务逻辑下沉到存储过程,C#这边只负责调用。MySQL里先写一个简单的存储过程:
DELIMITER // CREATE PROCEDURE sp_get_user_count_by_age(IN min_age INT, OUT total INT) BEGIN SELECT COUNT(*) INTO total FROM users WHERE age > min_age; END // DELIMITER ;C#端调用:
await using var connection = new MySqlConnection(_connStr); await connection.OpenAsync(); await using var command = new MySqlCommand("sp_get_user_count_by_age", connection); command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("min_age", 18); command.Parameters.Add("total", MySqlDbType.Int32).Direction = ParameterDirection.Output; await command.ExecuteNonQueryAsync(); var count = (int)command.Parameters["total"].Value;记得CommandType要设置为StoredProcedure,否则连接器会尝试把它当普通SQL执行然后报语法错。另一个坑是参数名不要带@前缀,存储过程定义里用了什么名就写什么名;带@在某些版本会匹配不到参数。
4. 连接池与批量写入:高并发场景的必修课
聊完基本CRUD,该进入真正的生产环境话题了。很多单机小项目CRUD跑得欢,一上线并发一上来就拉胯,问题通常出在连接管理和写入效率上。
4.1 连接池的工作机制与关键参数
MySQL连接是一个重资源,每建立一次TCP连接,MySQL都要进行认证、握手、资源分配,整个过程少说十几毫秒。高并发下频繁创建/销毁连接,数据库CPU会持续飙升。
MySqlConnector默认开着连接池,所谓连接池就是一段缓存:程序释放连接时,连接并没有真正销毁,而是被放回池子,下次要连接时直接从池子里取,省掉握手开销。
与连接池相关的连接串参数:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
Pooling | true | 保持 | 关闭连接池会明显降低性能 |
Min Pool Size | 0 | 5 | 启动时预建的连接数,避免流量突增时建连毛刺 |
Max Pool Size | 100 | 200~500 | 池内最多存放的连接数,超了会排队等待 |
ConnectionLifeTime | 0(无限) | 300 | 连接最大生命周期(秒),定期淘汰旧连接,避免占用后不释放 |
ConnectionIdlePing | false | true | 空闲连接会自动探测数据库健康度 |
调参的核心逻辑:Min Pool Size调高,能避免流量洪峰时瞬间拉起一堆连接带来的性能抖动;ConnectionLifeTime调到300秒,是防止数据库wait_timeout把长期空闲连接断掉之后,池里的连接变成“半死”状态,下次一取就用就报错。
我实测过一个压测场景:数据库连接等待时间从20多秒降到几毫秒,就是靠把Min Pool Size从0调到10并预热连接。连接池在底层为业务扛住了瞬时压力。
4.2 使用MySqlBulkCopy做批量导入
如果要往MySQL里插入几万条数据,一条一条INSERT绝对是最蠢的方式。C#里有三种优化方案:MySqlBulkCopy、INSERT INTO ... VALUES (...), (...), (...)多值插入、以及存储过程循环。这里重点介绍MySqlBulkCopy,它跟SQL Server的SqlBulkCopy类似。
public static async Task BulkInsertUsersAsync(IEnumerable<User> users) { var table = new DataTable(); table.Columns.Add("username", typeof(string)); table.Columns.Add("email", typeof(string)); table.Columns.Add("age", typeof(int)); foreach (var user in users) { table.Rows.Add(user.Username, user.Email ?? (object)DBNull.Value, user.Age); } await using var connection = new MySqlConnection(_connStr); await connection.OpenAsync(); var bulkCopy = new MySqlBulkCopy(connection) { DestinationTableName = "users", BulkCopyTimeout = 120 }; await bulkCopy.WriteToServerAsync(table); }MySqlBulkCopy内部走的是LOAD DATA LOCAL INFILE协议(实际上是客户端流式写数据),能把大批量插入的性能提升一个数量级。我做过一个上万记录导入的对比:逐条插入耗时45秒,MySqlBulkCopy耗时不到1秒,差异就是这么夸张。
不过要注意:使用MySqlBulkCopy要求连接串里允许AllowLoadLocalInfile=true。这个参数涉及安全考量,生产环境如果对安全要求极严格,可以由DBA在MySQL端关闭LOAD DATA LOCAL INFILE,那就不能用BulkCopy。折中方案是拆成多值INSERT,每批500条:
var chunks = users.Chunk(500); foreach (var chunk in chunks) { var values = string.Join(",", chunk.Select(u => $"('{u.Username.Replace("'", "''")}', {(u.Email == null ? "NULL" : $"'{u.Email.Replace("'", "''")}'")}, {u.Age})")); var sql = $"INSERT INTO users (username, email, age) VALUES {values}"; await using var command = new MySqlCommand(sql, connection); await command.ExecuteNonQueryAsync(); }这个方法简单粗暴,但必须注意:值里有单引号时必须转义,否则SQL会直接断裂。上面代码里Replace("'", "''")就是干这个的。把每一批500条合并成一条SQL,比逐条执行快得多,因为少了几百次网络往返。
4.3 Dapper:手写ADO.NET太痛苦时的折中方案
我不打算在此长篇大论ORM选型,但Dapper值得单独说。它是介于原生ADO.NET和重量级ORM(EF Core)之间的轻量级映射库,让你不用手写DataReader的循环取值,SQL还能完全由自己掌控。
在NuGet安装Dapper后,之前的查询可以简化为:
using Dapper; await using var connection = new MySqlConnection(_connStr); var users = await connection.QueryAsync<User>( "SELECT id, username, email, age, created_at FROM users WHERE age > @minAge", new { minAge = 18 });这就是全部代码。QueryAsync<T>会自动把查询结果行映射到User对象的属性,参数对象不用手动AddWithValue。我认为Dapper是C#连接MySQL的“甜点”方案:它保留了SQL的透明性,又把你从无聊的样板代码里解放出来。如果你的项目没有强需求必须用EF Core,Dapper能显著减少代码量,而且几乎零学习成本。
5. C# MySQL实操中的常见故障与排查实录
这一章是我最想写的部分。每一个问题我都亲手踩过或者帮别人排查过,全部来自真实环境。
5.1 “Authentication method 'caching_sha2_password' not supported” / 认证方式不兼容
这个问题在官方驱动MySql.Data里最容易出现,提示一般是:
Authentication method 'caching_sha2_password' not supported by any of the available pluginsMySQL 8.0默认的认证插件是caching_sha2_password,但某些旧版本的连接器不认识它。两种解决方案:
方案A(推荐):更换驱动。用MySqlConnector替代MySql.Data,这种认证方式从一开始就被支持。
方案B(被迫时):修改MySQL用户的认证插件为mysql_native_password:
ALTER USER 'demo_user'@'%' IDENTIFIED WITH mysql_native_password BY 'YourStrongPass123'; FLUSH PRIVILEGES;这样旧驱动也能连了。但注意MySQL 8.0官方明确表示未来会移除mysql_native_password,所以这只能当临时方案。
顺便提一句:如果你的MySQL装在Linux上,而这个错误出现在用
localhost连接时,还有一个可能是驱动在走socket通道而不是TCP,把连接串里的Server从localhost改成127.0.0.1试一下。
5.2MySqlConnection打开超时:网络是通的为什么连不上
遇到最多的情况是:MySQL服务明明在跑,端口也放行了,但OpenAsync一直等到超时,报:
connect timed out排查顺序:
- 先确认MySQL有没有监听3306端口。Linux上执行
ss -lntp | grep 3306,看是否监听0.0.0.0:3306;如果只监听127.0.0.1:3306,就是bind-address没改。 - 确认防火墙/安全组。Windows上检查防火墙入站规则是否放行3306;云服务器查安全组。
- 用telnet做端口通断测试。在C#代码所在机器执行
telnet mysql服务器IP 3306,通的话直接显示连接成功或者光标闪烁,不通会报Could not open connection。 - 确认账号的host权限。
demo_user创建时用了'%'表示任何主机可连,这是好的;如果你当时写了'localhost',远端IP一律拒连,报错信息里会带上Access denied for user 'demo_user'@'客户端IP'。
5.3ConnectionString里的密码有特殊字符导致连不上
这是个特别容易忽视的坑。连接串是KV键值对,如果你的密码里有分号、引号或者#,会直接被当作配置分隔符截断。
解决方案有两个:一是包一层引号:
Password="Your;Pass#123"二是用MySqlConnectionStringBuilder来避免手拼字符串:
var builder = new MySqlConnectionStringBuilder { Server = "127.0.0.1", Database = "demo_db", UserID = "demo_user", Password = "Your;Pass#123", SslMode = MySqlSslMode.None }; string connStr = builder.ConnectionString;这个类会自动处理特殊字符,我是强烈建议用它的。
5.4 连接池耗尽:Pool exhaustion错误
这个报错长这样:
MySqlConnector: The connection pool is exhausted. Either increase Max Pool Size or check whether connections are being leaked.通常不是Max Pool Size太小,而是程序里某个分支忘了释放连接。最常见原因:
- 用了
new MySqlConnection(...)但只在某个分支里调用了Open(),Dispose()没执行。 - 把
MySqlConnection当作静态字段,在多线程环境共享,然后把连接用完后没放回池子。 - 在
try块里Open了连接,catch块里没Dispose,异常时连接一直挂着。
排查技巧:在MySqlConnection上注册状态变化事件,打印每次连接打开/释放的时机,或者用dotnet-counters监控线程池和连接数。更简单的做法是,全局搜索代码,确保所有new MySqlConnection都配了await using或try/finally。
5.5 中文乱码:数据库里看正常,C#里读出来是乱码
这个问题大多出在三层:数据库字符集、连接串字符集、控制台显示编码。
前两层我们已经解决:建库用utf8mb4,连接串里CharSet=utf8mb4。第三层容易被忽略——如果你在Windows的控制台直接Console.WriteLine读出的中文字符串,而控制台代码页是GBK(默认936),就会显示成乱码。处理方式:
Console.OutputEncoding = System.Text.Encoding.UTF8;在程序开头加上这行。还要注意读取代码文件的编码,确保源文件本身以UTF-8保存,不然字符串常量里的中文先一步就乱了。
5.6Access violation c0000005这类底层崩溃
有些老驱动在某些场景下会触发原生层崩溃,典型报错就是:
Access violation c0000005我在网上搜索热词时看到不少人遇到C#调用C++访问MySQL出现access violation c0000005。这类问题成因复杂,但有一个经验值得一提:如果非托管库和托管驱动在内存布局上互相干扰,优先考虑更换驱动版本或升级运行时。很多次“灵异”崩溃,换一个版本或换一个驱动就从世界上消失了。MySQL连接的崩溃多半是版本不匹配或指针释放重复导致的,排查思路先用最小示例复现,再逐步加代码缩小范围。
6. 进阶技巧:从“能跑”到“跑得稳”
最后一节,聊聊我认为C#连MySQL从“能跑”变成“跑得稳”的几个关键习惯。
6.1 用异步编程,但别滥用并发
不要在主线程里Wait()阻塞地等OpenAsync。在UI程序里,这会卡死界面;在ASP.NET Core里,会造成线程池饥饿。正确做法是让异步一路冒泡到调用链最外层。但同时要明白:并发不是越多越好。假设数据库连接池最多200条,你硬开50个线程同时开连接,只会引起排队和超时。好的并发策略是根据池大小做协调,比如用信号量限制最大并发数量。
6.2 日志、监控与慢查询
生产环境必须把SQL执行时间记录下来。在C#侧,可以封装一个命令执行包装器,每次执行完记录耗时和SQL文本。MySQL侧,用慢查询日志:
slow_query_log = 1 long_query_time = 1这个配置把超过1秒的SQL全部记录下来。我接手过一个老项目,性能差得不行,开慢查询日志一看,90%的慢查询都是同一个没有索引的LIKE '%keyword%'查询。加上全文索引或改用LIKE 'keyword%',性能立刻从2秒跌到50毫秒。
6.3 升级驱动的时序
MySqlConnector和MySQL本身都在持续更新。生产环境不要追新,但也不要长年停在老版本。我的习惯是:小版本更新(如2.1到2.2)先在开发环境跑一周,没问题再上预发,最后再上生产。尤其涉及认证协议、SSL/TLS相关变更的版本,必须重点测。
6.4 备份是底线
MySQL的备份常规手段是mysqldump:
mysqldump -u root -p --all-databases > /backup/all_$(date +%Y%m%d).sql但我会建议再配合binlog做时间点恢复。很多公司有备份,但没演练过恢复。数据库运维的核心不是“能备份”,而是“能恢复”。真遇到磁盘损坏时,才发现备份文件本身都是坏的,那才叫欲哭无泪。
坦白说,C#连MySQL这条路,网上教程一大堆,但大多只教你“照抄能跑”,没教“为什么能跑”和“坏了怎么排查”。我写这篇东西的初衷就是把自己踩过的坑和排查思路完整摊开,能帮读者省下几个深夜调库的时间。
最后分享一个个人体会:连接串别乱拼,驱动别乱选,事务别乱丢,连接记得释放。这四句话做到,C#操作MySQL的项目基本就成功了一大半。剩下的,都是细节里磨出来的功力。