基于微服务的小程序商城系统:拆分、联调与避坑实战
简介这是一套面向微信小程序电商开发者的微服务架构商城系统学习资源适合具备一定Java或后端基础、希望掌握分布式电商项目实战的开发者与计算机专业学生。系统围绕用户中心、商品中心、订单中心、支付中心等核心模块展开并集成微信端入口、商家管理平台以及服务治理、监控与追踪能力可用于课程设计、毕业项目或企业级电商方案参考。资源以zip压缩包形式提供整体约58.77MB文件类型明细与文件总数上游暂未提供故不展开说明。目前已有367人学习关注说明该主题在开发者社区中具有一定热度。通过这套资料读者可以理解微服务拆分思路、各中心模块的职责边界与协作方式掌握订单流程、支付集成、服务治理与监控追踪等关键设计并借鉴其模块化、可扩展的架构组织方式为自建小程序商城或优化现有系统提供可落地的参考。1. 基于微服务的小程序商城系统从单体到拆分的真实落地路径很多团队做小程序商城第一版都是单体 Spring Boot 打天下日订单几百单时跑得挺欢一旦上秒杀、上直播带货订单、库存、支付、营销全挤在一个进程里改一处发全站数据库连接池被打满一个慢 SQL 拖垮整个商城。基于微服务的小程序商城系统本质是把「商品、订单、库存、支付、营销、用户」这些变化频率和伸缩需求完全不同的模块拆成独立服务让小程序端只面对一个网关后端各自演进。它适合已经跑通单体商城、日活过万、团队超过 5 人、开始被发布耦合和局部扩容折磨的团队如果你还在验证商业模式单体加模块化分包反而更快。这篇笔记按「拆什么、怎么拆、怎么联调、坑在哪」讲一遍我实际趟过的路径微服务架构图不是画给领导看的是给排障用的。2. 微服务拆分小程序商城该按什么边界切服务2.1 先定拆分维度再谈微服务架构图小程序商城的业务天然分几类高频读、低频写、强一致、弱一致。商品详情和分类是典型高频读适合独立商品服务加多级缓存订单创建和库存扣减是强一致写必须放一起或至少同库事务支付回调、退款是外部依赖重、幂等要求高的场景单独拆支付服务优惠券、满减、秒杀属于营销域流量尖峰明显独立出来才能单独扩容。我一般按「业务能力 数据一致性边界」两个维度切而不是按 controller 数量切。一个常见的微服务架构图落地成服务清单是这样的服务名职责数据一致性要求扩容特征gateway小程序统一入口、鉴权、限流无状态随流量水平扩user-service登录、收货地址、会员最终一致平稳product-service商品、分类、SKU最终一致读多写少缓存扛order-service下单、订单状态机强一致写峰值明显inventory-service库存扣减、回滚强一致与订单联动pay-service支付、回调、退款幂等强一致外部依赖重marketing-service优惠券、秒杀、满减最终一致尖峰极强这张表比任何架构图都实用因为它直接告诉你哪些服务要单独压测、哪些要配独立数据库。小程序端不感知服务拆分它只调网关暴露的聚合接口比如/api/order/confirm由 order-service 聚合商品和库存信息返回。2.2 用 Spring Cloud Alibaba 搭最小可运行骨架选型上国内团队做小程序商城Spring Cloud Alibaba 是踩坑最少的组合Nacos 做注册和配置中心Sentinel 做限流降级Seata 处理跨服务事务Gateway 做网关。下面是一个 order-service 的最小骨架重点是注册、配置、远程调用三件事。# order-service/src/main/resources/bootstrap.yml spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 注册中心地址 config: server-addr: 127.0.0.1:8848 # 配置中心地址 file-extension: yaml server: port: 8082// OrderController.java 关键片段 RestController RequestMapping(/order) public class OrderController { Autowired private ProductFeignClient productFeignClient; // 远程调商品服务 PostMapping(/confirm) public ResultOrderConfirmVO confirm(RequestBody OrderConfirmDTO dto) { // 1. 远程查商品和库存Feign 自带负载均衡 ProductVO product productFeignClient.getSku(dto.getSkuId()); // 2. 本地组装确认单不落库 return Result.ok(buildConfirmVO(product, dto)); } }// ProductFeignClient.java FeignClient(name product-service, fallback ProductFallback.class) public interface ProductFeignClient { GetMapping(/sku/{skuId}) ProductVO getSku(PathVariable(skuId) Long skuId); }逻辑说明bootstrap.yml 在应用启动最早期加载保证连上 Nacos 后才初始化其他 BeanFeign 客户端用服务名而非 IPNacos 负责实例列表和健康检查fallback 是降级兜底商品服务挂了不能直接让下单页白屏。参数上file-extension: yaml要和 Nacos 里配置的 Data ID 后缀一致否则拉不到配置这是新手最常翻车的地方。端口规划建议网关 8080、用户 8081、订单 8082、商品 8083、库存 8084、支付 8085本地联调时一眼能对上。2.3 库存和订单的边界别把强一致拆没了很多人一上来就把库存独立成服务结果下单时 order-service 调 inventory-service 扣库存网络超时后订单状态和库存状态对不上用户看到下单成功但库存没扣。常见做法是下单主流程里订单和库存扣减放在同一个本地事务通过同库或 Seata AT 模式保证真正独立的库存服务只负责库存查询和异步对账。如果非要拆必须给扣减接口设计幂等键订单号 SKU并在 order-service 侧记录扣减流水定时对账补偿。这个边界没定好后面全是血泪经验。3. 小程序端对接微服务网关请求封装与联调3.1 微信小程序请求封装让前端只认一个域名小程序端不关心后端有几个服务它只认一个 HTTPS 域名。网关层做统一鉴权、统一返回结构、统一错误码。前端封装要解决三件事token 注入、loading 管理、错误统一提示。下面是我常用的请求封装基于原生wx.request不引第三方库。// utils/request.js const BASE_URL https://api.your-mall.com/gateway; function request(options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer token : // 网关统一校验 }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); // token 失效跳登录 reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明Authorization头由网关的全局过滤器解析解析失败直接返回 401业务服务不用各自写鉴权。参数上BASE_URL指向网关而非具体服务网关根据路径前缀路由比如/api/order/**转 order-service。注意小程序要求所有请求域名在后台配置白名单本地联调时可以在开发者工具里勾选「不校验合法域名」但上线前必须配好否则真机直接请求失败。3.2 本地联调go 微服务与系统如何启动与联调的思路迁移虽然这套商城是 Java 栈但联调思路和 go 微服务与系统如何启动与联调是一样的先起注册中心和配置中心再起依赖服务最后起网关和前端。我一般写一个start-all.sh按依赖顺序拉起避免手动一个个点。#!/bin/bash # 启动顺序注册中心 - 配置中心 - 基础服务 - 业务服务 - 网关 nohup java -jar nacos-server.jar -m standalone nacos.log 21 sleep 15 nohup java -jar user-service.jar user.log 21 nohup java -jar product-service.jar product.log 21 nohup java -jar inventory-service.jar inventory.log 21 nohup java -jar order-service.jar order.log 21 nohup java -jar gateway.jar gateway.log 21 echo all services started, check logs for errors逻辑说明Nacos 单机模式启动约 10 到 15 秒sleep 是给它留注册时间否则后面的服务连不上注册中心会启动失败。参数上每个服务建议配spring.cloud.nacos.discovery.register-enabledtrue本地调试某个服务时可以临时关掉注册避免半成品实例被网关路由到。联调时先看 Nacos 控制台的服务列表实例数为 1 且健康才说明注册成功再去调网关接口这个顺序能省掉大量瞎猜。3.3 网关路由与跨域小程序不需要 CORS 但 H5 需要小程序请求不受浏览器同源策略限制所以纯小程序端不需要配 CORS。但如果你的商城同时有 H5 版网关必须处理跨域。Gateway 里加一个全局 CORS 配置即可注意allowedOriginPatterns不要用*加allowCredentialstrue浏览器会直接拒绝。spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOriginPatterns: https://h5.your-mall.com allowedMethods: * allowedHeaders: * allowCredentials: true routes: - id: order_route uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1逻辑说明lb://表示走负载均衡到服务名StripPrefix1去掉路径第一段/api这样 order-service 收到的就是/order/confirm。参数上路由顺序会影响匹配精确路径放前面通配放后面。踩坑提醒网关超时默认较短下单接口涉及多个远程调用建议把spring.cloud.gateway.httpclient.response-timeout调到 10 秒以上否则高峰期大量请求被网关提前掐断日志里只看到 504很难定位。4. 数据一致性与分布式事务订单、库存、支付怎么不打架4.1 优先用本地消息表别急着上 Seata跨服务事务是微服务商城最容易过度设计的地方。我的经验是能不用分布式事务就不用。下单扣库存如果订单和库存同库直接本地事务如果必须跨库用本地消息表加定时补偿比 Seata AT 模式更可控。Seata 的全局锁在高并发下会成为瓶颈而且回滚日志膨胀很快。本地消息表的做法order-service 在本地事务里同时写订单和一条「待扣库存」消息然后异步发 MQ 给 inventory-service库存服务消费成功后回调确认失败则重试。关键表结构CREATE TABLE order_local_msg ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, sku_id BIGINT NOT NULL, quantity INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待发送 1已发送 2已完成 3失败, retry_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_sku (order_no, sku_id) );逻辑说明uk_order_sku唯一索引保证同一订单同一 SKU 只扣一次这是幂等的最后一道防线。参数上retry_count超过 5 次就告警人工介入不要无限重试。定时任务扫status0的记录补发扫status1且超过 30 秒未完成的记录查询库存服务实际结果这个「查询补偿」比盲目重试安全得多。4.2 支付回调的幂等后悔药要提前吃支付回调是外部系统驱动微信可能重复通知网络抖动也可能让你收到多次。pay-service 处理回调必须做三件事验签、幂等、状态机校验。幂等键用微信的transaction_id落库唯一索引。状态机只允许「待支付 - 已支付」重复回调直接返回成功但不重复改订单。Transactional public void handlePayCallback(PayCallbackDTO dto) { // 1. 验签失败直接抛异常 if (!verifySign(dto)) { throw new BizException(签名校验失败); } // 2. 幂等插入唯一索引冲突说明已处理过 try { payRecordMapper.insert(buildRecord(dto)); } catch (DuplicateKeyException e) { log.warn(重复回调transaction_id{}, dto.getTransactionId()); return; // 直接返回不重复处理 } // 3. 状态机校验后更新订单 orderService.markPaid(dto.getOrderNo(), dto.getTransactionId()); }逻辑说明先验签再幂等顺序不能反否则伪造请求会污染幂等表。参数上transaction_id字段长度按微信文档留够别用varchar(32)卡边界。踩坑提醒回调接口必须快速返回微信超时会重试业务逻辑重的部分比如发券、发短信应该丢到 MQ 异步做回调里只做核心状态变更。4.3 缓存与数据库一致性商品详情别读脏商品服务读多写少缓存是必须的。常见做法是「先更新数据库再删除缓存」而不是更新缓存。删除失败时用 MQ 补偿删除。商品详情页缓存 key 用product:detail:{skuId}过期时间加随机值防止雪崩。public ProductVO getSku(Long skuId) { String key product:detail: skuId; String cached redis.get(key); if (cached ! null) { return JSON.parseObject(cached, ProductVO.class); } // 缓存空值防穿透过期时间短一些 ProductVO vo productMapper.selectById(skuId); if (vo null) { redis.setex(key, 60, ); // 空值缓存 60 秒 return null; } // 过期时间加随机 0-300 秒防雪崩 int expire 1800 new Random().nextInt(300); redis.setex(key, expire, JSON.toJSONString(vo)); return vo; }逻辑说明空值缓存是防穿透的廉价手段但过期时间要短否则商品上架后 60 秒内查不到。参数上基础过期 1800 秒是商品详情比较合适的值太长会导致改价不及时太短缓存命中率上不去。更新商品时先update数据库再redis.del(key)删除失败发 MQ 重试不要图省事直接更新缓存并发下容易写出旧值覆盖新值。5. 避坑与排查微服务商城上线后最常翻的 5 个车5.1 服务注册上了但网关 503现象Nacos 控制台能看到实例但通过网关调接口返回 503 Service Unavailable。原因通常是网关和业务服务不在同一个 Nacos 命名空间或者服务注册的是内网 IP 而网关所在机器访问不到。解决检查spring.cloud.nacos.discovery.namespace是否一致检查实例 IP 是否可达本地多网卡时显式指定spring.cloud.nacos.discovery.ip。5.2 Feign 调用超时但被调服务日志正常现象order-service 报 Read timed out但 product-service 日志显示接口 200 毫秒就返回了。原因多半是 Feign 默认超时太短或者 Ribbon 负载到了某个假死实例。解决配置feign.client.config.default.readTimeout5000并开启重试spring.cloud.loadbalancer.retry.enabledtrue同时检查 Nacos 健康检查是否及时剔除假死实例。5.3 小程序端 token 过期后疯狂弹登录页现象token 失效时页面多个并发请求同时返回 401触发多次跳转登录。原因是没有做 401 的全局去重。解决在 request 封装里加一个isRedirecting标志第一个 401 触发跳转后续 401 直接 reject 不跳转跳转完成后重置标志。5.4 秒杀时库存扣成负数现象压测时库存出现负值。原因是扣减 SQL 写成了先查再减并发下超卖。解决用UPDATE inventory SET stock stock - #{num} WHERE sku_id #{skuId} AND stock #{num}根据影响行数判断是否成功影响行数为 0 直接返回库存不足。这是最经典也最容易犯的错。5.5 配置中心改了配置但服务没生效现象Nacos 里改了限流阈值服务行为没变。原因是没有加RefreshScope或者配置的 Data ID 和服务名不匹配。解决需要动态刷新的 Bean 加RefreshScopeData ID 用${spring.application.name}.${file-extension}格式改完在 Nacos 控制台确认「监听查询」里有这个服务。6. 进阶用 Sentinel 给秒杀接口做热点参数限流秒杀是商城流量的核弹网关层限流只能挡总量挡不住「同一个热门 SKU 被瞬间打爆」。Sentinel 的热点参数限流能按参数维度控制比如限制同一个skuId每秒最多 500 次请求超过的直接快速失败保护库存服务。配置分两步代码里加SentinelResource控制台配热点规则。SentinelResource(value seckill, blockHandler seckillBlock) PostMapping(/seckill/{skuId}) public ResultString seckill(PathVariable Long skuId) { // 核心秒杀逻辑走 MQ 异步下单 seckillProducer.send(new SeckillMsg(skuId, getCurrentUserId())); return Result.ok(排队中); } // 被限流时的兜底方法参数和返回值要和原方法一致最后多一个 BlockException public ResultString seckillBlock(Long skuId, BlockException ex) { return Result.fail(当前抢购人数过多请稍后再试); }逻辑说明blockHandler方法必须和原方法同签名且多一个BlockException参数否则 Sentinel 找不到兜底会直接抛异常。参数上热点规则在 Sentinel 控制台配资源名seckill参数索引 0对应 skuId单机阈值 500统计窗口 1 秒。注意参数索引从 0 开始PathVariable的顺序就是索引顺序写错索引规则不生效这个坑我踩过。验证限流是否生效可以用 JMeter 或 wrk 对同一个 skuId 压测观察 Sentinel 控制台的 QPS 曲线和 block 数。如果 block 数一直是 0先检查规则是否推送成功再看SentinelResource的 value 是否和控制台资源名完全一致大小写敏感。最后说个习惯我每次上线新服务前都会先在本地用start-all.sh全量拉起跑一遍「登录 - 浏览商品 - 下单 - 支付回调模拟 - 查订单」的冒烟链路确认网关路由、Feign 调用、缓存、MQ 都通再发测试环境。微服务商城的复杂度不在写代码在联调和排障把冒烟链路脚本化比任何架构图都值钱。希望帮到你。本文还有配套的精品资源点击获取