ARTICLE DETAIL

资讯详情

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

go-view大屏图片外移:告别Base64,解决数据库膨胀与性能问题

go-view大屏图片外移:告别Base64,解决数据库膨胀与性能问题 做数据大屏的朋友大概率都遇到过类似的事go-view大屏配好了组件也不复杂无非放了两张背景图、几个图片素材结果往数据库里一存光content字段就飙到好几兆。我最近在ruoyi-vue-pro的二次开发项目里就踩了这么个坑——同一个大屏报表在数据库里存了大量图片列表页查询直接拉回来几MB的JSON页面卡成幻灯片备份脚本也跟着变慢。定位下来问题根源就是go-view保存大屏配置时把图片组件里的base64字符串全量写进了数据库里存的那份JSON。这个问题的典型场景很多人应该也有共鸣开发环境里一切正常一旦上了生产随着大屏数量变多、历史版本积累、复制模板变多数据库体积肉眼可见地膨胀。而且这类问题不是靠“把字段类型改大”就能解决的越往后拖查询越慢存储成本越高甚至还会触发MySQL行大小的限制。这篇文章就围绕“go-view图片外移”这个优化思路把问题成因、方案选型、具体落地步骤和常见坑一次讲清楚。1. 先说清楚问题到底出在哪1.1 go-view大屏配置是怎么存进数据库的go-view本身是一个Vue3 TypeScript开发的拖拽式可视化大屏设计器它的核心概念是“大屏即配置”。你在画布上拖一个图表、放一张背景图、调一下间距最终都会产出一份JSON结构的数据包括大屏的宽高、背景色、主题配置以及components数组。数组里的每个component都记录了组件的类型、坐标、尺寸、样式还有图表的数据源配置。关键就是组件内容里的图片是怎么表示的。go-view的前端在插入图片时如果直接粘贴本地文件或者通过上传控件读入图片默认会转成base64字符串塞到组件的某个属性里常见的就是src或者backgroundImage字段。这个base64字符串看起来是这样的data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKiMIgAAAABJRU5ErkJggg而go-view的“保存大屏”功能会把这整份JSON作为一个整体提交给后端接口。后端在ruoyi-vue-pro这类框架里集成go-view时通常就是设计一张可视化大屏表主键、名称、内容、更新时间之类的字段其中content字段存的就是go-view的完整配置JSON。也就是说一张大屏配置里如果带了3张图片那这3张图片的base64文本就会跟随整份配置一起被塞进content字段落进数据库。这就是问题的雏形一张普通的截图如果转成base64体积会比原图增加约33%。一张PNG原始文件200KB转完就变成270KB左右的纯文本。这些文本全部堆在一条数据库记录里。1.2 “同一个大屏报表”为什么特别会膨胀标题里说的是“同一个大屏报表”这个表述其实很贴切因为在真实业务里我见过的膨胀路径通常有几条第一条反复保存。运营人员在go-view里做微调每隔几分钟点一次“保存”每保存一次前端就会把最新的完整配置包括那几张图片的base64整包提交给后端后端再把content字段整体覆盖。也就是说哪怕只是把标题文字从“一月”改成“二月”3张图片的base64也会原封不动地在数据库里重新存一遍。第二条复制大屏。业务方觉得某个大屏做得不错想拿它当模板就在后台点了“复制大屏”。复制时会把原大屏的content字段复制一份生成新的一行记录。复制几次数据库里就有几份完全相同的base64图片数据。第三条历史版本。如果你们基于go-view做过大屏版本管理每次发布都会把当前大屏配置存到一张历史表里那问题就更明显。一个发布记录存一份带base64图片的完整JSON10个版本就是10份。我拿实际感受来算一组数字。假设一台1920x1080的电脑大屏截图大概200KB-500KB转成base64后按300KB估算大屏里有3张这类图片content字段就能到900KB以上加上图表配置和组件样式妥妥超过1MB。同一个大屏保存10次这一条记录的数据量虽然只会覆盖但历史表里会积累10MB。再配合复制成20个模板数据库里直接多出200MB的图片文本。更麻烦的是MySQL层面的限制。如果你表结构里用的是最常规的TEXT类型最大只能存64KB几张图片进去直接就保存失败了。用MEDIUMTEXT能撑到16MB但每次列表查询如果做了SELECT *那可真是一场灾难。而且InnoDB的行大小、事务日志、binlog都会因为超大字段而受到影响性能问题会一层层传导到数据库备份、主从同步和日常运维。2. 梳理优化方向图片外移去重2.1 方案选型base64变URL引用解决问题的常规思路主要有几种一是压缩图片后再存base64二是把content字段改成LONGTEXT硬扛三是把图片从大屏JSON里“抠出来”数据库只存URL。先说压缩。同样一张图压缩确实能减小一截体积但base64本质上还是文本压完以后仍然存在JSON里后续的列表查询、备份同步问题依然在只是数字变小了治标不治本。再说LONGTEXT硬扛。这属于拖延战术数据库可能会越扛越慢而且一旦数据量上了规模光靠字段扩容解决不了存储成本。最推荐的还是图片外移也就是把content里的base64图片统一上传到文件存储服务本地目录、Nginx静态目录、OSS、MinIO都行然后在JSON里替换成图片URL。这样做的好处是数据库的content字段大幅缩水一张带3张图片的大屏能从1MB降到几十KB。列表页查询只做常规字段的索引和投影不会因为读图片文本拖慢速度。图片本身适合走CDN缓存用户访问大屏时的图片加载体验反而更好。同一个图片在数据库里只存一个URL引用天然支持去重。最关键的是对前端go-view来说URL图片和base64图片渲染方式完全一致用户根本感知不到变化。2.2 复用ruoyi-vue-pro现有的文件上传能力看到“图片外移”很多人的第一反应是要不要单独搭一套上传服务。其实没必要。ruoyi-vue-pro在后台管理模块里有比较完整的文件管理能力一般位于infra模块下提供了文件上传、文件访问、文件分页查询等功能。接口路径常见的是/admin-api/infra/file/upload通过这个接口上传文件后后端会返回文件相关的信息你在业务里只需要把返回的URL或者可访问地址取出来用于替换JSON里的base64即可。需要注意的一点是不同版本的项目对文件存储的抽象不太一样。有的版本默认把文件存到本地磁盘也支持S3协议、云对象存储的对接有的版本在上传接口返回url字段有的则返回文件ID或相对路径。所以你在接的时候建议先看一眼项目的FileController和FileProperties这些类确认清楚返回值里到底哪个字段能直接用来拼URL。如果你所在项目里还没有上传接口那就更简单了新建一个简单的上传接口把MultipartFile写到本地或对象存储返回一个公开访问地址就行。核心的替换逻辑与具体存储方式无关哪家存储都一样。2.3 图片指纹去重避免上传重复文件图片外移之后同一个大屏里的同一张图或者不同大屏里的相同背景图都可能反复上传产生多份重复文件。为了更彻底地解决“大量图片”的问题我在做的时候顺手加了图片指纹去重逻辑。原理并不复杂图片上传给文件服务之前先计算这个文件的MD5或者SHA-256指纹。文件服务在上传接口里先查一下文件表里有没有相同指纹的记录如果已经有就说明这个文件之前传过了直接返回已有文件的URL如果没有才真正把文件写入磁盘或对象存储同时记录指纹。这样实现的效果是同一个大屏复制十几次数据库里存的都是同一个URL文件存储里也只有一份文件。配合上一步的图片外移基本上就能把“同一个大屏报表在数据库中存储大量图片”的问题从根上解决掉。3. 实操从JSON里把图片“抠出来”替换成URL3.1 先做一个“大屏JSON图片外移”工具类这一步是整个优化的核心动作。我的做法是写一个独立的工具类输入是go-view大屏的content字符串输出是替换完成后的content字符串。先上最简单的版本import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * go-view大屏JSON图片外移工具 */ public class GoViewImageShift { private static final Pattern BASE64_IMG_PATTERN Pattern.compile( data:image\\/(png|jpeg|jpg|gif|webp|svg\\xml);base64,[A-Za-z0-9/] ); /** * 将content中的base64图片替换为URL * param content 大屏配置JSON字符串 * param uploader 上传函数接收base64字符串返回可访问的URL * return 替换后的JSON字符串 */ public static String shiftImages(String content, FunctionString, String uploader) { if (content null || !content.contains(data:image)) { return content; } MapString, String cache new HashMap(); Matcher matcher BASE64_IMG_PATTERN.matcher(content); StringBuffer sb new StringBuffer(); while (matcher.find()) { String base64 matcher.group(); // 同一个base64只上传一次避免重复文件 String url cache.computeIfAbsent(base64, uploader); matcher.appendReplacement(sb, Matcher.quoteReplacement(url)); } matcher.appendTail(sb); return sb.toString(); } }看到这里有人会问如果直接用正则替换整段JSON替换出来的URL会不会替换到奇怪的位置其实在大屏JSON里“data:image开头的base64”几乎必然出现在图片类的属性值里直接替换整段文本没有太大风险。但如果你想把事情做得更严谨建议先解析成JSON对象再递归遍历所有字符串字段只要发现字段值以data:image开头就执行上传并替换。public static JSONObject shiftInObject(JSONObject json, FunctionString, String uploader) { for (String key : json.keySet()) { Object value json.get(key); if (value instanceof String) { String text (String) value; if (text.startsWith(data:image)) { json.put(key, uploader.apply(text)); } } else if (value instanceof JSONObject) { shiftInObject((JSONObject) value, uploader); } else if (value instanceof JSONArray) { for (Object item : (JSONArray) value) { if (item instanceof JSONObject) { shiftInObject((JSONObject) item, uploader); } } } } return json; }两种方式各有适用场景。如果只是想快速清理存量数据正则法够用如果是接在保存大屏的正式接口里我更建议用JSON解析遍历的方式毕竟可以对“哪些字段是图片属性”做更精细的控制后续加白名单、黑名单也方便。这里还有一个细节上传函数的实现要考虑重试。我实际测试中发现本地文件服务响应不稳定的时候偶发上传失败会导致整个大屏保存失败所以上传函数里最好加上try-catch失败后继续抛出但要让调用方感知而不是默默地把base64替换成空字符串。3.2 在保存大屏的接口里加上“外移”逻辑工具类写完之后接入点就很清晰了。以ruoyi-vue-pro里的典型代码结构来说大屏保存的逻辑在一段类似VisualInfoService.saveVisualInfo()的方法里。改造前大致是VisualInfoDO info new VisualInfoDO(); // ... 设置基本信息 info.setContent(visualInfoDTO.getContent()); // 直接保存前端传来的大屏JSON visualInfoMapper.insert(info);改造后只需要在保存前加一行转换逻辑String content visualInfoDTO.getContent(); // 将content中的base64图片上传并替换为URL content GoViewImageShift.shiftImages(content, base64 - { // 1. base64解码为二进制 byte[] bytes Base64.getDecoder().decode(base64.split(,, 2)[1]); // 2. 调用文件上传服务这里以ruoyi-vue-pro的FileApi为例 // 具体接口名以你项目版本为准 FileDO file fileApi.uploadBytes(bytes, detectFileSuffix(base64)); // 3. 返回可访问的图片URL return buildFileUrl(file); }); info.setContent(content); visualInfoMapper.insert(info);整体逻辑就是前端正常提交大屏配置后端多做一步“先外移再落库”。前端go-view对这种改动完全无感知因为对前端而言它提交的仍然是它的JSON后端也没要求前端做任何格式调整。这里有一个设计上的取舍要不要把外移后的JSON再回传给前端我建议不要。回传反而会让前端把URL当成新配置再存一次搞不好还会再次把URL当图片源读取后转回base64。后端只需要保证读出来的content是外移以后的版本即可。3.3 存量数据批量迁移线上已经积累了大量带base64图片的大屏记录这种存量数据跑一个定时任务或者一次性脚本就能解决。我的方案是查出所有疑似包含base64图片的记录。因为base64文本里大概率包含data:image字样SQL可以直接用LIKE过滤SELECT id, content FROM visual_info WHERE content LIKE %data:image% LIMIT 100;分批处理防止一次性加载太多大字段把内存打爆。比如每批取100条处理完后更新记录再继续下一批。处理逻辑跟保存接口里一致解析content、上传图片、替换URL、写回content。更新语句要注意只更新必要字段避免把其他字段在脚本里覆盖UPDATE visual_info SET content ?, update_time NOW() WHERE id ?;跑批之前务必备份一次数据表。毕竟这是全量数据操作万一脚本里有正则写错把某些非图片字段也替换掉至少还能回滚。幂等判断。迁移脚本跑完一遍后如果担心有遗漏可以再查一次LIKE %data:image%。但这里有个小坑如果你用的是正则法检测data:image那可能连URL里带data:image参数的某些HTTP链接也会被匹配但正常业务里URL不该长这样所以问题不大。为了更稳妥第二次迁移时我会在代码里加一个判断如果content里已经不存在data:image开头的字段就跳过。迁移脚本跑完后强烈建议对比一下表体积。我实测过的项目里迁移前visual_info表5GB迁移后不到200MB效果立竿见影。4. 常见问题与避坑实录4.1 替换后前端图片裂了先查这五个地方图片外移上线后最容易遇到的问题就是前端大屏图片显示不出来。我踩坑总结下来重点检查以下五个方向第一上传接口返回的URL是不是可以直接访问。如果项目用的本地存储、返回的是相对路径前端拼接域名时可能没有拼对。你可以先手工在浏览器里访问一下返回的URL看是能打开还是直接404。第二上传接口返回的是文件ID还是文件URL。ruoyi-vue-pro老版本和新版本对这个字段的处理不太一样有的返回url有的返回name或者path。替换时一定要从返回值里取出正确的可访问字段。第三跨域问题。如果大屏页面部署在一个域名图片存储在另一个域名但存储服务又没有配置跨域头浏览器里图片可能会被拦截。特别是本地存储用Nginx代理时检查一下Nginx返回的响应头里是否有Access-Control-Allow-Origin。第四HTTP和HTTPS混合。如果大屏页面是HTTPS图片URL写的是HTTP浏览器会默认阻止这种“不安全”的资源加载。所以替换后的URL协议尽量与前端页面保持一致。第五图片URL里有没有携带鉴权参数。如果你们的文件服务做了accessKey或者token鉴权URL里一定要带上有效参数。不过通常来说大屏页面是需要公开访问的我一般会建议把图片存储桶设置成公开读或者走签名字段恒定的CDN地址。4.2 清理历史版本与重复数据做了外移迁移之后历史表里旧的那些带base64图片的版本数据也可以一并清理。先定位SELECT id, LENGTH(content) AS content_len FROM visual_info_history WHERE content LIKE %data:image% ORDER BY content_len DESC LIMIT 20;看哪些历史记录占用了最大的空间评估一下这些历史版本有没有保留价值。如果只是为操作留底可以直接把历史表里超大字段的记录清理掉或者做同表的脱敏替换也就是把历史记录里的content同样跑一遍图片外移脚本。另外如果你在文件存储服务里做了图片指纹去重可以再加一个定时任务定期扫描文件表里那些“没有任何大屏引用的孤儿文件”把已经不再被引用的文件删除避免文件存储里的垃圾文件越来越多。4.3 还有一些必须提前知道的坑第一字段类型的选择。如果你的表目前用的是TEXT外移之后数据量会很小TEXT完全够用。但如果历史数据还没迁移建议先把字段类型改成LONGTEXT避免中间状态写入失败。第二列表页查询别用SELECT *。就算图片外移之后content字段变小了列表页还带着整段的图表配置也无意义。ruoyi-vue-pro的BaseMapper分页查询默认会查全字段如果你发现列表页依然慢建议单独写VO只查询id、name、status、update_time等必要字段。第三上传并发问题。大屏编辑多人同时保存时同一个base64图片可能被并发上传。如果没做指纹去重文件存储里会产生两份相同内容的文件。加了指纹去重也不保险因为并发查询文件表时可能都查不到已有记录然后都去上传。这种情况需要在文件表上传写入路径上加分布式锁或者直接对文件指纹字段建唯一索引写库冲突时捕获异常再查一次。第四go-view版本差异。go-view在迭代过程中组件结构有所变化——有的版本里图片组件属性是src有的版本里背景图属性在backgroundImage里。正则法能匹配一切所以相对稳妥如果用JSON遍历法要确保遍历所有字段而不是只遍历某几个固定属性否则容易漏掉背景图。第五文件存储的容量和备份策略。图片外移之后数据库压力小了但文件服务器的磁盘压力起来了。本地存储一定要有备份方案和容量监控否则磁盘满了大屏图片全部裂开问题从数据库转移到了文件服务。如果公司有条件建议直接上云对象存储或者自建MinIO稳定性会好很多。这个优化做完之后我个人最大的体会是数据大屏这类“配置即页面”的应用只要涉及图片资源就应该把“图片外移”当成默认规范而不是等数据库膨胀了再救火。ruoyi-vue-pro本身提供了很好的文件上传基础能力go-view也只是一个配置生产端把这两者衔接好base64图片就不该出现在数据库里。最后再补充一个小经验你可以把“图片外移”的逻辑再往前推一步做成保存大屏时的一个前置校验。比如go-view提交上来的content里如果还检测到data:image开头的base64直接给前端一个警告信息提醒“图片未外移可能导致保存失败”。这样一来新产生的大屏记录都不会再携带图片base64数据库体积长期控制在稳定水平。这套思路也不局限于go-view富文本编辑器里粘贴图片、Excel导入的小图标图形只要出现“文本里嵌base64图片”的情况都可以直接复用这个方案。
返回列表