
簡介gulimall谷粒商城是一套覆蓋電商全流程的Java微服務實戰(zhàn)項目資料包面向具備JavaWeb基礎、希望掌握Spring Cloud Alibaba、分布式事務與高并發(fā)集群方案的開發(fā)者。資源將完整筆記、配套資料、可運行代碼整合在一起集群篇已更新完成可對照服務注冊、配置中心、網(wǎng)關路由、集群容災等真實生產(chǎn)場景逐段學習。壓縮包共4429個文件、約287.77MB既有711個Java源碼和126個Vue組件構(gòu)成的業(yè)務代碼也有大量圖片素材輔助界面說明配合SQL腳本、YML配置、Dockerfile、Nginx與Registry集群配置以及貫穿全階段的MD筆記便于按模塊查閱和二次開發(fā)。目前已有2910人學習下載對于正在系統(tǒng)梳理微服務技術?;驕蕚浞植际较嚓P面試的后端開發(fā)者來說是一份可操作性很強的參考資料。1. 集群篇谷粒商城從單機到集群先想清楚這三件事拿到這套「gulimall谷粒商城」資料包我最先翻的不是業(yè)務代碼而是集群篇。微服務項目教程遍地都是但能把注冊中心、配置中心、網(wǎng)關、中間件集群和 Nginx 編排串成一條完整鏈路的資料很少。資料包里筆記、代碼、conf 文件都齊集群篇已經(jīng)完成對正想把單機版商城往生產(chǎn)級環(huán)境遷移的 Java 開發(fā)者來說可以直接對著復現(xiàn)。它實際解決三個問題服務如何被動態(tài)發(fā)現(xiàn)、流量如何在網(wǎng)關和中間件層分攤、部署后如何驗證集群真的生效。我按拆包的視角把架構(gòu)、中間件、部署、驗證四條線依次拆開講。2. 谷粒商城集群架構(gòu)從 gulimall 模塊拆分到 Nacos 注冊中心2.1 模塊邊界與調(diào)用鏈gulimall 的代碼結(jié)構(gòu)是典型的多模塊 Maven 工程業(yè)務服務按電商域拆成 product、order、member、coupon、ware 等獨立服務。集群化部署前必須先把每塊服務的職責和端口理清楚否則配置網(wǎng)關和負載均衡時容易把 upstream 地址寫錯。模塊默認端口主要職責集群化關注點gulimall-gateway88統(tǒng)一入口路由轉(zhuǎn)發(fā)需要多實例上層用 Nginx 做負載均衡gulimall-auth-server12000認證授權(quán)OAuth2 接入會話和 token 一致性gulimall-product10000商品、分類、品牌管理強依賴 Redis 緩存和搜索gulimall-order9000訂單、購物車、狀態(tài)機異步消息、分布式事務gulimall-member8000會員等級、積分無狀態(tài)方便水平擴容gulimall-coupon7000優(yōu)惠券發(fā)放與分攤注意券庫存恢復gulimall-ware11000庫存、采購數(shù)據(jù)一致性要求高這些端口在資料包的 SQL 和 YAML 里都能對上。集群化時除了 gateway每個服務都可以多實例注冊到 Nacos。服務間調(diào)用不再寫 IP而是用lb://服務名讓負載均衡器去選實例。2.1.1 端口規(guī)劃與防火墻注意部署多節(jié)點集群時防火墻放行不能只開業(yè)務端口。Nacos 除了 8848 還有 9848 的 gRPC 端口Redis Cluster 節(jié)點間通信是 16379Kafka 是 9092網(wǎng)關是 88。我習慣在安全組里按服務角色分組放行避免圖省事全部放通。調(diào)用鏈在單機版是瀏覽器 → 網(wǎng)關 → 業(yè)務服務。上了集群之后動態(tài)請求先到 Nginx由 Nginx 把流量分攤給多個網(wǎng)關實例網(wǎng)關再通過注冊中心找到目標服務靜態(tài)資源則由 Nginx 直接返回。2.2 注冊中心為什么選 Nacosregistry.conf 到底改了什么服務多實例之后第一件事是服務發(fā)現(xiàn)。gulimall 選 Nacos 而不是 Eureka原因很實際Nacos 同時提供注冊中心和配置中心一個組件解決兩個需求還支持 namespace 隔離環(huán)境。資料包里的 registry.conf 是我在集群篇里重點看的一份文件我習慣先把它放在 Nacos 服務端 conf 目錄下比對看節(jié)點列表和數(shù)據(jù)庫源是否一致。Nacos 集群部署時真正的節(jié)點列表在conf/cluster.conf按行寫各節(jié)點地址。一般我會這樣寫192.168.1.31:8848 192.168.1.32:8848 192.168.1.33:8848然后在application.properties里接上 MySQL讓 Nacos 共享配置和實例數(shù)據(jù)spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://192.168.1.50:3306/nacos_config?useUnicodetruecharacterEncodingutf8 db.userroot db.passwordyourpassword注意 Nacos 集群每個節(jié)點的數(shù)據(jù)庫配置必須一致否則節(jié)點間拉不到同一份配置。db.num默認是 1多數(shù)據(jù)源時按db.url.0、db.url.1遞增。2.2.1 namespace 與多環(huán)境隔離業(yè)務服務側(cè)的注冊配置也有幾個關鍵點。每個參與集群的服務都要聲明注冊地址和命名空間資料包里的標準寫法是這樣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配置多個節(jié)點時客戶端會把所有地址當作已知節(jié)點某個節(jié)點掛了不影響注冊。namespace必須和服務端一致否則控制臺能看到服務實際調(diào)用卻是 503。file-extension: yaml是讓配置中心去加載gulimall-order.yaml這份文件統(tǒng)一放在 Nacos 配置列表里修改后無需重啟服務。2.3 網(wǎng)關集群與路由規(guī)則網(wǎng)關是流量的第一道關卡。gulimall-gateway 本身也要注冊進 Nacos路由才能用lb://方式轉(zhuǎn)發(fā)到目標服務。資料包里的網(wǎng)關配置大致如下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剝離前綴內(nèi)部服務不需要感知外部 URL 規(guī)范。網(wǎng)關多實例后每臺實例的路由規(guī)則必須一致最簡單的做法是把網(wǎng)關配置也放到 Nacos 配置中心避免漏改。3. 中間件集群化Redis Cluster、Kafka 與 Sentinel 的協(xié)同部署3.1 Redis Cluster緩存與分布式鎖的落點gulimall 的商品詳情、首頁輪播、用戶驗證碼都走 Redis 緩存。單機 Redis 只要重啟緩存擊穿就能把數(shù)據(jù)庫打掛。集群篇的方案是 Redis Cluster把 16384 個 slot 分布到多個主從節(jié)點上。我一般用 Docker 起最小拓撲做驗證核心配置是這幾個參數(shù)cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 15000 appendonly yes bind 0.0.0.0cluster-enabled開啟集群模式cluster-config-file是節(jié)點保存集群狀態(tài)的文件每次啟動會重寫cluster-node-timeout是節(jié)點判斷 PONG 超時的時間單位毫秒設太短容易誤判太長故障轉(zhuǎn)移慢appendonly yes開啟 AOF避免節(jié)點重啟丟數(shù)據(jù)。節(jié)點起好后用 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表示每個主節(jié)點配一個從節(jié)點三個主三從組成最小容錯集群。某一主節(jié)點掛了從節(jié)點會在cluster-node-timeout之后被提升為主節(jié)點這是自動的。3.1.1 槽位遷移與擴縮容集群擴容時槽位不會自動遷移需要手動執(zhí)行redis-cli --cluster rebalance或reshard。常見做法是先加入新節(jié)點再 reshard把部分 slot 從老節(jié)點搬過去。搬移過程中客戶端可能出現(xiàn)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 重定向的最大次數(shù)。正常情況一次跳轉(zhuǎn)就能找到目標節(jié)點擴容或槽位遷移時需要多次。這里也容易踩坑Redis Cluster 要求所有節(jié)點密碼一致Spring Boot 的spring.redis.password在集群模式下會應用到每個節(jié)點不需要逐個配。3.2 Kafka 集群異步消息與故障切換訂單服務創(chuàng)建訂單后要通知庫存服務鎖定庫存還要給優(yōu)惠券服務發(fā)消息這些跨服務動作在 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重點解釋幾個參數(shù)offsets.topic.replication.factor決定消費者 offset 提交主題的副本數(shù)如果小于 broker 數(shù)而某個 broker 宕了部分消費者將無法提交 offsetmin.insync.replicas控制寫數(shù)據(jù)時最少幾個副本寫入成功才算完成配合 producer 的acksall消息基本不丟log.dirs不要放系統(tǒng)盤Kafka 對磁盤順序?qū)懸蕾嚭艽?。Producer 端用 KafkaTemplate 發(fā)送寫法比較簡潔Resource private KafkaTemplateString, String kafkaTemplate; public void publishOrderPaidEvent(OrderPaidEvent event) { String json JSON.toJSONString(event); kafkaTemplate.send(order-paid-event, event.getOrderSn(), json); }注意send的第二個參數(shù)是 key拿訂單號當 key同一訂單的支付、取消、超時消息會落在同一分區(qū)消費端能按順序處理。消費端要把 group-id 設置成和項目內(nèi)一致否則會重復或漏消費KafkaListener(topics order-paid-event, groupId order-event-group) public void onMessage(String json) { OrderPaidEvent event JSON.parseObject(json, OrderPaidEvent.class); // 解鎖倉庫庫存、更新訂單狀態(tài) }集群環(huán)境下同一 group 的消費實例數(shù)不要大于分區(qū)數(shù)否則多出來的實例會一直空轉(zhuǎn)。order-paid-event 如果開了 8 個分區(qū)消費實例最多 8 個。這幾個參數(shù)光看注釋不容易記我整理了一個對照表部署時直接對著寫參數(shù)單機默認集群建議原因offsets.topic.replication.factor13防止 broker 宕機導致 offset 丟失min.insync.replicas12配合 acksall 保證不丟消息default.replication.factor13新建 topic 默認副本數(shù)避免單點3.3 Sentinel 集群限流保護網(wǎng)關與核心服務gulimall 學習版常用單機 Sentinel生產(chǎn)集群則需要打開 cluster 模式。Sentinel 集群限流需要一個 Token Server 做全局流量統(tǒng)計普通 client 會把請求數(shù)據(jù)上報給 Token Server由它統(tǒng)一判定是否放行。給限流規(guī)則加集群開關通常同時把規(guī)則持久化到 Nacos[ { resource: gulimall-product, count: 500, grade: 1, limitApp: default, strategy: 0, clusterMode: true, clusterConfig: { flowId: 1001, thresholdType: 0 } } ]grade1表示按 QPS 限流thresholdType0表示全局閾值資源在所有節(jié)點共享一個 count如果每個節(jié)點單獨計數(shù)用戶刷新兩次就觸發(fā)限流了這是常見誤配。Sentinel 客戶端還需要配置 Token Server 地址spring: cloud: sentinel: transport: port: 8719 dashboard: 192.168.1.40:8858 cluster: server-addr: 192.168.1.41:12000port: 8719是客戶端接收控制臺心跳的端口。多個服務在同一臺機器上時需要改成不同值否則端口沖突會讓控制臺顯示心跳斷開。集群限流適合網(wǎng)關和商品服務這種熱點入口不建議對內(nèi)部數(shù)據(jù)庫操作開啟。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 是靜態(tài)資源default.conf 和 nginx.conf 屬于 Nginx 層gulimall.conf 是商城站點的 server 配置。file.conf 和 registry.conf 在集群篇里多用于中間件初始化比如 Nacos 的數(shù)據(jù)源初始化或 Sentinel 集群參數(shù)。我一般先按部署角色分類避免拿到包不知道先改哪個。文件歸屬層部署時的作用.babelrc前端編譯控制 ES6 語法轉(zhuǎn)譯規(guī)則file.conf數(shù)據(jù)源/初始化組件啟動前的數(shù)據(jù)源或日志配置registry.conf注冊中心Nacos 集群節(jié)點與庫表連接信息default.confNginx默認站點兜底配置nginx.confNginx全局配置包含 http、event、日志gulimall.confNginx商城反向代理和靜態(tài)資源規(guī)則GL.css / index.css前端靜態(tài)資源商城頁面樣式由 Nginx 靜態(tài)托管有一點需要提醒registry.conf 在 Nacos 集群里不是必須的文件名Nacos 原生用 cluster.conf這里的 registry.conf 更像資料作者整理后的可復制模板。放進 Nacos conf 目錄前記得先比對 cluster.conf 節(jié)點列表。4.2 gulimall.conf 的動靜分離配置集群部署時網(wǎng)關前面必須放一層 Nginx負責 SSL 終端、靜態(tài)資源緩存和網(wǎng)關負載均衡。資料包里的 gulimall.conf 提供了完整的反向代理模板我在此基礎上調(diào)整了上游網(wǎng)關列表讓多個 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 本地有沒有靜態(tài)文件有就直接返回沒有就把請求交給 upstream 中的網(wǎng)關。proxy_pass不帶 URI 后綴會把原始請求路徑原樣轉(zhuǎn)發(fā)給網(wǎng)關。location ~*正則匹配靜態(tài)資源expires 7d做強緩存。如果前端文件帶 hash緩存可以留更久不帶 hash 的話建議只緩存 1 小時避免發(fā)版后用戶看到舊樣式。4.2.1 靜態(tài)資源緩存策略gulimall 的前端里GL.css 和 index.css 這類文件名如果不帶 hash更新后瀏覽器可能仍使用緩存。常見做法是把 Nginx 緩存時間改短同時在后端接口響應頭里加 ETag。Nginx 默認會對靜態(tài)文件生成弱 ETag必要時可以用etag on開啟。實際部署時我一般把 CSS、JS 的expires設為 1 小時頁面 HTML 設為 no-cache這樣既不會每次請求都打到源站也不會出現(xiàn)發(fā)版后樣式錯亂。4.3 集群啟動順序與驗證集群部署和單機最大的區(qū)別是組件有依賴順序。我按“基礎存儲 → 注冊中心 → 中間件 → 業(yè)務服務 → 網(wǎng)關 → 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. 網(wǎng)關和業(yè)務服務用同一份 JAR 啟動通過啟動參數(shù)區(qū)分端口 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 → 網(wǎng)關 → product 服務這條鏈路已通。接著看 Nacos 控制臺的服務列表確認每個服務實例數(shù)大于 1、健康狀態(tài)為 true。再看 Redis Cluster 的狀態(tài)redis-cli --cluster check 192.168.1.11:6379該命令會輸出槽位分布、節(jié)點狀態(tài)、主從關系。如果輸出里沒有 inconsistent 之類的內(nèi)容說明緩存層正常。排錯時最常見的有三個問題。第一服務注冊到 Nacos 但網(wǎng)關 503優(yōu)先檢查 namespace 是否一致以及服務名是否被寫成了下劃線。第二頁面能打開但數(shù)據(jù)加載不出查看 Redis 連接是否用了集群模式單機客戶端連集群會報 MOVED 錯誤。第三Kafka 消費者重復消費檢查 group-id 是否換到了新值新 group 會從頭消費歷史消息。5. 集群壓測與驗證技巧用一筆訂單走完整條鏈路集群起來以后驗證不能只看控制臺綠燈。我習慣先用 wrk 對網(wǎng)關和商品接口做一輪壓測確認集群部署沒有引入明顯的性能損耗wrk -t8 -c200 -d60s --latency http://127.0.0.1/api/product/info/23關注輸出里的 Requests/sec 和 Latency 分布。如果單實例和集群模式的吞吐差不多就要檢查流量是否被網(wǎng)關或 Nginx 配置限制了比如 keepalive 沒開、線程池太小。接著驗證故障轉(zhuǎn)移。停掉一個網(wǎng)關實例立刻用 curl 循環(huán)請求觀察失敗窗口是否過長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 之后把請求導到健康節(jié)點返回碼依然 200。Redis Cluster 的驗證方式是殺掉一個主節(jié)點然后執(zhí)行redis-cli -c set test-key hello正常情況下從節(jié)點自動晉升為主節(jié)點命令仍然成功。Kafka 的驗證則是啟動控制臺消費者手動向 topic 發(fā)一條消息看能否消費到/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 頻繁先調(diào)大堆內(nèi)存不要急著改限流閾值集群模式強調(diào)水平擴展單節(jié)點堆上限反而可能掩蓋問題。Sentinel 規(guī)則是否生效可以用連續(xù)快速請求驗證當接口返回Blocked by Sentinel (flow limiting)說明規(guī)則已下發(fā)生效。如果這輪驗證全過把上面的命令整理成一個verify-cluster.sh每次發(fā)版后跑一遍比人工盯著多個控制臺直觀得多。本文還有配套的精品資源點擊獲取