1. 方案选型与技术背景:为什么你的图片“存”进MySQL那么难
做后端开发的兄弟,十有八九都遇到过图片存储的需求,用户头像、商品图、文章封面、身份证件扫描件,一堆文件要落到库里。最常见的错误思路就是把图片路径当作字符串存进MySQL的varchar字段里,然后文件本体丢到服务器某个目录下,图省事。这种方案短期内看着没问题,但后面一旦涉及数据迁移、多节点部署、备份恢复,路径一断,图片全挂,哭都来不及。
另一个思路就是今天要聊的正题——把图片的二进制数据直接存进MySQL的BLOB类型字段里。标题里说的“保存和取出”,本质就是JDBC(Java这边的标准数据库连接方式)对BLOB字段的写入与读取。这个方案最大的好处是数据和业务记录永远在一起,备份一份SQL文件就等于把所有的图片也备份了,不会出现库里引用了文件、文件却被误删的尴尬局面。
适合什么场景呢?我个人认为,小规模应用、内网管理系统、个人项目、快速原型,BLOB方案完全够用。单张图片在几百KB到几MB之间,一天也就几千张的写入量,MySQL扛得住。但如果你做的是短视频平台那种每天千万级图片请求的架构,这边建议直接上对象存储或分布式文件系统,MySQL存路径和元数据就好。这是后话,本文不展开。
在动手之前,先统一一下技术栈,我用的是MySQL 8.0 + JDBC(mysql-connector-java 8.x)+ Java 11这套组合,IDE用的IDEA,数据库管理工具Navicat或MySQL Workbench都行。你在实操时版本不要差太多,尤其是驱动包和MySQL版本要对应,不然连接都建立不起来。
这里先给一个总览,后面每一行代码都会有注释,保证你能看懂每一步到底在干什么。
2. 环境准备与数据库设计:建表这一步就决定了后面顺不顺
2.1 建库建表:BLOB类型到底选哪个
MySQL里的二进制大对象类型一共有四个:TINYBLOB(最大255字节)、BLOB(最大64KB)、MEDIUMBLOB(最大16MB)、LONGBLOB(最大4GB)。选哪个完全看你的图片大小。一般来说,手机拍的照片动辄3~8MB,用BLOB肯定放不下,最少得MEDIUMBLOB起步。但话说回来,真正生产环境中存原始照片的场景极少,大部分都是压缩后的缩略图或者WebP、JPEG优化过的图,几百KB到1MB左右,BLOB也凑合,不过我还是建议直接用MEDIUMBLOB,省得以后图片尺寸变大再来一次ALTER TABLE迁移,那种操作在数据量大时非常痛苦。
建表语句我直接放出来,注释里说清楚了每个字段的用意:
-- 创建图片存储表 -- 业务上每一条记录对应一张图片 CREATE TABLE `image_store` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键', `biz_type` VARCHAR(50) NOT NULL COMMENT '业务类型,比如avatar、product、article,方便按业务隔离', `file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名,取出时用来还原文件名,做下载用', `content_type` VARCHAR(100) NOT NULL COMMENT 'MIME类型,比如image/jpeg、image/png,浏览器靠这个识别图片格式', `img_data` MEDIUMBLOB NOT NULL COMMENT '图片二进制数据', `file_size` INT UNSIGNED NOT NULL COMMENT '文件大小,单位字节,可以用来校验数据完整性', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', PRIMARY KEY (`id`), KEY `idx_biz_type` (`biz_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图片二进制存储表';几个细节说一下。
第一,content_type这个字段很多人忽略,但它在取出展示的时候至关重要。你从数据库拿到的是没有后缀的字节流,怎么知道它是JPEG还是PNG?就得靠这个MIME类型。如果漏了,前端或者浏览器就没法准确渲染。
第二,file_name不能省。虽然数据库不依赖文件名来识别图片,但你想做“点击下载原图”这种功能时,HTTP响应头里的Content-Disposition字段需要文件名,从库里取原始文件名最可靠。
第三,字符集为什么用utf8mb4?因为file_name里可能存中文文件名,比如“头像.png”,utf8mb4兼容性最好,不会出乱码。
2.2 调整MySQL最大允许包大小参数
BLOB字段写入一个2MB的图片,本质上就是往MySQL发送一个2MB的SQL语句参数包。MySQL默认的max_allowed_packet是64MB(8.0版本默认值),正常情况下够用。但如果你用的是老版本,或者某些云厂商默认配置很低(4MB、1MB都有见过),插入稍大一点的图就会报Packet for query is too large错误。这个坑很隐蔽,我第一次踩到的时候还以为是驱动配置出了问题,查了半天。
提前检查一下你的MySQL配置:
-- 查看当前的参数值 SHOW VARIABLES LIKE 'max_allowed_packet';如果小于16MB,建议改一下。在my.cnf或my.ini的[mysqld]段下加一行:
[mysqld] max_allowed_packet=64M改完重启MySQL服务生效。注意,这个参数是数据库服务端的限制,客户端也要对应设置,不过JDBC驱动默认会继承服务端参数,一般不用额外处理。
2.3 引入JDBC驱动依赖
Maven项目直接在pom.xml里加这一段,版本号建议和你本地装的MySQL版本匹配上,8.0.x的MySQL用8.0.33这个驱动版本,稳得很:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>如果你是非Maven项目,手动把驱动jar包扔到项目classpath里也行,思路一样,不用纠结。
3. MySQL保存图片的实现:从流到BLOB字段,一行一行拆解
环境准备好了,表也建好了,直接上核心代码。我习惯把数据库操作封装成一个工具类,避免每次写重复的Connection获取逻辑。这里先给出最简版,保证你能跑通,之后再按工程化标准优化。
3.1 数据库连接工具类
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * 数据库连接工具类 * 纯手写JDBC连接,方便理解每一步 */ public class DBUtil { // 驱动类全限定名,8.0+版本的驱动类名是com.mysql.cj.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // 连接地址:jdbc:mysql://主机:端口/数据库名 // useSSL=false 本地开发不需要SSL加密 // characterEncoding=utf8 保证中文不乱码 // serverTimezone=Asia/Shanghai 指定时区,8.0版本必须配置,否则报错 // allowPublicKeyRetrieval=true 允许客户端向服务器请求公钥,8.0驱动在非SSL连接下必须设这个,否则报Public Key Retrieval错误 private static final String URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这个工具类看着简单,但URL里的几个参数是从坑里爬出来的经验。serverTimezone不加,驱动会报The server time zone value '�й���ʱ��' is unrecognized这个错,乱码一看就是时区识别不了;allowPublicKeyRetrieval=true不加,8.0版本在非SSL连接下会直接报错,连不上。这些参数一次配好,能省掉后面一大堆排查时间。
3.2 图片保存核心逻辑:PreparedStatement + setBinaryStream
保存图片的核心思路就一句话:把图片文件转成输入流,再通过PreparedStatement.setBinaryStream()方法绑定到这个输入流,最后执行executeUpdate()。但是千万不能用Statement拼接SQL——SQL注入风险是一方面,另一方面二进制数据里可能包含特殊字符,拼SQL大概率直接报语法错误。
请看完整代码:
import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; /** * 图片保存实现类 */ public class ImageSaver { /** * 保存图片的入口方法 * * @param bizType 业务类型,比如 avatar 或 product * @param imageFile 图片文件 */ public void saveImage(String bizType, File imageFile) { // try-with-resources自动关闭连接、声明,防止连接泄漏 // 注意:InputStream不能放在这个try里提前关,因为PreparedStatement执行时可能还在读取流 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO image_store (biz_type, file_name, content_type, img_data, file_size) VALUES (?, ?, ?, ?, ?)" )) { // 1. 设置业务类型 ps.setString(1, bizType); // 2. 设置文件名,getName()只取文件名,不包含路径 ps.setString(2, imageFile.getName()); // 3. 根据文件后缀推断MIME类型,这里写一个简单的映射,实际项目中可以抽成工具方法 String contentType = guessContentType(imageFile.getName()); ps.setString(3, contentType); // 4. 这一步是核心中的核心: // 通过FileInputStream打开文件,然后把输入流传给setBinaryStream() // 第二个参数传InputStream,第三个参数传文件大小 // MySQL驱动内部会调用InputStream.read()方法把数据写入BLOB字段 // 这里不要用ps.setBlob(),setBlob的性能和兼容性不如setBinaryStream try (FileInputStream fis = new FileInputStream(imageFile)) { ps.setBinaryStream(4, fis, imageFile.length()); } // 5. 设置文件大小,单位字节,方便以后做数据校验 ps.setLong(5, imageFile.length()); // 6. 执行插入,返回受影响的行数 int rows = ps.executeUpdate(); System.out.println("成功插入 " + rows + " 行,图片大小:" + imageFile.length() + " 字节"); } catch (Exception e) { e.printStackTrace(); } } /** * 根据文件名后缀推断MIME类型 * 实际项目中建议用Apache Tika这样的组件,这里演示用最简单的方式 */ private String guessContentType(String fileName) { if (fileName.endsWith(".png")) { return "image/png"; } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { return "image/jpeg"; } else if (fileName.endsWith(".gif")) { return "image/gif"; } else if (fileName.endsWith(".webp")) { return "image/webp"; } else { return "application/octet-stream"; } } // 测试入口 public static void main(String[] args) { ImageSaver saver = new ImageSaver(); saver.saveImage("avatar", new File("C:/Users/Admin/Pictures/head.png")); } }这里解释一下为什么用setBinaryStream而不是setBlob。setBlob需要先把整个文件读进内存构建一个Blob对象,如果图片是几百MB的大文件,一次就内存溢出了。setBinaryStream是流式写入,MySQL驱动边读边传,内存占用稳定,性能反而更好。这也是JDBC规范里推荐的BLOB写入方式。
上面代码还有一个细节:setBinaryStream(4, fis, imageFile.length())的第三个参数,一定得传文件的总字节数。这个值可以在File对象上通过length()获得,但要注意,如果文件在读取过程中被其他程序修改,这个长度和实际长度不一致就会报错。生产环境里要提前做好文件锁定或者先复制到临时目录再上传入库。
3.3 保存后如何校验:从库里把数据读回来对比
保存成功不等于万事大吉,建议在插入完成后立刻做一次反向校验。最简单的做法是查询刚插入的记录的file_size,和原始文件的字节数比对:
SELECT id, biz_type, file_name, file_size FROM image_store ORDER BY id DESC LIMIT 1;如果库里存的file_size和本地文件大小一致,说明二进制数据完整写入。要做到更严谨的验证,可以把两个文件都转成MD5进行比较,这里不展开,等有时间单独写一篇。
4. MySQL取出图片的实现:从BLOB字段到浏览器可预览的文件
存进去不是目的,能完整取出来才是本事。取出图片的核心逻辑跟保存刚好相反:把BLOB字段读成InputStream,然后写到目标文件或输出到HTTP响应里。下面把两种常见的取出场景——保存成文件和输出到浏览器——都讲一遍。
4.1 取出并保存为本地文件:getBlob + getBinaryStream
import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; /** * 图片读取实现类 */ public class ImageReader { /** * 根据主键ID查询图片,并保存到指定路径 * * @param id 图片记录的ID * @param savePath 保存文件的完整路径,例如 "C:/download/head.png" * @return 是否成功 */ public boolean readImageToFile(int id, String savePath) { // SQL只查询需要的字段,不要查file_size之外无用的列,减少网络传输 String sql = "SELECT file_name, content_type, img_data FROM image_store WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 绑定查询参数 ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { System.out.println("未找到ID=" + id + "的图片记录"); return false; } // 从结果集里取出原始文件名和MIME类型,打印出来方便核对 String fileName = rs.getString("file_name"); String contentType = rs.getString("content_type"); System.out.println("文件名:" + fileName + ",类型:" + contentType); // 核心:获取Blob对象,然后调用getBinaryStream()拿到输入流 // Blob.getBinaryStream()返回的是InputStream,可以边读边写,不会一次性加载到内存 try (InputStream is = rs.getBlob("img_data").getBinaryStream(); OutputStream os = new FileOutputStream(savePath)) { // 缓冲区大小设成8192字节(8KB),太小性能差,太大浪费内存 byte[] buffer = new byte[8192]; int length; long totalBytes = 0; // 循环读取输入流,写入输出流 while ((length = is.read(buffer)) != -1) { os.write(buffer, 0, length); totalBytes += length; } os.flush(); System.out.println("文件保存成功,总字节数:" + totalBytes); } } return true; } catch (Exception e) { e.printStackTrace(); return false; } } // 测试入口 public static void main(String[] args) { ImageReader reader = new ImageReader(); reader.readImageToFile(1, "C:/download/head_copy.png"); } }读出来之后直接拿本地文件管理器打开看看,如果能正常显示且清晰度跟原图一致,说明二进制数据没有丢。这个操作我建议每次“存-取”流程做完都验证一遍,肉眼可见比写一堆单元测试管用。
4.2 取出并直接响应到浏览器:以Spring Boot接口为例
很多时候业务场景不是下载到本地,而是在网页上直接显示用户头像、商品图片。这时不需要把数据落盘,直接往HTTP响应里扔字节流即可。用Spring Boot写一个接口,一行核心代码都不多:
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.OutputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; /** * 图片展示接口 */ @RestController public class ImageController { /** * 根据图片ID输出到浏览器 * * @param id 图片ID * @param response HTTP响应对象 */ @GetMapping("/image/{id}") public void getImage(@PathVariable int id, HttpServletResponse response) { String sql = "SELECT content_type, img_data FROM image_store WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 设置响应的Content-Type,浏览器根据这个值来渲染 String contentType = rs.getString("content_type"); response.setContentType(contentType); // 设置缓存有效期为1小时,减少重复查询数据库的频率 response.setHeader("Cache-Control", "max-age=3600"); // 把BLOB的数据流直接复制到response的输出流 try (InputStream is = rs.getBlob("img_data").getBinaryStream(); OutputStream os = response.getOutputStream()) { byte[] buffer = new byte[8192]; int length; while ((length = is.read(buffer)) != -1) { os.write(buffer, 0, length); } os.flush(); } } } catch (Exception e) { e.printStackTrace(); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } }用浏览器访问http://localhost:8080/image/1,如果一切正常,浏览器直接显示出图片。这里建议你用浏览器的开发者工具看一眼网络请求,Response Headers里的Content-Type是不是你入库时设置的image/png。如果这里设置错了,浏览器会直接下载一个没有扩展名的文件而不是显示图片,这个问题和数据库无关,纯粹是响应头设置的问题,排查路径别跑偏。
4.3 输出图片时给文件名动态加后缀:Content-Disposition用法
如果要做“点击下载原图”的功能,光靠Content-Type还不够,需要额外设置Content-Disposition响应头:
// 注意文件名如果包含中文,需要用URLEncoder转一下,否则浏览器会乱码 String fileName = rs.getString("file_name"); response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");这样浏览器收到响应后,会触发下载行为而不是直接预览。这个需求最常见的场景就是电商后台的“营业资质下载”、管理系统的“导出的报表图片”等等,你需要判断清楚业务到底需要预览还是下载,接口写法差一行Header,用户体验完全不同。
5. 实操过程实录:一次完整的“存入-取出-验证”全流程
理论基础讲完了,下面演示一次完整的实操过程。假设我要把本机一张名为demo_avatar.png的图片(实际大小约1.2MB)存进数据库,再从库里读出来保存到另一个目录做对照。
5.1 第一步:准备测试图片与目标目录
在C:/pic_test/目录下放一张测试图demo_avatar.png,在该目录下再建一个output子目录专门放从库里导出的文件。源文件目录和输出目录分开,避免读出的文件覆盖原文件,造成“看起来一样,实际根本没从库里读出来”的假象。这个细节亏我吃过:一开始把导出的文件直接放在源文件同目录,运行完对比时间戳才发现文件根本没被覆盖。
5.2 第二步:执行保存,观察控制台输出
运行ImageSaver类的main方法,控制台输出如下:
成功插入 1 行,图片大小:1258291 字节注意这个数字,1258291字节约等于1.2MB,和文件属性里显示的大小完全一致,说明FileInputStream正确读到了文件的全部内容。
接着用Navicat查一下这张表,可以看到img_data字段显示的是BLOB类型,点击这个单元格,工具会提示“二进制数据,是否以十六进制形式查看”。不用纠结十六进制内容,只看file_size字段是否等于1258291即可。
5.3 第三步:执行读取,对比文件完整性
运行ImageReader类,把读取结果保存到C:/pic_test/output/demo_avatar_copy.png,控制台输出:
文件名:demo_avatar.png,类型:image/png 文件保存成功,总字节数:1258291看到总字节数和保存时完全一致,基本可以放心。之后打开两张图放在一起肉眼对比,色彩、尺寸、透明度,完全一致。如果图片有损,多半是你在浏览器里手动另存过、或者某一步代码用了字符流转二进制,这种“图片能打开但模糊/花屏”的情况,十有八九是字符集转换导致二进制数据被破坏了。密码学角度画的图都不带这么花的。
5.4 第四步:用SQL层面直接查询验证二进制数据长度
最后一步纯SQL验证,写一个查询语句确认数据确实在库里:
SELECT id, file_name, content_type, file_size, -- LENGTH()函数返回字段存储的字节数,等价于图片的实际大小 LENGTH(img_data) AS blob_len, create_time FROM image_store;如果blob_len等于file_size,说明BLOB字段完整存储了二进制内容,流程闭环。到这里,MySQL图片保存和取出的主流程就已经完全跑通了。
6. 常见问题与排查技巧实录:值得写进你笔记里的那几个坑
6.1 插入时报错Packet for query is too large
前面提过,这是max_allowed_packet参数太小导致的。解决方案就是修改MySQL配置文件后重启服务。如果你用的是云数据库,可能没有改配置的权限,那就只能在应用层面做图片压缩,把图片压到1MB以内再入库,同时把BLOB列改成MEDIUMBLOB之间做好预估。运维层面的路走不通,就得靠开发层面妥协,这是我在线上环境被迫学会的。
6.2 读取时中文文件名变乱码
出现这种问题的原因几乎都是数据库连接URL里没有加characterEncoding=utf8。很多初学者只会在数据库设置字符集,却忽略了驱动连接URL也需要显式指定。解决办法:
jdbc:mysql://localhost:3306/test_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai同时确保建表时用了utf8mb4,双管齐下基本不会乱码。反过来,如果你把数据库字符集设成了latin1,即使URL里写utf8也白搭,因为服务端已经把字符集转掉了。检查顺序:先看表字符集,再看连接URL。
6.3 图片能插入但读取时IO异常或数据截断
这通常是setBinaryStream的第三个参数(长度)传错了。比如一个文件正在被写入,程序打开流的时候还没写完,导致传入的长度比实际长度小,就会出现数据截断。解决思路是写入前先调用File对象的length(),并且做好文件不可变处理,比如微信上传接口先落盘到临时目录,再去读取入库。Java的NIOFiles.copy()也可以把流内容先复制到数据库,但和直接传InputStream相比,语义上不太一样,建议用本文的方案。
6.4 从BLOB取出来生成的图片提示“文件损坏或已损坏”
这个问题最常见的原因是写入和读取的编码方式不对称。一个典型错误:写入时用setString(),把二进制数据当作字符串处理;读取时用getString()。MySQL驱动内部会做字符集转换,二进制被强行按UTF-8解码再编码,图片直接废掉。记住一条铁律:二进制数据只能通过BLOB字段和InputStream读写,永远不要用字符串的方式处理图片。同理,在SQL层面查看时不要用SELECT img_data直接输出到文本终端,那不是给人看的,而是要用LENGTH()等函数查看长度。
6.5 性能提示:大批量图片存取的正确姿势
批量保存图片时避免一条一条插入,用JDBC的addBatch()和executeBatch()批处理功能,性能提升非常明显。我实测过一个内网管理系统,500张图片逐条插入耗时约47秒,改成批量后直接降到9秒。批量插入的代码如下:
// 假设有一批File对象 File[] files = ...; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO image_store (biz_type, file_name, content_type, img_data, file_size) VALUES (?, ?, ?, ?, ?)")) { for (File imageFile : files) { try (FileInputStream fis = new FileInputStream(imageFile)) { ps.setString(1, "batch_test"); ps.setString(2, imageFile.getName()); ps.setString(3, guessContentType(imageFile.getName())); ps.setBinaryStream(4, fis, imageFile.length()); ps.setLong(5, imageFile.length()); ps.addBatch(); } } // 一次性批量执行 int[] results = ps.executeBatch(); System.out.println("批量插入完成,共执行 " + results.length + " 条"); }批量插入的核心是每次循环里通过addBatch()把参数缓存起来,最后统一executeBatch()。但注意一点:如果某些图片特别大(超过10MB),批处理时内存占用会比较高,需要控制每批的数量,比如每500条提交一次,防止内存溢出。测试环境可以先放开跑,生产环境务必做压测。
6.6 补充方案:Base64与文件路径,什么时候该选它们
说完BLOB,补充两个经常在社区讨论里见到的方案。Base64方案是通过把图片二进制编码成Base64文本再存入TEXT字段,这方案的优点是数据可以直接嵌入JSON返回给前端,图片在接口响应里是一长串以data:image/png;base64,开头的字符串,前端直接放到<img>的src属性里就能显示。但缺点也很明显:Base64编码大约让体积膨胀33%,存储成本增加,DB读取性能也下降,不适合大量图片场景。
文件路径方案就是把图片存到磁盘或对象存储,数据库只存路径字符串。这套方案更适合大规模、图片访问压力极高的场景。BLOB方案和文件路径方案的核心取舍,可以用一句话概括:你要的是数据一致性和管理便利,还是要性能和存储成本?在小规模应用里,BLOB的便利性碾压一切。
7. 结尾的一些个人经验
这套BLOB存取方案我前后在不同项目里用过四五次,从最初踩乱码的坑到后来成体系地封装成工具类,感受最深的一点是:图片存储这件事,没有银弹,只有适不适合当前场景。如果项目生命周期短、并发低、跟你合作的运维也没有专门的图床方案,MySQL BLOB就是最短路径。一旦项目有成长为大型系统的可能,建议尽早抽象好存储接口,把“存哪”“怎么存”从业务代码里剥离开,方便以后无缝迁移到对象存储。
再分享一个小技巧:存取图片时,最好都把biz_type带上,同一个表里可以同时存头像、商品图、文章封面,取出时按类型加一层目录隔离,无论是调试还是人工排查数据都很方便。很多初学者喜欢给每种图片建一张表,完全没必要,加个类型字段就搞定了。
最后提醒一句,BLOB字段不要放到频繁更新的表上,InnoDB对大字段的处理是行外存储,频繁Update会把碎片放大,影响查询性能。如果业务上既有图片又有大量变更的文本字段,建议拆成两张表通过外键关联。这一点很少有人提,但生产环境遇到性能瓶颈时你会感谢这个建议。