ARTICLE DETAIL

资讯详情

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

基于微服务的小程序商城系统实战:拆分、搭建与避坑指南

基于微服务的小程序商城系统实战:拆分、搭建与避坑指南 简介这是一套面向微信小程序开发者与电商项目学习者的微服务商城系统源码资源适合具备一定Java或后端基础、希望理解微服务拆分与电商业务闭环的中高级开发者参考。系统围绕用户中心、商品中心、订单中心与支付中心等模块展开并配套微信端与管理平台可用于学习服务解耦、独立部署与动态扩展的落地思路。资源以zip压缩包形式提供整体约58.77MB文件总数与具体类型明细上游暂未提供可结合包内目录自行梳理工程结构与模块划分。目前已有367人学习下载说明其在同类电商项目中具备一定参考热度。通过该资源读者可重点研究微服务架构下各业务中心的职责边界、微信支付集成方式以及服务治理、监控与追踪等配套设计为搭建可扩展的小程序商城或优化现有系统提供可借鉴的工程样本。1. 从单体到微服务小程序商城系统为什么值得重做一遍如果你做过微信小程序商城大概率经历过这种局面一个 Spring Boot 单体包商品、订单、库存、支付、优惠券全塞在一起刚开始跑得挺顺日订单过千之后开始出问题——改一个优惠券规则要重新部署整个应用库存扣减和订单创建抢同一把数据库锁大促前想单独扩容订单模块却发现只能整体加机器。这不是代码写得差是架构到了天花板。基于微服务的小程序商城系统核心思路是把商城按业务域拆成独立服务商品服务管 SPU/SKU 和类目订单服务管下单、状态机、超时取消库存服务管扣减和回滚用户服务管微信授权登录和收货地址支付服务对接微信支付回调。每个服务独立部署、独立数据库、独立扩缩容小程序端通过网关统一入口调用。这套方案适合已经有单体商城但遇到性能瓶颈的团队也适合从零搭建、一开始就想把边界划清楚的项目。下面从拆分、搭建、联调、避坑到进阶把这条路走通。2. 微服务拆分商城业务域怎么切才不后悔2.1 按业务能力切不是按技术分层切很多团队第一次拆微服务容易按「Controller 层一个服务、Service 层一个服务」来切这是典型的翻车做法。微服务的边界应该对齐业务能力而不是技术分层。商城的业务能力天然有这么几块商品含类目、品牌、SKU、订单含购物车、结算、状态流转、库存含锁定、扣减、回滚、用户含微信登录、地址、会员等级、营销含优惠券、满减、秒杀、支付含统一下单、回调、退款。拆的时候问自己三个问题这个模块的数据是否被其他模块频繁联表查询这个模块的变更频率是否和其他模块差异很大这个模块是否需要独立扩缩容如果三个答案都是「是」就该拆出去。比如库存大促时扣减 QPS 是平时的几十倍但商品信息读多写少两者放一起就是互相拖累。常见做法是先用领域驱动设计的限界上下文画一遍再对照实际团队规模调整。五到八人的团队拆五到六个服务就够了拆太细运维成本会吃掉收益。2.2 数据库拆分策略与会话一致性服务拆了数据库跟不跟着拆我的血泪经验是第一版可以共享一个物理库但分 Schema每个服务只访问自己的 Schema禁止跨 Schema 联表。这样迁移成本低又能强制约束边界。等业务稳定了再逐步迁到独立实例。跨服务的数据一致性是绕不开的。下单要同时创建订单和扣库存这两个操作在不同服务里不能用本地事务。常见方案是方案适用场景一致性强度复杂度TCC资金、库存等强一致强高本地消息表订单创建后异步通知最终一致中Seata AT快速接入分布式事务较强中Saga长流程、多步骤补偿最终一致中高我一般会推荐本地消息表 定时补偿理由是它对业务代码侵入小不依赖额外中间件出问题也容易排查。具体做法是在订单库建一张local_message表创建订单的本地事务里同时插入一条「扣库存」消息然后由定时任务或独立线程投递到库存服务库存服务处理成功后回调确认。CREATE TABLE local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_type VARCHAR(32) NOT NULL COMMENT 业务类型如 ORDER_CREATE, biz_id VARCHAR(64) NOT NULL COMMENT 业务ID如订单号, payload TEXT NOT NULL COMMENT 消息体JSON, status TINYINT DEFAULT 0 COMMENT 0待投递 1已投递 2已完成 3失败, retry_count INT DEFAULT 0, next_retry_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status_retry (status, next_retry_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表的关键字段是status和next_retry_at。投递线程只扫status0 且 next_retry_at now()的记录失败后按指数退避更新next_retry_at重试超过阈值置为失败并告警。biz_id用来做幂等库存服务收到消息先查自己的处理记录表处理过就直接返回成功。2.3 服务间通信同步还是异步商品详情页要展示库存这是同步调用下单后扣库存这是异步消息。判断标准很简单调用方是否需要立即拿到结果来决定下一步。需要就是同步Feign/gRPC不需要就是异步RocketMQ/RabbitMQ。同步调用要设超时和熔断。我见过太多项目 Feign 默认超时是 60 秒库存服务卡住直接把订单服务线程池打满整条链路雪崩。配置示例feign: client: config: default: connectTimeout: 2000 readTimeout: 3000 loggerLevel: basic circuitbreaker: enabled: true resilience4j: circuitbreaker: instances: inventory: slidingWindowSize: 20 failureRateThreshold: 50 waitDurationInOpenState: 10sconnectTimeout设 2 秒、readTimeout设 3 秒是经验值商城接口超过 3 秒用户已经想关页面了。熔断器滑动窗口 20 次、失败率超 50% 就打开打开后 10 秒内直接走降级逻辑比如返回「库存查询繁忙」而不是报错。3. 用 Spring Cloud 搭骨架从注册中心到网关的最小可跑系统3.1 服务注册与配置中心选型注册中心用 Nacos 还是 Eureka2026 年了Eureka 2.x 已经停止维护新项目没有理由再选它。Nacos 同时做注册中心和配置中心一个组件解决两件事运维成本低。Consul 也可以但国内社区资料 Nacos 更丰富。Nacos 服务端启动# 单机模式启动生产环境用集群模式 sh startup.sh -m standalone # 验证 curl http://127.0.0.1:8848/nacos/v1/ns/operator/metrics服务端跑起来后每个微服务的bootstrap.yml里配 Nacos 地址spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: mall-dev group: DEFAULT_GROUP config: server-addr: 127.0.0.1:8848 namespace: mall-dev file-extension: yaml shared-configs: ->spring: cloud: gateway: routes: - id: product-service uri: lb://product-service predicates: - Path/api/product/** filters: - StripPrefix2 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200 - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2StripPrefix2去掉/api/product前缀再转发服务内部 Controller 不用带前缀。限流用 Redis 令牌桶replenishRate是每秒补充令牌数burstCapacity是桶容量商品查询这种读接口可以设大一点下单接口要设小防止刷单。小程序端请求封装要注意微信小程序的wx.request不支持拦截器需要自己包一层。同时网关鉴权后要把用户 ID 透传到下游通常放在请求头X-User-Id里。// utils/request.js const BASE_URL https://gateway.example.com; function request(options) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); 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 401) { // token 过期重新走微信登录 wx.removeStorageSync(token); reject(new Error(UNAUTHORIZED)); return; } if (res.data.code ! 0) { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); return; } resolve(res.data.data); }, fail(err) { reject(err); } }); }); } module.exports { request };这段封装做了三件事统一拼网关地址、自动带 token、统一处理业务错误码。注意401的处理——不要在这里直接跳登录页而是 reject 一个特定错误让调用方决定因为有些页面允许游客访问。3.3 微信登录与用户服务对接小程序登录流程wx.login拿 code → 后端用 code 换 openid → 查用户是否存在 → 不存在则注册 → 签发 JWT。用户服务接口PostMapping(/auth/login) public ResultLoginVO login(RequestBody LoginDTO dto) { // 1. 调用微信接口换 openid String url https://api.weixin.qq.com/sns/jscode2session ?appid appId secret appSecret js_code dto.getCode() grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); if (openid null) { return Result.fail(微信登录失败: json.getString(errmsg)); } // 2. 查或建用户 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户); userMapper.insert(user); } // 3. 签发 JWT String token jwtUtil.sign(user.getId(), user.getOpenid()); return Result.ok(new LoginVO(token, user.getNickname())); }appId和appSecret必须放配置中心加密存储不能硬编码。JWT 的过期时间建议 7 天配合 refresh token 做续期。注意微信的jscode2session接口有频率限制同一用户短时间内重复登录要加本地缓存。4. 本地联调与部署go 微服务与 Java 微服务混合怎么跑通4.1 混合技术栈的注册与调用有些团队商品服务用 Go 写性能好、内存占用低订单服务用 Java 写两者都要注册到同一个 Nacos。Go 侧用nacos-sdk-go// go 服务注册到 Nacos client, _ : vo.NewClient(vo.NacosClientParam{ ClientConfig: constant.ClientConfig{ NamespaceId: mall-dev, TimeoutMs: 5000, }, ServerConfigs: []constant.ServerConfig{ {IpAddr: 127.0.0.1, Port: 8848}, }, }) client.RegisterInstance(vo.RegisterInstanceParam{ Ip: 192.168.1.100, Port: 8081, ServiceName: product-service, GroupName: DEFAULT_GROUP, Weight: 10, Enable: true, Healthy: true, Ephemeral: true, })Java 侧通过 Feign 调用 Go 服务时注意 Go 服务返回的 JSON 字段命名风格可能和 Java 不一致。统一约定用 snake_case 还是 camelCase在网关或 Feign 的 Decoder 里做转换。我一般会在 Go 侧用json:product_id显式指定 tagJava 侧 DTO 用JsonProperty(product_id)对齐。4.2 本地一键启动脚本微服务本地开发最烦的是要手动一个个启动。写个脚本按依赖顺序拉起#!/bin/bash # start-all.sh 本地开发环境启动脚本 set -e echo 1. 启动 Nacos... cd ~/nacos/bin sh startup.sh -m standalone sleep 5 echo 2. 启动 Redis... redis-server --daemonize yes echo 3. 启动 MySQL... brew services start mysql8.0 echo 4. 启动基础服务... java -jar user-service/target/user-service.jar --spring.profiles.activedev java -jar product-service/target/product-service.jar --spring.profiles.activedev sleep 8 echo 5. 启动业务服务... java -jar order-service/target/order-service.jar --spring.profiles.activedev java -jar inventory-service/target/inventory-service.jar --spring.profiles.activedev java -jar gateway/target/gateway.jar --spring.profiles.activedev echo 6. 等待健康检查... for port in 8081 8082 8083 8084 8080; do until curl -s http://localhost:$port/actuator/health | grep -q UP; do sleep 2 done echo port $port is UP done echo 所有服务已启动关键点是启动顺序Nacos 和 Redis 必须先起用户和商品服务不依赖订单可以并行订单依赖库存和商品放后面。健康检查用 Spring Boot Actuator 的/actuator/health确保服务真正就绪再继续。4.3 接口联调与链路追踪联调时最怕的是「订单创建失败但不知道哪个服务出的问题」。加 Sleuth Zipkin 做链路追踪spring: sleuth: sampler: probability: 1.0 zipkin: base-url: http://localhost:9411 sender: type: webprobability: 1.0是本地开发全采样生产环境设 0.1 或更低避免性能损耗。每个请求会带一个 traceId在日志里打印出来出问题直接拿 traceId 去 Zipkin 查完整调用链。日志格式建议配成logging: pattern: level: %5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]这样每条日志都带服务名和 traceIdgrep 一下就能串起来。5. 避坑指南小程序商城微服务化最容易翻车的五个点5.1 库存超卖现象是活动开始后库存变成负数现象秒杀活动开始库存 100 件实际卖出 130 件数据库库存字段出现负值。原因扣减库存用了「先查再改」的写法两个请求同时查到库存 1都判断够都执行了扣减。或者用了 Redis 预扣但没做原子操作。解决数据库层用UPDATE inventory SET stock stock - #{num} WHERE sku_id #{skuId} AND stock #{num}靠stock num这个条件保证不会扣成负数返回影响行数为 0 就说明库存不足。Redis 层用 Lua 脚本保证查和扣的原子性。两者配合Redis 挡流量数据库做最终兜底。5.2 小程序端 token 过期后请求全部失败现象用户用了一段时间后所有接口返回 401页面白屏。原因JWT 过期后前端没有续期机制直接 reject 了但页面没有统一处理。解决在 request 封装里加 refresh token 逻辑。收到 401 时先用 refresh token 换新 token换成功则重试原请求换失败才跳登录。注意要加一个「正在刷新」的标志位避免并发请求同时去刷新。5.3 服务间循环依赖导致启动失败现象订单服务启动时报「circular dependency」或者启动后调用超时。原因订单服务调库存服务库存服务又回调订单服务查订单状态形成环。解决把回调改成异步消息库存服务处理完发消息到 MQ订单服务消费消息更新状态。如果必须同步把公共逻辑抽到第三个服务或者用事件驱动解耦。启动时的循环依赖检查可以临时用spring.main.allow-circular-referencestrue绕过但这是治标不治本。5.4 Nacos 配置变更后部分实例没生效现象在 Nacos 改了限流阈值有的服务实例生效了有的还是旧值。原因配置没有加RefreshScope或者用了静态变量导致刷新不生效。解决需要动态刷新的配置类加RefreshScope注解配置项用Value注入而不是静态字段。另外检查 Nacos 的shared-configs和extension-configs是否都配了refresh: true。改完在 Nacos 控制台看「监听查询」确认所有实例都在监听列表里。5.5 网关转发后丢失请求头现象小程序带了Authorization头但下游服务拿不到鉴权失败。原因Spring Cloud Gateway 默认会过滤掉一些敏感头或者自定义过滤器里没有把原始头传下去。解决在网关配置里加spring.cloud.gateway.default-filters里的PreserveHostHeader自定义过滤器里用exchange.getRequest().mutate().header(...)显式传递。另外注意下划线开头的头可能被 Nginx 或网关丢弃用中划线命名。6. 进阶技巧用灰度发布和压测把商城微服务跑稳微服务拆完之后真正的挑战是「怎么安全地上线新版本」。全量发布一旦出问题就是全站故障灰度发布是必备能力。基于 Nacos 的元数据做灰度给实例打上versionv2的标签网关根据用户 ID 哈希决定路由到 v1 还是 v2。// 网关灰度过滤器 public class GrayFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (userId ! null Math.abs(userId.hashCode()) % 100 10) { // 10% 流量走灰度版本 exchange.getAttributes().put(service.version, v2); } else { exchange.getAttributes().put(service.version, v1); } return chain.filter(exchange); } Override public int getOrder() { return -100; } }配合 Nacos 的NacosRule或自定义负载均衡器从实例列表里筛选对应 version 的实例。灰度比例从 1% 开始观察 30 分钟错误率和 RT 没有明显上升再逐步放大。压测是另一个必须做的动作。用 JMeter 或 wrk 对网关压重点看三个指标P99 延迟、错误率、各服务线程池活跃数。我一般会先压单接口找到瓶颈服务再压混合场景看整体容量。压测时把 Nacos 的日志级别调到 WARN否则注册中心的心跳日志会把磁盘写满。最后说一个我踩过的坑微服务数量上去之后本地开发机器的内存根本不够跑全套。我的习惯是本地只跑正在开发的那两三个服务其余全部连开发环境的 Nacos 和数据库。这样既省资源又能保证联调环境一致。配置上把spring.cloud.nacos.discovery.register-enabled设为 false本地服务不注册到开发环境避免污染别人的调用。希望帮到你。本文还有配套的精品资源点击获取
返回列表