
简介gulimall谷粒商城是一套覆盖电商全流程的Java微服务实战项目资料包面向具备JavaWeb基础、希望掌握Spring Cloud Alibaba、分布式事务与高并发集群方案的开发者。资源将完整笔记、配套资料、可运行代码整合在一起集群篇已更新完成可对照服务注册、配置中心、网关路由、集群容灾等真实生产场景逐段学习。压缩包共4429个文件、约287.77MB既有711个Java源码和126个Vue组件构成的业务代码也有大量图片素材辅助界面说明配合SQL脚本、YML配置、Dockerfile、Nginx与Registry集群配置以及贯穿全阶段的MD笔记便于按模块查阅和二次开发。目前已有2910人学习下载对于正在系统梳理微服务技术栈或准备分布式相关面试的后端开发者来说是一份可操作性很强的参考资料。1. 集群篇谷粒商城从单机到集群先想清楚这三件事拿到这套「gulimall谷粒商城」资料包我最先翻的不是业务代码而是集群篇。微服务项目教程遍地都是但能把注册中心、配置中心、网关、中间件集群和 Nginx 编排串成一条完整链路的资料很少。资料包里笔记、代码、conf 文件都齐集群篇已经完成对正想把单机版商城往生产级环境迁移的 Java 开发者来说可以直接对着复现。它实际解决三个问题服务如何被动态发现、流量如何在网关和中间件层分摊、部署后如何验证集群真的生效。我按拆包的视角把架构、中间件、部署、验证四条线依次拆开讲。2. 谷粒商城集群架构从 gulimall 模块拆分到 Nacos 注册中心2.1 模块边界与调用链gulimall 的代码结构是典型的多模块 Maven 工程业务服务按电商域拆成 product、order、member、coupon、ware 等独立服务。集群化部署前必须先把每块服务的职责和端口理清楚否则配置网关和负载均衡时容易把 upstream 地址写错。模块默认端口主要职责集群化关注点gulimall-gateway88统一入口路由转发需要多实例上层用 Nginx 做负载均衡gulimall-auth-server12000认证授权OAuth2 接入会话和 token 一致性gulimall-product10000商品、分类、品牌管理强依赖 Redis 缓存和搜索gulimall-order9000订单、购物车、状态机异步消息、分布式事务gulimall-member8000会员等级、积分无状态方便水平扩容gulimall-coupon7000优惠券发放与分摊注意券库存恢复gulimall-ware11000库存、采购数据一致性要求高这些端口在资料包的 SQL 和 YAML 里都能对上。集群化时除了 gateway每个服务都可以多实例注册到 Nacos。服务间调用不再写 IP而是用lb://服务名让负载均衡器去选实例。2.1.1 端口规划与防火墙注意部署多节点集群时防火墙放行不能只开业务端口。Nacos 除了 8848 还有 9848 的 gRPC 端口Redis Cluster 节点间通信是 16379Kafka 是 9092网关是 88。我习惯在安全组里按服务角色分组放行避免图省事全部放通。调用链在单机版是浏览器 → 网关 → 业务服务。上了集群之后动态请求先到 Nginx由 Nginx 把流量分摊给多个网关实例网关再通过注册中心找到目标服务静态资源则由 Nginx 直接返回。2.2 注册中心为什么选 Nacosregistry.conf 到底改了什么服务多实例之后第一件事是服务发现。gulimall 选 Nacos 而不是 Eureka原因很实际Nacos 同时提供注册中心和配置中心一个组件解决两个需求还支持 namespace 隔离环境。资料包里的 registry.conf 是我在集群篇里重点看的一份文件我习惯先把它放在 Nacos 服务端 conf 目录下比对看节点列表和数据库源是否一致。Nacos 集群部署时真正的节点列表在conf/cluster.conf按行写各节点地址。一般我会这样写192.168.1.31:8848 192.168.1.32:8848 192.168.1.33:8848然后在application.properties里接上 MySQL让 Nacos 共享配置和实例数据spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://192.168.1.50:3306/nacos_config?useUnicodetruecharacterEncodingutf8 db.userroot db.passwordyourpassword注意 Nacos 集群每个节点的数据库配置必须一致否则节点间拉不到同一份配置。db.num默认是 1多数据源时按db.url.0、db.url.1递增。2.2.1 namespace 与多环境隔离业务服务侧的注册配置也有几个关键点。每个参与集群的服务都要声明注册地址和命名空间资料包里的标准写法是这样spring: application: name: gulimall-order cloud: nacos: discovery: server-addr: nacos-1:8848,nacos-2:8848,nacos-3:8848 namespace: gulimall-prod config: server-addr: ${spring.cloud.nacos.discovery.server-addr} namespace: gulimall-prod file-extension: yaml server: port: 9000server-addr配置多个节点时客户端会把所有地址当作已知节点某个节点挂了不影响注册。namespace必须和服务端一致否则控制台能看到服务实际调用却是 503。file-extension: yaml是让配置中心去加载gulimall-order.yaml这份文件统一放在 Nacos 配置列表里修改后无需重启服务。2.3 网关集群与路由规则网关是流量的第一道关卡。gulimall-gateway 本身也要注册进 Nacos路由才能用lb://方式转发到目标服务。资料包里的网关配置大致如下spring: cloud: gateway: routes: - id: product_route uri: lb://gulimall-product predicates: - Path/api/product/** filters: - RewritePath/api/product/(?segment.*), /product/$\{segment} - id: order_route uri: lb://gulimall-order predicates: - Path/api/order/** filters: - RewritePath/api/order/(?segment.*), /order/$\{segment}这里最关键的是uri: lb://gulimall-product中的lb://它告诉 Spring Cloud Gateway 使用 LoadBalancerClient 从 Nacos 拉取gulimall-product实例列表而不是走写死的 IP。RewritePath用正则把/api/product/xxx重写为/product/xxx剥离前缀内部服务不需要感知外部 URL 规范。网关多实例后每台实例的路由规则必须一致最简单的做法是把网关配置也放到 Nacos 配置中心避免漏改。3. 中间件集群化Redis Cluster、Kafka 与 Sentinel 的协同部署3.1 Redis Cluster缓存与分布式锁的落点gulimall 的商品详情、首页轮播、用户验证码都走 Redis 缓存。单机 Redis 只要重启缓存击穿就能把数据库打挂。集群篇的方案是 Redis Cluster把 16384 个 slot 分布到多个主从节点上。我一般用 Docker 起最小拓扑做验证核心配置是这几个参数cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 15000 appendonly yes bind 0.0.0.0cluster-enabled开启集群模式cluster-config-file是节点保存集群状态的文件每次启动会重写cluster-node-timeout是节点判断 PONG 超时的时间单位毫秒设太短容易误判太长故障转移慢appendonly yes开启 AOF避免节点重启丢数据。节点起好后用 redis-cli 分配槽位redis-cli --cluster create \ 192.168.1.11:6379 192.168.1.12:6379 192.168.1.13:6379 \ 192.168.1.14:6379 192.168.1.15:6379 192.168.1.16:6379 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配一个从节点三个主三从组成最小容错集群。某一主节点挂了从节点会在cluster-node-timeout之后被提升为主节点这是自动的。3.1.1 槽位迁移与扩缩容集群扩容时槽位不会自动迁移需要手动执行redis-cli --cluster rebalance或reshard。常见做法是先加入新节点再 reshard把部分 slot 从老节点搬过去。搬移过程中客户端可能出现MOVED重定向所以 Spring Boot 配置中的max-redirects要给够spring: redis: cluster: nodes: - 192.168.1.11:6379 - 192.168.1.12:6379 - 192.168.1.13:6379 max-redirects: 3max-redirects是客户端跟随 MOVED/ASK 重定向的最大次数。正常情况一次跳转就能找到目标节点扩容或槽位迁移时需要多次。这里也容易踩坑Redis Cluster 要求所有节点密码一致Spring Boot 的spring.redis.password在集群模式下会应用到每个节点不需要逐个配。3.2 Kafka 集群异步消息与故障切换订单服务创建订单后要通知库存服务锁定库存还要给优惠券服务发消息这些跨服务动作在 gulimall 里走 Kafka。Kafka 集群的配置核心在server.properties每台 broker 需要有独立且唯一的broker.id其他配置保持一致broker.id1 listenersPLAINTEXT://192.168.1.21:9092 log.dirs/data/kafka-logs num.partitions8 default.replication.factor3 offsets.topic.replication.factor3 min.insync.replicas2 zookeeper.connect192.168.1.31:2181,192.168.1.32:2181,192.168.1.33:2181重点解释几个参数offsets.topic.replication.factor决定消费者 offset 提交主题的副本数如果小于 broker 数而某个 broker 宕了部分消费者将无法提交 offsetmin.insync.replicas控制写数据时最少几个副本写入成功才算完成配合 producer 的acksall消息基本不丢log.dirs不要放系统盘Kafka 对磁盘顺序写依赖很大。Producer 端用 KafkaTemplate 发送写法比较简洁Resource private KafkaTemplateString, String kafkaTemplate; public void publishOrderPaidEvent(OrderPaidEvent event) { String json JSON.toJSONString(event); kafkaTemplate.send(order-paid-event, event.getOrderSn(), json); }注意send的第二个参数是 key拿订单号当 key同一订单的支付、取消、超时消息会落在同一分区消费端能按顺序处理。消费端要把 group-id 设置成和项目内一致否则会重复或漏消费KafkaListener(topics order-paid-event, groupId order-event-group) public void onMessage(String json) { OrderPaidEvent event JSON.parseObject(json, OrderPaidEvent.class); // 解锁仓库库存、更新订单状态 }集群环境下同一 group 的消费实例数不要大于分区数否则多出来的实例会一直空转。order-paid-event 如果开了 8 个分区消费实例最多 8 个。这几个参数光看注释不容易记我整理了一个对照表部署时直接对着写参数单机默认集群建议原因offsets.topic.replication.factor13防止 broker 宕机导致 offset 丢失min.insync.replicas12配合 acksall 保证不丢消息default.replication.factor13新建 topic 默认副本数避免单点3.3 Sentinel 集群限流保护网关与核心服务gulimall 学习版常用单机 Sentinel生产集群则需要打开 cluster 模式。Sentinel 集群限流需要一个 Token Server 做全局流量统计普通 client 会把请求数据上报给 Token Server由它统一判定是否放行。给限流规则加集群开关通常同时把规则持久化到 Nacos[ { resource: gulimall-product, count: 500, grade: 1, limitApp: default, strategy: 0, clusterMode: true, clusterConfig: { flowId: 1001, thresholdType: 0 } } ]grade1表示按 QPS 限流thresholdType0表示全局阈值资源在所有节点共享一个 count如果每个节点单独计数用户刷新两次就触发限流了这是常见误配。Sentinel 客户端还需要配置 Token Server 地址spring: cloud: sentinel: transport: port: 8719 dashboard: 192.168.1.40:8858 cluster: server-addr: 192.168.1.41:12000port: 8719是客户端接收控制台心跳的端口。多个服务在同一台机器上时需要改成不同值否则端口冲突会让控制台显示心跳断开。集群限流适合网关和商品服务这种热点入口不建议对内部数据库操作开启。4. 部署实录从 default.conf 到 nginx.conf 的服务编排4.1 资料包里的 conf 文件都是干什么的解开 gulimall 压缩包会看到 .babelrc、file.conf、registry.conf、default.conf、nginx.conf、gulimall.conf 和一些 CSS 文件。这些不全是后端配置。.babelrc 是前端 vue 工程的编译配置GL.css、index.css 是静态资源default.conf 和 nginx.conf 属于 Nginx 层gulimall.conf 是商城站点的 server 配置。file.conf 和 registry.conf 在集群篇里多用于中间件初始化比如 Nacos 的数据源初始化或 Sentinel 集群参数。我一般先按部署角色分类避免拿到包不知道先改哪个。文件归属层部署时的作用.babelrc前端编译控制 ES6 语法转译规则file.conf数据源/初始化组件启动前的数据源或日志配置registry.conf注册中心Nacos 集群节点与库表连接信息default.confNginx默认站点兜底配置nginx.confNginx全局配置包含 http、event、日志gulimall.confNginx商城反向代理和静态资源规则GL.css / index.css前端静态资源商城页面样式由 Nginx 静态托管有一点需要提醒registry.conf 在 Nacos 集群里不是必须的文件名Nacos 原生用 cluster.conf这里的 registry.conf 更像资料作者整理后的可复制模板。放进 Nacos conf 目录前记得先比对 cluster.conf 节点列表。4.2 gulimall.conf 的动静分离配置集群部署时网关前面必须放一层 Nginx负责 SSL 终端、静态资源缓存和网关负载均衡。资料包里的 gulimall.conf 提供了完整的反向代理模板我在此基础上调整了上游网关列表让多个 gateway 实例都能被代理upstream gulimall_gateway { server 192.168.1.10:88 max_fails2 fail_timeout10s; server 192.168.1.11:88 max_fails2 fail_timeout10s; server 192.168.1.12:88 max_fails2 fail_timeout10s; } server { listen 80; server_name mall.example.com; root /usr/share/nginx/html; location / { try_files $uri $uri/ gateway; } location gateway { proxy_pass http://gulimall_gateway; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 5s; proxy_read_timeout 30s; } location ~* \.(css|js|png|jpg|jpeg|gif|svg|woff2)$ { expires 7d; add_header Cache-Control public, immutable; access_log off; } }这里try_files $uri $uri/ gateway先检查 Nginx 本地有没有静态文件有就直接返回没有就把请求交给 upstream 中的网关。proxy_pass不带 URI 后缀会把原始请求路径原样转发给网关。location ~*正则匹配静态资源expires 7d做强缓存。如果前端文件带 hash缓存可以留更久不带 hash 的话建议只缓存 1 小时避免发版后用户看到旧样式。4.2.1 静态资源缓存策略gulimall 的前端里GL.css 和 index.css 这类文件名如果不带 hash更新后浏览器可能仍使用缓存。常见做法是把 Nginx 缓存时间改短同时在后端接口响应头里加 ETag。Nginx 默认会对静态文件生成弱 ETag必要时可以用etag on开启。实际部署时我一般把 CSS、JS 的expires设为 1 小时页面 HTML 设为 no-cache这样既不会每次请求都打到源站也不会出现发版后样式错乱。4.3 集群启动顺序与验证集群部署和单机最大的区别是组件有依赖顺序。我按“基础存储 → 注册中心 → 中间件 → 业务服务 → 网关 → Nginx”的顺序启动# 1. Nacos 集群模式启动 /opt/nacos/bin/startup.sh -m cluster # 2. Kafka 每台 broker 依次启动 /opt/kafka/bin/kafka-server-start.sh -daemon /opt/kafka/config/server.properties # 3. 网关和业务服务用同一份 JAR 启动通过启动参数区分端口 java -jar gulimall-gateway.jar --server.port88 java -jar gulimall-order.jar --server.port9000 # 4. 检查 Nginx 配置并热加载 nginx -t nginx -s reload启动后不要急着打开页面先用 curl 验证链路curl -H Host: mall.example.com http://127.0.0.1/api/product/info/23返回 JSON 而不是 503说明 Nginx → 网关 → product 服务这条链路已通。接着看 Nacos 控制台的服务列表确认每个服务实例数大于 1、健康状态为 true。再看 Redis Cluster 的状态redis-cli --cluster check 192.168.1.11:6379该命令会输出槽位分布、节点状态、主从关系。如果输出里没有 inconsistent 之类的内容说明缓存层正常。排错时最常见的有三个问题。第一服务注册到 Nacos 但网关 503优先检查 namespace 是否一致以及服务名是否被写成了下划线。第二页面能打开但数据加载不出查看 Redis 连接是否用了集群模式单机客户端连集群会报 MOVED 错误。第三Kafka 消费者重复消费检查 group-id 是否换到了新值新 group 会从头消费历史消息。5. 集群压测与验证技巧用一笔订单走完整条链路集群起来以后验证不能只看控制台绿灯。我习惯先用 wrk 对网关和商品接口做一轮压测确认集群部署没有引入明显的性能损耗wrk -t8 -c200 -d60s --latency http://127.0.0.1/api/product/info/23关注输出里的 Requests/sec 和 Latency 分布。如果单实例和集群模式的吞吐差不多就要检查流量是否被网关或 Nginx 配置限制了比如 keepalive 没开、线程池太小。接着验证故障转移。停掉一个网关实例立刻用 curl 循环请求观察失败窗口是否过长for i in $(seq 1 20); do curl -s -o /dev/null -w %{http_code}\n http://127.0.0.1/api/product/info/23 sleep 0.5 done正常情况下Nginx 会在 fail_timeout 之后把请求导到健康节点返回码依然 200。Redis Cluster 的验证方式是杀掉一个主节点然后执行redis-cli -c set test-key hello正常情况下从节点自动晋升为主节点命令仍然成功。Kafka 的验证则是启动控制台消费者手动向 topic 发一条消息看能否消费到/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server 192.168.1.21:9092 \ --topic order-event --from-beginning --group verify-cluster压测过程中还要盯着 JVM用jstat -gcutil pid 1000 10观察老年代和 GC 停顿。如果 Full GC 频繁先调大堆内存不要急着改限流阈值集群模式强调水平扩展单节点堆上限反而可能掩盖问题。Sentinel 规则是否生效可以用连续快速请求验证当接口返回Blocked by Sentinel (flow limiting)说明规则已下发生效。如果这轮验证全过把上面的命令整理成一个verify-cluster.sh每次发版后跑一遍比人工盯着多个控制台直观得多。本文还有配套的精品资源点击获取