得物:接口幂等性的保证方案

一、幂等性基础概念

1. 幂等性定义

  • 数学定义:f(x) = f(f(x))

  • 计算机定义:对同一接口的多次相同请求与一次请求产生相同效果

  • 核心要求:重复请求不会产生副作用

2. HTTP方法的幂等性

http

复制

下载

复制代码
幂等方法:
GET     ✅ 幂等(读取操作)
PUT     ✅ 幂等(全量更新)
DELETE  ✅ 幂等(删除不存在资源应返回相同结果)
POST    ❌ 非幂等(创建资源)
PATCH   ❌ 非幂等(部分更新)

二、业务场景分类

1. 天然幂等场景

java

复制

下载

复制代码
// 查询类操作
GET /api/users/{id}
GET /api/orders?status=paid

// 删除操作(删除不存在资源应返回成功)
DELETE /api/users/{id}

// 全量更新
PUT /api/users/{id}

2. 需要保证幂等场景

java

复制

下载

复制代码
// 创建类
POST /api/orders           // 下单
POST /api/payments         // 支付
POST /api/coupons/claim    // 领券

// 更新类  
PATCH /api/orders/{id}     // 更新订单状态
POST /api/orders/{id}/cancel  // 取消订单
POST /api/inventory/deduct // 扣减库存

// 特殊操作
POST /api/transfers        // 转账
POST /api/withdrawals      // 提现

三、技术解决方案对比

方案 原理 优点 缺点 适用场景
Token机制 客户端先获取token,请求时携带 实现简单,用户体验好 需要前后端配合 表单提交、用户交互
唯一索引 数据库层面唯一约束 绝对可靠,数据库保障 只能用于创建操作 数据创建、防重复
乐观锁 版本号或状态机控制 并发性能好 需要设计版本字段 库存扣减、余额变更
分布式锁 全局锁控制并发 强一致性保证 性能开销较大 高并发扣减、抢购
全局ID 请求ID+幂等表 通用性强,可追溯 实现相对复杂 分布式系统、异步消息

四、详细实现方案

方案1:Token机制(最常用)

1.1 基本流程

图表

代码

下载

全屏

客户端

  1. 获取Token

服务端生成Token并存储

返回Token

  1. 提交请求携带Token

服务端验证并删除Token

Token存在?

执行业务

返回重复提交错误

返回成功

1.2 服务端实现

java

复制

下载

复制代码
@Component
public class TokenService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 生成Token
    public String createToken(String businessKey) {
        String token = UUID.randomUUID().toString();
        String key = buildTokenKey(businessKey, token);
        // 存储Token,5分钟过期
        redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
        return token;
    }
    
    // 验证并删除Token
    public boolean verifyAndDeleteToken(String businessKey, String token) {
        String key = buildTokenKey(businessKey, token);
        // 使用Lua脚本保证原子性
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            "1"
        );
        return result != null && result == 1;
    }
    
    private String buildTokenKey(String businessKey, String token) {
        return String.format("token:%s:%s", businessKey, token);
    }
}
1.3 接口实现

java

复制

下载

复制代码
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private TokenService tokenService;
    
    // 获取Token
    @GetMapping("/token")
    public ApiResponse<String> getOrderToken() {
        String userId = getCurrentUserId();
        String token = tokenService.createToken(userId);
        return ApiResponse.success(token);
    }
    
    // 提交订单(幂等)
    @PostMapping
    public ApiResponse<OrderDTO> createOrder(
            @RequestBody OrderCreateRequest request,
            @RequestHeader("X-Order-Token") String token) {
        
        String userId = getCurrentUserId();
        
        // 1. 验证Token
        if (!tokenService.verifyAndDeleteToken(userId, token)) {
            return ApiResponse.error("重复提交或Token已过期");
        }
        
        // 2. 执行业务逻辑
        OrderDTO order = orderService.createOrder(request);
        
        return ApiResponse.success(order);
    }
}

方案2:数据库唯一索引

2.1 防重表设计

sql

复制

下载

复制代码
-- 创建幂等记录表
CREATE TABLE idempotent_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    biz_type VARCHAR(64) NOT NULL COMMENT '业务类型',
    biz_id VARCHAR(128) NOT NULL COMMENT '业务唯一ID',
    request_id VARCHAR(64) COMMENT '请求ID',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-处理中,1-成功,2-失败',
    result TEXT COMMENT '处理结果',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE KEY uk_biz (biz_type, biz_id),
    INDEX idx_request_id (request_id)
) ENGINE=InnoDB COMMENT='幂等记录表';
2.2 业务实现

java

复制

下载

复制代码
@Service
@Transactional
public class IdempotentService {
    
    @Autowired
    private IdempotentRecordMapper recordMapper;
    
    /**
     * 幂等执行
     * @param bizType 业务类型
     * @param bizId 业务唯一ID
     * @param processor 业务处理器
     * @return 执行结果
     */
    public <T> T execute(String bizType, String bizId, Supplier<T> processor) {
        // 1. 尝试插入幂等记录
        IdempotentRecord record = new IdempotentRecord();
        record.setBizType(bizType);
        record.setBizId(bizId);
        record.setStatus(0); // 处理中
        
        try {
            recordMapper.insert(record);
        } catch (DuplicateKeyException e) {
            // 2. 记录已存在,查询结果
            IdempotentRecord existing = recordMapper.selectByBiz(bizType, bizId);
            if (existing.getStatus() == 1) {
                // 已成功,返回缓存结果
                return JSON.parseObject(existing.getResult(), new TypeReference<T>(){});
            } else if (existing.getStatus() == 0) {
                // 正在处理中,抛出异常或等待
                throw new BusinessException("请求正在处理中");
            } else {
                // 之前失败,可以重试(需考虑业务是否允许重试)
                throw new BusinessException("上次处理失败,请重试");
            }
        }
        
        try {
            // 3. 执行业务逻辑
            T result = processor.get();
            
            // 4. 更新为成功状态
            record.setStatus(1);
            record.setResult(JSON.toJSONString(result));
            recordMapper.updateById(record);
            
            return result;
        } catch (Exception e) {
            // 5. 更新为失败状态
            record.setStatus(2);
            recordMapper.updateById(record);
            throw e;
        }
    }
}
2.3 使用示例

java

复制

下载

复制代码
@Service
public class OrderService {
    
    @Autowired
    private IdempotentService idempotentService;
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        String userId = request.getUserId();
        String orderNo = generateOrderNo(); // 生成唯一订单号
        
        // 使用订单号作为幂等键
        return idempotentService.execute(
            "ORDER_CREATE",
            orderNo,
            () -> {
                // 实际的订单创建逻辑
                Order order = new Order();
                order.setOrderNo(orderNo);
                order.setUserId(userId);
                // ... 其他业务逻辑
                
                // 保存订单
                orderMapper.insert(order);
                
                // 扣减库存
                inventoryService.deduct(request.getItems());
                
                // 发送消息
                messageService.sendOrderCreated(order);
                
                return convertToDTO(order);
            }
        );
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

方案3:乐观锁机制

3.1 基于版本号

java

复制

下载

复制代码
// 实体类
@Data
@TableName("product_stock")
public class ProductStock {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String productCode;
    
    private Integer stock;
    
    @Version  // MyBatis Plus乐观锁注解
    private Integer version;
    
    private LocalDateTime updateTime;
}

// Mapper
public interface ProductStockMapper extends BaseMapper<ProductStock> {
    // 乐观锁更新库存
    @Update("UPDATE product_stock SET stock = stock - #{quantity}, " +
            "version = version + 1, update_time = NOW() " +
            "WHERE id = #{id} AND version = #{version} " +
            "AND stock >= #{quantity}")
    int deductStockWithVersion(@Param("id") Long id,
                               @Param("quantity") Integer quantity,
                               @Param("version") Integer version);
}

// 业务服务
@Service
public class InventoryService {
    
    @Autowired
    private ProductStockMapper stockMapper;
    
    @Transactional
    public boolean deductStock(Long productId, Integer quantity) {
        // 1. 查询当前库存和版本号
        ProductStock stock = stockMapper.selectById(productId);
        if (stock.getStock() < quantity) {
            throw new BusinessException("库存不足");
        }
        
        // 2. 乐观锁更新
        int rows = stockMapper.deductStockWithVersion(
            productId, quantity, stock.getVersion());
        
        if (rows == 0) {
            // 3. 更新失败,重试或抛异常
            throw new ConcurrentException("库存扣减冲突,请重试");
        }
        
        return true;
    }
}
3.2 基于状态机

java

复制

下载

复制代码
// 订单状态枚举
public enum OrderStatus {
    INIT(0, "待支付"),
    PAYING(1, "支付中"),
    PAID(2, "已支付"),
    CANCELLED(-1, "已取消");
    
    private final int code;
    private final String desc;
    
    // 状态转换验证
    public static boolean canChange(OrderStatus from, OrderStatus to) {
        Map<OrderStatus, Set<OrderStatus>> allowedTransitions = Map.of(
            INIT, Set.of(PAYING, CANCELLED),
            PAYING, Set.of(PAID, CANCELLED),
            PAID, Set.of(CANCELLED)  // 已支付只能取消
        );
        
        return allowedTransitions.getOrDefault(from, Collections.emptySet())
                .contains(to);
    }
}

// Mapper
public interface OrderMapper extends BaseMapper<Order> {
    
    @Update("UPDATE orders SET status = #{newStatus} " +
            "WHERE id = #{id} AND status = #{oldStatus}")
    int updateStatus(@Param("id") Long id,
                     @Param("oldStatus") Integer oldStatus,
                     @Param("newStatus") Integer newStatus);
}

// 业务服务
@Service
public class OrderService {
    
    public boolean cancelOrder(Long orderId) {
        // 乐观锁更新状态
        int rows = orderMapper.updateStatus(
            orderId, 
            OrderStatus.PAYING.getCode(),  // 只有支付中才能取消
            OrderStatus.CANCELLED.getCode()
        );
        
        if (rows == 0) {
            // 可能订单已支付或已取消
            Order order = orderMapper.selectById(orderId);
            if (order.getStatus() == OrderStatus.CANCELLED.getCode()) {
                return true; // 幂等:已取消的订单再次取消返回成功
            }
            throw new BusinessException("订单状态不允许取消");
        }
        
        return true;
    }
}

方案4:分布式锁

4.1 Redis分布式锁实现

java

复制

下载

复制代码
@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";
    private static final String LOCK_VALUE = "1";
    private static final long DEFAULT_EXPIRE = 30; // 秒
    private static final long DEFAULT_WAIT = 5;    // 秒
    private static final long RETRY_INTERVAL = 100; // 毫秒
    
    /**
     * 尝试获取锁(支持重试)
     */
    public boolean tryLock(String key, long expireSeconds, long waitSeconds) 
            throws InterruptedException {
        String lockKey = LOCK_PREFIX + key;
        long endTime = System.currentTimeMillis() + waitSeconds * 1000;
        
        while (System.currentTimeMillis() < endTime) {
            // 使用SET NX EX命令原子性加锁
            Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, LOCK_VALUE, expireSeconds, TimeUnit.SECONDS);
            
            if (Boolean.TRUE.equals(success)) {
                return true;
            }
            
            // 等待后重试
            Thread.sleep(RETRY_INTERVAL);
        }
        
        return false;
    }
    
    /**
     * 释放锁
     */
    public boolean unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        return Boolean.TRUE.equals(redisTemplate.delete(lockKey));
    }
    
    /**
     * Lua脚本保证原子性的锁续期
     */
    public boolean renewLock(String key, long expireSeconds) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('expire', KEYS[1], ARGV[2]) " +
            "else " +
            "    return 0 " +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(LOCK_PREFIX + key),
            LOCK_VALUE,
            String.valueOf(expireSeconds)
        );
        
        return result != null && result == 1;
    }
}
4.2 业务层使用

java

复制

下载

复制代码
@Service
public class PaymentService {
    
    @Autowired
    private RedisDistributedLock distributedLock;
    
    private static final String PAYMENT_LOCK_KEY = "payment:";
    
    @Transactional
    public PaymentResult processPayment(PaymentRequest request) {
        String lockKey = PAYMENT_LOCK_KEY + request.getOrderNo();
        
        try {
            // 1. 获取分布式锁
            if (!distributedLock.tryLock(lockKey, 30, 5)) {
                throw new BusinessException("支付处理中,请勿重复操作");
            }
            
            // 2. 检查幂等性(先查后处理)
            Payment existing = paymentMapper.selectByOrderNo(request.getOrderNo());
            if (existing != null) {
                // 幂等返回:如果已支付成功,返回相同结果
                return convertToResult(existing);
            }
            
            // 3. 执行业务逻辑
            Payment payment = createPayment(request);
            paymentMapper.insert(payment);
            
            // 4. 调用支付网关
            boolean success = paymentGateway.pay(payment);
            
            if (success) {
                payment.setStatus(PaymentStatus.SUCCESS);
                // 更新订单状态
                orderService.markAsPaid(request.getOrderNo());
            } else {
                payment.setStatus(PaymentStatus.FAILED);
            }
            
            paymentMapper.updateById(payment);
            
            return convertToResult(payment);
            
        } catch (Exception e) {
            throw new BusinessException("支付处理失败", e);
        } finally {
            // 5. 释放锁
            distributedLock.unlock(lockKey);
        }
    }
}

方案5:全局唯一ID + 幂等表(分布式)

5.1 架构设计

java

复制

下载

复制代码
// 幂等记录实体
@Data
@TableName("distributed_idempotent")
public class DistributedIdempotent {
    @TableId(type = IdType.INPUT)
    private String requestId;      // 全局唯一请求ID
    
    private String serviceName;    // 服务名
    private String bizType;        // 业务类型
    private String bizKey;         // 业务唯一键
    
    private Integer status;        // 0-处理中, 1-成功, 2-失败
    private String result;         // 处理结果(JSON)
    
    private Integer retryCount;    // 重试次数
    private String errorMsg;       // 错误信息
    
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    
    @TableField(exist = false)
    private Map<String, Object> context; // 上下文信息
}

// 幂等管理器
@Component
public class DistributedIdempotentManager {
    
    @Autowired
    private DistributedIdempotentMapper idempotentMapper;
    
    /**
     * 生成全局请求ID
     */
    public String generateRequestId(String serviceName, String bizType) {
        // 雪花算法生成ID
        Snowflake snowflake = new Snowflake(1, 1);
        long id = snowflake.nextId();
        
        return String.format("%s:%s:%s", 
            serviceName, bizType, id);
    }
    
    /**
     * 幂等执行
     */
    public <T> T execute(String requestId, String bizKey, 
                        Supplier<T> processor, Class<T> resultType) {
        // 1. 查询是否已处理
        DistributedIdempotent record = 
            idempotentMapper.selectById(requestId);
        
        if (record != null) {
            return handleExistingRecord(record, resultType);
        }
        
        // 2. 创建处理中记录
        record = new DistributedIdempotent();
        record.setRequestId(requestId);
        record.setStatus(0);
        record.setBizKey(bizKey);
        record.setCreateTime(LocalDateTime.now());
        
        try {
            idempotentMapper.insert(record);
        } catch (DuplicateKeyException e) {
            // 并发插入,重新查询
            record = idempotentMapper.selectById(requestId);
            return handleExistingRecord(record, resultType);
        }
        
        try {
            // 3. 执行业务逻辑
            T result = processor.get();
            
            // 4. 更新为成功
            record.setStatus(1);
            record.setResult(JSON.toJSONString(result));
            record.setUpdateTime(LocalDateTime.now());
            idempotentMapper.updateById(record);
            
            return result;
        } catch (Exception e) {
            // 5. 更新为失败
            record.setStatus(2);
            record.setErrorMsg(e.getMessage());
            record.setRetryCount(record.getRetryCount() + 1);
            record.setUpdateTime(LocalDateTime.now());
            idempotentMapper.updateById(record);
            
            throw e;
        }
    }
    
    private <T> T handleExistingRecord(DistributedIdempotent record, 
                                      Class<T> resultType) {
        if (record.getStatus() == 1) {
            // 已成功,返回结果
            return JSON.parseObject(record.getResult(), resultType);
        } else if (record.getStatus() == 0) {
            // 处理中,根据业务决定等待或抛异常
            throw new BusinessException("请求正在处理中");
        } else {
            // 失败,根据重试策略决定
            if (record.getRetryCount() < 3) {
                throw new RetryableException("上次处理失败,可重试");
            } else {
                throw new BusinessException("处理失败次数过多");
            }
        }
    }
}
5.2 消息队列幂等消费

java

复制

下载

复制代码
@Component
@Slf4j
public class MessageConsumer {
    
    @Autowired
    private DistributedIdempotentManager idempotentManager;
    
    @RabbitListener(queues = "order.create.queue")
    public void handleOrderCreate(OrderCreateMessage message) {
        String requestId = message.getRequestId();
        String orderNo = message.getOrderNo();
        
        // 使用消息ID作为幂等键
        idempotentManager.execute(
            requestId,
            orderNo,
            () -> {
                // 业务处理逻辑
                return orderService.processOrderCreate(message);
            },
            OrderResult.class
        );
    }
}

五、多级幂等保障体系

5.1 分层防御架构

java

复制

下载

复制代码
// 三层幂等保障
@Component
public class MultiLayerIdempotentService {
    
    // 第一层:请求层(防重复提交)
    public class RequestLayer {
        private final TokenService tokenService;
        
        public ApiResponse processWithToken(OrderRequest request, String token) {
            if (!tokenService.verifyToken(token)) {
                return ApiResponse.error("重复提交");
            }
            // 继续处理
        }
    }
    
    // 第二层:业务层(防并发重复)
    public class BusinessLayer {
        private final RedisDistributedLock lockService;
        
        @Transactional
        public OrderDTO createOrderWithLock(OrderRequest request) {
            String lockKey = "order:create:" + request.getUserId();
            
            if (!lockService.tryLock(lockKey, 10, 3)) {
                throw new BusinessException("正在处理中");
            }
            
            try {
                // 业务逻辑 + 数据库唯一约束
                return orderService.createOrder(request);
            } finally {
                lockService.unlock(lockKey);
            }
        }
    }
    
    // 第三层:数据层(最终保障)
    public class DataLayer {
        // 依赖数据库唯一索引
        // 依赖乐观锁
        // 依赖状态机约束
    }
}

5.2 幂等键设计原则

java

复制

下载

复制代码
class IdempotentKeyDesign {
    
    // 幂等键组成要素
    class KeyComponents {
        // 1. 业务标识
        String businessType;  // ORDER_CREATE, PAYMENT
        
        // 2. 业务唯一标识
        String businessKey;   // orderNo, paymentNo
        
        // 3. 操作标识(可选)
        String operation;     // CREATE, UPDATE, CANCEL
        
        // 4. 用户标识(可选)
        String userId;        // user123
        
        // 组合方式
        String key = String.format("%s:%s:%s:%s", 
            businessType, userId, operation, businessKey);
    }
    
    // 不同场景的幂等键设计
    Map<String, String> keyExamples = Map.of(
        // 支付业务:用户+订单号
        "payment", "PAYMENT:user123:CREATE:ORDER202312345678",
        
        // 库存扣减:商品ID+订单号  
        "inventory", "INVENTORY:DEDUCT:PROD001:ORDER202312345678",
        
        // 优惠券领取:用户+优惠券ID
        "coupon", "COUPON:CLAIM:user123:COUPON20231234",
        
        // 消息消费:消息ID+消费者组
        "message", "MESSAGE:CONSUME:group1:MSG202312345678"
    );
}

六、特殊场景处理

6.1 前端重复提交

javascript

复制

下载

复制代码
// 前端防重复提交
class FrontendIdempotent {
    
    // 1. 按钮禁用
    submitOrder() {
        this.submitting = true;
        // 发送请求
        // 完成后重置状态
    }
    
    // 2. 请求队列
    requestQueue = [];
    asyncRequest(request) {
        // 相同请求去重
        if (this.requestQueue.includes(request.id)) {
            return Promise.reject('重复请求');
        }
        this.requestQueue.push(request.id);
        
        // 发送请求
        // 完成后移除
    }
    
    // 3. Token机制
    async submitWithToken() {
        // 先获取Token
        const token = await api.getToken();
        
        // 携带Token提交
        return api.createOrder({data}, {headers: {'X-Token': token}});
    }
}

6.2 网络超时重试

java

复制

下载

复制代码
// 客户端重试策略
@Component
public class RetryableHttpClient {
    
    private final RestTemplate restTemplate;
    private final IdempotentStorage storage;
    
    // 重试时保证幂等
    public <T> T postWithRetry(String url, Object request, 
                              Class<T> responseType, String requestId) {
        
        // 1. 生成请求ID
        if (requestId == null) {
            requestId = UUID.randomUUID().toString();
        }
        
        // 2. 检查是否已成功
        IdempotentRecord record = storage.get(requestId);
        if (record != null && record.isSuccess()) {
            return record.getResult();
        }
        
        // 3. 设置幂等头
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-Request-ID", requestId);
        headers.set("X-Idempotency-Key", requestId);
        
        HttpEntity<Object> entity = new HttpEntity<>(request, headers);
        
        // 4. 重试逻辑
        int maxRetries = 3;
        for (int i = 0; i < maxRetries; i++) {
            try {
                ResponseEntity<T> response = restTemplate.postForEntity(
                    url, entity, responseType);
                
                if (response.getStatusCode().is2xxSuccessful()) {
                    // 5. 存储成功结果
                    storage.saveSuccess(requestId, response.getBody());
                    return response.getBody();
                }
            } catch (Exception e) {
                if (i == maxRetries - 1) {
                    throw e;
                }
                // 指数退避
                Thread.sleep(100 * (long) Math.pow(2, i));
            }
        }
        
        throw new RuntimeException("重试失败");
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

6.3 消息队列幂等消费

java

复制

下载

复制代码
// RocketMQ幂等消费
@Component
@RocketMQMessageListener(
    topic = "ORDER_TOPIC",
    consumerGroup = "ORDER_CONSUMER_GROUP"
)
public class OrderMessageConsumer implements RocketMQListener<MessageExt> {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Override
    public void onMessage(MessageExt message) {
        String msgId = message.getMsgId();
        String keys = message.getKeys(); // 业务键
        
        // 1. Redis原子性判断
        String redisKey = "mq:consumed:" + msgId;
        Boolean notConsumed = redisTemplate.opsForValue()
            .setIfAbsent(redisKey, "1", 7, TimeUnit.DAYS);
        
        if (Boolean.FALSE.equals(notConsumed)) {
            log.info("消息已消费,msgId: {}", msgId);
            return; // 幂等返回
        }
        
        try {
            // 2. 业务处理
            OrderMessage orderMessage = JSON.parseObject(
                message.getBody(), OrderMessage.class);
            
            orderService.processOrderMessage(orderMessage);
            
        } catch (Exception e) {
            // 3. 处理失败,删除Redis记录允许重试
            redisTemplate.delete(redisKey);
            throw e;
        }
    }
}

七、监控与治理

7.1 幂等监控指标

java

复制

下载

复制代码
@Component
@Slf4j
public class IdempotentMonitor {
    
    private final MeterRegistry meterRegistry;
    
    // 记录幂等相关指标
    public void recordMetrics(String bizType, String result) {
        // 计数器
        Counter.builder("idempotent.requests.total")
            .tag("biz_type", bizType)
            .tag("result", result)
            .register(meterRegistry)
            .increment();
        
        // 处理时长直方图
        Timer.builder("idempotent.process.duration")
            .tag("biz_type", bizType)
            .register(meterRegistry);
    }
    
    // 告警规则
    @Scheduled(fixedDelay = 60000) // 每分钟检查
    public void checkIdempotentAlerts() {
        // 1. 重复请求率过高
        double duplicateRate = calculateDuplicateRate();
        if (duplicateRate > 0.1) { // 10%重复率
            alertService.sendAlert("幂等重复率过高: " + duplicateRate);
        }
        
        // 2. 幂等冲突频繁
        long conflictCount = getConflictCountLastMinute();
        if (conflictCount > 100) {
            alertService.sendAlert("幂等冲突频繁: " + conflictCount);
        }
    }
}

7.2 幂等配置管理

yaml

复制

下载

复制代码
# application-idempotent.yml
idempotent:
  config:
    # Token配置
    token:
      enabled: true
      expire-seconds: 300      # 5分钟
      prefix: "idempotent:token"
    
    # 分布式锁配置  
    lock:
      enabled: true
      prefix: "idempotent:lock"
      expire-seconds: 30
      wait-seconds: 5
    
    # 幂等记录配置
    record:
      enabled: true
      table-name: "sys_idempotent_record"
      clean-enabled: true
      clean-days: 30          # 保留30天
    
    # 业务配置
    business:
      order.create:
        strategy: "token+lock+unique"  # 组合策略
        timeout: 5000                  # 5秒超时
        retry-times: 3                 # 重试3次
        
      payment.process:
        strategy: "requestId+record"
        timeout: 10000
        
      inventory.deduct:
        strategy: "optimistic_lock"
        version-field: "version"

八、总结与选择建议

8.1 方案选择矩阵

场景特征 推荐方案 原因
表单提交 Token机制 简单易用,用户体验好
创建业务 唯一索引 数据库保障,绝对可靠
更新业务 乐观锁 并发性能好,无锁竞争
高并发扣减 分布式锁+乐观锁 强一致+高性能
分布式系统 全局ID+幂等表 可追溯,通用性强
消息消费 Redis+唯一键 快速判断,避免重复

8.2 最佳实践组合

java

复制

下载

复制代码
// 生产环境推荐组合
public class ProductionIdempotentStrategy {
    
    // 三级保障体系
    public ApiResponse process(OrderRequest request) {
        // 1. 前端:Token防重复提交 ✓
        // 2. 网关:请求ID去重 ✓
        // 3. 业务层:分布式锁+乐观锁 ✓
        // 4. 数据层:唯一索引+状态机 ✓
        // 5. 监控:全链路跟踪 ✓
        
        return doProcess(request);
    }
    
    // 金融级幂等要求
    public PaymentResult processPayment(PaymentRequest request) {
        // 必须包含以下所有措施:
        // 1. 请求全局唯一ID
        // 2. 数据库唯一约束
        // 3. 分布式锁
        // 4. 状态机校验
        // 5. 对账机制
        // 6. 人工核查入口
        
        return paymentService.process(request);
    }
}

8.3 幂等性检查清单

markdown

复制

下载

复制代码
✅ 是否识别了所有非幂等接口?
✅ 是否为关键业务接口设计了幂等?
✅ 幂等键设计是否唯一且有意义?
✅ 是否考虑了并发场景?
✅ 是否有超时重试机制?
✅ 是否有防重复提交措施?
✅ 数据库约束是否生效?
✅ 异常情况是否妥善处理?
✅ 是否有监控和告警?
✅ 是否有降级方案?
✅ 是否有数据对账?
✅ 是否经过充分测试?

记住:没有完美的幂等方案,只有最适合业务场景的方案。根据业务重要性、并发程度、数据一致性要求等因素,选择合适的组合方案。

相关推荐
qwepoilkjasd7 小时前
DMC发送M-SEARCH请求,DMR响应流程
后端
全栈独立开发者7 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
dmonstererer7 小时前
【k8s设置污点/容忍】
java·容器·kubernetes
super_lzb7 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
程序之巅7 小时前
VS code 远程python代码debug
android·java·python
心在飞扬7 小时前
langchain学习总结:Python + OpenAI 原生 SDK 实现记忆功能
后端
张志鹏PHP全栈7 小时前
Solidity智能合约快速入门
后端
ihgry7 小时前
SpringCloud_Nacos
后端
我是Superman丶7 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
墨雨晨曦887 小时前
Nacos
java