找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4446

积分

0

好友

581

主题
发表于 3 小时前 | 查看: 5| 回复: 0

每次在团队里做技术方案评审,只要碰上“传文件并保存业务数据”这种涉及跨数据源操作的场景,总会有兄弟跳出来焦虑分布式一致性的问题。

大家脑子里立马蹦出来的全是高大上的词:分布式事务、最终一致性、补偿机制……

说实话,这其实是典型的把简单问题复杂化。

坚决抛弃任何试图在 DB 和云存储之间做强一致性的方案!

别焦虑一致性问题

比如我们云存储会用 MinIO,在生产环境最实用、最稳妥的策略就是:无脑选择先传 MinIO,后写数据库,写失败了产生的废弃文件直接摆烂不管,或者弄个定时任务异步回收。

上传 MinIO 是个走网络的 HTTP 请求,本地数据库写入是本地磁盘 I/O 或者内网极速 I/O,这两个动作压根就不在一个维度上。更何况,MinIO 这种对象存储天生就不支持类似关系型数据库的 rollback 操作。

在业务代码里,最不容易出生产事故的代码流程:

// 核心心法:网络 I/O 绝对不能包裹在数据库事务里!
public String uploadAndSave(MultipartFile file){
    // 第一步:在毫无事务负担的情况下,裸调 MinIO
    // 这一步就算网络抖动卡了 10 秒,也只是阻塞一个 Tomcat 的工作线程,绝不会拖死数据库
    String fileUrl = minioService.upload(file); 

    // 第二步:拿着 URL 去调带事务的数据库写入方法
    try {
        dbService.saveRecord(fileUrl);
    } catch (Exception e) {
        // 第三步:数据库崩了?啥也别干,直接报错。
        // MinIO 里那个孤儿文件随它去吧,别在这儿手欠写什么 try-catch 补偿删除逻辑!
        throw new BusinessException("保存失败");
    }
    return fileUrl;
}

这段代码量少得可怜,完全可控,团队刚毕业的新人过来一看就懂。最关键的是,这套逻辑天然避开了长事务问题,不管 MinIO 那边网络多卡,都不会占用数据库的连接。

为什么说试图做强一致性是过度设计?

我是最怕遇到那种爱拍脑袋搞过度设计的人。有的人觉得既然要保证一致,那就直接上 Seata 之类的分布式事务框架。

这纯纯属于病急乱投医。Seata 的 AT 模式是靠解析 SQL 和记录 undo_log 来实现回滚的,请问 MinIO 提供这玩意了吗?

没有

为了让 MinIO 支持分布式事务,你就得搞 TCC 模式,必须自己硬着头皮手写一套 Try-Confirm-Cancel 的补偿代码。就为了让用户上传一张破头像,你把整个业务的复杂度翻了三倍。将来线上出了 Bug,排查报错的时候能在好几个微服务之间折腾半天,性价比极低。

还有人说,那我退一步,用 本地消息表 + RocketMQ 做最终一致性总行了吧?

思路听着很正,先写本地消息表发 MQ,消费者拿到消息再去异步传 MinIO。但这在对象存储场景下是个大坑。你仔细想想,传文件是极其消耗带宽和内存的操作。

你打算把庞大的二进制文件流直接塞进 MQ 里?还是把文件暂存在本地磁盘,再让 MQ 通知消费者去读盘?前者直接把高并发的 MQ 当成了运送大件垃圾的卡车,后者凭空多引入了一个极易出故障的本地状态流转中间件。

这完全是自嗨式架构!!!

废弃文件撑爆硬盘怎么办?

讲到这里,肯定有人心里犯嘀咕:如果写数据库失败了,MinIO 里不就留下了一个永远没人用的垃圾文件吗?时间长了把硬盘撑爆怎么办?

实话实说,这根本不是当前阶段该你考虑的问题。

在大厂的实际业务中,要明白一个成本铁律:存储空间是所有 IT 资源里最不值钱的。 程序员的开发成本、系统的排障成本,远远高于买几块硬盘的钱。

哪怕你的系统一天产生一万个上传失败的垃圾图片,一个月攒下来也就几十个 G,放到 MinIO 里算个啥?用高昂的代码复杂度和运维成本,去抠这点极其廉价的磁盘空间,这笔账怎么算都亏。

如果实在有代码洁癖,或者你们团队就是想严谨一点,解决办法也很简单:做异步回收。

搞个定时任务,每天半夜两点扫一遍昨天上传到 MinIO 的文件记录,拿着文件路径去数据库里 IN 一把。如果在数据库里找不到这个路径,直接调用 removeObjects 批量删掉。

这样一来,你的业务主链路清清爽爽,没有任何耦合,一致性问题在后台静默就解决了。

极端业务场景怎么破?

当然,肯定有喜欢较真的兄弟会跳出来杠:万一我传的不是几十 KB 的图片,而是几个 G 的绝密监控视频呢?留在 MinIO 里既占地方又不安全,怎么搞?

真碰上这种特殊的业务边界,可以用 临时桶(Temp Bucket)策略

MinIO 里建两个桶,一个叫 temp,配置生命周期规则(Lifecycle Rule),存活超过 24 小时自动物理销毁;另一个叫 formal,作为永久存储。

  1. 业务端上传视频,一律先丢进 temp 桶,马上拿到 URL,然后去写数据库。
  2. 数据库写成功了,发个极轻量的异步事件(MQ 或者普通异步线程),后台偷偷把文件从 temp 桶 copy 到 formal 桶,顺便更新一下数据库里的 URL。
  3. 如果数据库写失败了呢?你什么都不用做。 24小时后,MinIO 的底层机制自己就把那个大文件给删了。

这样连一行定时任务的代码都不用写,MinIO 的原生特性就帮着把一致性兜底了。

说在最后

最后,在这个话题上,我的经验是永远记住一条架构红线:

任何涉及外部网络调用的操作,不管是上传 MinIO、调第三方的 HTTP 接口,还是发验证码邮件,千万、绝对不能包裹在数据库的 @Transactional 事务代码块里!

把网络 I/O 剥离出事务,系统至少能避开 80% 莫名其妙的生产宕机。有类似踩坑经验的兄弟,不妨多去云栈社区交流一下,分享些务实的开发心得。





上一篇:刚入职就被要求请2888海鲜?聊聊职场“新人税”和自除数算法题
下一篇:Java + LangChain4j 三阶语义切片实战:让 RAG 召回率突破瓶颈
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-25 11:22 , Processed in 0.616200 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表