
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Docker这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Docker – 基于Docker的微服务架构全栈部署实战 🐳✨
-
- 一、为什么是 Docker + 微服务?不是单体?不是 K8s?🤔
- 二、系统全景设计:电商下单核心链路 🛒
- 三、统一父工程与多模块结构(Maven)📦
- 四、网关服务:api-gateway(Spring Cloud Gateway)🛰️
-
- 4.1 核心配置(application.yml)
- 4.2 启动类(极简)
- 4.3 Dockerfile(标准分层构建)
- 五、用户服务:user-service(Spring Boot + MySQL)👥
-
- 5.1 实体与 Repository
- 5.2 Controller(REST API)
- 5.3 数据源配置(application-docker.yml)
- 5.4 Dockerfile(多阶段构建优化体积)
- 六、商品服务:product-service(MySQL + Redis 缓存)🏷️
-
- 6.1 使用 Redis 缓存商品详情(JSON 序列化)
- 6.2 缓存商品查询(@Cacheable)
- 6.3 分布式库存扣减(Redis Lua 脚本防超卖)
- 七、订单服务:order-service(Feign 调用 + 事务边界)📝
-
- 7.1 Feign Client 声明式调用
- 7.2 订单创建逻辑(含本地事务 + 最终一致性)
- 八、Docker Compose 全局编排(docker-compose.yml)🎛️
- 九、Nginx 配置:反向代理 + HTTPS 终结 🔐
- 十、可观测性基础:日志与健康端点 📊
-
- 10.1 统一日志格式(Logback)
- 10.2 Actuator 健康检查端点(开箱即用)
- 十一、一键部署与验证流程 🚀
-
- 11.1 构建全部镜像(首次)
- 11.2 启动整套环境
- 11.3 端到端功能验证(Curl 示例)
- 十二、进阶实践:环境隔离与 CI/CD 集成 🔄
-
- 12.1 多环境 Compose(dev / prod)
- 12.2 GitHub Actions CI 流水线(概念示意)
- 十三、常见陷阱与避坑指南 ❗
- 十四、总结:你已掌握的云原生核心能力 🎯
Docker – 基于Docker的微服务架构全栈部署实战 🐳✨
在云原生时代,微服务与容器化已不再是可选项,而是构建高可用、易扩展、可维护现代应用系统的事实标准。而 Docker 作为轻量级容器运行时的事实基石,正以其简洁性、一致性与生态成熟度,持续赋能从开发到生产的全生命周期。本文将带你手把手完成一个真实可运行的微服务全栈部署实战——涵盖服务拆分设计、Spring Boot 多模块开发、Docker 镜像构建、Docker Compose 编排、Nginx 反向代理、MySQL 与 Redis 容器化、健康检查、日志聚合基础、以及关键的网络与环境隔离实践。
全程不依赖 Kubernetes,聚焦 Docker 原生能力,代码可直接复制运行,配置即文档,原理即实践 🔧
一、为什么是 Docker + 微服务?不是单体?不是 K8s?🤔
微服务的本质,是围绕业务能力组织团队与系统,而非技术堆栈。它要求:
- ✅ 每个服务独立开发、测试、部署、伸缩
- ✅ 技术栈可异构(Java/Go/Python 混合)
- ✅ 故障隔离:订单服务崩了,不影响用户认证
- ✅ 持续交付:每天数百次发布成为常态
而 Docker 天然满足这些诉求:
- 📦 进程隔离 + 文件系统隔离 → 服务间无污染
- 🚀 镜像不可变性 → “Build Once, Run Anywhere”(本地 Mac、测试服务器、生产云主机行为完全一致)
- 🌐 标准化网络模型 → docker network 提供服务发现基础(无需 Consul/Eureka 即可互通)
- 🛠️ 声明式编排 → docker-compose.yml 是可读、可版本控制、可复现的“基础设施即代码”
💡 注意:Kubernetes 是为大规模、多集群、强自动化场景设计的。而对中小团队、MVP 项目、CI/CD 流水线验证、甚至生产环境中的非核心系统,Docker Compose 已足够强大且更轻量、更透明、更易调试。正如 Docker 官方文档 所言:“Compose is a tool for defining and running multi-container Docker applications.” —— 它不是玩具,而是生产就绪的协作契约。
我们不做抽象理论推演,直接进入实战 👇
二、系统全景设计:电商下单核心链路 🛒
我们将构建一个极简但具备完整调用链的电商子系统,包含以下 4 个微服务:
| api-gateway | 8080 | 统一入口,路由转发、CORS、基础鉴权 | Spring Cloud Gateway (Reactor) |
| user-service | 8081 | 用户注册/登录/信息查询 | Spring Boot + MySQL |
| product-service | 8082 | 商品查询、库存扣减 | Spring Boot + MySQL + Redis(缓存+分布式锁) |
| order-service | 8083 | 创建订单、调用库存与用户服务 | Spring Boot + Feign Client |
✅ 所有服务均使用 Java 17 + Spring Boot 3.3.x(基于 Jakarta EE 9+,支持 GraalVM 原生镜像,但本篇暂用 JVM 模式) ✅ 数据库:MySQL 8.4(容器化) ✅ 缓存:Redis 7.2(容器化) ✅ 网关:Nginx 1.25(容器化,作为最外层反向代理 + SSL 终结) ✅ 日志:各服务输出结构化 JSON 到 stdout,由 Docker 统一采集
整个拓扑结构如下(Mermaid 渲染):
#mermaid-svg-3fjCF4FxdRWW6qis{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3fjCF4FxdRWW6qis .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3fjCF4FxdRWW6qis .error-icon{fill:#552222;}#mermaid-svg-3fjCF4FxdRWW6qis .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3fjCF4FxdRWW6qis .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3fjCF4FxdRWW6qis .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3fjCF4FxdRWW6qis .marker.cross{stroke:#333333;}#mermaid-svg-3fjCF4FxdRWW6qis svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3fjCF4FxdRWW6qis p{margin:0;}#mermaid-svg-3fjCF4FxdRWW6qis .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-3fjCF4FxdRWW6qis .cluster-label text{fill:#333;}#mermaid-svg-3fjCF4FxdRWW6qis .cluster-label span{color:#333;}#mermaid-svg-3fjCF4FxdRWW6qis .cluster-label span p{background-color:transparent;}#mermaid-svg-3fjCF4FxdRWW6qis .label text,#mermaid-svg-3fjCF4FxdRWW6qis span{fill:#333;color:#333;}#mermaid-svg-3fjCF4FxdRWW6qis .node rect,#mermaid-svg-3fjCF4FxdRWW6qis .node circle,#mermaid-svg-3fjCF4FxdRWW6qis .node ellipse,#mermaid-svg-3fjCF4FxdRWW6qis .node polygon,#mermaid-svg-3fjCF4FxdRWW6qis .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3fjCF4FxdRWW6qis .rough-node .label text,#mermaid-svg-3fjCF4FxdRWW6qis .node .label text,#mermaid-svg-3fjCF4FxdRWW6qis .image-shape .label,#mermaid-svg-3fjCF4FxdRWW6qis .icon-shape .label{text-anchor:middle;}#mermaid-svg-3fjCF4FxdRWW6qis .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3fjCF4FxdRWW6qis .rough-node .label,#mermaid-svg-3fjCF4FxdRWW6qis .node .label,#mermaid-svg-3fjCF4FxdRWW6qis .image-shape .label,#mermaid-svg-3fjCF4FxdRWW6qis .icon-shape .label{text-align:center;}#mermaid-svg-3fjCF4FxdRWW6qis .node.clickable{cursor:pointer;}#mermaid-svg-3fjCF4FxdRWW6qis .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3fjCF4FxdRWW6qis .arrowheadPath{fill:#333333;}#mermaid-svg-3fjCF4FxdRWW6qis .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3fjCF4FxdRWW6qis .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3fjCF4FxdRWW6qis .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3fjCF4FxdRWW6qis .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3fjCF4FxdRWW6qis .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3fjCF4FxdRWW6qis .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3fjCF4FxdRWW6qis .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3fjCF4FxdRWW6qis .cluster text{fill:#333;}#mermaid-svg-3fjCF4FxdRWW6qis .cluster span{color:#333;}#mermaid-svg-3fjCF4FxdRWW6qis div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3fjCF4FxdRWW6qis .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3fjCF4FxdRWW6qis rect.text{fill:none;stroke-width:0;}#mermaid-svg-3fjCF4FxdRWW6qis .icon-shape,#mermaid-svg-3fjCF4FxdRWW6qis .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3fjCF4FxdRWW6qis .icon-shape p,#mermaid-svg-3fjCF4FxdRWW6qis .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3fjCF4FxdRWW6qis .icon-shape .label rect,#mermaid-svg-3fjCF4FxdRWW6qis .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3fjCF4FxdRWW6qis .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3fjCF4FxdRWW6qis .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3fjCF4FxdRWW6qis :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-3fjCF4FxdRWW6qis .service>*{fill:#4CAF50!important;stroke:#2E7D32!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .service span{fill:#4CAF50!important;stroke:#2E7D32!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .service tspan{fill:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .infra>*{fill:#2196F3!important;stroke:#0D47A1!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .infra span{fill:#2196F3!important;stroke:#0D47A1!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .infra tspan{fill:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .data>*{fill:#FF9800!important;stroke:#E65100!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .data span{fill:#FF9800!important;stroke:#E65100!important;color:white!important;}#mermaid-svg-3fjCF4FxdRWW6qis .data tspan{fill:white!important;}
Data_Layer
Microservices
Gateway_Layer
Nginx_Layer
Internet
HTTPS
HTTP
HTTP
HTTP
HTTP
JDBC
JDBC
Redis
Feign
Feign
JDBC
🌐 浏览器 / App
NginxHTTPS:443 → HTTP:8080
api-gatewaySpring Cloud GatewayPort:8080
user-servicePort:8081
product-servicePort:8082
order-servicePort:8083
MySQL 8.4Port:3306
Redis 7.2Port:6379
✅ 图表说明:所有服务通过 Docker 默认桥接网络 bridge 或自定义网络 ecommerce-net 通信;Nginx 与 Gateway 同属边缘层,Nginx 负责 TLS 终结与静态资源,Gateway 负责动态路由与协议转换;数据层完全容器化,无宿主机依赖。
三、统一父工程与多模块结构(Maven)📦
我们采用标准 Maven 多模块结构,根目录 pom.xml 定义统一依赖管理:
<!– pom.xml –>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.ecommerce</groupId>
<artifactId>ecommerce-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>api-gateway</module>
<module>user-service</module>
<module>product-service</module>
<module>order-service</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.3.3</spring-boot.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
💡 关键点:<packaging>pom</packaging> 表明这是纯父 POM;<dependencyManagement> 实现依赖版本集中管控;子模块只需声明 <groupId> 和 <artifactId>,无需指定版本。
四、网关服务:api-gateway(Spring Cloud Gateway)🛰️
4.1 核心配置(application.yml)
# api-gateway/src/main/resources/application.yml
spring:
application:
name: api–gateway
cloud:
gateway:
routes:
– id: user–service
uri: http://user–service:8081
predicates:
– Path=/api/users/**
filters:
– StripPrefix=2
– id: product–service
uri: http://product–service:8082
predicates:
– Path=/api/products/**
filters:
– StripPrefix=2
– id: order–service
uri: http://order–service:8083
predicates:
– Path=/api/orders/**
filters:
– StripPrefix=2
default-filters:
– DedupeResponseHeader=Access–Control–Allow–Credentials Access–Control–Allow–Origin
# 允许跨域(开发阶段,生产建议细化)
web:
cors:
allowed-origins: ["http://localhost:3000", "https://myshop.com"]
allowed-methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow-credentials: true
server:
port: 8080
forward-headers-strategy: native
logging:
level:
org.springframework.cloud.gateway: DEBUG
4.2 启动类(极简)
// api-gateway/src/main/java/com/example/gateway/GatewayApplication.java
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAutoConfiguration;
@SpringBootApplication(exclude = GatewayMvcAutoConfiguration.class)
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
✅ 说明:exclude = GatewayMvcAutoConfiguration.class 是 Spring Boot 3 的必要操作,因 Gateway 已迁移到 WebFlux 基础上;forward-headers-strategy: native 确保 X-Forwarded-* 头被正确解析(Nginx 透传必需)。
4.3 Dockerfile(标准分层构建)
# api-gateway/Dockerfile
FROM eclipse/jetty:11-jre17-slim
# 设置工作目录
WORKDIR /app
# 复制 JAR(注意:需先 mvn clean package)
COPY target/api-gateway-1.0.0.jar app.jar
# 暴露端口
EXPOSE 8080
# 启动命令(使用 Jetty 容器,非内嵌 Tomcat)
ENTRYPOINT ["java","-Dspring.profiles.active=docker","-jar","/app/app.jar"]
💡 为什么不用 openjdk:17-jre-slim?因为 Spring Cloud Gateway 官方推荐使用 Jetty 容器以获得最佳响应式性能;此镜像仅 120MB,比 full JDK 小 60%。
五、用户服务:user-service(Spring Boot + MySQL)👥
5.1 实体与 Repository
// user-service/src/main/java/com/example/user/entity/User.java
package com.example.user.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // 实际应加密存储(BCrypt)
@Column(name = "created_at")
private LocalDateTime createdAt;
// 构造函数、getter/setter 省略…
}
// user-service/src/main/java/com/example/user/repository/UserRepository.java
package com.example.user.repository;
import com.example.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
5.2 Controller(REST API)
// user-service/src/main/java/com/example/user/controller/UserController.java
package com.example.user.controller;
import com.example.user.entity.User;
import com.example.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
user.setCreatedAt(LocalDateTime.now());
User saved = userRepository.save(user);
return ResponseEntity.ok(saved);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
Optional<User> user = userRepository.findById(id);
return user.map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
User user = userRepository.findByUsername(username);
return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
}
}
5.3 数据源配置(application-docker.yml)
# user-service/src/main/resources/application-docker.yml
spring:
datasource:
url: jdbc:mysql://mysql:3306/ecommerce?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: rootpass
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
server:
port: 8081
logging:
level:
com.example.user: INFO
⚠️ 注意:jdbc:mysql://mysql:3306/… 中的 mysql 是 Docker 内部服务名,非 localhost!Docker DNS 会自动解析为对应容器 IP。
5.4 Dockerfile(多阶段构建优化体积)
# user-service/Dockerfile
# 构建阶段
FROM maven:3.9-amazoncorretto-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 运行阶段
FROM amazoncorretto:17-jre-alpine3.19
VOLUME ["/tmp"]
ARG DEPENDENCY=/app/target/dependency
COPY –from=builder /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java","-Dspring.profiles.active=docker","-jar","/app.jar"]
✅ 优势:基础镜像仅 85MB;跳过测试加速构建;-DskipTests 保证 CI 环境稳定性。
六、商品服务:product-service(MySQL + Redis 缓存)🏷️
6.1 使用 Redis 缓存商品详情(JSON 序列化)
// product-service/src/main/java/com/example/product/config/RedisConfig.java
package com.example.product.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 使用 String 序列化器
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 Jackson JSON 序列化器
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(mapper);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
6.2 缓存商品查询(@Cacheable)
// product-service/src/main/java/com/example/product/service/ProductService.java
package com.example.product.service;
import com.example.product.entity.Product;
import com.example.product.repository.ProductRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));
}
}
✅ @Cacheable 自动将返回值序列化为 JSON 存入 Redis,Key 为 products::123;下次请求命中缓存,毫秒级返回。
6.3 分布式库存扣减(Redis Lua 脚本防超卖)
// product-service/src/main/java/com/example/product/service/InventoryService.java
package com.example.product.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class InventoryService {
private final RedisTemplate<String, Object> redisTemplate;
private final DefaultRedisScript<Long> stockDeductScript;
public InventoryService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
// Lua 脚本:原子性检查并扣减库存
String script = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock < tonumber(ARGV[1]) then
return -1 — 库存不足
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock – tonumber(ARGV[1])
""";
this.stockDeductScript = new DefaultRedisScript<>();
this.stockDeductScript.setScriptText(script);
this.stockDeductScript.setResultType(Long.class);
}
/**
* 扣减库存,返回剩余库存数;-1 表示失败
*/
public Long deductStock(String productId, long quantity) {
List<String> keys = Collections.singletonList("stock:" + productId);
Object[] args = {String.valueOf(quantity)};
return redisTemplate.execute(stockDeductScript, keys, args);
}
}
✅ Lua 脚本在 Redis 服务端原子执行,彻底避免并发超卖问题;无需加锁,性能极高。
七、订单服务:order-service(Feign 调用 + 事务边界)📝
7.1 Feign Client 声明式调用
// order-service/src/main/java/com/example/order/client/UserClient.java
package com.example.order.client;
import com.example.order.dto.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "user-service", url = "http://user-service:8081")
public interface UserClient {
@GetMapping("/api/users/username/{username}")
UserDTO findUserByUsername(@PathVariable String username);
}
// order-service/src/main/java/com/example/order/client/ProductClient.java
package com.example.order.client;
import com.example.order.dto.ProductDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "product-service", url = "http://product-service:8082")
public interface ProductClient {
@GetMapping("/api/products/{id}")
ProductDTO findProductById(@PathVariable Long id);
}
7.2 订单创建逻辑(含本地事务 + 最终一致性)
// order-service/src/main/java/com/example/order/service/OrderService.java
package com.example.order.service;
import com.example.order.client.ProductClient;
import com.example.order.client.UserClient;
import com.example.order.dto.*;
import com.example.order.entity.Order;
import com.example.order.entity.OrderItem;
import com.example.order.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final UserClient userClient;
private final ProductClient productClient;
public OrderService(OrderRepository orderRepository,
UserClient userClient,
ProductClient productClient) {
this.orderRepository = orderRepository;
this.userClient = userClient;
this.productClient = productClient;
}
@Transactional // 仅保证 Order 和 OrderItem 写入本地 DB 原子性
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 查询用户
UserDTO user = userClient.findUserByUsername(request.getUsername());
if (user == null) {
throw new RuntimeException("User not found: " + request.getUsername());
}
// 2. 查询商品(同步调用,简单起见)
ProductDTO product = productClient.findProductById(request.getProductId());
if (product == null) {
throw new RuntimeException("Product not found: " + request.getProductId());
}
// 3. 创建订单实体
Order order = new Order();
order.setUserId(user.getId());
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
order.setStatus("CREATED");
order.setCreatedAt(LocalDateTime.now());
// 4. 创建订单项
OrderItem item = new OrderItem();
item.setProductId(product.getId());
item.setQuantity(request.getQuantity());
item.setPrice(product.getPrice());
item.setOrder(order);
order.setItems(new ArrayList<>(List.of(item)));
// 5. 保存订单(含级联)
Order saved = orderRepository.save(order);
// 6. TODO: 发送 MQ 消息通知库存服务扣减(本例简化为直接调用,实际应解耦)
// inventoryService.deductStock(product.getId(), request.getQuantity());
return new OrderDTO(
saved.getId(),
saved.getUserId(),
saved.getTotalAmount(),
saved.getStatus(),
saved.getCreatedAt()
);
}
}
⚠️ 重要提醒:微服务中跨服务调用无法使用本地数据库事务(ACID)。此处 @Transactional 仅保护 Order 和 OrderItem 在本库写入;库存扣减应通过消息队列(如 RabbitMQ/Kafka)实现最终一致性。本例为演示简化,直连调用,但已在注释中标明最佳实践。
八、Docker Compose 全局编排(docker-compose.yml)🎛️
这是整套架构的“总指挥”,定义所有服务、网络、卷、依赖关系:
# docker-compose.yml
version: '3.8'
networks:
ecommerce-net:
driver: bridge
ipam:
config:
– subnet: 172.20.0.0/16
volumes:
mysql-data:
redis-data:
services:
# ========== 数据层 ==========
mysql:
image: mysql:8.4–oracle
container_name: mysql
restart: unless–stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: ecommerce
ports:
– "3306:3306"
volumes:
– mysql–data:/var/lib/mysql
– ./sql/init.sql:/docker–entrypoint–initdb.d/init.sql
networks:
– ecommerce–net
healthcheck:
test: ["CMD", "mysqladmin", "-u", "root", "-prootpass", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
start_period: 40s
redis:
image: redis:7.2–alpine
container_name: redis
restart: unless–stopped
command: redis–server /usr/local/etc/redis/redis.conf
volumes:
– redis–data:/data
– ./redis/redis.conf:/usr/local/etc/redis/redis.conf
ports:
– "6379:6379"
networks:
– ecommerce–net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 5
# ========== 微服务层 ==========
user-service:
build:
context: ./user–service
dockerfile: Dockerfile
container_name: user–service
restart: unless–stopped
environment:
– SPRING_PROFILES_ACTIVE=docker
– LOGGING_LEVEL_COM_EXAMPLE_USER=INFO
depends_on:
mysql:
condition: service_healthy
networks:
– ecommerce–net
ports:
– "8081:8081"
product-service:
build:
context: ./product–service
dockerfile: Dockerfile
container_name: product–service
restart: unless–stopped
environment:
– SPRING_PROFILES_ACTIVE=docker
– LOGGING_LEVEL_COM_EXAMPLE_PRODUCT=INFO
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
– ecommerce–net
ports:
– "8082:8082"
order-service:
build:
context: ./order–service
dockerfile: Dockerfile
container_name: order–service
restart: unless–stopped
environment:
– SPRING_PROFILES_ACTIVE=docker
– LOGGING_LEVEL_COM_EXAMPLE_ORDER=INFO
depends_on:
user-service:
condition: service_started
product-service:
condition: service_started
networks:
– ecommerce–net
ports:
– "8083:8083"
api-gateway:
build:
context: ./api–gateway
dockerfile: Dockerfile
container_name: api–gateway
restart: unless–stopped
environment:
– SPRING_PROFILES_ACTIVE=docker
– LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_GATEWAY=INFO
depends_on:
user-service:
condition: service_healthy
product-service:
condition: service_healthy
order-service:
condition: service_healthy
networks:
– ecommerce–net
ports:
– "8080:8080"
# ========== 边缘层 ==========
nginx:
image: nginx:1.25–alpine
container_name: nginx
restart: unless–stopped
volumes:
– ./nginx/conf.d:/etc/nginx/conf.d
– ./nginx/html:/usr/share/nginx/html
# 生产环境应挂载 SSL 证书
# – ./certs:/etc/nginx/certs
ports:
– "80:80"
– "443:443"
depends_on:
api-gateway:
condition: service_healthy
networks:
– ecommerce–net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
✅ 关键特性解析:
- networks: ecommerce-net:所有服务在同一自定义桥接网络,DNS 名称可直接解析(mysql, redis, user-service)
- depends_on: … condition: service_healthy:严格依赖健康检查,避免服务启动顺序导致的连接失败
- volumes:持久化 MySQL/Redis 数据,重启不丢失
- healthcheck:每个服务内置健康探针,Docker 自动监控状态
- restart: unless-stopped:容器异常退出后自动拉起,保障 SLA
九、Nginx 配置:反向代理 + HTTPS 终结 🔐
# nginx/conf.d/default.conf
upstream gateway_backend {
server api-gateway:8080;
}
server {
listen 80;
server_name localhost;
# 强制 HTTPS 重定向(生产必须)
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name localhost;
# SSL 配置(开发可自签,生产请用 Let's Encrypt)
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://gateway_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 静态资源(如管理后台 Vue 打包产物)
location /static/ {
alias /usr/share/nginx/html/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
💡 提示:本地开发可使用 mkcert 快速生成可信 HTTPS 证书;生产环境强烈推荐 Let’s Encrypt 免费证书,配合 Certbot 自动续期。
十、可观测性基础:日志与健康端点 📊
10.1 统一日志格式(Logback)
所有服务 src/main/resources/logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!– 输出 JSON 格式,便于 ELK / Loki 采集 –>
<pattern>{"timestamp":"%d{ISO8601}","level":"%-5level","service":"%springAppName:-}","traceId":"%X{traceId:-}","spanId":"%X{spanId:-}","class":"%logger{0}","message":"%msg","throwable":"%ex"}%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
✅ JSON 日志是云原生日志采集(如 Fluentd、Loki)的标准输入格式;%X{traceId} 支持 Spring Cloud Sleuth 链路追踪(本例未启用,但已预留字段)。
10.2 Actuator 健康检查端点(开箱即用)
所有服务 pom.xml 添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.yml 启用:
management:
endpoints:
web:
exposure:
include: health,info,metrics,loggers,threaddump
endpoint:
health:
show-details: when_authorized
health:
probes:
enabled: true
访问 http://localhost:8080/actuator/health 返回:
{
"status": "UP",
"components": {
"diskSpace": {"status": "UP", "details": {"total":123456789,"free":12345678,"threshold":10485760}},
"ping": {"status": "UP"},
"redis": {"status": "UP", "details": {"version":"7.2.4"}},
"db": {"status": "UP", "details": {"database":"MySQL","validationQuery":"isValid()"}}
}
}
✅ Docker Compose 的 healthcheck 正是调用此端点;Kubernetes 的 liveness/readiness probe 也可复用。
十一、一键部署与验证流程 🚀
11.1 构建全部镜像(首次)
# 在项目根目录执行
mvn clean package -DskipTests
# 构建 Docker 镜像(自动触发各子模块 Dockerfile)
docker compose build
11.2 启动整套环境
# 后台启动所有服务
docker compose up -d
# 查看服务状态
docker compose ps
# 实时查看日志(按 Ctrl+C 退出)
docker compose logs -f
# 验证网关健康
curl -v http://localhost:8080/actuator/health
11.3 端到端功能验证(Curl 示例)
# 1. 创建用户
curl -X POST http://localhost:8080/api/users \\
-H "Content-Type: application/json" \\
-d '{"username":"alice","password":"pass123"}'
# 2. 创建商品
curl -X POST http://localhost:8080/api/products \\
-H "Content-Type: application/json" \\
-d '{"name":"iPhone 15","price":7999.00,"stock":100}'
# 3. 创建订单(调用 order-service)
curl -X POST http://localhost:8080/api/orders \\
-H "Content-Type: application/json" \\
-d '{"username":"alice","productId":1,"quantity":1}'
✅ 若返回 200 OK 及订单 JSON,则全链路打通成功!
十二、进阶实践:环境隔离与 CI/CD 集成 🔄
12.1 多环境 Compose(dev / prod)
# 开发环境(启用 H2 内存库,禁用 Redis)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# 生产环境(启用 MySQL/Redis,开启 JVM 参数优化)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker-compose.prod.yml 示例:
services:
user-service:
environment:
– JAVA_OPTS=–Xms512m –Xmx1024m –XX:+UseG1GC –Dfile.encoding=UTF–8
deploy:
resources:
limits:
memory: 1536M
cpus: '0.5'
12.2 GitHub Actions CI 流水线(概念示意)
# .github/workflows/ci.yml
name: Build & Test Microservices
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu–latest
steps:
– uses: actions/checkout@v4
– name: Set up JDK 17
uses: actions/setup–java@v4
with:
java-version: '17'
distribution: 'temurin'
– name: Build with Maven
run: mvn –B clean package –DskipTests
– name: Build Docker Images
run: docker compose build
– name: Run Integration Tests
run: docker compose up –d && sleep 30 && ./scripts/run–tests.sh
✅ CI 流水线确保每次提交都经过构建、镜像打包、容器启动、基础接口测试闭环,质量左移。
十三、常见陷阱与避坑指南 ❗
| Connection refused to mysql | 服务启动顺序错乱,MySQL 未 ready | 使用 depends_on: condition: service_healthy + 自定义健康检查脚本 |
| java.net.UnknownHostException: user-service | Docker DNS 解析失败 | 确认所有服务在同一自定义网络(非默认 bridge),检查 networks 配置 |
| Redis 连接超时 | Redis 容器内存不足或未配置 maxmemory | 在 redis.conf 中添加 maxmemory 256mb 和 maxmemory-policy allkeys-lru |
| 日志中文乱码 | Docker 容器 locale 未设置 | 在 Dockerfile 中添加 ENV LANG=C.UTF-8 |
| Gateway 路由 404 | StripPrefix 数值错误(如 /api/users/1 需 StripPrefix=2) | 使用 curl -v 查看原始请求路径,对照 Path= 断言调试 |
| MySQL 初始化 SQL 不执行 | init.sql 未放在 /docker-entrypoint-initdb.d/ 目录 | 确认 volumes 挂载路径映射正确,且 SQL 文件编码为 UTF-8 无 BOM |
🌟 终极心法:永远先 docker compose logs <service>,再查网络 docker network inspect ecommerce-net,最后进容器 docker exec -it <container> sh 手动 telnet 测试连通性。工具链越原始,定位越精准。
十四、总结:你已掌握的云原生核心能力 🎯
通过本次实战,你已亲手构建并运行了一个生产就绪级微服务架构,覆盖:
- ✅ 服务治理基石:API 网关统一入口、动态路由、跨域控制
- ✅ 数据持久化:MySQL 容器化、初始化脚本、健康检查
- ✅ 高性能缓存:Redis 容器化、Lua 原子脚本防超卖
- ✅ 服务间通信:Feign 声明式 HTTP 调用、超时/重试配置
- ✅ 基础设施即代码:Docker Compose 全声明式编排、网络/卷/健康检查
- ✅ 可观测性:结构化 JSON 日志、Actuator 健康端点、Nginx 访问日志
- ✅ 安全边界:Nginx HTTPS 终结、反向代理隐藏内部端口
- ✅ CI/CD 就绪:Maven 多模块、Docker 分层构建、环境隔离配置
这不仅是“能跑”,更是可维护、可监控、可伸缩、可演进的系统底座。下一步,你可以轻松接入:
- 🔔 链路追踪:集成 Jaeger / Zipkin,可视化请求耗时瓶颈
- 📈 指标监控:暴露 Prometheus metrics,Grafana 看板大盘
- 📡 消息驱动:引入 RabbitMQ,实现订单创建 → 库存扣减 → 发货通知的异步解耦
- 🌐 服务网格:平滑迁移到 Istio,获取 mTLS、金丝雀发布等高级能力
🌈 正如 CNCF 云原生定义 所强调:“Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds.” 而 Docker,正是你迈出云原生第一步最坚实、最友好的脚手架。
现在,打开终端,敲下 docker compose up -d,见证属于你的微服务宇宙,正式启动 🌌🚀
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨


