
简介一份面向C#开发者的实用工程资源专注解决照片等二进制文件存储到MySQL数据库的问题。压缩包内共48个文件体积333KB以cs源代码、sql脚本、config配置文件、datasource数据源文件及exe可执行文件为主并包含资源文件与项目工程文件能够完整体现Visual Studio解决方案结构。核心示例围绕WindowsFormsApplication2项目展开演示了File.ReadAllBytes读取图片、MySqlConnection建立连接、创建BLOB字段表以及参数化SQL插入数据的完整流程并附带Dump20170419.sql建表脚本供参考。已有2013人学习下载说明该资源对初学数据库二进制存储或计划开发图片上传功能的开发者具有实际借鉴价值。读者可据此快速搭建原型掌握防止SQL注入的参数化写法并将其迁移到社交网络、个人博客等需要存储用户照片的应用场景。1. 照片存MySQL先想清楚是存路径还是存二进制我在做过几个上位机项目和会员系统的照片存档功能后对“C# 将照片存储到MySQL数据库”这个需求的第一反应是你先把“照片”这两个字想清楚。照片在MySQL里只有两种存法——一种是把文件路径存进表里照片实体放硬盘另一种是把照片二进制数据直接塞进BLOB字段。前一种查询快、备份快但文件会散后一种迁移方便、事务一致但数据库体积会迅速膨胀。下面从最小环境搭建写到BLOB读写完整链路再给出生产环境的坑和混合方案判断适合正在做上位机拍照存档、会员头像上传、工单证据图片入库这类功能的 .NET 开发者。全文代码基于 .NET 6 / 8 与 MySQL 8.0驱动用 NuGet 安装。2. 从零搭好C#与MySQL的环境驱动选择与最小连接2.1 驱动选择MySql.Data 与 MySqlConnector 的取舍在 NuGet 里搜 MySQL 驱动排序靠前的是 MySql.Data 和 MySqlConnector 两个包。MySql.Data 是 Oracle 官方维护历史最久遇到问题能搜到的答案最多MySqlConnector 是社区异步实现API 几乎兼容但在异步 I/O、性能、连接池回收上比官方包更稳。我的选型习惯是老 WinForm/WPF 上位机.NET Framework 4.x用 MySql.Data.NET 6/8 新项目用 MySqlConnector。安装命令.NET 6/8 项目# 官方驱动 dotnet add package MySql.Data --version 8.0.33 # 或社区驱动 dotnet add package MySqlConnector --version 2.3.5逻辑说明dotnet add package会把包引用写进 .csproj同时在 NuGet 缓存里下载对应程序集。不指定--version时默认装最新版但对照片入库这类长期运行的项目我建议锁一个明确的版本避免团队里有人顺手升级驱动导致连接行为变化。版本不是越高越好MySql.Data 8.0.33 之后部分小版本对 TLS 默认行为有变化内网照片系统不想折腾证书的话锁定 8.0.x 的具体版本更稳。这里有一个很常见的翻车点在 .NET Framework 4.8 项目里装 MySql.Data 8.0 后会提示 “Could not load file or assembly” 或System.IO.FileNotFoundException。原因是 8.0 的官方驱动部分组件需要的运行时版本比 net452 高老项目应该用带net452目标的 8.0.xx 分发包。反过来MySQL 用 8.0 时老驱动 6.9 系列连接上去会报认证方式不支持因为 MySQL 8.0 默认用 caching_sha2_password老驱动只认 mysql_native_password。我实际处理过好几个这种案子都是版本新旧错配不是代码问题。维度MySql.DataMySqlConnector维护方Oracle 官方社区开源异步支持有老版本有死锁案例原生异步推荐.NET Framework 4.x8.0.x 有 net452 版本需用 1.x 旧版MySQL 8.0 默认认证支持支持连接池默认开启默认开启回收更激进典型坑SSL 报错、异步死锁版本更新快接口偶有变化选型上没有绝对答案如果团队里已经全是 MySql.Data 的代码继续用官方包没问题新项目从零起步我倾向 MySqlConnector。两个驱动在下面所有示例代码里的用法几乎一样替换时只需要改using语句和包引用。2.2 最小连接代码连接字符串参数逐个说明先写一个最小可跑通的连接代码控制台项目或 WinForm 里都能用using System; using MySql.Data.MySqlClient; // 或 using MySqlConnector; class Program { static void Main() { // 连接字符串服务器、端口、账号、密码、数据库、连接池、SSL、字符集 string connStr Server127.0.0.1;Port3306;Databasephoto_db; Userroot;Passwordyour_password; Poolingtrue;Maximum Pool Size50; SslModeNone;CharSetutf8mb4;; using (var conn new MySqlConnection(connStr)) { try { conn.Open(); Console.WriteLine(连接成功状态 conn.State); } catch (Exception ex) { Console.WriteLine(连接失败 ex.Message); } } } }逻辑说明这段代码只做一件事——打开一条到 MySQL 的物理连接并立刻释放。using块结束时连接会归还到连接池不是真正断开下一次Open直接复用池里的连接省掉 TCP 握手和 MySQL 认证的开销。照片批量写入时这个池的复用是性能的关键。参数说明Server和PortMySQL 地址和端口。内网临时验证可以用 root生产环境务必建专用账号并限制主机。Database目标库需要先执行建库语句否则 Open 都过不去。Poolingtrue和Maximum Pool Size50开启连接池并限制最大 50 个并发连接。如果不写 Pooling驱动默认也是 true但显式写出上限能防止并发入库时连接数把 MySQL 的 max_connections 打满。SslModeNone内网连接且数据不涉密时关闭 SSL 检查能省一堆 TLS 报错这也是 “mysql ssl连接错误” 最常见的内网成因。外网或生产环境应改为SslModePreferred并配证书。CharSetutf8mb4建议直接加上后面存中文文件名、中文备注时能少一大半乱码问题。跑通这段之后还需要确认 MySQL 本身监听的是远程地址还是只监听本机。Linux 上用 rpm 方式安装的 MySQL默认bind-address127.0.0.1C# 程序在另一台机器上连接时会报 “Host ... is not allowed to connect” 或 “Cant connect to MySQL server on x.x.x.x”。处理是在/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf的[mysqld]段改bind-address0.0.0.0然后systemctl restart mysqld并给应用账号授权username%。这一步和网上很多 mysql安装教程里说的 “skip-networking” 是两回事先确认netstat -an | grep 3306看到0.0.0.0:3306再连。连接验证通过后环境部分就齐了可以进入照片表设计和写入。3. 把照片写进MySQLBLOB字段设计与参数化写入3.1 表结构为什么用 MEDIUMBLOB 而不是 BLOB / TINYBLOBMySQL 提供四种二进制大对象类型TINYBLOB 最大 255 字节适合小图标BLOB 最大 64KB适合微型缩略图MEDIUMBLOB 最大 16MB适合单张照片LONGBLOB 最大 4GB适合超大文件。常见做法是照片用 MEDIUMBLOB——绝大多数手机照片压缩后是 2~8MB16MB 上限够用备份和索引压力又比 LONGBLOB 小很多。如果只存上位机截图BLOB 的 64KB 其实够但为了后续不返工直接上 MEDIUMBLOB 最省心。建表语句CREATE DATABASE IF NOT EXISTS photo_db DEFAULT CHARACTER SET utf8mb4; USE photo_db; CREATE TABLE IF NOT EXISTS photo_archive ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 自增主键, photo_title VARCHAR(200) NOT NULL COMMENT 照片标题/文件名, photo_data MEDIUMBLOB NOT NULL COMMENT 照片二进制数据, photo_type VARCHAR(20) DEFAULT image/jpeg COMMENT MIME类型, photo_size INT NOT NULL DEFAULT 0 COMMENT 字节数, remark VARCHAR(500) DEFAULT NULL COMMENT 备注可能含中文, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT照片归档表;逻辑说明photo_data 用 MEDIUMBLOB 容纳原始二进制photo_type 记录 MIME读取端根据它判断用 JPG 还是 PNG 解码photo_size 冗余存一个字节数让列表页不读 BLOB 也能显示文件大小created_at 用 DEFAULT CURRENT_TIMESTAMP入库时不用手动赋值。有人会问为什么不用 TEXT 存 Base64Base64 让体积膨胀约 33%而且 MySQL 处理 TEXT 时会走字符集转换BLOB 是纯二进制不走字符集性能更好也更不容易触发 max_allowed_packet 问题。二进制照片用 BLOB 系列是标准答案TEXT 只适合真的需要人类可读的场景照片不属于这种场景。3.2 参数化写入读取文件→参数绑定→ExecuteNonQuery假设已经从摄像头或相册拿到了本地文件比如 D:/captures/20250101_153000.jpg把它写进 photo_archiveusing System; using System.IO; using MySql.Data.MySqlClient; class PhotoWriter { static void Main() { string filePath D:/captures/20250101_153000.jpg; byte[] photoBytes File.ReadAllBytes(filePath); // 读成二进制 string connStr Server127.0.0.1;Port3306;Databasephoto_db; Userroot;Passwordyour_password;CharSetutf8mb4;; string sql INSERT INTO photo_archive (photo_title, photo_data, photo_type, photo_size, remark) VALUES (title, data, type, size, remark); using (var conn new MySqlConnection(connStr)) using (var cmd new MySqlCommand(sql, conn)) { // 参数化写入二进制绝不进 SQL 文本 cmd.Parameters.Add(new MySqlParameter(title, Path.GetFileName(filePath))); cmd.Parameters.Add(new MySqlParameter(data, MySqlDbType.MediumBlob) { Value photoBytes }); cmd.Parameters.Add(new MySqlParameter(type, image/jpeg)); cmd.Parameters.Add(new MySqlParameter(size, photoBytes.Length)); cmd.Parameters.Add(new MySqlParameter(remark, 上位机自动存档)); conn.Open(); cmd.CommandTimeout 60; // 大图或慢网络时给足时间 int affected cmd.ExecuteNonQuery(); Console.WriteLine($入库成功影响行数{affected}); } } }逻辑说明核心是把 byte[] 绑定到 data 参数避免把二进制拼接进 SQL 字符串。ExecuteNonQuery对 INSERT 返回影响行数照片写入用它最直接。MySqlDbType.MediumBlob要和表字段的 MEDIUMBLOB 对应如果类型不匹配驱动虽然能发出去但 MySQL 端可能做隐式转换批量场景下拉低性能。参数说明size用photoBytes.Length冗余存储这个数字列表页就不会被迫读 BLOB。cmd.CommandTimeout 60默认 30 秒照片超过 10MB 或者网络带宽紧张时容易超时显式调大是保命操作。Path.GetFileName只取文件名避免把整条路径D:\captures\...存进库跨机器迁移时路径永远对不上。再补一个批量入库场景同一连接、同一事务循环插入多张照片比逐条开连接快很多using (var tx conn.BeginTransaction()) { cmd.Transaction tx; for (int i 0; i photoFiles.Count; i) { byte[] bytes File.ReadAllBytes(photoFiles[i]); cmd.Parameters[title].Value Path.GetFileName(photoFiles[i]); cmd.Parameters[data].Value bytes; cmd.Parameters[size].Value bytes.Length; cmd.ExecuteNonQuery(); // 同一条命令对象循环复用 } tx.Commit(); }逻辑说明连接池解决的是“连接”复用事务解决的是“提交”合并。每张照片单独事务意味着每次都要 fsync 刷盘批量场景下慢得肉眼可见改成一个大事务后磁盘只有一次提交100 张照片的耗时能降一个数量级。注意事务期间不要在里面写File.ReadAllBytes之外的重逻辑长时间占着连接会让连接池紧张。照片入库前还有一个值得做的优化压缩。System.Drawing 里把原图缩到 1920 宽、JPEG 质量 80一张 5MB 照片能压到 300KB 左右不变的需求下 MEDIUMBLOB 几乎没有压力。压缩代码在后面缩略图部分会给出入库时顺手调同一套方法即可。压缩的代价是 CPU 和几毫秒延迟对绝大多数业务都划算。4. 把照片读出来DataSet 到图片控件的完整链路4.1 读取BLOBDataReader 单张读取点开某条记录时要显示大图用 DataReader 按 id 精确捞 BLOBusing (var conn new MySqlConnection(connStr)) using (var cmd new MySqlCommand( SELECT photo_title, photo_type, photo_data FROM photo_archive WHERE id id, conn)) { cmd.Parameters.Add(new MySqlParameter(id, photoId)); conn.Open(); using (var reader cmd.ExecuteReader()) { if (reader.Read()) { string title reader.GetString(photo_title); string type reader.GetString(photo_type); byte[] data (byte[])reader[photo_data]; // byte[] - MemoryStream - Image using (var ms new MemoryStream(data)) { Image img Image.FromStream(ms); pictureBox1.Image img; // WinForm 赋值 } } } }逻辑说明DataReader 是只进只读的流式访问reader[photo_data]返回 object强转成 byte[] 再用 MemoryStream 包一层就能交给 System.Drawing。这里有一个隐蔽的坑Image.FromStream不是把字节完整拷贝进 Image 对象而是保留了对流的引用。上面代码里 MemoryStream 在 using 块结束后被释放如果 pictureBox 之后还要绘制这张图就可能看到 “参数无效” 或 “内存流无效”。所以务实的做法是把 Image 复制一份再释放流Image img; using (var ms new MemoryStream(data)) { img new Bitmap(Image.FromStream(ms)); // 拷贝新实例 } pictureBox1.Image img;参数说明Image.FromStream要求流支持 SeekMemoryStream 天然支持所以不能用 NetworkStream 直接传。如果照片是 PNG 透明图new Bitmap(Image.FromStream(ms))会保留 Alpha 通道放到 PictureBox 里没问题。读取端同样要注意CommandTimeout大图在网络不好的时候可能读一半断掉。4.2 列表页与缩略图别在列表查询里 SELECT photo_data照片系统的第一条性能规则是列表页绝不允许SELECT photo_data。几十条记录的 BLOB 全捞出来内存和带宽立刻失控。正确做法是先查元数据点某一行时再按 id 取二进制string listSql SELECT id, photo_title, photo_type, photo_size FROM photo_archive ORDER BY id DESC LIMIT 50; DataTable dt new DataTable(); using (var da new MySqlDataAdapter(listSql, connStr)) { da.Fill(dt); // 只拿元数据不碰 BLOB } dataGridView1.DataSource dt;逻辑说明MySqlDataAdapter.Fill 在内部会开连接、执行查询、填充 DataTable、归还连接。列表页只有几列元数据量小用 DataTable 直接绑定 DataGridView 很方便。但注意Fill返回后 DataGridView 里点选事件会带着 DataRow 走你拿dt.Rows[i][id]去查详情不要把 photo_data 塞进 DataTable。如果列表要直接显示缩略图比如会员头像网格两条路一是入库时生成 200x200 缩略图作为独立列或独立表存储二是读取原图后现场缩放。第二种在列表 100 张图时会卡成幻灯片我的做法是入库时顺手生成缩略图using (var src Image.FromFile(filePath)) { int thumbW 200; int thumbH (int)(src.Height * ((double)thumbW / src.Width)); using (var bmp new Bitmap(thumbW, thumbH)) using (var g Graphics.FromImage(bmp)) { g.InterpolationMode System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(src, 0, 0, thumbW, thumbH); using (var ms new MemoryStream()) { bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); byte[] thumbBytes ms.ToArray(); // 随原图一起 INSERT或单独 thumbnails 表 } } }参数说明InterpolationMode.HighQualityBicubic是缩放质量和速度的平衡点200 宽的缩略图肉眼几乎看不出区别。bmp.Save到 MemoryStream 时用 JPEG 而非 PNG因为照片类缩略图 JPEG 体积至少小一个数量级。如果图库很大缩略图生成要放到后台队列或入库定时任务里别卡在 UI 线程上。这是我在列表页踩过的血泪经验。如果项目已经用 MySqlConnector 上了 .NET 6/8详情页也可以走异步读取避免线程阻塞byte[] data await reader.GetFieldValueAsyncbyte[](2);注意MySql.Data 老版本的异步方法在 UI 线程里如果用.Result或.Wait()会死锁这是 C# Task 用法里最常见的坑。一律用await不要在同步线程里同步等待异步方法。5. 照片存储避坑从连接超时到数据库膨胀的5个高频问题5.1 Error 2002连接字符串写成 localhostLinux 上走 socket 失败现象C# 程序连着连着报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。原因连接字符串里Serverlocalhost时MySQL 客户端在 Linux 上默认优先走 Unix socket而不是 TCP。C# 端口里写的 3306 根本没被用到socket 又不存在或权限不够就报这个错。解决把Server显式写成127.0.0.1或内网 IP强行走 TCP。如果 MySQL 装在另一台机器还要确认bind-address监听的是0.0.0.0还是单 IPsudo netstat -an | grep 3306 # 看到 0.0.0.0:3306 或 192.168.x.x:3306 即为 TCP 监听正常这个坑在上位机场景里尤其多因为开发机和部署机环境往往不一样本地测试用 localhost 好好的一部署到工控机就报 socket 错误。注意 C# 程序是 32 位还是 64 位与这个报错无关别往这个方向查。5.2 max_allowed_packet 默认太小大图写入直接报 Packet too large现象照片 8MB 左右INSERT 执行到一半抛Error Code: 1153 Got a packet bigger than max_allowed_packet bytes。原因MySQL 的max_allowed_packet控制单次传输报文上限rpm 安装的 MySQL 8.0 默认经常只有 4MB。照片二进制的参数化写入最终是一个大报文超了就拒绝。解决先查当前值再临时调大SHOW VARIABLES LIKE max_allowed_packet; -- 看到 4M 时基本确认问题 SET GLOBAL max_allowed_packet 64 * 1024 * 1024;要持久化就改 my.cnf 的[mysqld]段max_allowed_packet64M net_buffer_length16M然后重启 MySQL。64MB 对 MEDIUMBLOB 的 16MB 上限够用如果你决定用 LONGBLOB 存几百 MB 的原始视频或批量文件这个值还要更大。注意net_buffer_length是会话初始缓冲不是硬上限别把它当 max_allowed_packet 用。5.3 Open DataReader 没关闭连接池被拖死现象列表页连续操作几十次后程序报There is already an open DataReader associated with this Connection which must be closed first或者连接池超时Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool。原因代码里var reader cmd.ExecuteReader()之后没有及时 Dispose连接一直不归还或者在一个连接上开了第二个 DataReader 没有关闭第一个。图片读取这种高频操作一点泄漏就放大。解决所有 DataReader 用using包起来并且一个连接同一时刻只允许一个 DataReader。错误示范长这样var reader cmd.ExecuteReader(); // 忘了 using while (reader.Read()) { /* 处理 */ } // 后续再开新连接/新命令时旧的 reader 还挂着改正后是using (var reader cmd.ExecuteReader()) { ... }离开作用域连接立刻归还。还有一类伴随问题图片对象释放不及时导致 GDI 句柄耗尽表现是OutOfMemoryException——名字听着像玄学实际是 GDI 图形句柄用光了。确保每个Bitmap/Image都走using或Dispose特别是缩略图循环里。5.4 中文乱码连接字符串没加 CharSet文件名全是问号现象入库后 photo_title 和 remark 在 Navicat 里看到?????或者读回 WinForm 显示成鍚嶇О这种怪字符。原因连接字符串没设CharSet表字符集不是 utf8mb4或者会话字符集和表不一致。MySQL 8.0 里utf8实际是 utf8mb3存 emoji 或部分生僻字会失败。解决连接字符串加CharSetutf8mb4建表用DEFAULT CHARSETutf8mb4。老表如果是 latin1先转换ALTER TABLE photo_archive CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;转换后确认SHOW CREATE TABLE里字段已经是 utf8mb4再重新写入中文测试。如果 Navicat 正常但程序读出来乱码多半是连接层字符集问题优先检查连接字符串。5.5 数据库膨胀BLOB 方案一个月后 30GB现象photo_db 一个月后占用 30GB备份慢、查询慢、binlog 打爆磁盘。原因把原图全量塞进 BLOB忽略了业务生命周期。大量照片其实只在头几天被查看之后一直躺着占空间。MySQL 8.0 默认 ROW 模式 binlogBLOB 字段的变更会完整写进 binlog一个月几十万张照片binlog 能占几倍磁盘。解决先做数据分层。需要永久保存、必须和业务记录一起原子提交的证据照片放 MySQL BLOB 是合理的临时展示的头像、预览图、告警截图放文件系统或对象存储MySQL 只存路径。判断标准很简单照片和业务记录是否需要“要么一起成功、要么一起失败”的事务一致性。需要就 BLOB不需要就路径。不需要做时间点恢复的归档库可以考虑log_binOFF需要保留 binlog 的话定期PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY。混合方案是生产上最常见的落地形态具体表结构在下一章给。6. 生产环境进阶混合存储与验证方法6.1 混合存储文件系统存照片实体、MySQL 存元数据路径真实项目里我最终常用的方案是照片文件写到本地或网络盘的按日目录MySQL 只存相对路径、尺寸和校验值。表结构CREATE TABLE photo_archive_path ( id INT PRIMARY KEY AUTO_INCREMENT, relative_path VARCHAR(255) NOT NULL COMMENT 相对路径如 2025/01/153000.jpg, photo_type VARCHAR(20), photo_size INT, md5 CHAR(32) COMMENT 文件校验用于对账, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;写入路径时注意 Windows 和 Linux 分隔符差异统一存/的正斜杠拼路径时再转string baseDir D:/photo_storage/; string relativePath ${DateTime.Now:yyyy}/{DateTime.Now:MM}/{fileName}; string fullPath Path.Combine(baseDir, relativePath);写入顺序上我一般是先把照片文件写到磁盘并确认写入完成再插入 MySQL 元数据记录。如果 MySQL 插入失败就回删文件反过来如果先写库再写文件文件写一半系统崩溃库里会有指向不存在文件的脏路径。两个操作没法真正原子只能容忍一个方向的脏数据并定期跑 md5 对账任务清理。这样设计的收益是 MySQL 小、备份快、照片能用系统看图器直接打开代价是文件系统和数据库事务不一致对大多数非金融场景这个成本可以接受。6.2 验证方法与收尾习惯跑通后做两个验证。第一拿 100 张 5MB 照片做批量写入压测记录总耗时和连接池峰值var sw System.Diagnostics.Stopwatch.StartNew(); for (int i 0; i 100; i) { cmd.Parameters[data].Value File.ReadAllBytes(files[i]); cmd.ExecuteNonQuery(); } sw.Stop(); Console.WriteLine($100张5MB照片入库耗时{sw.ElapsedMilliseconds} ms);重点看耗时是否随张数线性增长如果 50 张和 100 张耗时几乎一样说明卡在单条提交的固定开销上改事务批量提交如果线性增长明显瓶颈在 File.ReadAllBytes 或网络带宽。第二对 photo_title 加索引后用EXPLAIN SELECT ... WHERE photo_title xx确认没走全表扫描。我保留的一个习惯是所有涉及 BLOB 的变更先在测试库备份一次再动表结构。照片这条链路真正耗时间的从来不是写代码而是驱动、字符集、连接池这些边缘问题。希望这篇能帮你一次走通少花几天在报错上。本文还有配套的精品资源点击获取