分布式接口幂等性实战指南【完整版】

文章目录

    • 一、核心概念深度解析
      • [1.1 什么是幂等性](#1.1 什么是幂等性)
        • [1.1.1 数学定义](#1.1.1 数学定义)
        • [1.1.2 分布式系统中的幂等性](#1.1.2 分布式系统中的幂等性)
        • [1.1.3 HTTP方法幂等性详解](#1.1.3 HTTP方法幂等性详解)
        • [1.1.4 业务场景深度分析](#1.1.4 业务场景深度分析)
      • [1.2 为什么需要幂等性](#1.2 为什么需要幂等性)
        • [1.2.1 分布式环境下的挑战](#1.2.1 分布式环境下的挑战)
        • [1.2.2 八大典型问题场景](#1.2.2 八大典型问题场景)
      • [1.3 幂等性设计原则](#1.3 幂等性设计原则)
    • 二、六大幂等性实现方案
      • [2.1 数据库唯一约束](#2.1 数据库唯一约束)
        • [2.1.1 方案原理](#2.1.1 方案原理)
        • [2.1.2 方案一: 单字段唯一约束](#2.1.2 方案一: 单字段唯一约束)
        • [2.1.3 方案二: 联合唯一约束](#2.1.3 方案二: 联合唯一约束)
      • [2.2 分布式锁方案](#2.2 分布式锁方案)
      • [2.3 Token令牌方案](#2.3 Token令牌方案)
      • [2.4 状态机方案](#2.4 状态机方案)
      • [2.5 乐观锁版本号](#2.5 乐观锁版本号)
      • [2.6 消息队列幂等](#2.6 消息队列幂等)
    • 三、综合实战案例
      • [3.1 电商下单场景 (多重幂等保证)](#3.1 电商下单场景 (多重幂等保证))
      • [3.2 支付回调幂等处理](#3.2 支付回调幂等处理)
      • [3.3 定时任务幂等](#3.3 定时任务幂等)
    • 四、方案选型对比
    • 五、最佳实践建议
      • [5.1 设计原则](#5.1 设计原则)
      • [5.2 常见陷阱](#5.2 常见陷阱)
      • [5.3 性能优化](#5.3 性能优化)
      • [5.4 监控指标](#5.4 监控指标)

一、核心概念深度解析

1.1 什么是幂等性

1.1.1 数学定义

在数学和计算机科学中,幂等性指的是某操作执行多次与执行一次的效果相同:

复制代码
f(f(x)) = f(x)

例如:
- 绝对值函数: abs(abs(-5)) = abs(-5) = 5
- 设置操作: set(x, 10) 无论执行多少次,x的值都是10
1.1.2 分布式系统中的幂等性

在分布式系统中,幂等性是指使用相同参数重复执行同一操作,系统状态和返回结果保持一致

关键要点:

  • ✅ 相同的输入参数
  • ✅ 相同的业务语义
  • ✅ 系统最终状态一致
  • ✅ 可能返回相同或不同的响应(如第一次返回订单ID,后续返回"已存在")
1.1.3 HTTP方法幂等性详解
方法 幂等性 说明 示例
GET ✅ 是 查询操作,不改变服务器状态 GET /api/users/1
DELETE ✅ 是 删除资源,第一次删除成功,后续返回404 DELETE /api/users/1
UPDATE ✅ 是 更新资源,多次执行结果可能相同 PUT /api/users/1 {"name":"张三"}
❌ 否 更新资源,多次执行结果可能不同 PUT /api/users/1 {"age": age+1}
POST ✅ 是 创建资源,每次创建新实体,有主键唯一性约束 POST /api/orders
❌ 否 创建资源,每次创建新实体,没有有主键唯一性约束 POST /api/orders
1.1.4 业务场景深度分析

1. 天然幂等的操作:

针对GET、DELETE、未做计算的UPDATE、有唯一性约束的POST

java 复制代码
// 示例1: 设置用户状态
UPDATE t_user SET status = 'ACTIVE' WHERE id = 1001;
// 无论执行多少次,用户状态都是ACTIVE

// 示例2: 删除订单
DELETE FROM t_order WHERE order_no = 'ORD20241225001';
// 第一次删除成功,后续执行影响行数为0,但结果一致

// 示例3: 查询余额
SELECT balance FROM t_account WHERE user_id = 1001;
// 查询操作不改变状态,天然幂等

// 示例4: 设置商品价格
UPDATE t_product SET price = 99.00 WHERE product_id = 2001;

2. 非幂等的操作(需要改造):

java 复制代码
// 示例1: 创建订单
INSERT INTO t_order (order_no, user_id, amount) 
VALUES ('ORD20241225001', 1001, 999.00);
// 问题: 每次执行都创建新订单
// 解决: 订单号唯一约束 + 异常处理

// 示例2: 扣减库存
UPDATE t_product SET stock = stock - 1 WHERE product_id = 2001;
// 问题: 每次执行都减1,重复执行库存会变负
// 解决: 乐观锁 + 库存检查

// 示例3: 增加积分
UPDATE t_user_points SET points = points + 100 WHERE user_id = 1001;
// 问题: 每次执行都加100,重复执行积分增多
// 解决: 积分流水表 + 唯一约束

// 示例4: 发送通知
sendEmail(userId, "订单支付成功");
// 问题: 每次执行都发送邮件,用户收到多封
// 解决: 通知记录表 + 去重

3. 容易混淆的场景:

java 复制代码
// 场景1: 看似幂等,实则非幂等
UPDATE t_order SET update_time = NOW() WHERE order_no = 'ORD001';
// update_time每次都不同,严格来说非幂等
// 但业务上可接受,关键字段未变

// 场景2: 条件删除的幂等性
DELETE FROM t_order WHERE user_id = 1001 AND status = 'PENDING';
// 如果有多条PENDING订单,第一次可能删除多条
// 第二次执行删除0条
// 结果不同,但最终状态一致,可认为幂等

// 场景3: 批量操作的幂等性
UPDATE t_product SET status = 'ONLINE' 
WHERE category = '电子产品' AND status = 'OFFLINE';
// 第一次: 更新100条
// 第二次: 更新0条(都已ONLINE)
// 最终状态一致,幂等

1.2 为什么需要幂等性

1.2.1 分布式环境下的挑战

场景1: 网络不稳定导致重试

复制代码
时间线:  t0        t1        t2        t3        t4
        |
客户端:   |--发送请求-->          重试请求-->
        |
ネット:    |         <丢包>              OK
        |
服务端:   |           处理完成              再次处理
        |             ↓                      ↓
结果:      |           订单A创建            订单B创建 ❌*

问题:
- 用户只想创建1个订单,但系统创建了2个
- 用户可能被扣款2次
- 库存被扣减2次

场景2: 超时时间设置不当

java 复制代码
// 客户端配置
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(1000);  // 连接超时1秒
factory.setReadTimeout(3000);     // 读取超时3秒
restTemplate.setRequestFactory(factory);

// 服务端处理
@PostMapping("/api/orders")
public Order createOrder(@RequestBody OrderRequest request) {
    // 业务处理需要5秒
    Order order = orderService.createOrder(request);  // 耗时5秒
    return order;
}

// 问题分析:
// t0: 客户端发起请求
// t3: 客户端读取超时(3秒),抛出SocketTimeoutException
// t5: 服务端处理完成,创建订单成功
// 客户端认为失败,重试 -> 创建第二个订单
1.2.2 八大典型问题场景

场景1: 前端重复提交

javascript 复制代码
// 问题代码
<button onclick="submitOrder()">提交订单</button>

function submitOrder() {
    // 没有防重复点击
    fetch('/api/orders', {
        method: 'POST',
        body: JSON.stringify(orderData)
    });
}

// 用户快速点击3次 -> 创建3个订单

解决方案:

javascript 复制代码
// 方案1: 按钮禁用
let submitting = false;

function submitOrder() {
    if (submitting) return;
    
    submitting = true;
    document.getElementById('submitBtn').disabled = true;
    
    fetch('/api/orders', {
        method: 'POST',
        body: JSON.stringify(orderData)
    }).finally(() => {
        submitting = false;
        document.getElementById('submitBtn').disabled = false;
    });
}

// 方案2: Token机制(后文详述)

场景2: HTTP客户端自动重试

java 复制代码
// Spring Cloud Feign默认重试配置
@FeignClient(name = "order-service")
public interface OrderClient {
    @PostMapping("/api/orders")
    Order createOrder(@RequestBody OrderRequest request);
}

// application.yml
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
  httpclient:
    max-connections: 200
    connection-timeout: 2000

# Ribbon重试配置(危险!)
ribbon:
  MaxAutoRetries: 1              # 同一台服务器重试1次
  MaxAutoRetriesNextServer: 1    # 切换服务器重试1次
  OkToRetryOnAllOperations: true # ⚠️ 所有操作都重试,包括POST

# 问题:
# POST创建订单接口,如果超时会自动重试
# 第一次请求: 创建订单A
# 重试请求: 创建订单B

正确配置:

yaml 复制代码
ribbon:
  MaxAutoRetries: 0
  MaxAutoRetriesNextServer: 0
  OkToRetryOnAllOperations: false  # 只重试GET请求

# 或使用Resilience4j
resilience4j:
  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 1000
        retryExceptions:
          - java.net.SocketTimeoutException
        ignoreExceptions:
          - com.example.BusinessException

场景3: 消息队列At-Least-Once语义

java 复制代码
// Kafka消费者
@KafkaListener(topics = "order-paid")
public void handleOrderPaid(ConsumerRecord<String, String> record) {
    String orderId = record.value();
    
    // 处理支付成功逻辑
    addUserPoints(orderId);  // 增加用户积分
    deductStock(orderId);     // 扣减库存
    sendNotification(orderId); // 发送通知
    
    // ⚠️ 如果处理过程中JVM崩溃,消息未提交offset
    // 重启后会再次消费同一消息
}

// 问题:
// 第一次消费: 增加100积分
// JVM崩溃,offset未提交
// 重启后再次消费: 又增加100积分
// 用户实际获得200积分 ❌

Kafka重复消费的4种情况:

复制代码
1. 消费者处理完消息,但提交offset前崩溃
2. 网络抖动导致offset提交失败
3. Rebalance触发,offset未及时提交
4. 消费者处理时间过长,被剔除消费组,重新消费

场景4: 微服务调用链重试

复制代码
用户 -> 网关 -> 订单服务 -> 库存服务 -> 数据库
                    ↓
                 超时3s
                    ↓
                 重试 -> 库存服务 -> 数据库
                              ↓
                         再次扣减库存 ❌

详细案例:

java 复制代码
// 订单服务
@Service
public class OrderService {
    
    @Autowired
    private StockClient stockClient;
    
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = saveOrder(request);
        
        // 2. 调用库存服务扣减库存
        try {
            stockClient.deductStock(request.getProductId(), request.getQuantity());
        } catch (ReadTimeoutException e) {
            // 超时,但不知道库存服务是否执行成功
            // 重试 -> 可能导致库存重复扣减
            stockClient.deductStock(request.getProductId(), request.getQuantity());
        }
        
        return order;
    }
}

// 库存服务(非幂等)
@Service
public class StockService {
    
    public void deductStock(Long productId, Integer quantity) {
        // 非幂等的SQL
        jdbcTemplate.update(
            "UPDATE t_product SET stock = stock - ? WHERE product_id = ?",
            quantity, productId
        );
        
        // 问题: 第一次扣减成功,返回响应超时
        // 订单服务重试,再次扣减
        // 实际库存扣减了2倍 ❌
    }
}

场景5: 定时任务重叠执行

java 复制代码
// 每天凌晨1点执行订单自动完成任务
@Scheduled(cron = "0 0 1 * * ?")
public void autoCompleteOrders() {
    List<Order> orders = orderRepository.findPendingOrders();
    
    for (Order order : orders) {
        // 处理订单,耗时较长
        completeOrder(order);  // 假设处理1万个订单需要2小时
    }
}

// 问题:
// 1:00 开始执行任务A
// 2:00 任务A还在执行中
// 次日1:00 启动任务B,与任务A并发执行
// 同一订单被处理2次 ❌

场景6: 数据库主从延迟

java 复制代码
// 写主库
@Transactional
public void createOrder(OrderRequest request) {
    Order order = new Order();
    order.setOrderNo(request.getOrderNo());
    orderRepository.save(order);  // 写入主库
}

// 立即读从库
public Order getOrder(String orderNo) {
    return orderRepository.findByOrderNo(orderNo);  // 读从库
}

// 问题:
// t0: 创建订单,写入主库
// t1: 客户端超时,认为失败
// t2: 客户端重试创建订单
// t2: 查询订单是否存在(读从库),因主从延迟,查询不到
// t2: 认为不存在,再次创建 ❌

场景7: 分布式事务回滚

java 复制代码
// Seata分布式事务
@GlobalTransactional
public void createOrder(OrderRequest request) {
    // 1. 创建订单
    orderService.createOrder(request);
    
    // 2. 扣减库存
    stockService.deductStock(request.getProductId(), request.getQuantity());
    
    // 3. 扣减余额
    accountService.deductBalance(request.getUserId(), request.getAmount());
    
    // 问题: 如果第3步失败,前2步回滚
    // 但如果回滚过程中网络异常,可能导致部分回滚,部分未回滚
}

场景8: 缓存击穿导致重复写入

java 复制代码
public Product getProduct(Long productId) {
    // 1. 查缓存
    String cacheKey = "product:" + productId;
    Product product = redisTemplate.opsForValue().get(cacheKey);
    
    if (product != null) {
        return product;
    }
    
    // 2. 缓存未命中,查数据库
    product = productRepository.findById(productId).orElse(null);
    
    if (product != null) {
        // 3. 写入缓存
        redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
    }
    
    return product;
}

// 问题:
// 高并发下,缓存失效瞬间,1000个请求同时到达
// 1000个请求都发现缓存未命中
// 1000个请求都去查数据库
// 1000个请求都写入缓存
// 数据库压力暴增 ❌

1.3 幂等性设计原则

识别操作类型:

  • 天然幂等: 查询、删除、绝对值设置
  • 需要保证: 创建、累加、状态流转

唯一性标识:

  • 业务主键: 订单号、流水号
  • 技术唯一键: 幂等Token、请求ID

状态机设计:

  • 严格的状态转换规则
  • 记录状态变更历史

二、六大幂等性实现方案

2.1 数据库唯一约束

2.1.1 方案原理

核心思想: 利用数据库的UNIQUE约束特性,在数据库层面保证数据唯一性。

工作流程:

复制代码
1. 设计唯一业务主键(如订单号、流水号)
2. 在数据库表上创建UNIQUE约束或UNIQUE索引
3. 插入数据时,如果主键重复,数据库抛出异常
4. 应用层捕获异常,返回已存在的数据

优势分析:

  • 简单可靠: 数据库原生特性,不需要额外组件
  • 性能优秀: 单次数据库操作,无需分布式锁
  • 强一致性: 数据库事务保证,ACID特性
  • 维护成本低: 不依赖Redis/Zookeeper等中间件

劣势分析:

  • 业务主键设计: 需要提前约定唯一键生成规则
  • 异常处理: 需要精确识别DuplicateKeyException
  • 无法控制时间窗口: 一旦创建,永久存在(除非手动清理)
  • 跨表场景复杂: 多表操作需要额外设计
2.1.2 方案一: 单字段唯一约束

场景: 订单创建、支付流水记录

数据库设计:

sql 复制代码
-- 订单表
CREATE TABLE t_order (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    order_no VARCHAR(50) UNIQUE NOT NULL COMMENT '订单号(业务主键)',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    product_id BIGINT NOT NULL COMMENT '商品ID',
    quantity INT NOT NULL COMMENT '数量',
    unit_price DECIMAL(10,2) NOT NULL COMMENT '单价',
    total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
    status VARCHAR(20) DEFAULT 'CREATED' COMMENT '状态',
    remark VARCHAR(500) COMMENT '备注',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  
    -- 唯一约束
    UNIQUE KEY uk_order_no (order_no),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

完整业务实现:

java 复制代码
package com.example.idempotent.service.impl;

import com.example.idempotent.entity.Order;
import com.example.idempotent.dto.CreateOrderRequest;
import com.example.idempotent.repository.OrderRepository;
import com.example.idempotent.util.OrderNoGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Slf4j
@Service
public class OrderServiceImpl {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private OrderNoGenerator orderNoGenerator;
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private UserService userService;
    
    /**
     * 创建订单 - 方式1: 后端生成订单号
     * 
     * 幂等保证: 订单号唯一约束
     * 适用场景: 后端控制订单号生成
     */
    @Transactional(rollbackFor = Exception.class)
    public Order createOrderV1(CreateOrderRequest request) {
        log.info("[createOrderV1] 开始创建订单: userId={}, productId={}, quantity={}",
                request.getUserId(), request.getProductId(), request.getQuantity());
        
        // 1. 生成订单号
        String orderNo = orderNoGenerator.generate(request.getUserId());
        
        try {
            // 2. 构建订单对象
            Order order = buildOrder(orderNo, request);
            
            // 3. 保存订单(唯一约束保证幂等)
            Order saved = orderRepository.save(order);
            
            // 4. 后续业务逻辑
            afterOrderCreated(saved);
            
            return saved;
        } catch (DuplicateKeyException e) {
            // 订单号重复,理论上不应该发生(因为包含时间戳+随机数)
            log.error("[createOrderV1] 订单号重复(极小概率): orderNo={}", orderNo, e);
            
            // 重新生成订单号再试一次
            return createOrderV1(request);
        } catch (Exception e) {
            log.error("[createOrderV1] 创建订单失败", e);
            throw new RuntimeException("创建订单失败: " + e.getMessage());
        }
    }
   
    /**
     * 创建订单 - 方式2: 客户端请求ID
     * 
     * 幂等保证: 使用客户端的requestId作为唯一键
     * 适用场景: 需要精确追踪每次请求
     */
    @Transactional(rollbackFor = Exception.class)
    public Order createOrderV3(String requestId, CreateOrderRequest request) {
        log.info("[createOrderV3] 开始创建订单: requestId={}", requestId);
        
        // 1. 先查询是否已处理该请求
        Order existOrder = orderRepository.findByRequestId(requestId);
        if (existOrder != null) {
            log.info("[createOrderV3] 请求已处理,幂等返回: requestId={}, orderId={}",
                    requestId, existOrder.getId());
            return existOrder;
        }
        
        // 2. 生成订单号
        String orderNo = orderNoGenerator.generate(request.getUserId());
        
        try {
            // 3. 构建订单(包含requestId)
            Order order = buildOrder(orderNo, request);
            order.setRequestId(requestId);
            
            // 4. 保存订单
            Order saved = orderRepository.save(order);
            
            log.info("[createOrderV3] 订单创建成功: orderId={}", saved.getId());
            
            afterOrderCreated(saved);
            
            return saved;
            
        } catch (DuplicateKeyException e) {
            // requestId重复,幂等处理
            log.warn("[createOrderV3] 请求ID重复: requestId={}", requestId);
            return orderRepository.findByRequestId(requestId);
        }
    }
    
    /**
     * 订单创建后的后续处理
     */
    private void afterOrderCreated(Order order) {
        // 1. 扣减库存
        // 2. 锁定优惠券
        // 3. 发送MQ消息
        // 4. 发送通知
    }
}
2.1.3 方案二: 联合唯一约束

场景: 防止同一用户重复操作

数据库设计:

sql 复制代码
-- 点赞记录表
CREATE TABLE t_like_record (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    target_type VARCHAR(20) NOT NULL COMMENT '目标类型: POST/COMMENT/VIDEO',
    target_id BIGINT NOT NULL COMMENT '目标ID',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    -- 联合唯一约束: 同一用户对同一目标只能点赞一次
    UNIQUE KEY uk_user_target (user_id, target_type, target_id),
    
    INDEX idx_target (target_type, target_id),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点赞记录表';

业务实现:

java 复制代码
package com.example.idempotent.service;

import com.example.idempotent.entity.LikeRecord;
import com.example.idempotent.repository.LikeRecordRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class LikeService {
    
    @Autowired
    private LikeRecordRepository likeRecordRepository;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 点赞 - 联合唯一约束保证幂等
     * 
     * @param userId 用户ID
     * @param targetType 目标类型
     * @param targetId 目标ID
     * @return true-点赞成功, false-已点赞
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean like(Long userId, String targetType, Long targetId) {
        log.info("[like] 用户点赞: userId={}, targetType={}, targetId={}",
                userId, targetType, targetId);
        
        try {
            // 1. 创建点赞记录
            LikeRecord record = new LikeRecord();
            record.setUserId(userId);
            record.setTargetType(targetType);
            record.setTargetId(targetId);
            
            // 2. 保存(联合唯一约束保证幂等)
            likeRecordRepository.save(record);
            
            // 3. 增加点赞数(Redis计数)
            String countKey = "like:count:" + targetType + ":" + targetId;
            redisTemplate.opsForValue().increment(countKey);
            
            log.info("[like] 点赞成功");
            return true;
            
        } catch (DuplicateKeyException e) {
            // 已经点赞过,幂等返回成功
            log.warn("[like] 已点赞,幂等返回: userId={}, targetId={}",
                    userId, targetId);
            return true;  // 幂等操作,返回成功
        }
    }
}

2.2 分布式锁方案

适用场景: 高并发、复杂业务逻辑

Redis分布式锁实现:

java 复制代码
@Service
public class PaymentService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Transactional
    public boolean payOrder(String orderNo, BigDecimal amount) {
        String lockKey = "order:pay:" + orderNo;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待3秒,锁10秒后自动释放
            if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            
            // 查询订单
            Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
            
            // 幂等判断
            if ("PAID".equals(order.getStatus())) {
                return true;  // 已支付,幂等返回
            }
            
            // 执行支付
            callPaymentGateway(orderNo, amount);
            
            // 更新状态
            order.setStatus("PAID");
            orderRepository.save(order);
            
            return true;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁失败");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

优点: ✅ 跨JVM ✅ 防死锁 ✅ 可重入
缺点: ❌ 依赖Redis ❌ 性能开销 ❌ 锁超时处理


2.3 Token令牌方案

适用场景: 前端提交、表单防重

流程:

复制代码
1. 前端请求Token
2. 后端生成Token存Redis
3. 前端提交携带Token
4. 后端验证并删除Token(原子操作)

实现:

java 复制代码
@Service
public class TokenService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 生成Token
    public String generateToken(String bizType, String bizId) {
        String token = UUID.randomUUID().toString().replace("-", "");
        String key = "token:" + bizType + ":" + bizId + ":" + token;
        redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
        return token;
    }
    
    // 验证并消费Token(Lua脚本保证原子性)
    public boolean verifyAndConsumeToken(String bizType, String bizId, String token) {
        String key = "token:" + bizType + ":" + bizId + ":" + token;
        String script = 
            "if redis.call('get', KEYS[1]) then " +
            "    redis.call('del', KEYS[1]) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            (RedisCallback<Long>) connection -> connection.eval(
                script.getBytes(), ReturnType.INTEGER, 1, key.getBytes()
            )
        );
        
        return result != null && result == 1;
    }
}

Controller:

java 复制代码
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private TokenService tokenService;
    
    // 获取Token
    @GetMapping("/token")
    public Result<String> getToken(@RequestParam Long userId) {
        String token = tokenService.generateToken("CREATE_ORDER", userId.toString());
        return Result.success(token);
    }
    
    // 创建订单(携带Token)
    @PostMapping
    public Result<Order> createOrder(
            @RequestHeader("X-Idempotent-Token") String token,
            @RequestBody OrderRequest request) {
        
        // 验证并消费Token
        boolean valid = tokenService.verifyAndConsumeToken(
            "CREATE_ORDER", request.getUserId().toString(), token);
        
        if (!valid) {
            return Result.error("请勿重复提交");
        }
        
        Order order = orderService.createOrder(request);
        return Result.success(order);
    }
}

优点: ✅ 防前端重复提交 ✅ 一次性Token ✅ 可设置有效期
缺点: ❌ 两次请求 ❌ 依赖Redis


2.4 状态机方案

适用场景: 订单流程、工单流转

状态定义:

java 复制代码
@Getter
public enum OrderStatus {
    CREATED("已创建"),
    PAID("已支付"),
    SHIPPED("已发货"),
    COMPLETED("已完成"),
    CANCELLED("已取消");
    
    private final String desc;
    
    OrderStatus(String desc) {
        this.desc = desc;
    }
    
    // 判断是否可以转换
    public boolean canTransitionTo(OrderStatus target) {
        switch (this) {
            case CREATED:
                return target == PAID || target == CANCELLED;
            case PAID:
                return target == SHIPPED || target == CANCELLED;
            case SHIPPED:
                return target == COMPLETED;
            case COMPLETED:
            case CANCELLED:
                return false;  // 终态
            default:
                return false;
        }
    }
}

服务实现:

java 复制代码
@Service
public class OrderStateMachineService {
    
    @Transactional
    public boolean transitionStatus(String orderNo, OrderStatus targetStatus) {
        // 查询订单
        Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
        OrderStatus currentStatus = OrderStatus.valueOf(order.getStatus());
        
        // 幂等判断: 已经是目标状态
        if (currentStatus == targetStatus) {
            return true;
        }
        
        // 状态机校验
        if (!currentStatus.canTransitionTo(targetStatus)) {
            throw new RuntimeException(
                String.format("不允许从[%s]转换到[%s]", 
                    currentStatus.getDesc(), targetStatus.getDesc())
            );
        }
        
        // 更新状态
        order.setStatus(targetStatus.name());
        orderRepository.save(order);
        
        // 记录历史
        saveStatusHistory(order, currentStatus, targetStatus);
        
        return true;
    }
}

优点: ✅ 业务语义清晰 ✅ 可追溯 ✅ 天然幂等
缺点: ❌ 状态设计复杂 ❌ 需要详细规则


2.5 乐观锁版本号

适用场景: 库存扣减、余额变更

数据库设计:

sql 复制代码
CREATE TABLE t_product_stock (
    id BIGINT PRIMARY KEY,
    product_id BIGINT UNIQUE,
    stock INT NOT NULL,
    version INT DEFAULT 0,  -- 版本号
    update_time DATETIME
);

实现:

java 复制代码
@Service
public class StockService {
    
    @Transactional
    public boolean deductStock(Long productId, Integer quantity) {
        for (int i = 0; i < 3; i++) {  // 最多重试3次
            // 查询当前库存
            ProductStock stock = stockRepository.findByProductId(productId)
                .orElseThrow();
            
            // 检查库存
            if (stock.getStock() < quantity) {
                return false;
            }
            
            // 乐观锁更新
            int updated = stockRepository.deductStockWithVersion(
                productId, quantity, stock.getVersion());
            
            if (updated > 0) {
                return true;  // 更新成功
            }
            
            // 版本冲突,重试
            Thread.sleep(50);
        }
        
        return false;  // 超过重试次数
    }
}

SQL:

xml 复制代码
<update id="deductStockWithVersion">
    UPDATE t_product_stock
    SET stock = stock - #{quantity},
        version = version + 1,
        update_time = NOW()
    WHERE product_id = #{productId}
      AND stock >= #{quantity}
      AND version = #{version}
</update>

优点: ✅ 无锁高并发 ✅ 数据库保证 ✅ 性能好
缺点: ❌ ABA问题 ❌ 需要重试机制


2.6 消息队列幂等

适用场景: 异步消息处理、事件驱动

方案一: 消息ID去重表

sql 复制代码
CREATE TABLE t_message_record (
    id BIGINT PRIMARY KEY,
    message_id VARCHAR(100) UNIQUE NOT NULL,
    topic VARCHAR(50),
    content TEXT,
    process_time DATETIME
);
java 复制代码
@Component
public class OrderPaidConsumer {
    
    @KafkaListener(topics = "order-paid")
    @Transactional
    public void handleOrderPaid(ConsumerRecord<String, String> record) {
        String messageId = record.key();
        
        try {
            // 记录消息(唯一约束保证幂等)
            MessageRecord mr = new MessageRecord();
            mr.setMessageId(messageId);
            mr.setTopic("order-paid");
            mr.setContent(record.value());
            messageRecordRepository.save(mr);
            
            // 处理业务
            processOrderPaid(record.value());
            
        } catch (DuplicateKeyException e) {
            // 消息已处理,幂等跳过
            log.warn("消息已处理: {}", messageId);
        }
    }
}

方案二: Redis布隆过滤器

java 复制代码
@Component
public class MessageIdempotentFilter {
    
    private BloomFilter<String> bloomFilter;
    
    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(StandardCharsets.UTF_8),
            100_000_000,  // 1亿元素
            0.0001        // 误判率0.01%
        );
    }
    
    public boolean isDuplicate(String messageId) {
        if (bloomFilter.mightContain(messageId)) {
            // 布隆过滤器判断可能存在,数据库二次确认
            return messageRecordRepository.existsByMessageId(messageId);
        }
        return false;
    }
    
    public void markProcessed(String messageId) {
        bloomFilter.put(messageId);
    }
}

优点: ✅ 适合海量消息 ✅ 内存占用小
缺点: ❌ 存在误判 ❌ 需二次确认


三、综合实战案例

3.1 电商下单场景 (多重幂等保证)

业务流程:

复制代码
创建订单 -> 扣库存 -> 锁优惠券 -> 调用支付 -> 发MQ

完整实现:

java 复制代码
@Service
public class OrderCreateService {
    
    @Autowired
    private DistributedLockUtil lockUtil;
    
    @Transactional
    public Order createOrder(CreateOrderDTO dto) {
        String orderNo = dto.getOrderNo();
        
        // 1. 业务主键防重复创建
        Order order = tryCreateOrderRecord(dto);
        if (order != null && "COMPLETED".equals(order.getStatus())) {
            return order;  // 已完成,幂等返回
        }
        
        // 2. 分布式锁保证串行
        String lockKey = "order:create:" + orderNo;
        return lockUtil.executeWithLock(lockKey, 5, 30, () -> {
            
            // 3. 扣减库存(乐观锁)
            boolean stockOk = stockService.deductStock(
                dto.getProductId(), dto.getQuantity());
            if (!stockOk) {
                throw new RuntimeException("库存不足");
            }
            
            // 4. 锁定优惠券(唯一约束)
            if (dto.getCouponId() != null) {
                boolean couponOk = couponService.lockCoupon(
                    dto.getUserId(), dto.getCouponId(), orderNo);
                if (!couponOk) {
                    stockService.rollbackStock(dto.getProductId(), dto.getQuantity());
                    throw new RuntimeException("优惠券锁定失败");
                }
            }
            
            // 5. 调用支付(幂等接口)
            boolean payOk = paymentService.pay(orderNo, dto.getAmount());
            if (!payOk) {
                // 回滚优惠券和库存
                rollbackAll(dto, orderNo);
                throw new RuntimeException("支付失败");
            }
            
            // 6. 更新订单状态(状态机)
            Order finalOrder = orderRepository.findByOrderNo(orderNo).orElseThrow();
            finalOrder.setStatus("COMPLETED");
            orderRepository.save(finalOrder);
            
            // 7. 发送MQ(消息ID幂等)
            sendOrderCreatedEvent(finalOrder);
            
            return finalOrder;
        });
    }
    
    private Order tryCreateOrderRecord(CreateOrderDTO dto) {
        try {
            Order order = new Order();
            order.setOrderNo(dto.getOrderNo());
            order.setUserId(dto.getUserId());
            order.setStatus("CREATED");
            return orderRepository.save(order);
        } catch (DuplicateKeyException e) {
            return orderRepository.findByOrderNo(dto.getOrderNo()).orElse(null);
        }
    }
}

3.2 支付回调幂等处理

场景: 支付网关可能多次回调

java 复制代码
@RestController
@RequestMapping("/api/payment")
public class PaymentCallbackController {
    
    @Autowired
    private PaymentCallbackService callbackService;
    
    @PostMapping("/callback")
    public String handleCallback(@RequestBody PaymentCallbackDTO dto) {
        
        // 1. 验签
        if (!verifySign(dto)) {
            return "FAIL";
        }
        
        // 2. 幂等处理(业务流水号+状态机)
        try {
            callbackService.processCallback(dto.getOrderNo(), dto.getPaymentNo());
            return "SUCCESS";
            
        } catch (DuplicateKeyException e) {
            // 已处理过,幂等返回成功
            return "SUCCESS";
        } catch (Exception e) {
            log.error("处理支付回调失败", e);
            return "FAIL";
        }
    }
}

@Service
public class PaymentCallbackService {
    
    @Transactional
    public void processCallback(String orderNo, String paymentNo) {
        
        // 1. 记录支付流水(唯一约束)
        PaymentRecord record = new PaymentRecord();
        record.setPaymentNo(paymentNo);  // 唯一键
        record.setOrderNo(orderNo);
        record.setStatus("SUCCESS");
        paymentRecordRepository.save(record);  // 重复会抛异常
        
        // 2. 更新订单状态(状态机)
        Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
        
        if ("PAID".equals(order.getStatus())) {
            return;  // 已支付,幂等返回
        }
        
        if (!"CREATED".equals(order.getStatus())) {
            throw new RuntimeException("订单状态异常");
        }
        
        order.setStatus("PAID");
        orderRepository.save(order);
        
        // 3. 发送MQ通知
        sendPaymentSuccessEvent(order);
    }
}

3.3 定时任务幂等

场景: 定时任务可能重复执行

java 复制代码
@Component
public class OrderAutoCompleteTask {
    
    @Autowired
    private RedissonClient redissonClient;
    
    // 每小时执行一次
    @Scheduled(cron = "0 0 * * * ?")
    public void autoCompleteOrders() {
        
        String lockKey = "task:auto-complete:" + 
            LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,获取不到说明任务正在执行
            if (!lock.tryLock(0, 3600, TimeUnit.SECONDS)) {
                log.warn("任务正在执行中,本次跳过");
                return;
            }
            
            // 执行任务
            List<Order> orders = findPendingOrders();
            for (Order order : orders) {
                completeOrder(order);
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

四、方案选型对比

方案 适用场景 优点 缺点 推荐指数
数据库唯一约束 创建操作 简单、强一致 依赖数据库 ⭐⭐⭐⭐⭐
分布式锁 复杂业务 跨JVM、防并发 性能开销大 ⭐⭐⭐⭐
Token令牌 前端提交 防重复提交 两次请求 ⭐⭐⭐⭐⭐
状态机 流程控制 业务清晰 设计复杂 ⭐⭐⭐⭐⭐
乐观锁 库存扣减 高并发 需要重试 ⭐⭐⭐⭐
消息去重 MQ消费 异步处理 依赖存储 ⭐⭐⭐⭐⭐

五、最佳实践建议

5.1 设计原则

  1. 识别幂等边界: 明确哪些操作需要幂等
  2. 选择合适方案: 根据场景选择最优方案
  3. 多重保障: 关键业务使用多种方案组合
  4. 日志追踪: 记录所有幂等处理过程
  5. 监控告警: 监控幂等失败率

5.2 常见陷阱

错误做法:

java 复制代码
// 不幂等的扣库存
UPDATE t_stock SET stock = stock - 1 WHERE product_id = ?

正确做法:

java 复制代码
// 幂等的扣库存(乐观锁)
UPDATE t_stock 
SET stock = stock - 1, version = version + 1
WHERE product_id = ? AND version = ? AND stock >= 1

5.3 性能优化

  1. 减少锁粒度: 使用细粒度锁(订单号而非全局锁)
  2. 异步处理: 非核心流程异步化
  3. 缓存优化: Redis缓存Token和处理记录
  4. 批量操作: 消息批量去重

5.4 监控指标

java 复制代码
// 幂等处理监控
public class IdempotentMetrics {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    public void recordIdempotentSkip(String bizType) {
        meterRegistry.counter("idempotent.skip", "type", bizType).increment();
    }
    
    public void recordIdempotentSuccess(String bizType) {
        meterRegistry.counter("idempotent.success", "type", bizType).increment();
    }
    
    public void recordIdempotentFailure(String bizType) {
        meterRegistry.counter("idempotent.failure", "type", bizType).increment();
    }
}
相关推荐
航Hang*2 小时前
第3章:复习篇——第5-1节:数据库编程1
数据库·笔记·sql·mysql·sqlserver
海清河晏1112 小时前
Linux进阶篇:深入理解线程
java·jvm·算法
2301_797312262 小时前
学习Java32天
java·开发语言
TAEHENGV2 小时前
提醒列表模块 Cordova 与 OpenHarmony 混合开发实战
android·java·harmonyos
weixin_46682 小时前
K8S- Calico
云原生·容器·kubernetes
一个天蝎座 白勺 程序猿2 小时前
破局困境:Oracle迁移金仓KingbaseES数据库的深度实践
数据库·sql·oracle·kingbase·kingbasees·金仓数据库
航Hang*2 小时前
第3章:复习篇——第6节:数据库安全管理与日常维护
数据库·笔记·sql·mysql
源码获取_wx:Fegn08952 小时前
基于springboot + vue宠物寄养系统
java·vue.js·spring boot·后端·spring·宠物
Coder_Boy_2 小时前
SpringAI与LangChain4j的智能应用-(理论篇)
人工智能·spring·mybatis·springai·langchain4j