恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

易物小店微服务架构复盘:SpringBoot+Vue+SpringCloud分布式交换系统实践

  • 首页
  • 资讯中心
  • /
  • 易物小店微服务架构复盘:SpringBoot+Vue+SpringCloud分布式交换系统实践

相关资讯

真正好用的软件:从不难用到懂你的设计原则 2026/10/10 11:05:39
AppData占用87.81GB?用Codex安全清理C盘缓存与系统垃圾 2026/10/10 11:05:39
企业AI工具被封后:统一网关、账号治理与多模型备份实战 2026/10/10 11:05:39

最新资讯

基于SpringBoot的运动会管理系统:数据建模、并发控制与部署实践
装完 Tinycast 后 10 分钟该做什么:首启引导全流程实录
jacobian-lens实验数据详解(下):ignition点燃阈值、capacity容量与dual-task干扰实验
Flow 内建 Linter:基于类型信息的静态检查框架与 Lint 规则配置实战
微软盖章:60GB 内存台式机就能跑 V4 Flash,部分编程任务赢过 GPT-5?
PHP与ThinkPHP区别详解:语言与框架的定位、选型与实战指南

今日推荐

Codex 总用英文回答?从 AGENTS.md 到 config.toml 的中文输出调优指南
OpenClaw 自定义插件开发完整指南(2026最新版):从 TypeScript 到 npm 发布
基于Spark的电影推荐系统全链路实战:从爬虫到Web展示

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

易物小店微服务架构复盘:SpringBoot+Vue+SpringCloud分布式交换系统实践

发布时间:2026/10/10 11:05:39
易物小店微服务架构复盘:SpringBoot+Vue+SpringCloud分布式交换系统实践 三年前给社区做过一个闲置物品循环利用的小程序用户手里有不用的电饭煲、儿童绘本、九成新的蓝牙音箱想换别人手里需要的东西。后来这个小程序演进成了完整的微服务分布式SpringBootVueSpringcloud易物小店物品交换系统。翻看当初的设计文档和代码踩过的坑、改过的边界、为了并发和一致性熬的夜都还在眼前。这篇内容不是课程广告而是一份项目复盘覆盖业务拆解、技术选型、服务搭建、分布式锁、跨服务一致性、Vue前端对接、部署避坑适合正在学微服务、想用真实业务练手的朋友也适合准备做C2C交换类产品的技术团队参考。1. 易物小店做的是什么交换场景的业务闭环与拆分逻辑1.1 核心流程从发布物品到完成交换易物小店和闲鱼这类平台最大的区别在于交易媒介不是钱是物品。用户将自己的闲置物品拍照发布描述使用成色、希望交换的方向其他用户看到后提交交换申请物品主人同意后双方进入交换履约流程。核心业务闭环是用户注册登录维护个人信息和信用标签发布物品填写标题、描述、图片、分类、期望交换物品类型浏览物品按分类、关键词、最新发布检索提交交换申请可以选择对自己物品的交换报价物品主人通过或拒绝申请通过后生成交换单双方在站内确认交换物品和联系方式线下交付或快递交换最后互相确认完成这个流程看起来简单技术上的复杂度全在交换两个字上。买家直接下单的商品是静态库存减库存就行交换场景是双向匹配物品在交换期间不能被其他人再申请还得处理两个用户同时申请同一件物品、交换步子走到一半有人反悔等状况。1.2 为什么我决定拆微服务规模拐点和现实约束如果按功能量算这个项目单体能跑很多社区团购系统到现在还是单体。但做技术选型不能只看当前功能还要看谁在维护、要扩展成什么。我当时评估下来拆微服务有几个明确理由用户端、后台管理、运营端对服务的依赖和发布节奏完全不同耦合在一个工程里每次发布都要全量回归物品画像、搜索、消息推送这些模块后续要独立扩展比如接Elasticsearch做全文检索挂在单体里面变更成本很高团队虽然不大但按领域划分成小组后各自维护独立服务能减少代码冲突技术上也需要一套完整微服务落地经验为后续更多项目打底当然这里有个反面教训如果团队只有一两个人业务量日均几百单不要强行微服务。这个项目能拆是因为功能边界确实清晰而且我预期后续会有多端接入不是单纯为了简历好看。1.3 服务清单五个业务服务加基础设施的职责划分围绕业务闭环我把服务边界画成了五条线服务名职责关键表/资源端口gateway-service统一入口、JWT鉴权、跨域处理网关路由配置8080user-service用户、登录注册、账户资料user_account, user_auth, user_credit8091item-service物品发布、分类、浏览、状态管理item, item_category, item_image8092exchange-service交换申请、交换单、状态流转exchange_order, exchange_message_record8093message-service站内通知、私信message_notify, message_letter8094file-service图片上传下载、对象存储管理MinIO存储桶8095公共基础设施是Nacos做注册中心和配置中心Redis做缓存和分布式锁MySQL做业务数据存储MinIO做图片文件存储。这套组合在中小规模微服务项目里非常成熟覆盖面完整还不会过度设计。2. 技术选型复盘这套微服务体系里哪些组件必须上2.1 版本组合SpringBoot 2.7 SpringCloud 2021 SpringCloudAlibaba 2021微服务项目最头疼的不是写代码是版本兼容。我从搜索引擎看到springboot版本太高这个词的时候特别有感触很多人直接用了SpringBoot 3.x然后发现SpringCloudAlibaba对应的组件版本、Nacos客户端版本都对不上一脸懵回头换版本。我最终采用的是一套稳妥组合SpringBoot 2.7.18SpringCloud 2021.0.8SpringCloudAlibaba 2021.0.5.0Nacos Server 2.2.3Redisson 3.20.0MinIO 8.5.xSpringBoot版本对应SpringCloud版本对应SpringCloudAlibaba版本结论2.7.x2021.0.x2021.0.5.0稳定中文资料多常用组件兼容3.2.x2023.0.x2023.0.x新项目可用但不少组件配置方式变更2.6.x2021.0.x2021.0.2.0偏旧不建议新开在Maven父工程里我通过BOM统一管理依赖不在业务模块里写版本号这是多服务工程的基础卫生习惯。这么做的原因是各服务之间如果依赖版本不一致联调时出现的诡异问题会消耗大量排查时间。2.2 Nacos注册中心顺手解决配置管理服务发现我选Nacos而不是Eureka原因是Nacos同时具备注册中心和配置中心两个能力。Eureka早就停更Consul配置能力和中文资料又不如Nacos。Nacos客户端自带配置热更新后面调线上参数不用重启服务。多说一句如果你的微服务只是demo级别的几台机器Nacos单机模式就够不用一上来就上集群但生产环境至少三节点。我自己吃过亏Nacos挂一次所有服务间调用全部变雪崩。2.3 GatewayFeign请求入口与服务间通信网关我选了SpringCloud Gateway因为SpringCloud中Zuul系列已经停更Gateway基于WebFlux非阻塞模型性能好天然适合做统一入口。最需要提醒的是Gateway模块千万不能引入spring-boot-starter-web它会跟WebFlux冲突导致启动直接报错。我当时在pom里顺手加了web依赖启动时出现RouterFunction和DispatcherServlet冲突的异常排查了很久才发现是这个低级原因。服务间调用用的OpenFeign声明式HTTP客户端配合Nacos做负载均衡。Feign这里有几个配置细节后面会专门讲。2.4 MinIO文件存储的一段弯路图片存储最开始我用了本地磁盘存储想着文件量不大没必要上对象存储。后来发现用户上传图片一天天增加服务器磁盘扛不住迁移时也麻烦。后来替换成MinIO。选MinIO的理由很现实S3协议兼容以后想换云厂商的对象存储不用改业务代码可以私有化部署在局域网资源占用比HDFS这类分布式文件系统小很多。易物小店里的物品图片、用户头像、交换凭证照片都放在这。这里提一下MinIO在开发环境下内存管理比较稳但要注意Bucket的访问策略我一开始只设置了私有权限前端拿到图片URL直接404排查发现还需要配置访问策略或者生成预签名URL。3. Maven父工程与公共模块从零拉起可运行的服务骨架3.1 父工程pom的dependencyManagement思路微服务第一件事不是写代码是把多模块工程立起来。整个项目是一个Maven多模块结构父工程只做两件事定义所有依赖版本、管理子模块清单。父工程pom.xml关键内容如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties spring-cloud.version2021.0.8/spring-cloud.version spring-cloud-alibaba.version2021.0.5.0/spring-cloud-alibaba.version redisson.version3.20.0/redisson.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring-cloud.version}/version typepom/type scopeimport/scope /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring-cloud-alibaba.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement子模块列表这样组织modules moduleeasy-exchange-common/module moduleeasy-exchange-gateway/module moduleeasy-exchange-user/module moduleeasy-exchange-item/module moduleeasy-exchange-exchange/module moduleeasy-exchange-message/module moduleeasy-exchange-file/module /modules之所以用BOM统一管理是避免每个服务自己定义SpringCloud版本。版本冲突在微服务里是最难查的问题之一日志压根不会告诉你是版本不匹配只会在运行时抛奇怪的ClassNotFoundException。3.2 公共模块决定代码卫生公共模块easy-exchange-common是整个项目的基石所有服务都会依赖它。我在这里放了几类内容统一返回结构ResultT和状态码枚举全局异常处理器工具类包括时间、字符串、JSON处理上下文工具类用来存储当前登录用户信息常量类定义Redis key前缀和MQ topic统一返回结构特别重要。如果每个服务自己定义返回格式前端对接就变成灾难今天这个接口返回data明天那个返回result网关层根本没法统一处理。全局异常处理器示例RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BizException.class) public ResultVoid handleBizException(BizException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { log.error(系统异常, e); return Result.error(ResultCode.SYSTEM_ERROR); } }3.3 以user-service为例注册进Nacos一个业务服务要跑起来其实就是四步引入依赖、写主类、配bootstrap.yml、实现接口。核心依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-bootstrap/artifactId /dependencyapplication.yml核心配置server: port: 8091 spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yaml datasource: url: jdbc:mysql://localhost:3306/easy_exchange?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: xxxxxx启动主类SpringBootApplication EnableDiscoveryClient MapperScan(com.easy.exchange.user.mapper) public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }Nacos服务端如果是2.x建议配置spring.cloud.nacos.discovery.username和密码否则会一直报鉴权失败。我第一次搭的时候没注意服务注册不上日志里全是403。4. 物品被抢着要的瞬间Redis分布式锁在易物场景的实战4.1 竞争场景描述同一件物品被多人申请易物小店上线一周后遇到一个真实场景一件九成新的戴森吸尘器挂在网站上一小时内收到五个交换申请。五个申请同时创建物品状态被反复修改最后交换单里出现了三条已通过其他两个用户的聊天窗口弹出了错误的成功通知。这就是典型的并发竞争。在单体应用里可以用synchronized锁住方法但微服务按服务拆开后用户请求可能落到不同实例每个实例有自己的JVM锁互相之间根本感知不到这就是单机锁失效的根因。4.2 为什么锁要放在业务代码前面这个并发场景的竞争资源是一件物品的可交换状态。如果不加锁五个请求同时读到status AVAILABLE同时创建交换申请同时更新状态操作全部成功数据却乱了。分布式锁的作用是在多实例之间制造一个互斥窗口。我用Redisson这个框架来实现Redis分布式锁比手写SETNX加超时时间的方式更可靠它内置了看门狗机制可以自动续期避免业务没执行完锁就过期。手写方式最大的风险是忘记设置过期时间服务宕机后锁永远不会释放后面所有请求全部卡死。Redisson把这些问题都处理了所以能用成熟框架就不要自己重复造轮子。4.3 Redisson代码tryLock加状态校验加状态更新我在exchange-service里申请交换的核心逻辑是先获取物品信息校验状态创建交换申请然后将物品状态改为LOCKED。关键代码如下public ResultLong applyExchange(ApplyExchangeRequest request) { String lockKey exchange:lock:item: request.getItemId(); RLock lock redissonClient.getLock(lockKey); boolean locked false; try { locked lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { return Result.error(操作的人太多请稍后重试); } // 重新查询物品状态以数据库为准 ItemDTO item itemClient.getItemById(request.getItemId()); if (item null || !AVAILABLE.equals(item.getStatus())) { return Result.error(物品已被预约或者已下架); } // 创建交换申请单写入exchange-service自己的库 Long exchangeId exchangeMapper.createApply(request.getUserId(), request.getItemId(), request.getOfferItemId()); // 更新物品状态为LOCKED这里走的是item-service的原子接口 boolean updated itemClient.lockItem(request.getItemId(), exchangeId); if (!updated) { throw new BizException(物品状态更新失败); } return Result.success(exchangeId); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }注意几个细节tryLock(3, 10, TimeUnit.SECONDS)的含义是等待锁最多3秒锁自动释放时间是10秒。如果业务超过10秒还没执行完Redisson的看门狗会自动续期但前提是没有显式传leaseTime。这里传了10秒的leaseTime看门狗就不会生效所以必须确保持锁时间内只做快操作。锁的粒度是item:{itemId}不是全局锁。这非常关键如果锁粒度太大比如用一个exchange:lock:all那么所有交换申请全部串行系统的并发能力瞬间归零。锁不能替代业务校验。获取锁后必须重新查库确认物品状态因为可能在等待锁的过程中前面一个事务已经把状态改掉了。持有锁期间不要做耗时操作尤其是不要做Slow SQL查询和第三方接口调用。这里调用itemClient.lockItem是远程调用要保证它的响应足够快否则锁会被拖到临界点。item-service的lockItem接口必须设计成原子操作用乐观锁做兜底Update(UPDATE item SET status LOCKED, lock_exchange_id #{exchangeId}, update_time now() WHERE id #{itemId} AND status AVAILABLE) int lockItem(Param(itemId) Long itemId, Param(exchangeId) Long exchangeId);这里用受影响行数判断是否更新成功即使分布式锁出了意外数据库也会拦截重复更新。4.4 锁使用的边界与替代方案分布式锁不是万能的。我在后续迭代中又加了一个数据库唯一约束作为双保险给exchange_apply表加了一个业务唯一索引(item_id, applicant_id)同时保持状态为有效申请时唯一。这样如果Redis锁因为网络抖动失效数据库也能挡住重复申请。另外一个替代方案是纯数据库乐观锁也就是把version字段加到物品表里更新时检查版本号。但纯乐观锁的问题是申请方失败了要自己重试用户体验不好。分布式锁加乐观锁的组合在这个场景下体验和正确性都能兼顾。5. 跨服务状态流转不上分布式事务也能保证最终一致5.1 状态机设计AVAILABLE、LOCKED、EXCHANGING、FINISHED物品交换涉及多个服务但状态不能满天飞。必须由一个服务来主导状态机我选择让exchange-service作为交换流程的总导演item-service只提供物品状态的原子变更接口。物品状态流转AVAILABLE 可用 - LOCKED 已被预约 - EXCHANGING 双方确认交换中 - FINISHED 完成 LOCKED 可以回到 AVAILABLE比如申请被拒绝或超时未确认交换单状态流转PENDING 申请中 - ACCEPTED 已接受 - CANCELLED 已取消 - FINISHED 已完成设计原则是业务服务之间不互相操作对方的表。比如想取消交换exchange-service不能直接更新item表必须调用item-service的unlockItem接口。这样每个服务的数据库边界清晰后续改表结构不会互相破坏。5.2 本地消息表给站内消息减负交换申请通过后系统要通知物品主人和申请人。当时我第一个版本直接在业务代码里同步调用message-service结果message-service一抖动主流程整个失败用户看到的是交换申请失败请重试其实交换单已经创建成功了这就产生了脏数据。后来改成本地消息表方案。exchange-service在本地创建交换单时同时往notify_message表插入一条待通知记录CREATE TABLE notify_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exchange_id BIGINT NOT NULL, message_type VARCHAR(20) NOT NULL, receiver_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未发送 1已发送, retry_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );然后有一个定时任务兜底扫描Scheduled(fixedDelay 5000) public void scanUnsentMessages() { ListNotifyMessage list notifyMessageMapper.selectUnsent(100); for (NotifyMessage msg : list) { try { messageClient.sendNotify(msg.buildRequest()); notifyMessageMapper.markSent(msg.getId()); } catch (Exception e) { log.warn(通知发送失败messageId{}, msg.getId(), e); notifyMessageMapper.increaseRetry(msg.getId()); } } }这个方案的核心思想是最终一致性。业务主流程不依赖其他服务的即时响应跨服务副作用通过本地表和定时任务异步推进。对易物小店这个体量来说完全没有必要为了几个消息通知引入分布式事务框架本地消息表加上重试机制就能让数据最终一致。5.3 服务间调用的幂等与重试定时任务天然会重复因为一次调用失败后下一次扫描会重新发送同一条消息。所以message-service的sendNotify接口必须做幂等。幂等实现方式message-service建一张message_receive_record表用notify_message_id作为唯一键处理前先插入插入冲突说明已经处理过直接返回成功。还有一个我踩得很实的坑OpenFeign的重试机制。Feign默认在连接超时时会重试但重试对写接口来说极其危险。比如调用unlockItem第一次请求其实已经成功了只是响应超时Feign重试再次调用物品被解锁两次第二次调用因为状态已经不是LOCKED返回失败但业务侧看到的是失败实际数据却成功改变。后来我做了两件事关闭Feign对写接口的重试或者用幂等键保证重复请求安全所有写接口都设计成天然幂等请求中带上requestId服务端根据requestId判断是否已处理Feign超时配置如下feign: client: config: default: connectTimeout: 3000 readTimeout: 50005.4 超时问题的真实案例线上出现过一次很诡异的问题用户提交交换申请后前端一直转圈最后提示失败但库里有交换单记录。排查时发现是因为exchange-service调用item-service的lockItem超时Feign重试了一次第二次成功但第一次的响应超时让exchange-service认为整个调用失败直接把事务回滚了结果item-service那边物品已经被锁了。这个问题的根因是跨服务调用的事务边界不一致。exchange-service本地事务回滚并不会回滚item-service已提交的事务。解决思路有三层接口幂等item-service的lockItem用exchangeId做唯一约束重复调用返回相同结果尽量缩短服务间调用链把申请交换核心流程控制在两个服务内不在长事务中做过多远程调用如果业务允许把关键写操作设计成先落库再同步避免强一致这个案例让我深刻理解一个道理微服务架构里分布式事务不是靠某个框架一劳永逸解决的更重要的是业务流程设计如何降低对强一致性的依赖。6. Vue前端双工程对接前台大厅、后台管理与网关联调6.1 前台大厅和后台管理的工程划分易物小店用户端叫store-front面向C端用户负责浏览物品、发布物品、申请交换、聊天、个人中心。后台管理叫admin-web面向运营人员负责用户管理、物品审核、分类管理、交换单查询。两个工程都用Vue3加Vite加Pinia加Element Plus。Vue3和Vite是现在的默认选择构建速度快组合式API写起来比Options API更容易维护。工程目录我做了划分store-front/ src/ api/ # 接口请求封装 views/ # 页面组件 router/ # 路由配置 store/ # Pinia状态 components/ # 公共组件 utils/ # 工具函数前后台分开部署用户端打包后放到Nginx的/目录管理端放到/admin目录通过不同location反代到网关这样两者可以独立升级发布。6.2 开发期Vite转发与线上网关的双轨玩法开发环境最大的痛点是前端跑在localhost:5173后端服务跑在各自端口直接请求必然跨域。我的做法是开发环境用Vite的server.proxy配置把所有/api开头的请求转发到网关的8080端口。server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }网关侧的路由配置是StripPrefix1也就是说前端访问/api/user/info经网关转发到user-service时路径变成了/user/info。这样设计的好处是前端只需要关心统一的/api前缀后端可以自由调整服务拆分对前端透明。生产环境则不需要Vite转发前端构建后放在NginxNginx配置/api反向代理到网关location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里用proxy_pass加末尾斜杠的方式将前端的/api/user/info重写为网关的/user/info与开发环境的rewrite逻辑保持一致。6.3 动态路由与角色权限易物小店涉及两种角色普通用户和运营管理员。前台的C端路由基本固定直接写在静态路由里就行。后台管理则需要动态路由根据登录用户的角色和权限动态注册菜单路由。后台管理登录后后端返回当前用户的菜单列表格式大致是[ { path: /item/audit, component: item/Audit, name: ItemAudit, meta: { title: 物品审核, icon: document } }, { path: /user/manage, component: user/Manage, name: UserManage, meta: { title: 用户管理, icon: user } } ]前端核心代码const modules import.meta.glob(../views/**/*.vue) export function addDynamicRoutes(menus) { menus.forEach((menu) { const component modules[../views/${menu.component}.vue] router.addRoute(Layout, { path: menu.path, name: menu.name, component, meta: menu.meta }) }) }这里用了import.meta.glob作用是在构建时扫描views目录下的所有Vue文件这样动态注册路由时才能根据字符串找到对应的组件。如果不用这种写法直接写component: () import(menu.component)构建工具无法在编译期识别动态路径打包后组件找不到页面白屏。动态路由有个经典问题刷新页面后路由丢失。原因是Vue Router的路由表是运行时注册的刷新后重新走初始化流程如果没有重新拉取菜单再addRoute用户访问的页面就没有对应路由直接跳到404。我的解决方案是在路由全局守卫中加一个判断router.beforeEach(async (to, from, next) { const userStore useUserStore() if (userStore.token !userStore.menusLoaded) { const menus await userStore.fetchMenus() addDynamicRoutes(menus) next({ ...to, replace: true }) return } next() })next({ ...to, replace: true })是关键先把路由注册完再重新进入一次目标路由否则当前导航仍然找不到组件。6.4 图片上传文件服务与MinIO的对接物品发布表单里图片上传是最容易被忽视的环节。前端我封装了一个Upload组件选择图片后直接调用file-service的上传接口。const uploadFile async (file: File) { const formData new FormData() formData.append(file, file) const { data } await http.post(/api/file/upload, formData, { headers: { Content-Type: multipart/form-data } }) return data.url }file-service收到文件后存入MinIO并返回访问路径PostMapping(/upload) public ResultFileUploadVO upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext StringUtils.substringAfterLast(originalFilename, .); String objectName UUID.randomUUID().toString().replace(-, ) . ext; minioClient.putObject( PutObjectArgs.builder() .bucket(easy-exchange) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); String url http://your-domain/files/ objectName; return Result.success(new FileUploadVO(url, objectName)); }这里有两个容易踩的坑。第一个是MinIO Bucket访问权限如果Bucket是私有权限返回的URL直接访问会报403。我用的是预签名URL或者把Bucket设置为public并配置公开读策略考虑到物品图片本身不涉密公开读合理。第二个是图片访问的跨域问题前端如果在浏览器直接加载图片Oss跨域问题不会出现但如果是通过Canvas处理图片就可能有CORS需要在MinIO中配置Bucket跨域规则。7. 部署与复盘一台服务器上的微服务活下来7.1 资源紧张的部署策略项目验收阶段只有一台4核8G的服务器要把Nacos、MySQL、Redis、MinIO再加上五个微服务全部跑起来内存压力很大。如果不加控制光这些组件启动后就能吃满8G内存。我当时做了几件事把内存压了下来所有SpringBoot服务设置JVM参数-Xms256m -Xmx256mNacos用单机模式并调低JVM堆内存-Xmx256mMySQL和Redis用容器自带的默认配置不做额外调优MinIO单独放一个容器Docker Compose是这阶段最好的编排工具不需要上Kubernetes那些重型基础设施。服务之间用Docker内网互相访问只用网关端口对外暴露。version: 3 services: nacos: image: nacos/nacos-server:v2.2.3 environment: MODE: standalone JVM_XMS: 256m JVM_XMX: 256m ports: - 8848:8848 mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: xxxxxx volumes: - ./mysql-data:/var/lib/mysql user-service: build: ./easy-exchange-user depends_on: - nacos - mysql部署时最需要留意的是Nacos的注册地址。容器内启动的服务注册到Nacos的IP是容器IP外部服务通过宿主机访问不到。解决办法是在每个服务容器环境变量里配置NACOS_ADDR: nacos:8848 SPRING_CLOUD_NACOS_DISCOVERY_IP: 宿主机IP否则网关通过lb://user-service转发给服务时连接的全是容器内网IP外部请求根本到不了。7.2 网关日志与链路排查微服务联调时最痛苦的事情是定位一个请求到底在哪个服务出问题了。没有成熟的链路追踪系统时我用了一个轻量方案在网关生成一个traceId通过HTTP Header传给下游所有服务每个服务在日志里输出这个traceId。Component public class TraceIdFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String traceId UUID.randomUUID().toString().replace(-, ); ServerHttpRequest request exchange.getRequest().mutate() .header(X-Trace-Id, traceId) .build(); return chain.filter(exchange.mutate().request(request).build()); } Override public int getOrder() { return -100; } }各服务在日志pattern中加入%X{traceId}或者手动在logback配置里放MDC这样报错后直接按traceId搜索日志能在几十万行日志中快速切片出完整调用链。这个方案虽然没有SkyWalking那种可视化链路图但对于中小项目来说成本极低、效果直接。等系统规模大了再换分布式链路追踪中间件也不迟。7.3 后续演进从可用到好用项目上线跑稳之后我看清了后续要补的方向。物品搜索目前是简单的SQL模糊查询一旦数据量上来就会拖垮MySQL后面需要引入Elasticsearch把物品的标题、描述、分类做全文检索再用MQ异步同步索引数据。站内信目前是私信和通知用户双方聊天的实时性要求很高可以升级为WebSocket消息推送message-service作为一个长连接网关来承载在线会话。服务治理方面熔断限流目前只是靠Feign超时兜底后续要引入Sentinel给每个依赖接口配置流控和熔断规则保证某个服务被打垮时不影响其他服务。数据一致性设计上如果后续要增加积分、支付、物流等强一致场景就需要引入Seata的AT模式或者TCC模式。但我的经验是能通过业务设计规避的分布式事务尽量不要上框架框架本身也有性能和一致性风险。回头复盘这个项目最值钱的不是用了SpringCloud这套技术而是搞懂了服务边界在哪里、锁和事务怎么配合、跨服务调用如何保证最终一致。这套思路换到任何分布式系统都通用。如果你也在做类似的C2C交换或二手交易平台我建议先从单体结构把业务跑通然后把最容易出现并发和跨服务依赖的部分按本文的方式逐步微服务化不要一开始就追求技术架构的极致稳定跑起来再演进才是正路。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号