4. 微服务示例应用部署:使用 Spring Boot / Python Flask 构建两个模拟服务并部署到 K8s

本章将构建课程后续所有事务测试实验的“靶场”——两个轻量级微服务:订单服务(Order Service)与库存服务(Inventory Service)。我们分别使用 Spring Boot(Java)和 Python Flask 实现,并完整部署到 Kubernetes 集群。通过这一过程,你将掌握多语言微服务的容器化、镜像推送、K8s 资源清单编写以及服务间调用验证。

4.1 服务职责与 API 契约

两个服务共同模拟一次“下单扣库存”的原子操作场景。下表定义了核心 API 接口与预期行为:

服务 端点 方法 请求体(JSON) 成功响应 失败场景
订单服务 /api/order/create POST {"productId": "P001", "quantity": 2} {"orderId": "ORD-xxx", "status": "CREATED"} 库存不足、服务不可用
库存服务 /api/inventory/deduct POST {"productId": "P001", "quantity": 2} {"success": true, "remaining": 98} 库存不足、重复扣减
库存服务 /api/inventory/query GET 路径参数 ?productId=P001 {"productId": "P001", "stock": 100} 商品不存在
设计意图: 订单服务是事务发起方,库存服务是资源参与方。后续章节将围绕这两个服务之间的 API 调用链,注入网络延迟、服务崩溃、数据不一致等故障,验证分布式事务边界的正确性。

4.2 服务实现要点

4.2.1 订单服务(Spring Boot)

订单服务使用 Spring Boot 3.x + Maven 构建,核心逻辑包含:

  • 接收 HTTP POST 请求,解析 JSON 负载。
  • 通过 RestTemplate 调用库存服务的 /api/inventory/deduct 端点。
  • 根据库存服务返回结果决定订单状态(CREATED / FAILED)。
  • 返回标准化的订单响应。

关键代码片段(Controller 层):

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

    @Autowired
    private RestTemplate restTemplate;

    @Value("${inventory.service.url}")
    private String inventoryServiceUrl;

    @PostMapping("/create")
    public ResponseEntity<Map<String, Object>> createOrder(@RequestBody OrderRequest request) {
        Map<String, Object> response = new HashMap<>();
        try {
            // 调用库存服务扣减
            InventoryResponse invResp = restTemplate.postForObject(
                inventoryServiceUrl + "/api/inventory/deduct",
                request,
                InventoryResponse.class
            );
            if (invResp != null && invResp.isSuccess()) {
                response.put("orderId", "ORD-" + UUID.randomUUID().toString().substring(0, 8));
                response.put("status", "CREATED");
                return ResponseEntity.ok(response);
            } else {
                response.put("status", "FAILED");
                response.put("reason", "库存不足");
                return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
            }
        } catch (Exception e) {
            response.put("status", "FAILED");
            response.put("reason", "库存服务不可用: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response);
        }
    }
}

4.2.2 库存服务(Python Flask)

库存服务使用 Python 3.11 + Flask 实现,使用内存字典模拟数据库,支持并发安全(线程锁):

from flask import Flask, request, jsonify
import threading

app = Flask(__name__)

# 模拟库存数据
stock = {"P001": 100, "P002": 50}
lock = threading.Lock()

@app.route('/api/inventory/deduct', methods=['POST'])
def deduct():
    data = request.get_json()
    product_id = data.get('productId')
    quantity = data.get('quantity', 0)

    with lock:
        if product_id not in stock:
            return jsonify({"success": False, "message": "商品不存在"}), 404
        if stock[product_id] < quantity:
            return jsonify({"success": False, "message": "库存不足"}), 409
        stock[product_id] -= quantity
        return jsonify({"success": True, "remaining": stock[product_id]}), 200

@app.route('/api/inventory/query', methods=['GET'])
def query():
    product_id = request.args.get('productId')
    if product_id in stock:
        return jsonify({"productId": product_id, "stock": stock[product_id]}), 200
    return jsonify({"message": "商品不存在"}), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
注意: 生产环境不应使用内存存储。此处仅为简化实验环境,后续章节会引入 MySQL / Redis 作为真实数据源,并测试事务边界在持久化层的行为。

4.3 容器化与镜像构建

两个服务分别使用 Dockerfile 构建镜像。下表对比了构建方式:

服务 基础镜像 构建方式 暴露端口 镜像大小(约)
订单服务 eclipse-temurin:17-jre-alpine Maven 编译 + JAR 包运行 8080 180 MB
库存服务 python:3.11-slim pip 安装依赖 + 直接运行 5000 120 MB

构建命令示例(订单服务):

# 构建镜像
docker build -t order-service:1.0.0 -f Dockerfile.order .

# 推送至私有仓库(假设 registry 地址为 registry.example.com)
docker tag order-service:1.0.0 registry.example.com/order-service:1.0.0
docker push registry.example.com/order-service:1.0.0

4.4 Kubernetes 部署清单

每个服务包含 Deployment 和 Service 两个核心资源。以下为订单服务的完整 YAML 清单:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: microservices-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: registry.example.com/order-service:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: INVENTORY_SERVICE_URL
          value: "http://inventory-service:5000"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: microservices-demo
spec:
  selector:
    app: order-service
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP

库存服务的清单结构与订单服务类似,仅需调整镜像、端口(5000)和健康检查路径(/api/inventory/query?productId=P001)。

4.5 部署验证与调用链路

执行以下命令完成部署:

# 创建命名空间
kubectl create namespace microservices-demo

# 部署两个服务
kubectl apply -f order-service-deployment.yaml
kubectl apply -f inventory-service-deployment.yaml

# 检查 Pod 状态
kubectl get pods -n microservices-demo -w

# 等待所有 Pod 处于 Running 状态

部署成功后,使用临时 Pod 测试服务间调用:

# 进入集群内临时测试 Pod
kubectl run test-pod --image=curlimages/curl -it --rm --restart=Never -- sh

# 测试库存服务
curl -s http://inventory-service:5000/api/inventory/query?productId=P001

# 测试订单服务(会触发库存扣减)
curl -s -X POST http://order-service:80/api/order/create \
  -H "Content-Type: application/json" \
  -d '{"productId": "P001", "quantity": 2}'
验证结果: 首次调用应返回 {"orderId":"ORD-xxxx","status":"CREATED"},再次查询库存应显示剩余 98。若库存服务故意返回失败,订单服务应返回 HTTP 409 并携带失败原因。

4.6 服务间调用架构图

下图展示了两个微服务在 Kubernetes 集群内的部署拓扑与 API 调用关系:

微服务示例应用部署架构(K8s 集群内) Kubernetes Cluster 订单服务 (Spring Boot) Deployment: order-service Pod 副本数: 2 端口: 8080 Service: order-service:80 /api/order/create 库存服务 (Python Flask) Deployment: inventory-service Pod 副本数: 2 端口: 5000 Service: inventory-service:5000 /api/inventory/deduct /api/inventory/query HTTP POST 扣减库存请求 外部客户端 / 测试 Pod POST /api/order/create 说明:订单服务通过 K8s Service DNS(inventory-service:5000)调用库存服务,实现服务发现与负载均衡。

4.7 部署清单速查表

下表汇总了部署过程中需要关注的关键配置项,便于快速排查问题:

配置项 订单服务 库存服务 说明
命名空间 microservices-demo microservices-demo 统一命名空间便于网络策略管理
Service 名称 order-service inventory-service 作为 DNS 名称被其他服务引用
Service 端口 80 → 8080 5000 → 5000 ClusterIP 类型,仅集群内可达
环境变量 INVENTORY_SERVICE_URL 无 订单服务通过此变量定位库存服务
健康检查 /actuator/health /api/inventory/query?productId=P001 livenessProbe 确保 Pod 自动恢复
资源限制 256Mi/512Mi 128Mi/256Mi 避免单 Pod 耗尽节点资源
实验建议: 部署完成后,手动删除一个订单服务 Pod,观察 K8s 如何自动重建并保持服务可用。这是后续测试事务边界时“服务崩溃”场景的基础操作。