Spring Cloud Alibaba 实战:电商下单全流程落地
2026-06-29 17:35:00 · 标签:Spring Cloud、微服务、实战、分布式事务、消息队列
引言
上一篇 搭好了框架——Nacos、Gateway、OpenFeign、Sentinel、Seata、RocketMQ 六件套全部就位。但这只是"把工具买齐了"。本文基于这个框架,实现一个完整的电商下单流程,把每个组件都真正用起来。
业务流程:用户浏览商品 -> 下单 -> 扣库存 -> 发短信通知 -> 同步供应商数据 -> 定时对账。
读完这篇文章,你会知道:
- Feign 怎么在实际跨服务调用中传参、处理异常
- Sentinel 怎么同时保护自己的接口和 Feign 调用的熔断
- Seata 分布式事务怎么写业务代码(改造成本极低)
- RocketMQ 怎么在下单后异步解耦
- 网关怎么做鉴权拦截
- 数据同步服务怎么用适配器模式对接外部系统
一、业务场景与数据模型
1.1 服务与数据库划分
微服务的原则是一个服务一个库,服务间通过接口通信,绝不直接访问别人的数据库。
| 服务 | 数据库 | 核心表 |
|---|---|---|
| shop-user | shop_user | users |
| shop-goods | shop_goods | goods |
| shop-order | shop_order | orders, 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.yaml 和 shop-order-datasource.yaml 同理,把库名换成 shop_goods 和 shop_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
}
]
说明:
- 第一条:
/order/create接口 QPS 限制 10(URL 资源,入口流量) - 第二条:
createOrder方法 QPS 限制 5(@GlobalTransactional的 name,防止分布式事务过载)
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 启动顺序
- MySQL + Nacos + RocketMQ
- shop-user(8081)+ shop-goods(8083)
- shop-order(8082)
- 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
使用方式:
- Linux / macOS:
chmod +x startup.sh && ./startup.sh - 启动前确保 MySQL、Nacos、RocketMQ 已运行(
docker compose up -d) - 按
Ctrl+C会依次停止所有服务
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 配置中心 | 所有服务 | 数据源、限流规则统一管理 |
| Gateway | shop-gateway | 统一入口、路由转发、鉴权拦截 |
| OpenFeign | shop-order | 跨服务调用(调 user、调 goods) |
| Sentinel | shop-order + gateway | 入口限流 + Feign 熔断 fallback |
| Seata | shop-order.createOrder() | 订单+扣库存分布式事务 |
| RocketMQ | shop-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 日