跳到主要内容

🛒 电商微服务重构

在不宕机的情况下,将单体应用转型为可扩展的微服务架构。


1. 问题陈述

业务背景

电商平台在秒杀活动期间出现严重的性能问题。单体应用无法独立扩展特定组件,高峰期的数据库锁导致级联故障。

技术挑战

  • 流量峰值:秒杀期间 100 倍正常负载
  • 超卖问题:竞态条件导致库存超卖
  • 数据库瓶颈:高峰期单个 PostgreSQL 实例 CPU 100%
  • 单体复杂性:50 万+ 行代码,3 年以上技术债

成功标准

  • 零超卖 在秒杀活动期间
  • 亚秒级结算 响应时间 (p99)
  • 99.9% 可用性 在高峰事件期间
  • 关键服务独立部署

2. 研究与分析

拆分策略

我们使用领域驱动设计(DDD)原则分析单体应用:

迁移方案

策略优势劣势我们的选择
大爆炸式干净利落高风险、长时间冻结
绞杀者模式渐进式、低风险周期较长
并行运行安全验证双倍基础设施部分采用 ✅

决策:绞杀者模式,关键路径(库存、结算)采用并行运行。


3. 架构设计

目标架构

服务边界

服务职责数据库通信方式
用户认证、用户资料PostgreSQL同步 (REST)
商品目录、搜索PostgreSQL + ES同步 (REST)
库存库存管理Redis同步 (REST)
订单订单生命周期PostgreSQL异步 (MQ)
支付支付处理PostgreSQL异步 (MQ)
通知邮件、短信、推送-异步 (MQ)

4. 实现要点

秒杀库存:防止超卖

这是最关键的挑战。我们实现了多层解决方案:

第一层:Redis + Lua 原子操作

@Service
public class InventoryService {

private static final String DEDUCT_STOCK_SCRIPT = """
local stock = redis.call('GET', KEYS[1])
if not stock then
return -1 -- 商品未找到
end

local current = tonumber(stock)
local quantity = tonumber(ARGV[1])

if current < quantity then
return 0 -- 库存不足
end

redis.call('DECRBY', KEYS[1], quantity)
return 1 -- 成功
""";

public DeductResult deductStock(String productId, int quantity) {
// Redis 中的原子操作 - 无竞态条件
Long result = redisTemplate.execute(
RedisScript.of(DEDUCT_STOCK_SCRIPT, Long.class),
List.of("stock:" + productId),
String.valueOf(quantity)
);

return switch (result.intValue()) {
case 1 -> DeductResult.SUCCESS;
case 0 -> DeductResult.INSUFFICIENT_STOCK;
case -1 -> DeductResult.PRODUCT_NOT_FOUND;
default -> DeductResult.ERROR;
};
}
}

第二层:分布式限流

@Component
public class FlashSaleRateLimiter {

// 滑动窗口限流器
private static final String RATE_LIMIT_SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- 移除过期条目
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 统计当前窗口数量
local count = redis.call('ZCARD', key)

if count >= limit then
return 0 -- 被限流
end

-- 添加当前请求
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, window / 1000)

return 1 -- 允许
""";

public boolean allowRequest(String userId, String saleId) {
String key = String.format("rate:%s:%s", saleId, userId);
Long now = System.currentTimeMillis();

Long result = redisTemplate.execute(
RedisScript.of(RATE_LIMIT_SCRIPT, Long.class),
List.of(key),
"5", // 5 次请求
"1000", // 每 1 秒窗口
String.valueOf(now)
);

return result == 1;
}
}

第三层:带背压的请求队列

@Service
public class FlashSaleOrderService {

private final RocketMQTemplate rocketMQTemplate;
private final RedissonClient redisson;

public OrderResult placeOrder(OrderRequest request) {
// 1. 限流检查
if (!rateLimiter.allowRequest(request.userId(), request.saleId())) {
return OrderResult.rateLimited();
}

// 2. 扣减库存 (Redis)
DeductResult deductResult = inventoryService.deductStock(
request.productId(),
request.quantity()
);

if (deductResult != DeductResult.SUCCESS) {
return OrderResult.fromDeductResult(deductResult);
}

// 3. 生成订单号
String orderId = generateOrderId();

// 4. 发送到异步处理队列
rocketMQTemplate.asyncSend(
"flash-sale-orders",
new FlashSaleOrderMessage(orderId, request),
new SendCallback() {
@Override
public void onSuccess(SendResult result) {
// 订单已入队等待处理
}

@Override
public void onException(Throwable e) {
// 回滚库存
inventoryService.restoreStock(
request.productId(),
request.quantity()
);
}
}
);

return OrderResult.queued(orderId);
}
}

数据库拆分

从单一数据库迁移到每服务独立数据库:

// 分布式事务的 Saga 模式
@Service
public class OrderSagaOrchestrator {

public OrderResult createOrder(CreateOrderCommand command) {
Saga<OrderContext> saga = Saga.<OrderContext>builder()
.step("reserve-inventory")
.action(ctx -> inventoryClient.reserve(ctx.items()))
.compensation(ctx -> inventoryClient.release(ctx.reservationId()))
.step("create-payment")
.action(ctx -> paymentClient.createPayment(ctx.amount()))
.compensation(ctx -> paymentClient.cancelPayment(ctx.paymentId()))
.step("confirm-order")
.action(ctx -> orderRepository.confirm(ctx.orderId()))
.compensation(ctx -> orderRepository.cancel(ctx.orderId()))
.build();

return saga.execute(new OrderContext(command));
}
}

5. 挑战与解决方案

挑战 1:跨服务数据一致性

问题:分布式事务很难。跨服务的 ACID 是不可能的。

解决方案:通过发件箱模式实现最终一致性

@Transactional
public void completeOrder(Order order) {
// 1. 更新订单状态
orderRepository.save(order.complete());

// 2. 写入发件箱(同一事务)
outboxRepository.save(new OutboxEvent(
"order.completed",
order.getId(),
objectMapper.writeValueAsString(order)
));
}

// 独立进程轮询发件箱并发布事件
@Scheduled(fixedDelay = 100)
public void publishOutboxEvents() {
List<OutboxEvent> pending = outboxRepository.findPending(100);
for (OutboxEvent event : pending) {
try {
messageQueue.publish(event.getTopic(), event.getPayload());
outboxRepository.markPublished(event.getId());
} catch (Exception e) {
outboxRepository.incrementRetry(event.getId());
}
}
}

挑战 2:服务发现与负载均衡

解决方案:Kubernetes 原生服务发现 + Istio 流量管理

# Istio VirtualService 用于金丝雀发布
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: order-service
subset: v2
weight: 100
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10

挑战 3:分布式系统监控

解决方案:OpenTelemetry 分布式追踪

@RestController
public class OrderController {

private final Tracer tracer;

@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
Span span = tracer.spanBuilder("createOrder")
.setAttribute("order.user_id", request.getUserId())
.setAttribute("order.total", request.getTotal())
.startSpan();

try (Scope scope = span.makeCurrent()) {
// 业务逻辑
OrderResult result = orderService.createOrder(request);

span.setAttribute("order.id", result.getOrderId());
span.setStatus(StatusCode.OK);

return ResponseEntity.ok(result.toResponse());
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}

6. 结果与指标

秒杀活动性能(双十一事件)

指标优化前优化后提升
峰值 QPS5,00050,000+10 倍
超卖事故12 起0 起-100%
结算 p998.2s0.4s-95%
系统可用性94%99.95%+6%

基础设施影响

资源优化前(单体)优化后(微服务)
CPU 成本$5,000/月$3,200/月
扩容时间5-10 分钟30 秒
部署频率每周每日 20+ 次
MTTR45 分钟8 分钟

7. 经验教训

做得好的方面

  • ✅ 绞杀者模式有效降低了风险
  • ✅ Redis 用于库存是正确的选择
  • ✅ 异步处理优雅应对了流量峰值
  • ✅ 每服务独立数据库消除了锁竞争

可以改进的方面

  • ⚠️ 应该更早投资分布式追踪
  • ⚠️ 最初的服务边界划分过于细粒度(后来合并了一些)
  • ⚠️ 团队需要更多分布式系统概念的培训

核心要点

  1. Redis Lua 脚本非常强大 - 原子操作可以解决很多竞态条件
  2. 最终一致性是可以接受的 - 用户关心正确的结果,而不是即时更新
  3. 队列是你的朋友 - 解耦以增强韧性
  4. 可观测性至关重要 - 无法修复你看不到的问题
  5. 从最热的路径开始 - 把迁移重点放在最大的痛点上

架构演进时间线


核心要点

微服务本身并不解决问题——它们只是让解决方案成为可能。真正的工作在于理解系统的瓶颈,并应用正确的模式(如 Redis + Lua 实现原子性、消息队列实现解耦)。