← 返回博客
2026-06-29 17:35:00

Spring Cloud Alibaba 实战:电商下单全流程落地

Spring Cloud Alibaba 实战:电商下单全流程落地

2026-06-29 17:35:00 · 标签:Spring Cloud、微服务、实战、分布式事务、消息队列

引言

上一篇 搭好了框架——Nacos、Gateway、OpenFeign、Sentinel、Seata、RocketMQ 六件套全部就位。但这只是"把工具买齐了"。本文基于这个框架,实现一个完整的电商下单流程,把每个组件都真正用起来。

业务流程:用户浏览商品 -> 下单 -> 扣库存 -> 发短信通知 -> 同步供应商数据 -> 定时对账。

读完这篇文章,你会知道:


一、业务场景与数据模型

1.1 服务与数据库划分

微服务的原则是一个服务一个库,服务间通过接口通信,绝不直接访问别人的数据库。

服务数据库核心表
shop-usershop_userusers
shop-goodsshop_goodsgoods
shop-ordershop_orderorders, order_items

1.2 建表 SQL

在 MySQL 中创建三个库,然后在各库执行建表语句。

shop_user 库

CREATE DATABASE IF NOT EXISTS shop_user DEFAULT CHARSET utf8mb4;
USE shop_user;

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    phone VARCHAR(20),
    balance DECIMAL(10,2) DEFAULT 0 COMMENT '账户余额',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

INSERT INTO users (username, phone, balance) VALUES ('张三', '13800000001', 9999.00);
INSERT INTO users (username, phone, balance) VALUES ('李四', '13800000002', 5000.00);

shop_goods 库

CREATE DATABASE IF NOT EXISTS shop_goods DEFAULT CHARSET utf8mb4;
USE shop_goods;

CREATE TABLE goods (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

INSERT INTO goods (name, price, stock) VALUES ('iPhone 17', 6999.00, 100);
INSERT INTO goods (name, price, stock) VALUES ('MacBook Pro', 12999.00, 50);
INSERT INTO goods (name, price, stock) VALUES ('AirPods Pro', 1999.00, 200);

shop_order 库

CREATE DATABASE IF NOT EXISTS shop_order DEFAULT CHARSET utf8mb4;
USE shop_order;

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
    status VARCHAR(20) NOT NULL DEFAULT 'CREATED' COMMENT 'CREATED/PAID/CANCELLED',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE order_items (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    goods_id BIGINT NOT NULL,
    goods_name VARCHAR(200) NOT NULL,
    quantity INT NOT NULL,
    unit_price DECIMAL(10,2) NOT NULL,
    KEY idx_order_id (order_id)
) ENGINE=InnoDB;

1.3 Nacos 配置中心:各服务的数据源

在 Nacos 控制台(http://localhost:8848/nacos)为每个服务创建数据源配置。

shop-user-datasource.yaml(Data ID: shop-user-datasource.yaml, Group: SHOP_GROUP):

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shop_user?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ${DB_PASSWORD:root123}
    driver-class-name: com.mysql.cj.jdbc.Driver

shop-goods-datasource.yamlshop-order-datasource.yaml 同理,把库名换成 shop_goodsshop_order 即可。


二、用户服务 shop-user

用户服务提供两个接口:根据 ID 查用户、扣减余额。作为被调用方,它不需要 Feign,只需要做好自己的事。

2.1 依赖追加

shop-user/pom.xml 已有的基础上追加 MyBatis-Plus 和 MySQL 驱动:

<!-- MyBatis-Plus:Spring Boot 4.x 专属版本 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

2.2 更新 application.yml

在原有 shop-user/src/main/resources/application.yml 中追加数据源导入:

spring:
  config:
    import:
      - optional:nacos:${spring.application.name}.yaml
      - optional:nacos:shop-user-datasource.yaml

2.3 实体类

shop-user/src/main/java/com/shop/user/entity/User.java

package com.shop.user.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("users")
public class User {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String phone;
    private BigDecimal balance;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

2.4 Mapper

shop-user/src/main/java/com/shop/user/mapper/UserMapper.java

package com.shop.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shop.user.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

2.5 Service

shop-user/src/main/java/com/shop/user/service/UserService.java

package com.shop.user.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shop.common.BizException;
import com.shop.user.entity.User;
import com.shop.user.mapper.UserMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class UserService extends ServiceImpl<UserMapper, User> {

    public User getByIdOrThrow(Long userId) {
        User user = getById(userId);
        if (user == null) {
            throw new BizException("用户不存在: " + userId);
        }
        return user;
    }

    @Transactional
    public void deductBalance(Long userId, BigDecimal amount) {
        User user = getByIdOrThrow(userId);
        if (user.getBalance().compareTo(amount) < 0) {
            throw new BizException("余额不足,当前余额: " + user.getBalance());
        }
        user.setBalance(user.getBalance().subtract(amount));
        updateById(user);
    }
}

2.6 Controller

shop-user/src/main/java/com/shop/user/controller/UserController.java

package com.shop.user.controller;

import com.shop.common.Result;
import com.shop.user.entity.User;
import com.shop.user.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    /** 根据 ID 查询用户 */
    @GetMapping("/{userId}")
    public Result<User> getUserById(@PathVariable Long userId) {
        return Result.ok(userService.getByIdOrThrow(userId));
    }

    /** 扣减余额 */
    @PutMapping("/balance/deduct")
    public Result<Void> deductBalance(@RequestBody Map<String, Object> params) {
        Long userId = Long.valueOf(params.get("userId").toString());
        BigDecimal amount = new BigDecimal(params.get("amount").toString());
        userService.deductBalance(userId, amount);
        return Result.ok(null);
    }
}

三、商品服务 shop-goods

商品服务的结构和用户服务类似,但在库存扣减上需要保证并发安全。

3.1 依赖追加

同样追加 MyBatis-Plus 和 MySQL 驱动(参考 2.1),application.yml 导入 shop-goods-datasource.yaml

3.2 实体类

shop-goods/src/main/java/com/shop/goods/entity/Goods.java

package com.shop.goods.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("goods")
public class Goods {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

3.3 Mapper

shop-goods/src/main/java/com/shop/goods/mapper/GoodsMapper.java

package com.shop.goods.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shop.goods.entity.Goods;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface GoodsMapper extends BaseMapper<Goods> {

    /**
     * 原子扣库存。WHERE stock >= ? 保证不超卖,返回值 > 0 才算成功。
     */
    @Update("UPDATE goods SET stock = stock - #{count} WHERE id = #{goodsId} AND stock >= #{count}")
    int deductStock(@Param("goodsId") Long goodsId, @Param("count") Integer count);
}

3.4 Service

shop-goods/src/main/java/com/shop/goods/service/GoodsService.java

package com.shop.goods.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shop.common.BizException;
import com.shop.goods.entity.Goods;
import com.shop.goods.mapper.GoodsMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> {

    public Goods getByIdOrThrow(Long goodsId) {
        Goods goods = getById(goodsId);
        if (goods == null) {
            throw new BizException("商品不存在: " + goodsId);
        }
        return goods;
    }

    @Transactional
    public void deductStock(Long goodsId, Integer count) {
        int rows = baseMapper.deductStock(goodsId, count);
        if (rows == 0) {
            throw new BizException("库存不足,商品ID: " + goodsId);
        }
    }
}

3.5 Controller

shop-goods/src/main/java/com/shop/goods/controller/GoodsController.java

package com.shop.goods.controller;

import com.shop.common.Result;
import com.shop.goods.entity.Goods;
import com.shop.goods.service.GoodsService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/goods")
public class GoodsController {

    private final GoodsService goodsService;

    public GoodsController(GoodsService goodsService) {
        this.goodsService = goodsService;
    }

    /** 根据 ID 查询商品 */
    @GetMapping("/{goodsId}")
    public Result<Goods> getGoodsById(@PathVariable Long goodsId) {
        return Result.ok(goodsService.getByIdOrThrow(goodsId));
    }

    /** 扣减库存 */
    @PutMapping("/stock/deduct")
    public Result<Void> deductStock(@RequestBody Map<String, Object> params) {
        Long goodsId = Long.valueOf(params.get("goodsId").toString());
        Integer count = Integer.valueOf(params.get("count").toString());
        goodsService.deductStock(goodsId, count);
        return Result.ok(null);
    }
}

四、订单服务 shop-order:核心业务

订单服务是整个系统的核心——它要调用户服务、调商品服务、写订单表、发消息、还要处理分布式事务和熔断。

4.1 依赖追加

shop-order/pom.xml 已有基础上追加:

<!-- MyBatis-Plus + MySQL -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

<!-- Sentinel:限流 + Feign 熔断 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<!-- Seata:分布式事务 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

<!-- RocketMQ:Spring Cloud Stream Binder -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

4.2 更新 application.yml

spring:
  config:
    import:
      - optional:nacos:${spring.application.name}.yaml
      - optional:nacos:shop-order-datasource.yaml
      - optional:nacos:seata-config.yaml
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8858
        port: 8719
      datasource:
        flow:
          nacos:
            server-addr: localhost:8848
            username: nacos
            password: nacos
            data-id: ${spring.application.name}-flow-rules
            group-id: SENTINEL_GROUP
            data-type: json
            rule-type: flow
    stream:
      rocketmq:
        binder:
          name-server: localhost:9876
          producer:
            group: order-producer-group
      bindings:
        orderCreated-out-0:                   # 生产者通道(StreamBridge 用这个名发消息)
          destination: order-created-topic    # RocketMQ Topic
          content-type: application/json

feign:
  sentinel:
    enabled: true       # 开启 Feign 熔断

4.3 实体类

shop-order/src/main/java/com/shop/order/entity/Order.java

package com.shop.order.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("orders")
public class Order {

    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private BigDecimal totalAmount;
    private String status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

shop-order/src/main/java/com/shop/order/entity/OrderItem.java

package com.shop.order.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;

@Data
@TableName("order_items")
public class OrderItem {

    @TableId(type = IdType.AUTO)
    private Long id;
    private Long orderId;
    private Long goodsId;
    private String goodsName;
    private Integer quantity;
    private BigDecimal unitPrice;
}

4.4 Mapper

package com.shop.order.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shop.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
package com.shop.order.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shop.order.entity.OrderItem;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}

4.5 Feign 接口(带 fallback)

shop-order/src/main/java/com/shop/order/feign/UserClient.java

package com.shop.order.feign;

import com.shop.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.Map;

@FeignClient(name = "shop-user", path = "/user",
             fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

    @GetMapping("/{userId}")
    Result<Map<String, Object>> getUserById(@PathVariable Long userId);
}

Feign 命名规范:这里返回 Map 而不是强类型 VO,因为 VO 是调用方定义的简化视图,仅包含所需字段。业务代码按需取值即可,避免过度包装。

shop-order/src/main/java/com/shop/order/feign/UserClientFallbackFactory.java

package com.shop.order.feign;

import com.shop.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.Map;

/**
 * FallbackFactory 比 fallback 多一个优势:能拿到异常对象,
 * 可以区分是"服务挂了"还是"被 Sentinel 限流了",方便排查。
 */
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

    private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);

    @Override
    public UserClient create(Throwable cause) {
        log.error("UserClient 调用失败,触发降级: {}", cause.getMessage());
        return userId -> {
            log.warn("查询用户降级,userId={}", userId);
            return Result.fail("用户服务暂不可用,请稍后重试");
        };
    }
}

shop-order/src/main/java/com/shop/order/feign/GoodsClient.java

package com.shop.order.feign;

import com.shop.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Map;

@FeignClient(name = "shop-goods", path = "/goods",
             fallbackFactory = GoodsClientFallbackFactory.class)
public interface GoodsClient {

    @GetMapping("/{goodsId}")
    Result<Map<String, Object>> getGoodsById(@PathVariable Long goodsId);

    @PutMapping("/stock/deduct")
    Result<Void> deductStock(@RequestBody Map<String, Object> params);
}

shop-order/src/main/java/com/shop/order/feign/GoodsClientFallbackFactory.java

package com.shop.order.feign;

import com.shop.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.Map;

@Component
public class GoodsClientFallbackFactory implements FallbackFactory<GoodsClient> {

    private static final Logger log = LoggerFactory.getLogger(GoodsClientFallbackFactory.class);

    @Override
    public GoodsClient create(Throwable cause) {
        log.error("GoodsClient 调用失败,触发降级: {}", cause.getMessage());
        return new GoodsClient() {
            @Override
            public Result<Map<String, Object>> getGoodsById(Long goodsId) {
                log.warn("查询商品降级,goodsId={}", goodsId);
                return Result.fail("商品服务暂不可用");
            }

            @Override
            public Result<Void> deductStock(Map<String, Object> params) {
                log.warn("扣库存降级,params={}", params);
                return Result.fail("商品服务暂不可用");
            }
        };
    }
}

4.6 DTO

shop-order/src/main/java/com/shop/order/dto/CreateOrderDTO.java

package com.shop.order.dto;

import lombok.Data;
import java.util.List;

@Data
public class CreateOrderDTO {

    /** 下单用户 ID */
    private Long userId;

    /** 订单明细 */
    private List<OrderItemDTO> items;
}

shop-order/src/main/java/com/shop/order/dto/OrderItemDTO.java

package com.shop.order.dto;

import lombok.Data;

@Data
public class OrderItemDTO {

    /** 商品 ID */
    private Long goodsId;

    /** 购买数量 */
    private Integer quantity;
}

4.7 核心 Service:下单 + 分布式事务

shop-order/src/main/java/com/shop/order/service/OrderService.java

package com.shop.order.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shop.common.BizException;
import com.shop.common.Result;
import com.shop.order.dto.CreateOrderDTO;
import com.shop.order.dto.OrderItemDTO;
import com.shop.order.entity.Order;
import com.shop.order.entity.OrderItem;
import com.shop.order.feign.GoodsClient;
import com.shop.order.feign.UserClient;
import com.shop.order.mapper.OrderItemMapper;
import com.shop.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    private final UserClient userClient;
    private final GoodsClient goodsClient;
    private final OrderItemMapper orderItemMapper;
    private final StreamBridge streamBridge;

    public OrderService(UserClient userClient, GoodsClient goodsClient,
                        OrderItemMapper orderItemMapper,
                        StreamBridge streamBridge) {
        this.userClient = userClient;
        this.goodsClient = goodsClient;
        this.orderItemMapper = orderItemMapper;
        this.streamBridge = streamBridge;
    }

    /**
     * 下单核心流程。
     *
     * @GlobalTransactional 是 Seata 分布式事务的入口。
     * 当这个方法里任何一个跨服务调用失败,
     * Seata 会自动回滚所有已执行的本地事务。
     */
    @GlobalTransactional(name = "createOrder", timeoutMills = 300000)
    @Transactional
    public Order createOrder(CreateOrderDTO dto) {

        // ---- 第1步:验证用户是否存在 ----
        Result<Map<String, Object>> userResult = userClient.getUserById(dto.getUserId());
        if (userResult.getCode() != 200 || userResult.getData() == null) {
            throw new BizException("用户验证失败: " + userResult.getMessage());
        }
        log.info("用户验证通过,userId={}", dto.getUserId());

        // ---- 第2步:计算总金额 + 逐商品验证 ----
        BigDecimal totalAmount = BigDecimal.ZERO;
        Map<Long, Map<String, Object>> goodsCache = new HashMap<>();

        for (OrderItemDTO item : dto.getItems()) {
            Result<Map<String, Object>> goodsResult =
                    goodsClient.getGoodsById(item.getGoodsId());
            if (goodsResult.getCode() != 200 || goodsResult.getData() == null) {
                throw new BizException("商品查询失败: " + goodsResult.getMessage());
            }
            Map<String, Object> goodsData = goodsResult.getData();
            goodsCache.put(item.getGoodsId(), goodsData);

            BigDecimal price = new BigDecimal(goodsData.get("price").toString());
            totalAmount = totalAmount.add(
                    price.multiply(BigDecimal.valueOf(item.getQuantity())));
        }

        // ---- 第3步:创建订单 ----
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setTotalAmount(totalAmount);
        order.setStatus("CREATED");
        order.setCreatedAt(LocalDateTime.now());
        save(order);

        // ---- 第4步:创建订单明细 + 逐商品扣库存 ----
        for (OrderItemDTO item : dto.getItems()) {
            Map<String, Object> goodsData = goodsCache.get(item.getGoodsId());
            BigDecimal price = new BigDecimal(goodsData.get("price").toString());

            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getId());
            orderItem.setGoodsId(item.getGoodsId());
            orderItem.setGoodsName(goodsData.get("name").toString());
            orderItem.setQuantity(item.getQuantity());
            orderItem.setUnitPrice(price);
            orderItemMapper.insert(orderItem);

            // 扣库存——这一步失败,Seata 会回滚上面的订单和明细
            Map<String, Object> stockParam = new HashMap<>();
            stockParam.put("goodsId", item.getGoodsId());
            stockParam.put("count", item.getQuantity());
            Result<Void> stockResult = goodsClient.deductStock(stockParam);
            if (stockResult.getCode() != 200) {
                throw new BizException("扣库存失败: " + stockResult.getMessage());
            }
        }

        // ---- 第5步:发送异步消息(不在事务内,失败不影响下单) ----
        try {
            Map<String, Object> event = new HashMap<>();
            event.put("orderId", order.getId());
            event.put("userId", dto.getUserId());
            event.put("totalAmount", totalAmount.toString());
            event.put("itemCount", dto.getItems().size());
            boolean sent = streamBridge.send("orderCreated-out-0", event);
            if (sent) {
                log.info("下单消息已发送,orderId={}", order.getId());
            } else {
                log.warn("下单消息发送返回false,orderId={},需人工补偿", order.getId());
            }
        } catch (Exception e) {
            log.error("发送下单消息失败,orderId={},订单已创建,需人工补偿", order.getId(), e);
        }

        return order;
    }
}
关键理解@GlobalTransactional 配合 @Transactional 一起用。内部的每个本地操作(写订单、写明细)走 @Transactional,跨服务的远程调用(扣库存)由 Seata 协调。任何一个失败,所有服务的数据一起回滚。

4.8 Controller

shop-order/src/main/java/com/shop/order/controller/OrderController.java

package com.shop.order.controller;

import com.shop.common.Result;
import com.shop.order.dto.CreateOrderDTO;
import com.shop.order.entity.Order;
import com.shop.order.service.OrderService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    /**
     * 创建订单。
     *
     * 这个接口会被 Sentinel 保护——如果在 Nacos 配置了流控规则,
     * QPS 超过阈值时直接返回 429 或执行自定义限流逻辑。
     */
    @PostMapping("/create")
    public Result<Order> createOrder(@RequestBody CreateOrderDTO dto) {
        Order order = orderService.createOrder(dto);
        return Result.ok(order);
    }

    /** 查询订单详情 */
    @GetMapping("/{orderId}")
    public Result<Order> getOrderById(@PathVariable Long orderId) {
        Order order = orderService.getById(orderId);
        if (order == null) {
            return Result.fail("订单不存在");
        }
        return Result.ok(order);
    }
}

五、网关鉴权:shop-gateway

网关不仅是路由转发,还需要做统一的鉴权拦截。如果每个微服务各写一套鉴权逻辑,维护成本爆炸。

5.1 全局鉴权过滤器

shop-gateway/src/main/java/com/shop/gateway/filter/AuthFilter.java

package com.shop.gateway.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;

/**
 * 全局鉴权过滤器。
 * 拦截所有请求,检查 Token。
 * 白名单路径直接放行(登录、注册、公开接口)。
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    /** 不需要鉴权的路径 */
    private static final List<String> WHITE_LIST = List.of(
            "/api/user/login",
            "/api/user/register"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 白名单直接放行
        if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // 演示环境简化处理:检查请求头中是否有 token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (token == null || token.isBlank()) {
            log.warn("未携带 Token,拒绝访问: {}", path);
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // 生产环境这里应该调用认证中心验证 token 有效性
        // 比如解析 JWT、查 Redis 等
        log.debug("鉴权通过: path={}, token={}", path, token.substring(0, Math.min(8, token.length())) + "...");

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 数值越小越优先。在 Sentinel 过滤器之后执行
        return -100;
    }
}

5.2 更新 Gateway 的 application.yml

在网关 yml 中追加 Sentinel Gateway 限流配置:

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            # ... 路由规则同前 ...
    sentinel:
      transport:
        dashboard: localhost:8858
        port: 8720
      # 网关限流:按路由 ID 维度
      scg:
        fallback:
          mode: response
          response-status: 429
          response-body: '{"code":429,"message":"请求过于频繁,请稍后重试"}'

六、数据同步服务:shop-sync

真实电商系统需要从供应商同步商品数据(供应商可能提供 REST API、FTP CSV、甚至 Excel)。适配器模式是标准解法。

6.1 依赖

<dependency>
    <groupId>com.shop</groupId>
    <artifactId>shop-common</artifactId>
    <version>${project.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

6.2 适配器接口

shop-sync/src/main/java/com/shop/sync/adapter/SupplierAdapter.java

package com.shop.sync.adapter;

import java.util.List;
import java.util.Map;

/**
 * 供应商适配器接口。
 * 每种供应商实现这个接口,同步引擎统一调度。
 */
public interface SupplierAdapter {

    /** 供应商名称,用于日志和区分 */
    String getSupplierName();

    /** 拉取供应商全量商品数据 */
    List<Map<String, Object>> fetchGoods();

    /** 是否支持增量同步 */
    default boolean supportsIncremental() {
        return false;
    }
}

6.3 具体实现:REST API 供应商

shop-sync/src/main/java/com/shop/sync/adapter/RestApiSupplierAdapter.java

package com.shop.sync.adapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Component
public class RestApiSupplierAdapter implements SupplierAdapter {

    private static final Logger log = LoggerFactory.getLogger(RestApiSupplierAdapter.class);
    private final RestTemplate restTemplate = new RestTemplate();

    @Override
    public String getSupplierName() {
        return "SupplierA-REST";
    }

    @Override
    public List<Map<String, Object>> fetchGoods() {
        try {
            // 实际环境替换为真实供应商 URL
            String url = "http://supplier-a.example.com/api/goods";
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> goods = restTemplate.getForObject(url, List.class);
            log.info("从 {} 拉取到 {} 条商品数据", getSupplierName(),
                     goods != null ? goods.size() : 0);
            return goods != null ? goods : Collections.emptyList();
        } catch (Exception e) {
            log.error("从 {} 拉取数据失败", getSupplierName(), e);
            return Collections.emptyList();
        }
    }
}

6.4 同步引擎

shop-sync/src/main/java/com/shop/sync/engine/SyncEngine.java

package com.shop.sync.engine;

import com.shop.sync.adapter.SupplierAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 同步引擎:遍历所有适配器,拉数据、比对、更新、发消息通知。
 */
@Component
public class SyncEngine {

    private static final Logger log = LoggerFactory.getLogger(SyncEngine.class);

    private final List<SupplierAdapter> adapters;
    private final StreamBridge streamBridge;

    public SyncEngine(List<SupplierAdapter> adapters,
                      StreamBridge streamBridge) {
        this.adapters = adapters;
        this.streamBridge = streamBridge;
    }

    /**
     * 执行全量同步。
     * 由定时任务或管理后台手动触发。
     */
    public void syncAll() {
        for (SupplierAdapter adapter : adapters) {
            try {
                log.info("开始同步供应商: {}", adapter.getSupplierName());
                List<Map<String, Object>> goodsList = adapter.fetchGoods();

                // 这里应做数据比对、去重、写入 MySQL 等
                // 为简化演示,直接发消息通知缓存刷新
                Map<String, Object> event = new HashMap<>();
                event.put("supplier", adapter.getSupplierName());
                event.put("goodsCount", goodsList.size());
                event.put("syncTime", System.currentTimeMillis());

                streamBridge.send("goodsSynced-out-0", event);
                log.info("供应商 {} 同步完成,共 {} 条", adapter.getSupplierName(), goodsList.size());
            } catch (Exception e) {
                log.error("供应商 {} 同步异常", adapter.getSupplierName(), e);
            }
        }
    }
}

6.5 消息消费者

上面两个生产者把消息发到了 RocketMQ,但还没有人接收。消费者同样放在 shop-sync 中,因为它天生就是"辅助服务"的角色——发短信、刷新缓存这类旁路逻辑不应该耦合在核心下单流程里。

6.5.1 配置消费者绑定

在 shop-sync 的 application.yml 中追加 Stream 消费者绑定,与生产者使用同一个 Topic:

spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: localhost:9876
          consumer:
            group: sync-consumer-group      # 消费者组
      bindings:
        # ---- 消费者 ----
        orderCreated-in-0:                   # 函数名 orderCreated + -in-0
          destination: order-created-topic   # 与生产者同一个 Topic
          content-type: application/json
          group: sync-consumer-group         # 集群消费,每条消息只被一个实例处理
        goodsSynced-in-0:                    # 函数名 goodsSynced + -in-0
          destination: goods-synced-topic
          content-type: application/json
          group: sync-consumer-group
    function:
      definition: orderCreated;goodsSynced   # 注册两个消费者函数

命名规则:Spring Cloud Stream 的函数式绑定遵循 函数名-in-0(输入)和 函数名-out-0(输出)的约定。function.definition 里声明函数名后,框架自动把 Bean 方法与同名 binding 关联。

6.5.2 消费者代码

shop-sync/src/main/java/com/shop/sync/consumer/MessageConsumers.java

package com.shop.sync.consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.function.Consumer;

/**
 * RocketMQ 消费者。
 * 下单消息 -> 短信通知,同步消息 -> 缓存刷新。
 */
@Component
public class MessageConsumers {

    private static final Logger log =
            LoggerFactory.getLogger(MessageConsumers.class);

    /**
     * 消费下单消息 —— 模拟发短信通知。
     * 生产环境对接阿里云短信 / 腾讯云短信 / 钉钉机器人。
     */
    @Bean
    public Consumer<Map<String, Object>> orderCreated() {
        return event -> {
            Long orderId = Long.valueOf(event.get("orderId").toString());
            Long userId = Long.valueOf(event.get("userId").toString());
            Object amount = event.get("totalAmount");
            Object count = event.get("itemCount");

            log.info("==============================");
            log.info("[短信通知] 用户{} 的订单{} 已创建", userId, orderId);
            log.info("  金额: {} 元, 商品数: {} 件", amount, count);
            log.info("  实际对接: smsService.send(phone, template)");
            log.info("==============================");
        };
    }

    /**
     * 消费商品同步消息 —— 模拟刷新 Redis 缓存。
     * 生产环境清除 / 重建商品相关缓存 key。
     */
    @Bean
    public Consumer<Map<String, Object>> goodsSynced() {
        return event -> {
            String supplier = (String) event.get("supplier");
            Object count = event.get("goodsCount");

            log.info("------------------------------");
            log.info("[缓存刷新] 供应商 {} 同步了 {} 条商品", supplier, count);
            log.info("  已清除缓存: goods:list:{}, goods:detail:*", supplier);
            log.info("  实际对接: redisTemplate.delete(keys)");
            log.info("------------------------------");
        };
    }
}

6.5.3 消息流转全景

至此 RocketMQ 的完整链路就闭环了:

shop-order.createOrder()
    |
    +-- Seata 分布式事务 (订单 + 扣库存)
    |
    +-- streamBridge.send("orderCreated-out-0", event)
            |
            v
      [order-created-topic]  (RocketMQ Topic)
            |
            v
      shop-sync.orderCreated()  -->  短信通知 / 写操作日志 / 同步 ES

shop-sync.SyncEngine.syncAll()
    |
    +-- streamBridge.send("goodsSynced-out-0", event)
            |
            v
      [goods-synced-topic]    (RocketMQ Topic)
            |
            v
      shop-sync.goodsSynced()  -->  清除商品缓存 / 通知前端刷新

生产者只管发消息,消费者只管处理消息,双方完全解耦。后续要加"订单数据同步到 ES"只需要在 shop-sync 里再加一个消费者函数,核心下单代码不需要任何改动。


七、定时对账:兜底一致性保障

即使所有机制都正常工作,网络抖动、进程崩溃也可能导致 Redis 缓存和 MySQL 数据不一致。定时对账是最后的防线。

shop-order/src/main/java/com/shop/order/task/ReconciliationTask.java

package com.shop.order.task;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.shop.order.entity.Order;
import com.shop.order.mapper.OrderMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;

@Component
@EnableScheduling
public class ReconciliationTask {

    private static final Logger log = LoggerFactory.getLogger(ReconciliationTask.class);

    private final OrderMapper orderMapper;

    public ReconciliationTask(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    /**
     * 每 10 分钟:检查超过 30 分钟仍为 CREATED 状态的异常订单。
     * 正常流程下订单创建后应在几分钟内完成支付。
     * 长时间停留在 CREATED 的订单需要告警并人工处理。
     */
    @Scheduled(cron = "0 */10 * * * ?")
    public void checkStaleOrders() {
        LocalDateTime threshold = LocalDateTime.now().minusMinutes(30);
        List<Order> staleOrders = orderMapper.selectList(
                new LambdaQueryWrapper<Order>()
                        .eq(Order::getStatus, "CREATED")
                        .lt(Order::getCreatedAt, threshold));

        if (!staleOrders.isEmpty()) {
            log.warn("发现 {} 个异常订单(创建超过30分钟仍为CREATED),ID列表: {}",
                     staleOrders.size(),
                     staleOrders.stream().map(Order::getId).toList());
            // 生产环境应:1) 发钉钉/飞书告警  2) 写死信队列  3) 触发自动关单
        } else {
            log.debug("对账完成,无异常订单");
        }
    }

    /**
     * 每天凌晨 2 点:全量对账(与供应商数据对比)。
     * 这一步需要对接供应商的报表接口,本文不做展开。
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void fullReconciliation() {
        log.info("开始每日全量对账...");
        // 实际实现:拉取供应商报表,逐条对比订单和库存
        log.info("每日全量对账完成");
    }
}

八、Sentinel 流控规则

8.1 在 Nacos 中配置限流规则

在 Nacos 控制台 dev 命名空间下创建配置:

Data ID: shop-order-flow-rules

Group: SENTINEL_GROUP

配置格式: JSON

配置内容

[
    {
        "resource": "/order/create",
        "limitApp": "default",
        "grade": 1,
        "count": 10,
        "strategy": 0,
        "controlBehavior": 0
    },
    {
        "resource": "createOrder",
        "limitApp": "default",
        "grade": 1,
        "count": 5,
        "strategy": 0,
        "controlBehavior": 0
    }
]

说明:

8.2 Feign 熔断触发条件

当 shop-goods 服务连续失败达到阈值(比如 5 秒内失败 5 次),Sentinel 会自动熔断,后续 Feign 调用直接走 GoodsClientFallbackFactory 返回降级结果,不再傻等超时。等商品服务恢复后,Sentinel 会尝试放行少量请求探测,确认恢复后自动关闭熔断。

这个流程不需要额外配置,feign.sentinel.enabled=true 后自动生效。


九、Docker Compose 一键启动

把所有组件编排到一起。在项目根目录创建 docker-compose.yml

version: '3.8'

services:
  mysql:
    image: mysql:9.1
    environment:
      MYSQL_ROOT_PASSWORD: root123
    ports:
      - "3306:3306"
    volumes:
      - ./sql/init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5

  nacos:
    image: nacos/nacos-server:v3.1.1
    environment:
      MODE: standalone
    ports:
      - "8848:8848"
      - "9848:9848"

  rocketmq-namesrv:
    image: apache/rocketmq:5.3.1
    command: sh mqnamesrv
    ports:
      - "9876:9876"

  rocketmq-broker:
    image: apache/rocketmq:5.3.1
    command: sh mqbroker -n rocketmq-namesrv:9876
    ports:
      - "10911:10911"
    depends_on:
      - rocketmq-namesrv
    environment:
      JAVA_OPT_EXT: "-Xms512m -Xmx512m"

  shop-user:
    build: ./shop-user
    ports:
      - "8081:8081"
    environment:
      DB_PASSWORD: root123
    depends_on:
      mysql:
        condition: service_healthy
      nacos:
        condition: service_started

  shop-goods:
    build: ./shop-goods
    ports:
      - "8083:8083"
    environment:
      DB_PASSWORD: root123
    depends_on:
      mysql:
        condition: service_healthy
      nacos:
        condition: service_started

  shop-order:
    build: ./shop-order
    ports:
      - "8082:8082"
    environment:
      DB_PASSWORD: root123
    depends_on:
      mysql:
        condition: service_healthy
      nacos:
        condition: service_started
      rocketmq-broker:
        condition: service_started

  shop-gateway:
    build: ./shop-gateway
    ports:
      - "8080:8080"
    depends_on:
      - nacos

启动:docker compose up -d(等待 MySQL 健康检查通过后各服务依次启动)。


十、测试验证

10.1 启动顺序

  1. MySQL + Nacos + RocketMQ
  2. shop-user(8081)+ shop-goods(8083)
  3. shop-order(8082)
  4. shop-gateway(8080)

10.1.1 Windows 启动脚本

项目根目录下创建 startup.bat,每个服务在独立窗口运行,方便分别查看日志:

@echo off
chcp 65001 >nul
title SpringCloud Shop - 启动脚本

echo ============================================
echo   SpringCloud Shop 一键启动 (Windows)
echo ============================================
echo.

REM ---------- 第一步:基础设施 ----------
echo [1/4] 检查基础设施...
echo   提示:确保 MySQL、Nacos、RocketMQ 已启动
echo   如使用 Docker Compose,请先执行:docker compose up -d
echo.
pause

REM ---------- 第二步:基础服务 ----------
echo [2/4] 启动基础服务:shop-user (8081) + shop-goods (8083)
start "shop-user-8081" cmd /k "cd /d %~dp0shop-user && mvn spring-boot:run"
start "shop-goods-8083" cmd /k "cd /d %~dp0shop-goods && mvn spring-boot:run"
echo   等待基础服务启动(60 秒)...
timeout /t 60 /nobreak >nul

REM ---------- 第三步:订单服务 ----------
echo [3/4] 启动订单服务:shop-order (8082)
start "shop-order-8082" cmd /k "cd /d %~dp0shop-order && mvn spring-boot:run"
echo   等待订单服务启动(30 秒)...
timeout /t 30 /nobreak >nul

REM ---------- 第四步:网关 ----------
echo [4/4] 启动网关:shop-gateway (8080)
start "shop-gateway-8080" cmd /k "cd /d %~dp0shop-gateway && mvn spring-boot:run"
echo   等待网关启动(20 秒)...

timeout /t 20 /nobreak >nul

echo.
echo ============================================
echo   全部服务启动完成!
echo   shop-user    : http://localhost:8081
echo   shop-goods   : http://localhost:8083
echo   shop-order   : http://localhost:8082
echo   shop-gateway : http://localhost:8080
echo ============================================
echo   按任意键退出本窗口(不影响服务运行)
pause >nul

10.1.2 Linux / macOS 启动脚本

项目根目录下创建 startup.sh,后台启动 + 健康检查,确保依赖就绪后再启动下一批:

#!/bin/bash
set -e

PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="$PROJECT_DIR/logs"
mkdir -p "$LOG_DIR"

# 颜色
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

log_info()  { echo -e "${GREEN}[INFO]${NC}  $1"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

# 健康检查:轮询端口直到响应 200
wait_for() {
    local name=$1
    local url=$2
    local timeout=${3:-120}
    log_info "等待 $name ($url) 就绪..."
    local elapsed=0
    while [ $elapsed -lt $timeout ]; do
        if curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null | grep -q "200"; then
            log_info "$name 已就绪 (耗时 ${elapsed}s)"
            return 0
        fi
        sleep 3
        elapsed=$((elapsed + 3))
    done
    log_error "$name 启动超时(${timeout}s),请检查日志"
    return 1
}

# 启动一个 Spring Boot 服务(后台运行,日志输出到文件)
start_service() {
    local name=$1
    local dir=$2
    log_info "启动 $name ..."
    cd "$PROJECT_DIR/$dir"
    nohup mvn spring-boot:run > "$LOG_DIR/${name}.log" 2>&1 &
    echo $! >> "$LOG_DIR/pids.txt"
    cd "$PROJECT_DIR"
}

# 清理函数
cleanup() {
    log_warn "正在停止所有服务..."
    if [ -f "$LOG_DIR/pids.txt" ]; then
        while read -r pid; do
            kill "$pid" 2>/dev/null || true
        done < "$LOG_DIR/pids.txt"
        rm -f "$LOG_DIR/pids.txt"
    fi
    log_info "已停止"
}
trap cleanup EXIT INT TERM

echo "============================================"
echo "  SpringCloud Shop 一键启动 (Linux)"
echo "============================================"
echo ""

# ---------- 第一步:基础设施 ----------
log_info "[1/4] 检查基础设施..."
log_warn "确保 MySQL(3306)、Nacos(8848)、RocketMQ(9876) 已启动"
log_warn "如使用 Docker Compose,请先执行:docker compose up -d"
echo ""

# 快速检查基础设施端口
for port in 3306 8848 9876; do
    if nc -z localhost "$port" 2>/dev/null; then
        log_info "端口 $port 已监听"
    else
        log_error "端口 $port 未监听,请先启动基础设施"
        exit 1
    fi
done
echo ""

# ---------- 第二步:基础服务 ----------
log_info "[2/4] 启动基础服务:shop-user (8081) + shop-goods (8083)"
> "$LOG_DIR/pids.txt"  # 清空 PID 文件
start_service "shop-user" "shop-user"
start_service "shop-goods" "shop-goods"

wait_for "shop-user"  "http://localhost:8081/user/1"  120
wait_for "shop-goods" "http://localhost:8083/goods/1" 120

# ---------- 第三步:订单服务 ----------
log_info "[3/4] 启动订单服务:shop-order (8082)"
start_service "shop-order" "shop-order"
wait_for "shop-order" "http://localhost:8082/order/1" 90

# ---------- 第四步:网关 ----------
log_info "[4/4] 启动网关:shop-gateway (8080)"
start_service "shop-gateway" "shop-gateway"
wait_for "shop-gateway" "http://localhost:8080/actuator/health" 60

echo ""
echo "============================================"
echo "  全部服务启动完成!"
echo "  shop-user    : http://localhost:8081"
echo "  shop-goods   : http://localhost:8083"
echo "  shop-order   : http://localhost:8082"
echo "  shop-gateway : http://localhost:8080"
echo "  日志目录     : $LOG_DIR"
echo "============================================"
echo ""
echo "按 Ctrl+C 停止所有服务"

# 保持脚本运行
wait

使用方式:

10.2 接口测试

查询用户

curl http://localhost:8080/api/user/1
# {"code":200,"message":"success","data":{"id":1,"username":"张三",...}}

查询商品

curl http://localhost:8080/api/goods/1
# {"code":200,"message":"success","data":{"id":1,"name":"iPhone 17","price":6999.00,"stock":100}}

下单(网关 -> shop-order -> Feign 调 shop-user + shop-goods -> 扣库存 -> 发 RocketMQ):

curl -X POST http://localhost:8080/api/order/create \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer test-token-123" \
  -d '{
    "userId": 1,
    "items": [
      {"goodsId": 1, "quantity": 1},
      {"goodsId": 3, "quantity": 2}
    ]
  }'
# {"code":200,"message":"success","data":{"id":1,"userId":1,"totalAmount":10997.00,"status":"CREATED"}}

触发限流(用 Apache Bench 或 wrk 连打 20 次,QPS 超过 10 后收到 429):

ab -n 20 -c 20 -H "Authorization: Bearer test" \
   -p order.json -T application/json \
   http://localhost:8080/api/order/create

验证 Feign 熔断(停掉 shop-goods 后再下单,返回"商品服务暂不可用"而不是 500 超时)。


十一、总结

本文化用到的全部组件

组件使用位置作用
Nacos 注册中心所有服务服务发现,Feign 通过服务名调用
Nacos 配置中心所有服务数据源、限流规则统一管理
Gatewayshop-gateway统一入口、路由转发、鉴权拦截
OpenFeignshop-order跨服务调用(调 user、调 goods)
Sentinelshop-order + gateway入口限流 + Feign 熔断 fallback
Seatashop-order.createOrder()订单+扣库存分布式事务
RocketMQshop-order + shop-sync下单异步通知、数据同步通知
适配器模式shop-sync对接不同供应商数据格式
定时任务shop-order异常订单对账兜底
Docker Compose项目根目录一键启动全套环境

架构总览

浏览器/APP
    |
[shop-gateway :8080]  -- 鉴权 + 路由 + 网关限流
    |
    +-- /api/user/**  -->  [shop-user :8081]   数据库: shop_user
    +-- /api/goods/** -->  [shop-goods :8083]  数据库: shop_goods
    +-- /api/order/** -->  [shop-order :8082]  数据库: shop_order
                              |
                              +-- Feign --> shop-user(验证用户)
                              +-- Feign --> shop-goods(查商品、扣库存)
                              +-- Seata @GlobalTransactional(分布式事务)
                              +-- RocketMQ --> 短信/日志/ES 同步
                              
[shop-sync]  -- 定时拉取供应商数据 --> RocketMQ --> 刷新缓存

[Nacos :8848]   注册中心 + 配置中心
[Sentinel :8858] 流量监控 + 规则管理
[RocketMQ :9876] 消息队列

这套代码放到框架里就能跑,每个组件都在真实业务流程中发挥了作用。


写于 2026 年 6 月 29 日