☰
SpringBoot+Vue+SpringCloud微服务潮服商城设计与实践
2026/10/3 2:57:26 网站建设 项目流程

做电商项目这些年,"潮服购物"这种偏年轻化、重调性的场景其实比标准B2C商城更有意思。用户画像清晰、上新节奏快、限量发售和社区种草玩法多,业务上天然需要一套反应快的系统,而不是传统一柱擎天的单体应用。所以当时拿到"SpringBoot+Vue+SpringCloud微服务分布式的潮服购物服装商城系统"这个题目时,脑子里第一反应就是:技术栈几乎不用纠结,SpringBoot + Vue + SpringCloud这套组合在国内电商、产业互联网领域太成熟了,剩下的核心工作在于——如何把"微服务+分布式"从概念变成真正能抗住业务压力的系统。这篇博文就完整复盘一下这个系统的设计思路、技术选型、核心实现和踩坑记录,适合正在做毕业设计、或者准备把单体商城改成微服务架构的同学参考,也适合面试前想系统梳理SpringCloud分布式知识的人。

1. 系统整体设计与架构选型思路

1.1 潮服商城业务场景与核心需求拆解

潮服商城和普通服饰电商最大的区别在于"上新节奏"和"库存策略"。普通商城是海量SKU常驻,潮服更接近"每周限量发售、售完即止",加上尺码、版型、联名款式这些强个性化属性,用户对商品详情、穿搭推荐、社区评测的关注度极高。从实际业务拆解来看,核心模块大概有这么几个:用户中心(注册登录、会员等级、收货地址)、商品中心(类目、SPU/SKU、尺码库存、上下架)、购物车与订单中心(加购、下单、支付、状态流转)、库存中心(预占、扣减、回滚)、营销中心(优惠券、秒杀、限时购)、社区内容(穿搭分享、评价)、后台管理(商品管理、订单管理、数据统计)、搜索推荐(商品检索、热门推荐)。

这套模块如果全塞进一个SpringBoot单体里,前三个月开发确实快,但到后期必然出问题:商品秒杀和普通查询互相拖累、订单状态机耦合在业务代码里、每次发布都要整个服务重启、团队多人协作天天冲突。所以拆分微服务不是赶时髦,是业务复杂度到了一定程度后的必然选择。

1.2 技术选型:为什么是SpringBoot+Vue+SpringCloud这套组合

SpringBoot解决的是"服务怎么写"的问题。微服务架构下每个服务都是一个独立可运行的SpringBoot应用,它的自动装配、内嵌Tomcat、Actuator监控让开发者把精力集中在业务代码上,而不是反复搭环境。实际项目中我一般用2.7.x版本,稳定且和SpringCloud版本配套清晰,不建议一上来就追最高的3.x,后面会讲到版本坑。

SpringCloud解决的是"服务之间怎么协作"的问题。它不是一个框架,而是一套微服务治理全家桶:Nacos做注册中心和配置中心、Gateway做统一网关、OpenFeign做声明式服务调用、Sentinel做流量控制和熔断降级、Seata处理分布式事务。这些组件覆盖了"服务怎么发现、请求怎么路由、故障怎么隔离、数据怎么一致"四个核心问题,比自己去造轮子可靠太多。

Vue解决的是"用户端体验怎么做"的问题。商城前端页面交互密集,购物车、筛选器、商品详情、订单流程都需要实时反馈。Vue3的组合式API配合Element Plus组件库,开发效率很高,而且前后端分离后,前端团队和后端团队可以并行开发,联调时用Mock数据或者直接代理到网关即可。

之所以不用PHP单体、不用Dubbo直连、不用Next.js全栈,核心原因就一句话:这套业务需要清晰的模块边界、独立的伸缩能力和团队并行开发的自由度,而SpringBoot+Vue+SpringCloud是目前综合成本最低、人才最好找、生态最完整的组合。用生活点的类比,单体应用就像一家小店,老板收银、炒菜、端盘一肩挑,生意大了必然乱;微服务就像连锁店,后厨、前台、采购分开管,各自独立又协同。

1.3 微服务拆分逻辑与模块边界划分

微服务拆分最怕的是"为了拆而拆"。我当时定的拆分原则是:按业务域拆、按伸缩需求拆、按团队边界拆,三者取交集。商品、订单、用户、库存天然隶属于不同的业务域,每个域的数据独立性也强,所以直接拆成独立的服务,共用数据库严禁,服务之间只通过接口通信,绝不共享表。

具体到落地,我分了这几个服务:

  • tide-user-service:用户、会员、地址、积分
  • tide-goods-service:商品、类目、SKU、属性、评价
  • tide-order-service:购物车、订单、订单状态机、售后
  • tide-stock-service:库存预占、扣减、释放
  • tide-marketing-service:优惠券、秒杀活动、限时购
  • tide-search-service:基于Elasticsearch的商品搜索与推荐
  • tide-gateway:统一入口、鉴权、路由、限流
  • tide-auth-service:认证、登录、JWT签发

这里有个关键思考:购物车为什么放在订单服务里?因为购物车的生命周期和订单强相关,加购、结算、删除这一串动作最终目的就是生成订单,拆开反而要多一次远程调用、多一次数据一致性风险。库存单独拆的原因是秒杀场景下库存操作极其频繁,必须能独立扩容和加分布式锁,放在商品服务里会把商品查询拖垮。

每个服务持有自己的数据库模式,一般一个服务一个库(或一套库中的独立schema),库之间零外键关联。这带来一个直接问题:跨服务的数据查询怎么做?比如"我的订单列表"需要商品名称和图片、需要用户信息,解决方案不是去查别的库,而是在订单表冗余商品快照(名称、图片、单价),订单服务自己就能出列表页,商品信息变了也不影响历史订单展示。这个冗余思路贯穿整个系统设计,是微服务落地最实用的经验之一。

2. 核心技术模块与实现细节

2.1 用户认证与网关统一鉴权:JWT + Spring Security

登录认证在微服务架构下不能像单体那样用Session粘滞节点,因为请求经过网关后可能负载到任意一个用户服务实例,Session在A实例、下次请求落到B实例就丢了。所以选择了无状态的JWT方案:用户登录后,认证服务校验用户名密码,签发一个包含用户ID、角色、过期时间的JWT令牌,后续所有请求在Header里携带这个令牌,网关统一校验。

认证链路是这样设计的:用户访问任意业务接口 -> Gateway的GlobalFilter拦截请求 -> 从Header取出Token -> 通过JWT密钥解析校验 -> 校验通过后把用户ID放到Header中向下游服务传递 -> 下游服务直接从Header里取用户上下文。这样设计的好处是业务服务无需重复写解析逻辑,所有服务都共用一个UserContext工具类,从RequestContext里拿用户ID即可。

Token刷新机制这里容易踩坑。JWT一旦签发在过期前是没法主动失效的,强行让用户重新登录体验又差。我的做法是存储层加一层Redis黑名单:用户主动退出或修改密码时,把Token的唯一标识(jti)加进黑名单,设置有效期为Token剩余过期时间;网关每次校验时除了解析JWT本身,还要查一下黑名单是否包含这个jti。这样既保留了JWT的无状态优势,又能处理强制下线场景。

Spring Security的使用上不建议在网关里引入完整的安全过滤器链,太重了。网关只做轻量校验(解析、查黑名单、时间校验),真正的权限控制放到业务服务内。例如后台管理接口要求管理员角色,在Controller方法上用@PreAuthorize("hasRole('ADMIN')")注解配合方法级安全配置即可。

2.2 商品中心与搜索设计:缓存策略与ES全文检索

商品中心是整个商城的门面,它的数据特点就是"读多写极少"——一个商品的详情、图片、销量、评价在一天内可能被看千次,但真正修改只有运营上下架或者改价格的时候。因此缓存策略是商品服务的核心优化点。

我用的是两级缓存:本地Caffeine缓存 + Redis缓存。商品详情接口先查Caffeine(每个商品服务实例都有一份本地缓存),没命中再查Redis,Redis没有才查询数据库。Caffeine的过期时间设为60秒,Redis设为30分钟,商品发布或修改时通过Spring Cloud Stream发一个缓存失效的事件,各个实例监听到后主动淘汰本地缓存。这样做的实测效果是:商品详情接口的QPS从几千提升到几万,数据库基本没有压力。

缓存场景逃不开三个经典问题:穿透、击穿、雪崩。穿透是查了一个不存在的商品ID绕过缓存直击数据库,解决方式是接口入口做参数校验,对不存在的ID也缓存一个空值(比如NULL对象缓存5分钟),或者用布隆过滤器做快速判空。击穿是某个热点商品的缓存刚好过期,大量请求同时打到数据库,解决方式是对商品详情查询加互斥锁,保证同一时刻只有一个线程去查数据库,其他线程等待后还是从缓存拿数据。雪崩是大批量key同时失效,解决方式就是过期时间加随机值,不要让所有商品细节在同一秒内过期。

商品搜索这一块直接接入了Elasticsearch,商品服务在商品上下架、价格变更时同步数据到ES,搜索服务通过RestHighLevelClient执行查询。ES的索引设计里,商品名称用IK分词器做中文分词,类目、品牌、风格、标签用keyword类型做精确过滤,价格、销量用数值类型做排序。搜索接口支持关键词高亮、多字段聚合、分页查询,首页的"热门推荐""上新精选"就是调搜索服务聚合出来的。

2.3 订单、库存与分布式事务:从秒杀场景说起

潮服商城最刺激的场景就是限量发售。商品上架时间一到,几千人同时点击购买,库存可能只有一百件。这时候扣减库存的并发控制就是系统的生死线。我用的方案是Redis分布式锁 + 库存预扣两步走:

用户点下单时,首先要生成一个分布式锁,锁的key设计为stock:lock:{skuId},用Redisson框架获取锁,Redisson内部实现了可重入锁和看门狗续期机制(默认leaseTime 30秒,看门狗每10秒续期一次),避免锁在业务执行中途过期被其他线程抢到造成超卖。拿到锁后检查Redis中的库存预占数量,如果够,就在Redis里扣减一个占用名额,然后释放锁,异步发送MQ消息创建订单。这个方案在秒杀场景下的优势是:锁只存在于Redis,不会阻塞数据库,扣库存动作是纯内存操作,毫秒级完成。

如果有人支付成功,数据库库存表真正扣减,Redis里的预占名额转为实质扣减;如果超时未支付,订单关闭,MQ消息触发预占名额释放,Redis里的库存自动加回来。这套"预占-确认-释放"的流程,本质上是把库存这一最关键的资源从数据库层面解耦到了Redis层面,数据库只在最终确认时落一次。

那订单、库存、营销(核销优惠券)这三个服务的数据一致性怎么保证?我用了Seata的AT模式处理强一致场景。AT模式对业务代码侵入性很低,业务SQL照常写,Seata通过解析SQL自动生成undolog,在事务提交前记录数据快照,出现异常时自动回滚。对一般的创建订单+扣减库存+核销优惠券这种T1级别的事务,AT模式完全够用,不用手写TCC那种复杂的Confirm/Cancel逻辑。

但AT模式性能有限,因为事务期间会持有全局锁。高并发的秒杀下单就不适合全程用AT,我的做法是:秒杀链路走异步最终一致性,通过本地消息表 + RocketMQ实现。订单服务在本地事务里创建一条订单记录和一条消息记录,一起提交;消息表的内容通过定时任务扫出未发送的消息投递到MQ;库存服务消费MQ消息执行库存扣减,如果失败则消息进入重试队列,超过重试次数进入死信队列,人工介入。这套机制比AT模式更适合高并发场景,日志、对账、补偿都清清楚楚。

2.4 图片视频资源管理:MinIO接入SpringBoot与Vue播放方案

潮服商品详情页离不开图片和视频。常规方案是存云厂商的对象存储,但很多项目会有私有化部署的需求,所以我在这个系统里选了MinIO,部署在私有服务器上,S3协议完全兼容,将来想切云随时可以切。

SpringBoot接入MinIO很简单,引入minio依赖,配置endpoint、accessKey、secretKey,封装一个MinioStorageService提供上传、下载、生成预签名URL等方法。需要注意的一点:上传商品图片时不能直接把文件流存到业务服务器本地,正确姿势是前端直传MinIO或者后端接收文件后流式转存MinIO,存储和服务器的存储解耦,大视频文件才不会把应用服务器的磁盘打满。

上传完成后,MinIO返回的对象路径(比如goods/2024/10/12/shoe-01.jpg),存到商品表里,访问时通过预签名URL或者桶策略公共读。预签名URL的好处是可以设置过期时间,适合私密资源;商品图是公开资源,直接配置桶为公共读,配合Nginx反向代理MinIO的9000端口,域名后面加/minio前缀,这样静态资源请求根本不会到后端服务,压力全部被Nginx分流了。

商品短视频方面,直接上传MP4也可以,但为了稳定播放实现,我沿用了视频切片方案:用FFmpeg把视频转成HLS格式(m3u8 + ts分片),文件存储到MinIO的/video/目录下。这样做的原因是长视频流式加载体验更好,支持拖动进度条,网络差的时候也能流畅播。Vue前端播放m3u8时,如果不装额外的播放器,直接用原生HTML5的<video>标签是没法播放的,需要引入hls.js库,按需加载HLS流转换逻辑,Safari以外的浏览器也能稳定播放。

2.5 Vue前端设计与联调细节

前端侧我用的Vue3 + Vite + Pinia + Vue Router + Element Plus。工程结构上按模块拆分了views目录,商品列表、商品详情、购物车、订单结算、个人中心、后台管理每个模块独立路由。这里有一个和传统管理后台很不一样的点:商城的路由需要根据用户的登录状态和角色动态生成。普通用户看到的是购买、订单、社区;管理员登录后台后需要看到商品管理、订单管理、数据看板。所以我用了Vue Router的动态路由功能:用户登录后,前端根据用户角色动态添加路由表,而不是在静态路由里写死全量菜单。

跨域问题在开发阶段是必经的坑。前端调试时请求http://localhost:8080/api/user/info,后端网关在http://localhost:8081,直接请求必然跨域报错。我的解决方式分两层:开发环境使用Vite的server.proxy配置,把/api前缀代理到网关地址;生产环境用Nginx统一配置反向代理,前端请求同源,后端Gateway再做二次转发。只要代理配置正确,前端代码里根本不需要关心后端域名。

前后端联调还有一个痛点:契约管理。我引入了OpenAPI,后端写好接口后生成Swagger文档,前端从Swagger UI拿到接口定义直接生成TypeScript类型定义,这样字段名不一致的问题大幅减少。后端接口的返回结构也统一包装为Result<T>(code、message、data),前端在Axios拦截器里统一处理错误码,不用每个页面都写一遍错误的弹出提示。

3. 实操搭建过程与核心环节实现

3.1 环境准备与工程骨架初始化

开始动手之前,先把环境清单列清楚:JDK 8+(我用的JDK 17)、Maven 3.8+、Node 16+、MySQL 8.0、Redis 6+(我用的7.x)、Nacos 2.2+、MinIO私有服务器。这里特别要强调版本兼容,SpringBoot和SpringCloud的版本是一一对应的,搞错了连启动都会报错。我当时的搭配方案是:

  • SpringBoot 2.7.18
  • SpringCloud 2021.0.8
  • SpringCloud Alibaba 2021.0.5.0
  • Nacos 2.2.3
  • Seata 1.7.0
  • Redisson 3.20.1

不建议一上来就选SpringBoot 3.x,因为SpringCloud Alibaba的适配进度和SpringBoot 3.x的Jakarta迁移可能会让新手浪费时间排坑。用这套相对成熟的组合,社区资料、踩坑案例都多,出问题能查到解决方案。父工程是一个pom聚合工程,每个服务是一个子模块,公共依赖(比如tide-common模块里的Result类、UserContext、异常处理)单独抽成一个模块,其他服务引用它。

3.2 Nacos注册中心与Gateway网关配置

Nacos在这个系统里扮演两个角色:服务注册和配置中心。服务注册这一块几乎零成本,在Nacos控制台启动好服务后,SpringBoot应用加一条依赖spring-cloud-starter-alibaba-nacos-discovery,然后在application.yml里配置spring.cloud.nacos.discovery.server-addr,启动时服务名就会自动注册到Nacos,控制台服务列表里就能看到实例和健康状态。

Gateway网关是整个系统的流量入口,它的路由配置决定了外部请求如何分发到各个微服务。一个典型的路由规则是这样:

spring: cloud: gateway: routes: - id: user-service uri: lb://tide-user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: goods-service uri: lb://tide-goods-service predicates: - Path=/api/goods/** filters: - StripPrefix=1

lb://tide-user-service会从Nacos拿到服务实例列表,Gateway自动做负载均衡转发。StripPrefix=1的含义是去掉第一段路径,也就是说外部请求/api/user/info经过网关后变成/info转发给用户服务。我在Gateway里还加了全局的鉴权过滤器、跨域过滤器、限流过滤器,限流用的是基于Redis的RequestRateLimiter,按IP维度每秒最多10个请求,防止接口被盗刷。

3.3 核心业务代码实现要点

用户登录认证这块的核心代码主要分三段:登录接口、JWT生成、网关过滤器。登录接口的逻辑没什么特别,就是Spring Security的AuthenticationManager校验一下用户名密码,校验通过后从用户表查出用户信息,用Jwts.builder()生成Token。网关的过滤器是关键,它的代码逻辑是这样的:

@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header("X-User-Id", claims.get("uid").toString()) .header("X-User-Role", claims.get("role").toString()) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } catch (Exception e) { // 返回401 } } // 白名单路径直接放行(登录接口、商品浏览等) return chain.filter(exchange); } @Override public int getOrder() { return -100; } }

那段获取锁扣库存的代码,用Redisson的API写起来很清楚:

RLock stockLock = redissonClient.getLock("stock:lock:" + skuId); boolean locked = stockLock.tryLock(0, 10, TimeUnit.SECONDS); if (!locked) { throw new BizException("当前抢购人数过多,请稍后重试"); } try { Long remain = redisTemplate.opsForValue().decrement("stock:preoccupy:" + skuId); if (remain == null || remain < 0) { redisTemplate.opsForValue().increment("stock:preoccupy:" + skuId); throw new BizException("商品已经售罄"); } // 发送MQ消息,创建订单 } finally { stockLock.unlock(); }

注意这里有个细节:Redis的decrement操作是原子性的,即使并发请求都获取到了锁,最终也只能有一个请求把预占库存扣到负数以下,所以还要判断扣减后的值,如果小于0就回滚。分布式锁解决的是"同一线程不能同时扣两次"的问题,Redis原子操作解决的是"多个线程竞争时不能扣成负数"的问题,两个机制配合才严谨。

本地消息表的实现是最终一致性的兜底方案。在订单表旁边建一张order_message表(order_id、status、create_time、retry_count),创建订单和插入消息在同一次数据库事务里提交。定时任务每5分钟扫描消息表中状态为"待发送"且重试次数小于5的记录,把它们发送到RocketMQ,发送成功后状态改为"已发送"。下游库存服务消费消息后执行库存扣减,如果返回失败就进入重试;Retry超限就进入人工告警。这套机制的核心价值在于:本地表记录和业务数据同生共死,消息绝对不会丢。

3.4 前后端部署与运行验证

部署我采用了两套环境:开发环境用Docker Compose一键拉起基础设施(MySQL、Redis、Nacos、MinIO、RocketMQ),应用服务直接本地IDE启动,日志在控制台看;测试环境用Docker镜像部署所有微服务,注册到同一个Nacos。

前端构建时有个关键配置:Vite的base路径要设置为/,构建产物放在Nginx的html目录。Nginx配置里除了静态资源托管,还配了反向代理:

location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

这样请求/api/user/info会被转发给网关的/user/info,前端代码里请求地址直接写/api/user/info,所有流量都走同源。前端路由用的是History模式,所以Nginx还需要一个try_files配置:

location / { try_files $uri $uri/ /index.html; }

不配这个的话,用户刷新/goods/detail/123这种二级页面会直接404,这个问题在发布后特别容易遇到。

4. 常见问题与排查技巧实录

4.1 版本兼容与依赖冲突

这是我见过最多人踩的坑,也是最容易解决的。SpringCloud和SpringBoot版本没有对齐,项目启动时各种ClassNotFoundException、NoSuchMethodError,整个日志根本没法看。排查方法很粗暴但有效:打开项目的pom.xml,确认SpringBoot的版本号,然后去SpringCloud官方文档查版本对应关系,再根据SpringCloud的版本去SpringCloud Alibaba查对应版本。记住了,spring-boot-starter-parent里定义的SpringBoot版本和spring-cloud-dependencies里定义的SpringCloud版本必须匹配,否则后面写的所有微服务代码都是空中楼阁。

另外,热词里提到的"SpringBoot版本太高"同样值得警惕。新版本虽然性能更好,但生态适配往往滞后。比如SpringBoot 3.x要求JDK 17+,部分老项目的第三方SDK(比如某些支付SDK)还在用JDK 8编译,直接启动就报UnsupportedClassVersionError。做项目不是追新,能用、稳定、社区熟才是第一原则。

4.2 分布式锁与缓存经典问题

分布式锁失效的场景比想象中多。第一种是业务执行时间超过锁的leaseTime,锁自动释放后被别的线程拿到,两个线程同时操作同一份数据,超卖问题重新出现。Redisson的看门狗机制能解决大部分场景(默认每10秒自动续期),但如果业务方法里做了长时间IO操作或者大事务,续期依然可能来不及。稳妥的做法是评估业务的正常耗时上限,把leaseTime设置成平时的5倍以上,同时保证业务代码里不能用tryLock后不放锁,一定要在finally块里释放。

第二种是锁的粒度太大。如果你对整个用户ID加锁,那同一用户同时提交两笔订单没问题;如果你对整个商品SKU加锁,那所有用户同时抢购同一SKU都被串行化了,秒杀性能直接崩。我的经验是锁粒度要尽量细,比如"用户ID + SKU"组合作为锁key,单个用户单次购买单个商品是天然串行的,不同用户买同一个SKU却能并发执行,配合Redis原子扣减,既能防超卖又不损失性能。

缓存击穿和雪崩的排查工具其实很简单:观察Redis的监控面板,如果在某个时间点出现大面积查询打到数据库的P99曲线飙升,基本就是缓存集中失效。解决雪崩的办法前面说过是过期时间加随机值;解决击穿的互斥锁可以用Caffeine的get(key, loader)方法天然实现单飞模式,比手写Redis锁更轻量。

4.3 分布式事务与接口超时

Seata AT模式踩过最深的坑是"全局事务锁等待超时"。两个事务分别操作同一张表的同一行数据,后一个事务等待前一个事务释放全局锁时,如果前一个事务执行时间过长(比如内部调了一个慢接口),后一个事务就会在到达全局锁等待超时阈值后抛异常。排查思路分三步:一是在Seata控制台看全局事务的状态,确认是"Rollbacked"还是"Committing"卡住;二是看数据库里lock_table表,确认持锁事务的XID和耗时;三是找到那个慢接口,优化它的执行时间或者调整client.rm.lock.retryInterval和client.rm.lock.retryTimes配置。

Feign调用超时也是微服务架构里的高频问题。默认的Feign超时只有1秒,稍微慢一点的SQL或者跨服务调用必然超时。我在application.yml里的配置是:

feign: client: config: default: connectTimeout: 5000 readTimeout: 5000

但超时之后怎么处理才是关键。不能只是把超时时间改长,更重要的是重试机制。Feign的Ribbon重试默认是关闭的,如果开了重试,对于"扣款成功但返回超时"这种幂等性敏感的接口,必须保证接口实现了幂等(比如用OrderId做去重),否则重试会造成重复扣款。我在订单服务里对所有的写接口统一做了幂等校验:请求参数里必须携带一个唯一业务流水号,Redis里存一份,重复请求直接拒绝。

4.4 前端播放与跨域问题

Vue播放m3u8最常见的坑是CORS。m3u8文件和ts分片文件放在MinIO上,前端通过hls.js请求这些资源时,浏览器会发起跨域请求。解决方式是在MinIO的CORS配置里加上允许跨域的规则,或者更简单粗暴——直接用Nginx代理MinIO,让前端请求的是同源地址,不存在跨域问题。

开发阶段最容易出现的是"本地能播放、部署到服务器不能播放"。原因多半是服务器上的MinIO桶权限没配置公开读,或者Nginx代理MinIO时路径没对。排查顺序是:先用浏览器直接访问m3u8文件的地址看看有没有403,如果有就是桶权限问题;如果没有,看hls.js报的错误是网络错误还是解析错误,网络错误排查路径和响应头,解析错误就是文件本身切片有问题,重新用FFmpeg转一次即可。

4.5 性能排查的思路

系统上线后不可避免地会遇到"某个接口特别慢"的问题。我在排查时有一套固定的套路:第一看慢查询日志,在MySQL开启slow_query_log,抓出执行超过1秒的SQL,一般来说排序、分组或者没走索引的查询八成是罪魁祸首;第二看Redis的命中率,如果接口每次都查数据库,说明缓存策略失效;第三看Gateway的请求日志里有没有超时时间特别长的路由,如果有,说明下游服务本身处理逻辑复杂;第四看链路追踪,用Sleuth把一次请求在多个服务之间的耗时放大看,找到耗时占比最大的那个服务节点。

性能优化不是玄学,大部分性能问题都能归因到"某个环节的串行等待"上。比如我曾经优化过一个订单列表接口,原先接口查一次数据库就算完了,但为了拿商品封面图又调了一次商品服务,导致接口耗时从20ms涨到400ms。后来把商品图URL直接冗余进订单表,接口改回只查一次数据库,耗时立刻降到30ms以下。分布式系统里的每一次远程调用都是性能损耗点,能不调用就不调用,这是铁律。

5. 一些扩展方向

这个系统的技术骨架其实可以复用。比如做一个"SpringBoot+Vue+SpringCloud微服务平台"的通用底座,把认证、网关、日志、监控这些基础设施沉淀成一套公共服务,任何垂直业务(餐饮点单、二手交易、校园信息平台)都能在这套底座上快速迭代业务模块。我当时在商品搜索里接入过HanLP做中文标签分词(自动从穿搭描述里提取"街头风""日系""机能风"等风格标签),在订单通知里接入过微信公众号模板消息,这些场景都是底座之外的加分项。

再比如分布式事务里那套"本地消息表+MQ"的最终一致性方案,实际工作中很多系统在用的其实就是这个思路。如果你面试时被问到"分布式系统怎么保证数据一致",你应该先说清楚遇到的是什么级别的一致性需求:强一致的走Seata AT模式,最终一致的走本地消息表或者事务消息。能把"为什么这样选"讲透,比背十个技术名词有用得多。

6. 个人实操体会

项目做下来,最大的体会就是"技术服务于业务,架构服务于演进"。潮服商城这种业务,天然需要快速迭代上新、灵活扩容秒杀、前后端并行开发,所以微服务化的收益远大于成本。但也要清醒地认识到,微服务带来的复杂性(网络延迟、数据一致性、运维成本)是实打实的,如果做一个商场的官网站,单体足够,硬上微服务就是给自己找事。

最后再分享一个实用性很高的小技巧:在网关层把所有业务服务的请求日志统一打出来,包含请求路径、耗时、状态码、用户ID。以后排查线上问题,第一件事不是看数据库,而是去网关日志里翻一次完整的请求链路,根据耗时快速定位问题节点。日志打得好,排查不用愁,这个习惯我从这个项目开始一直保留到现在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询