构建可靠分布式系统幂等性设计实战

262 阅读9分钟

一、幂等性基础概念

1.1 什么是幂等性

幂等性(Idempotence)源自数学概念,指的是一个操作可以重复执行多次而不会改变结果。在软件工程中,幂等性意味着:

  • 相同的操作执行一次和执行多次的效果相同
  • 不会产生副作用或意外的状态变化
  • 系统能够安全地处理重复请求
graph LR
A[同一请求] -->|携带唯一标识| B{系统状态}
B --> C[首次执行] --> D[状态变更]
B --> E[重复执行] --> F[保持状态不变]
1.2.3.4.

1.2 幂等性的重要性

在分布式系统中,幂等性至关重要的原因包括:

  1. 网络不可靠性:网络可能出现超时、丢包等问题,导致客户端重发请求。
  2. 系统故障恢复:服务重启或故障恢复时可能需要重新处理某些操作。
  3. 负载均衡:请求可能被路由到不同的服务实例。
  4. 消息队列:消息可能被重复投递(at-least-once 语义)。

非幂等系统 == 分布式定时炸弹

非幂等系统的血泪经验教训: ① 某金融系统重复支付导致千万损失(网络重试引发) ② 某电商超卖事件(消息队列重复消费)

二、. HTTP 协议中的幂等性

2.1 HTTP 方法的幂等性特征

HTTP 方法幂等性说明
GET:white_check_mark:获取资源,不改变服务器状态
HEAD:white_check_mark:类似GET,但只返回头部信息
PUT:white_check_mark:完整更新资源,多次执行结果相同
DELETE:white_check_mark:删除资源,重复删除不会产生错误
POST:x:创建资源,重复执行会创建多个资源
PATCH:x:部分更新,结果可能依赖于执行顺序

2.2 POST 请求的幂等性设计

虽然 POST 本身不是幂等的,但我们可以通过设计使其具备幂等性。

POST /api/orders
Content-Type: application/json


{
  "idempotency_key": "order_2023_001",
  "customer_id": "12345",
  "items": [...],
  "total_amount": 100.00
}

服务器端实现:

def create_order(request):
    idempotency_key = request.json.get('idempotency_key')


    # 检查是否已经处理过该请求
    existing_order = get_order_by_idempotency_key(idempotency_key)
    if existing_order:
        return existing_order  # 返回已存在的订单


    # 创建新订单
    order = Order.create(request.json)
    save_idempotency_record(idempotency_key, order.id)


    return order

三、 数据库操作的幂等性

3.1 INSERT 操作的幂等性

方案一:使用 INSERT IGNORE

INSERT IGNORE INTO users (id, name, email) 
VALUES (1, 'John Doe', 'john@example.com');
1.2.

方案二:使用 ON DUPLICATE KEY UPDATE

INSERT INTO users (id, name, email) 
VALUES (1, 'John Doe', 'john@example.com')
ON DUPLICATE KEY UPDATE 
    name = VALUES(name),
    email = VALUES(email);

方案三:使用 UPSERT 语法(PostgreSQL)

INSERT INTO users (id, name, email) 
VALUES (1, 'John Doe', 'john@example.com')
ON CONFLICT (id) 
DO UPDATE SET 
    name = EXCLUDED.name,
    email = EXCLUDED.email;

3.2 UPDATE 操作的幂等性

绝对更新(天然幂等)

UPDATE users SET status = 'active' WHERE id = 1;
1.

相对更新(例如,每次在原来的基础上加100,需要特殊处理)

-- 非幂等
UPDATE accounts SET balance = balance + 100 WHERE id = 1;


-- 幂等改进
UPDATE accounts SET balance = balance + 100 
WHERE id = 1 AND transaction_id NOT IN (
    SELECT transaction_id FROM processed_transactions
);

四、 分布式系统中的幂等性模式

4.1 唯一标识符模式

使用全局唯一标识符确保操作的幂等性:

@Service
public class PaymentService {


    public PaymentResult processPayment(PaymentRequest request) {
        String idempotencyKey = request.getIdempotencyKey();


        // 检查是否已经处理过
        PaymentResult existing = paymentRepository
            .findByIdempotencyKey(idempotencyKey);


        if (existing != null) {
            return existing;
        }


        // 处理支付
        PaymentResult result = executePayment(request);
        result.setIdempotencyKey(idempotencyKey);


        // 保存结果
        paymentRepository.save(result);


        return result;
    }
}

4.2 状态机模式(复杂业务救星)

通过状态机确保操作的幂等性:

@Entity
public class Order {


    public enum Status {
        CREATED, PAID, SHIPPED, DELIVERED, CANCELLED
    }


    public void pay() {
        if (status == Status.CREATED) {
            // 执行支付逻辑
            processPayment();
            status = Status.PAID;
        }
        // 如果已经是PAID状态,不做任何操作(幂等)
    }


    public void ship() {
        if (status == Status.PAID) {
            // 执行发货逻辑
            processShipping();
            status = Status.SHIPPED;
        }
        // 如果已经是SHIPPED状态,不做任何操作(幂等)
    }
}

4.3 版本控制模式

使用版本号或时间戳确保更新的幂等性:

@Entity
public class Document {
    private Long id;
    private String content;
    private Long version;


    public boolean update(String newContent, Long expectedVersion) {
        if (this.version.equals(expectedVersion)) {
            this.content = newContent;
            this.version++;
            return true;
        }
        return false; // 版本不匹配,更新失败
    }
}

五、消息队列中的幂等性

5.1 消息重复处理的场景

在消息队列系统中,消息可能会被重复投递:

  • 网络异常:消费者处理完成但ACK失败
  • 消费者重启:处理过程中服务重启
  • 负载均衡:消息被分发到多个消费者

5.2 幂等性实现策略

策略一:消息去重

@Component
public class OrderMessageConsumer {


    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    @KafkaListener(topics = "order-events")
    public void handleOrderEvent(OrderEvent event) {
        String messageId = event.getMessageId();
        String lockKey = "message_lock:" + messageId;


        // 使用Redis实现分布式锁
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofMinutes(5));


        if (!acquired) {
            log.info("Message {} already processed", messageId);
            return;
        }


        try {
            // 处理业务逻辑
            processOrder(event);
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

策略二:数据库唯一约束

@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(columnNames = {"message_id"})
})
public class ProcessedMessage {
    @Id
    private String messageId;
    private LocalDateTime processedAt;
    private String result;
}


@Service
public class MessageProcessor {


    public void processMessage(Message message) {
        try {
            // 尝试保存处理记录
            ProcessedMessage record = new ProcessedMessage();
            record.setMessageId(message.getId());
            record.setProcessedAt(LocalDateTime.now());


            processedMessageRepository.save(record);


            // 执行业务逻辑
            handleBusinessLogic(message);


        } catch (DataIntegrityViolationException e) {
            // 消息已经处理过,忽略
            log.info("Message {} already processed", message.getId());
        }
    }
}

六、缓存系统中的幂等性

6.1 缓存更新的幂等性

@Service
public class CacheService {


    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    public void updateUserCache(User user) {
        String key = "user:" + user.getId();


        // 使用SET命令,天然幂等
        redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));


        // 或使用条件更新
        Long version = user.getVersion();
        String lockKey = key + ":version";


        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                operations.watch(lockKey);
                Long cachedVersion = (Long) operations.opsForValue().get(lockKey);


                if (cachedVersion == null || version > cachedVersion) {
                    operations.multi();
                    operations.opsForValue().set(key, user, Duration.ofHours(1));
                    operations.opsForValue().set(lockKey, version, Duration.ofHours(1));
                    return operations.exec();
                }


                operations.unwatch();
                return null;
            }
        });
    }
}

七、微服务架构中的幂等性

7.1 服务间调用的幂等性

方案一:HTTP 头部传递幂等性标识

@RestController
public class OrderController {


    @PostMapping("/orders")
    public ResponseEntity<Order> createOrder(
            @RequestBody OrderRequest request,
            @RequestHeader("Idempotency-Key") String idempotencyKey) {


        // 检查幂等性
        Order existingOrder = orderService.findByIdempotencyKey(idempotencyKey);
        if (existingOrder != null) {
            return ResponseEntity.ok(existingOrder);
        }


        Order order = orderService.createOrder(request, idempotencyKey);
        return ResponseEntity.ok(order);
    }
}

方案二:服务网格层面的幂等性

# Istio VirtualService 配置
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: order-service
spec:
  http:
  - match:
    - method:
        exact: POST
      uri:
        exact: /orders
    fault:
      delay:
        percentage:
          value: 0.1
        fixedDelay: 5s
    retries:
      attempts: 3
      perTryTimeout: 10s
      retryOn: gateway-error,connect-failure,refused-stream

7.2 分布式事务的幂等性

Saga 模式实现

Saga 是一种将长事务分解为一系列本地事务的模式。每个本地事务都会更新数据库并发布消息或事件来触发下一个本地事务。如果某个本地事务失败,Saga 会执行一系列补偿事务来撤销之前已完成的事务。

@Component
public class OrderSaga {


    @SagaStart
    public void createOrder(OrderCreatedEvent event) {
        // 步骤1:扣减库存
        inventoryService.reserveItems(event.getOrderId(), event.getItems());
    }


    @SagaProcess
    public void handleInventoryReserved(InventoryReservedEvent event) {
        // 步骤2:处理支付
        paymentService.processPayment(event.getOrderId(), event.getAmount());
    }


    @SagaProcess
    public void handlePaymentProcessed(PaymentProcessedEvent event) {
        // 步骤3:确认订单
        orderService.confirmOrder(event.getOrderId());
    }


    // 补偿操作
    @SagaCompensation
    public void compensateInventory(InventoryReservedEvent event) {
        inventoryService.releaseItems(event.getOrderId(), event.getItems());
    }
}

8. 实际案例分析

8.1 电商系统的订单处理

场景描述:用户点击"提交订单"按钮,由于网络延迟,用户多次点击,导致创建了多个订单。

解决方案

@RestController
public class OrderController {


    @Autowired
    private OrderService orderService;


    @PostMapping("/orders")
    public ResponseEntity<ApiResponse<Order>> createOrder(
            @RequestBody CreateOrderRequest request,
            HttpServletRequest httpRequest) {


        // 生成幂等性密钥
        String idempotencyKey = generateIdempotencyKey(request, httpRequest);


        Order order = orderService.createOrderIdempotent(request, idempotencyKey);


        return ResponseEntity.ok(ApiResponse.success(order));
    }


    private String generateIdempotencyKey(CreateOrderRequest request, 
                                         HttpServletRequest httpRequest) {
        // 基于用户ID、商品信息、时间窗口生成唯一标识
        String userId = getCurrentUserId();
        String itemsHash = DigestUtils.md5Hex(request.getItems().toString());
        String timeWindow = String.valueOf(System.currentTimeMillis() / 60000); // 1分钟窗口


        return String.format("%s_%s_%s", userId, itemsHash, timeWindow);
    }
}

8.2 支付系统的幂等性设计

场景描述:支付网关可能因为网络问题重发支付请求,需要确保不会重复扣款。

解决方案

@Service
public class PaymentService {


    @Autowired
    private PaymentRepository paymentRepository;


    @Transactional
    public PaymentResult processPayment(PaymentRequest request) {
        String paymentId = request.getPaymentId();


        // 检查支付是否已存在
        Payment existingPayment = paymentRepository.findByPaymentId(paymentId);
        if (existingPayment != null) {
            return PaymentResult.fromPayment(existingPayment);
        }


        // 创建支付记录(状态为PROCESSING)
        Payment payment = new Payment();
        payment.setPaymentId(paymentId);
        payment.setStatus(PaymentStatus.PROCESSING);
        payment.setAmount(request.getAmount());


        try {
            paymentRepository.save(payment);
        } catch (DataIntegrityViolationException e) {
            // 并发情况下,支付记录已存在
            existingPayment = paymentRepository.findByPaymentId(paymentId);
            return PaymentResult.fromPayment(existingPayment);
        }


        try {
            // 调用第三方支付接口
            ThirdPartyPaymentResult result = thirdPartyPaymentService.pay(request);


            // 更新支付状态
            payment.setStatus(result.isSuccess() ? 
                PaymentStatus.SUCCESS : PaymentStatus.FAILED);
            payment.setThirdPartyTransactionId(result.getTransactionId());
            paymentRepository.save(payment);


            return PaymentResult.fromPayment(payment);


        } catch (Exception e) {
            // 处理异常
            payment.setStatus(PaymentStatus.FAILED);
            payment.setErrorMessage(e.getMessage());
            paymentRepository.save(payment);


            throw new PaymentProcessingException("Payment processing failed", e);
        }
    }
}

九、幂等性设计的最佳实践

9.1 设计原则

  1. 明确幂等性边界:确定哪些操作需要幂等性,哪些不需要。
  2. 选择合适的幂等性策略:基于业务场景选择最适合的实现方式。
  3. 考虑性能影响:幂等性检查不应该成为性能瓶颈。
  4. 处理并发情况:使用适当的锁机制或数据库约束。

9.2 实现建议

// 幂等性工具类
@Component
public class IdempotencyUtils {


    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    public <T> T executeIdempotent(String key, Supplier<T> operation, 
                                  Duration timeout) {
        String lockKey = "idempotent:" + key;
        String resultKey = "result:" + key;


        // 检查是否已有结果
        String cachedResult = redisTemplate.opsForValue().get(resultKey);
        if (cachedResult != null) {
            return deserialize(cachedResult);
        }


        // 获取分布式锁
        Boolean acquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", timeout);


        if (!acquired) {
            // 等待并重试
            return waitAndRetry(resultKey, timeout);
        }


        try {
            // 再次检查结果(双重检查)
            cachedResult = redisTemplate.opsForValue().get(resultKey);
            if (cachedResult != null) {
                return deserialize(cachedResult);
            }


            // 执行操作
            T result = operation.get();


            // 缓存结果
            redisTemplate.opsForValue().set(resultKey, serialize(result), timeout);


            return result;


        } finally {
            redisTemplate.delete(lockKey);
        }
    }
}

9.3 监控和调试

// 幂等性监控
@Component
public class IdempotencyMonitor {


    private final MeterRegistry meterRegistry;


    public IdempotencyMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }


    public void recordIdempotentHit(String operation) {
        meterRegistry.counter("idempotent.hit", "operation", operation).increment();
    }


    public void recordIdempotentMiss(String operation) {
        meterRegistry.counter("idempotent.miss", "operation", operation).increment();
    }


    public void recordIdempotentError(String operation, String error) {
        meterRegistry.counter("idempotent.error", 
            "operation", operation, 
            "error", error).increment();
    }
}

9.4 测试策略

@Test
public void testOrderCreationIdempotency() {
    // 准备测试数据
    CreateOrderRequest request = new CreateOrderRequest();
    request.setCustomerId("12345");
    request.setItems(Arrays.asList(new OrderItem("item1", 2)));


    String idempotencyKey = "test_order_001";


    // 第一次创建订单
    Order order1 = orderService.createOrderIdempotent(request, idempotencyKey);
    assertNotNull(order1);
    assertEquals("12345", order1.getCustomerId());


    // 第二次创建订单(应该返回相同的订单)
    Order order2 = orderService.createOrderIdempotent(request, idempotencyKey);
    assertNotNull(order2);
    assertEquals(order1.getId(), order2.getId());


    // 验证数据库中只有一条记录
    long count = orderRepository.countByCustomerId("12345");
    assertEquals(1, count);
}

十、常见陷阱和注意事项

10.1 时间窗口问题

// 错误示例:没有考虑时间窗口
public String generateIdempotencyKey(String userId, String operation) {
    return userId + "_" + operation; // 永久有效,可能导致问题
}


// 正确示例:考虑时间窗口
public String generateIdempotencyKey(String userId, String operation) {
    long timeWindow = System.currentTimeMillis() / (5 * 60 * 1000); // 5分钟窗口
    return userId + "_" + operation + "_" + timeWindow;
}

10.2 部分失败处理

// 需要考虑部分成功的情况
@Transactional
public void processComplexOrder(Order order) {
    // 步骤1:扣减库存
    inventoryService.reserveItems(order.getItems());


    // 步骤2:处理支付
    paymentService.processPayment(order.getPaymentInfo());


    // 步骤3:发送通知
    notificationService.sendOrderConfirmation(order);


    // 如果步骤3失败,前面的操作不应该回滚
    // 应该设计成最终一致性
}

10.3 状态检查的时机

public class OrderService {


    // 错误:在检查状态之前就执行了业务逻辑
    public void payOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);


        // 业务逻辑
        PaymentResult result = paymentService.processPayment(order);


        // 状态检查太晚了
        if (order.getStatus() == OrderStatus.PAID) {
            throw new IllegalStateException("Order already paid");
        }


        order.setStatus(OrderStatus.PAID);
        orderRepository.save(order);
    }


    // 正确:先检查状态,再执行业务逻辑
    public void payOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);


        // 先检查状态
        if (order.getStatus() == OrderStatus.PAID) {
            return; // 已经支付,直接返回(幂等)
        }


        if (order.getStatus() != OrderStatus.CREATED) {
            throw new IllegalStateException("Invalid order status");
        }


        // 执行业务逻辑
        PaymentResult result = paymentService.processPayment(order);


        order.setStatus(OrderStatus.PAID);
        orderRepository.save(order);
    }
}

十一、性能优化建议

11.1 缓存优化

@Service
public class IdempotentCacheService {


    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    private final LoadingCache<String, String> localCache = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(key -> redisTemplate.opsForValue().get(key));


    public boolean isProcessed(String idempotencyKey) {
        try {
            // 先查本地缓存
            String result = localCache.get(idempotencyKey);
            return result != null;
        } catch (Exception e) {
            // 本地缓存失败,查Redis
            return redisTemplate.hasKey(idempotencyKey);
        }
    }
}

11.2 数据库优化

-- 创建适当的索引
CREATE INDEX idx_idempotency_key ON orders (idempotency_key);
CREATE INDEX idx_message_id ON processed_messages (message_id);


-- 使用分区表处理大量历史数据
CREATE TABLE processed_messages (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    message_id VARCHAR(255) NOT NULL,
    processed_at TIMESTAMP NOT NULL,
    UNIQUE KEY uk_message_id (message_id)
) PARTITION BY RANGE (YEAR(processed_at)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

总之,幂等性不仅是一个技术概念,更是一种设计思想。掌握并正确应用幂等性,将帮助我们构建更加可靠和高效的软件系统。