Spring Boot分布式商城微服务架构设计与实践
2026/9/16 21:09:25 网站建设 项目流程

简介:这是一份基于SpringBoot和Vue的分布式架构网上商城系统源代码,适合正在准备毕业设计、课程设计或期末大作业的Java方向学生,也适合希望了解前后端分离与分布式服务组合方式的入门到中级开发者。代码围绕商城常见业务展开,包含商品管理、订单处理、购物车、用户中心等完整闭环,能够帮助读者把SpringBoot、Mybatis、MySQL以及Vue等知识点串联起来,理解企业级项目的分层思路。整个资源包共包含791个文件,整体只有约19MB,体积紧凑;其中Java后端源码、Vue前端组件、JavaScript和CSS样式的数量最多,另有HTML页面、XML映射、Maven脚本和数据库配置文件,并附带一套可运行的构建与启动脚本,导入集成开发环境即可启动体验。目前该资源已有102人学习下载,所有源码均经过测试验证,适合直接作为毕业设计项目底子来扩展;预览中还能看到完整的后台管理界面组件,例如侧边栏、头部导航、面包屑和修改密码页面,配合SQLyog或Navicat导入数据库,能很快复现商城运行效果。

1. 这个标题说的是什么:从单体商城到 Spring Boot 微服务的必然演进

如果你在 2024 年之后还在用一套 Spring Boot 单体应用扛商城系统,要么流量还没到需要拆的规模,要么重构的账还没算清楚。网上搜「基于 springboot 的分布式架构网上商城系统代码」,本质上是两个诉求叠在一起:第一,业务模型要覆盖商城的主链路——商品、购物车、订单、库存、支付、用户;第二,架构上要体现分布式能力——服务拆分、注册发现、配置管理、网关路由、分布式事务。这套组合在毕业设计里是加分项,在企业内部是中小规模电商的常规形态,在面试里则是「微服务落地」最常被追问的题材。

常见做法并不是把 Spring Boot 用得多花哨,而是用「Spring Boot 做每个微服务的底座,Spring Cloud Alibaba 做分布式能力,Redis 扛热点,RabbitMQ 解耦异步」这一套标准组合。本文把这条链路从选型到可运行的代码一步一步铺开,覆盖服务怎么拆、组件怎么配、下单减库存这种分布式事务怎么落地、最后怎么部署验证。适合两类人:准备做商城类项目的 Java 开发,以及接手了类似系统但还没理清全貌的工程师。

2. 服务拆分与组件选型:Spring Boot 分布式商城的架构骨架

2.1 按业务域拆服务,而不是按技术层拆

拆服务是分布式架构的第一步,也是最容易拍脑袋的一步。我见过有人把 service、controller、dao 各拆成一个服务,结果一个下单请求穿四个网络调用,事务没法保证,排查问题时链路长得让人崩溃。正确的做法是按业务域拆,遵循领域驱动设计的限界上下文思想。

商城系统最稳定的拆分方式是五个核心服务加两个支撑服务:

服务名职责关键依赖
user-service注册登录、用户信息、收货地址MySQL、Redis
product-service商品分类、商品详情、库存查询MySQL、Redis、Elasticsearch(可选)
order-service订单创建、订单状态流转、超时取消MySQL、Redis、RabbitMQ
cart-service购物车读写、合并、数量变更Redis
payment-service支付单创建、支付回调、对账MySQL、RabbitMQ
gateway-service统一入口、路由转发、鉴权Spring Cloud Gateway
auth-servicetoken 签发与校验Redis、JWT

支付服务不一定每个商城都需要,如果你的项目只是演示性质,可以省略,把支付回调直接做成模拟接口。但订单和商品这两个服务是必须拆的,因为下单减库存这个动作天然跨服务,它是整个分布式架构里最有教学价值的业务场景。

2.2 用 Nacos 做注册中心和配置中心,选它的理由

服务之间要互相调用,第一步是互相找得到。Eureka 已经停止维护,Zookeeper 用起来偏重,Consul 在国内资料少,所以当前最主流的方案是 Nacos——它是阿里巴巴开源的产品,与 Spring Cloud Alibaba 生态绑定紧密,同时提供注册中心和配置中心两个能力,少部署一套组件。

Spring Boot 与 Spring Cloud 的版本对应关系容易踩坑,我用的是 Spring Boot 2.7.x 搭配 Spring Cloud Alibaba 2021.0.5.0,这个组合在市面上资料最全、问题最少。如果你是新建项目,直接选 Spring Boot 2.7.18,不要追 Spring Boot 3.x——Spring Boot 3 基于 Jakarta EE,很多老版本的依赖要跟着升级,对于学习型项目没必要增加排查成本。

pom.xml中引入依赖:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.5</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

提示:版本号不要自己随意组合。Spring Cloud、Spring Cloud Alibaba、Spring Boot 三者有严格的对应关系,配错的表现通常是启动时报NoSuchMethodErrorClassNotFoundException,而且报错位置千奇百怪,很难定位。

每个服务都要引入 Nacos 注册发现与配置中心的能力:

<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-alibaba-nacos-config</artifactId> </dependency>

bootstrap.yml中配置连接信息:

spring: application: name: order-service cloud: nacos: server-addr: 192.168.1.100:8848 discovery: namespace: mall group: DEFAULT_GROUP config: namespace: mall file-extension: yaml shared-configs: ->server: port: 8080 spring: application: name: gateway-service cloud: nacos: server-addr: 192.168.1.100:8848 gateway: routes: - id: user-route uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1 - id: product-route uri: lb://product-service predicates: - Path=/api/product/** filters: - StripPrefix=1 - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 # 限流过滤器,按 Redis + Token Bucket 实现 # - name: RequestRateLimiter # args: # redis-rate-limiter.replenishRate: 10 # redis-rate-limiter.burstCapacity: 20

lb://order-service是负载均衡协议头,Gateway 会去 Nacos 拉取 order-service 的实例列表,然后按负载均衡策略转发。StripPrefix=1的作用是去掉第一段路径,例如前端的/api/order/create转发到后端时变成/order/create。注释里的是限流配置RequestRateLimiter,基于 Redis 的令牌桶算法,replenishRate=10表示每秒放 10 个请求,burstCapacity=20表示突发容量 20。生产环境建议开启,学习项目可以先注释掉。

网关还应该统一处理 JWT 校验。最简单的方式是写一个全局过滤器,检查请求头里的 Authorization,在白名单外的请求一律拦截。逻辑不复杂,但一定要做,否则所有服务都要自己维护一份鉴权代码,那就失去网关的意义了。

3. 商品服务与订单服务:Spring Boot 商城核心业务代码实现

3.1 商品服务:先读缓存再查库,防止缓存穿透

商品详情是商城系统读压力最大的接口。用户逛商城时 90% 的请求都在看商品列表和商品详情,如果不加缓存,数据库扛不住。Redis 是首选,因为它是单线程模型、IO 多路复用,读写性能极高,而且支持过期时间。

商品详情的查询逻辑应该这样设计:

@Service public class ProductServiceImpl implements ProductService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private ProductMapper productMapper; private static final String PRODUCT_CACHE_KEY = "mall:product:"; @Override public ProductVO getProductById(Long productId) { // 1. 先查 Redis 缓存 String cacheKey = PRODUCT_CACHE_KEY + productId; String json = stringRedisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(json)) { return JSON.parseObject(json, ProductVO.class); } // 2. 缓存未命中,查数据库 Product product = productMapper.selectById(productId); if (product == null) { // 3. 防止缓存穿透:空值也缓存,设置短过期时间 stringRedisTemplate.opsForValue().set(cacheKey, "", Duration.ofMinutes(5)); throw new BizException("商品不存在"); } // 4. 写回缓存,设置 30 分钟过期 ProductVO vo = convert(product); stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), Duration.ofMinutes(30)); return vo; } }

这段代码里有三个关键设计。第一,先读缓存再读库,命中直接返回,Redis 单键读写耗时通常在 1ms 以内,比查 MySQL 快一两个数量级。第二,防穿透的处理方式容易忽略——如果一个商品 ID 在数据库中不存在,每次都穿透到数据库,恶意攻击时可以大量用不存在的 ID 请求,直接把数据库打挂。处理方式是把空值也写进缓存,过期时间设短一点,比如 5 分钟。第三,缓存更新策略用「更新数据库后删缓存」,而不是「更新数据库后更新缓存」——因为复杂对象的缓存更新逻辑容易出错,删掉让下次请求重建更安全,这也是旁路缓存模式的精髓。

3.2 订单服务:状态机设计是下单流程的骨架

订单服务是整个分布式商城中最复杂的服务,它的核心是订单状态机。一个订单从创建到完成要经过:待支付 → 已支付 → 已发货 → 已完成,中间还有取消和退款两个分支。如果不把状态流转收敛到一处,而是让业务代码里到处setStatus,不出三个版本就会变成垃圾代码。

我一般用状态模式加一张状态流转表来控制,代码结构如下:

public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { // 定义合法的状态流转:key 是当前状态,value 是可以流转到的状态集合 TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 4))); // 待支付 -> 已支付 / 已取消 TRANSITIONS.put(1, new HashSet<>(Arrays.asList(2, 4))); // 已支付 -> 已发货 / 已取消(退款) TRANSITIONS.put(2, new HashSet<>(Collections.singletonList(3))); // 已发货 -> 已完成 } public static void validateTransition(int from, int to) { Set<Integer> allowed = TRANSITIONS.get(from); if (allowed == null || !allowed.contains(to)) { throw new IllegalStateException("非法的订单状态流转: " + from + " -> " + to); } } }

状态机的价值在于把非法流转拦截在代码入口。比如一个已完成订单被重复点击取消,validateTransition会直接抛出异常,不会出现「已完成订单被改成已取消」这种生产事故。在OrderService中,每次状态变更前先调用校验方法,再执行 SQL 更新,加个乐观锁版本号防止并发问题。实际订单的状态可能比这个复杂,比如待支付订单超过 30 分钟自动取消,已支付但未发货时可以申请退款,这些都可以在状态机里加状态和流转规则,但骨架不变。

3.3 下单减库存的分布式事务:用本地消息表加 RabbitMQ 保证最终一致性

单体应用里下单减库存只是一个数据库事务的事,但拆成两个服务后,order-service 和 product-service 各自有自己的数据库,本地事务管不到对方。这里必须引入分布式事务方案。常见方案有 Seata 的 AT 模式、TCC 模式、本地消息表加消息队列。我的建议是学习项目用本地消息表加 RabbitMQ,理由有两个:一是 Seata 需要额外部署 TC 服务,增加部署复杂度;二是本地消息表的方案让你理解分布式事务的本质——最终一致性,而不是强一致。

核心思路:在 order-service 的数据库里建一张order_event表,下单时把订单数据和事件记录写在同一个本地事务里,然后通过一个定时任务把事件发送到 RabbitMQ,product-service 消费消息完成减库存。

CREATE TABLE `order_event` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` varchar(64) NOT NULL COMMENT '订单号', `event_type` varchar(32) NOT NULL COMMENT '事件类型:CREATE_ORDER', `payload` text NOT NULL COMMENT '事件内容,JSON格式', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待发送,1-已发送,2-已确认', `retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status_create_time` (`status`, `create_time`) ) ENGINE=InnoDB COMMENT='本地消息表';

下单接口的写入逻辑:

@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateRequest request) { // 1. 生成订单号,保存订单主表 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setTotalAmount(request.getTotalAmount()); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); // 2. 保存订单明细 orderItemMapper.insertBatch(request.getItems()); // 3. 写本地消息表 OrderEvent event = new OrderEvent(); event.setOrderId(order.getId()); event.setEventType("CREATE_ORDER"); event.setPayload(JSON.toJSONString(request)); event.setStatus(0); orderEventMapper.insert(event); // 4. 返回订单号 return order.getId(); }

关键在设计第 3 步的时机,它与订单写入在同一个@Transactional事务里,要么一起成功,要么一起回滚。这样保证订单数据和待发送的消息是原子的。

然后通过定时任务扫描状态为 0 的事件,发送到 RabbitMQ:

@Component public class OrderEventPublisher { @Autowired private RabbitTemplate rabbitTemplate; @Autowired private OrderEventMapper orderEventMapper; // 每 5 秒扫一次待发送事件 @Scheduled(fixedDelay = 5000) public void publishPendingEvents() { List<OrderEvent> pendingEvents = orderEventMapper.selectByStatus(0); for (OrderEvent event : pendingEvents) { try { rabbitTemplate.convertAndSend( "mall.order.exchange", "order.create", event.getPayload() ); // 发送成功后把消息标记为已发送 orderEventMapper.markSent(event.getId()); } catch (Exception e) { // 发送失败不做处理,下轮重试 orderEventMapper.increaseRetryCount(event.getId()); log.error("发送订单事件失败, eventId: {}", event.getId(), e); } } } }

product-service 那边消费消息减库存,消费成功后再调用 order-service 的一个回调接口确认消息已处理。如果消费失败,消息会进入死信队列,定时任务扫描死信队列人工处理。

这套方案的核心论点是「 把分布式问题转化为本地事务加异步重试」。它不保证强一致,但保证最终一致。极端情况下可能有多发一条消息的问题,所以消费者要做幂等——比如用事件 ID 做去重表。这个细节如果写进博客里,面试时非常加分。

4. 服务间调用与配置:Spring Boot 集成 OpenFeign 与 Sentinel

4.1 用 OpenFeign 声明式调用替代 RestTemplate

服务间调用是分布式架构的日常操作。order-service 创建订单前要查用户信息,支付完成后要通知 order-service 更新状态。如果每个地方都用 RestTemplate 拼 URL,代码里全是魔法字符串,而且没有负载均衡。OpenFeign 是 Spring Cloud 生态的标准答案——用接口加注解的方式声明 HTTP 客户端,底层由 Ribbon 做负载均衡,代码简洁可维护。

order-service 调用 user-service 的示例:

@FeignClient(name = "user-service", fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/user/{userId}") UserVO getUserById(@PathVariable("userId") Long userId); @PutMapping("/user/address") Boolean updateDefaultAddress(@RequestBody AddressUpdateRequest request); }

@FeignClient注解声明接口,name指定目标服务在注册中心的名称。方法注解的写法与 Spring MVC 完全一致,@PathVariable@RequestBody都会正常生效。fallback参数指定熔断降级实现类——当 user-service 不可用时,调用 fallback 中的方法返回一个默认值,避免异常向上抛导致整个下单流程中断。

Feign 在 Spring Boot 中要先开启:

@SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }

@EnableFeignClients放在启动类上,Spring 会扫描所有继承@FeignClient的接口,生成代理类注入到 Spring 容器。需要注意扫描包路径——如果你的 Feign Client 不在启动类所在包下,必须指定basePackages属性,这是新手最容易踩的坑。

4.2 用 Sentinel 给下单接口加流量控制,防止雪崩

商城系统最怕的是大促流量瞬间打进来,一个接口挂了把整个服务拖垮。Sentinel 是阿里开源的流量控制组件,与 Hystrix 相比,它更轻量、控制台功能更全、支持热点参数限流和系统自适应保护。Spring Cloud Alibaba 集成了 Sentinel,接入成本极低。

在 order-service 中加入依赖:

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>

添加配置:

spring: cloud: sentinel: transport: dashboard: 192.168.1.100:8858 eager: true datasource: # 生产环境建议把规则配置在 Nacos,这里先用本地文件 file: file: classpath:sentinel-rules.json rule-type: flow

在业务代码上加上@SentinelResource注解:

@SentinelResource(value = "createOrder", blockHandler = "createOrderBlock", fallback = "createOrderFallback") public Long createOrder(OrderCreateRequest request) { // 执行业务逻辑 return orderService.createOrder(request); } // 限流后执行的方法:返回友好提示 public Long createOrderBlock(OrderCreateRequest request, BlockException ex) { throw new BizException("当前下单人数较多,请稍后重试"); } // 业务异常时执行的方法 public Long createOrderFallback(OrderCreateRequest request, Throwable ex) { log.error("下单失败", ex); throw new BizException("下单失败,请稍后重试"); }

blockHandler处理的是 Sentinel 拦截的异常,比如限流、熔断;fallback处理的是业务代码自身的异常。两者不要混在一起用,否则会把真实的业务异常也吞掉。Sentinel 的规则既可以在控制台动态配置,也可以通过@SentinelResource注解配合 Nacos 持久化。控制台配置的规则默认保存在内存,服务重启就丢了,生产环境必须持久化到 Nacos。

4.3 用分布式定时任务处理超时订单

商城系统里有一个经典业务场景:用户下单后一直没支付,系统需要在 30 分钟后自动取消订单。单体时代直接用 Spring 的@Scheduled注解就能做,但微服务环境下要考虑一个问题——order-service 如果部署了多个实例,定时任务会在每个实例上同时执行,导致重复扫描和重复取消。

常见做法是引入 XXL-JOB,它自带分布式锁和任务分片功能。如果你的项目不想多部署一套调度中心,退而求其次可以用 Redis 的SETNX命令实现一个简单的分布式锁:

@Component public class OrderTimeoutCanceller { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private OrderMapper orderMapper; private static final String LOCK_KEY = "lock:order-timeout-cancel"; @Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟执行一次 public void cancelTimeoutOrders() { // 尝试获取分布式锁,加 5 分钟过期防止死锁 Boolean acquired = stringRedisTemplate.opsForValue() .setIfAbsent(LOCK_KEY, "1", Duration.ofMinutes(5)); if (!Boolean.TRUE.equals(acquired)) { log.info("另一个实例正在执行超时订单取消任务,跳过"); return; } try { // 查询超时订单并取消 List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(Duration.ofMinutes(30)); for (Order order : timeoutOrders) { // 校验状态机:只有待支付状态才能取消 OrderStatus.validateTransition(order.getStatus(), OrderStatus.CANCELED.getCode()); orderMapper.updateStatus(order.getId(), OrderStatus.CANCELED.getCode()); // 发送消息通知库存回滚 rabbitTemplate.convertAndSend("mall.order.exchange", "order.cancel", order.getId()); } } finally { // 释放锁 stringRedisTemplate.delete(LOCK_KEY); } } }

注意finally块里的锁释放逻辑,必须在任务执行完之后删除锁,否则下次任务无法执行。但如果有多个实例同时竞争,可能会出现「一个实例还没执行完,锁已经过期被另一个实例拿走」的情况,这就是为什么还需要看业务容忍度。学习项目用 Redis 分布式锁足够;生产环境更稳妥的方案是引入 XXL-JOB 的调度锁,它能确保同一时刻只有一个实例的同一个 Job 在执行。

5. 一个 Spring Boot 分布式商城代码的运行验证与压测技巧

代码写完了,怎么证明这套分布式架构真的能跑、能扛住流量?最后的收尾章节给三个具体动作:本地启动顺序、压测参数、验证分布式事务是否生效的排查方法。

5.1 本地启动顺序与依赖检查

启动整套系统时必须遵守固定的依赖顺序:先启动基础设施,再启动微服务。顺序错了会出现「服务启动时注册不到 Nacos」之类的假象。推荐顺序为:

  1. MySQL 与 Redis(用 Docker Compose 或者本机服务)
  2. Nacos Server(单机模式启动startup.cmd -m standalone
  3. RabbitMQ(依赖 Erlang 环境)
  4. gateway-service、auth-service
  5. user-service、product-service、cart-service
  6. order-service、payment-service

每个服务启动后在 Nacos 控制台的「服务管理」页面中确认服务列表已经出现,再启动下一个。跳过这一步,会在后续接口排查时浪费大量时间。

5.2 用 JMeter 压测下单接口,验证 Sentinel 限流是否生效

启动完成后从网关调用下单接口,确认链路通了。然后用 JMeter 做一个简单的压测,验证 Sentinel 配置的限流规则真正生效。创建一个线程组,设置为 200 个线程、循环 10 次,请求/api/order/create,观察下面的现象:

  • 未加 Sentinel 规则时,2000 个请求全部成功,服务响应时间随并发升高而变长
  • 加上规则后,超过阈值的请求被快速返回「当前下单人数较多,请稍后重试」,而不是堆积在服务端
  • 服务本身的 CPU 占用率维持稳定,没有出现线程池打满和内存飙升

如果压测时发现 JMeter 报连接超时而不是业务提示,大概率是网关线程池被占满,需要调server.tomcat.threads.max参数。相反地,如果是大量请求走了blockHandler,说明服务端限流生效了。

5.3 验证分布式事务最终一致性的简单方法

验证最终一致性比验证普通接口复杂一些。一个建议的验证步骤是:启动订单服务和商品服务,请求创建订单成功以后,直接停掉 RabbitMQ,观察订单状态是否创建成功、库存是否没减少;此时订单事件表里应该有一条status=0的待发送记录;重启 RabbitMQ,等待不超过 5 秒(定时任务轮询间隔),观察库存被正确扣减。整个过程无需写测试代码,只需要操作 RabbitMQ 的后台管理界面。

如果想做更严格的幂等验证,可以在 product-service 的消费逻辑里手动抛一次异常,观察消息是否进入死信队列、事件表里retry_count是否递增、重试后最终是否成功。这套排查路径是分布式商城系统最值得自己做一遍的演练,也是从「代码能跑」到「出了问题能查」的分水岭。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询