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

一、幂等性基础概念

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

复制

下载

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

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

相关推荐
曹牧6 小时前
文档格式:OFD
java
豆角焖肉7 小时前
Maven进阶与搭建私服
java·maven
大不点wow7 小时前
Java序列化与反序列化:让对象走出JVM
java·开发语言·jvm
止语Lab7 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活7 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
噢,我明白了7 小时前
Java中日期和字符串的处理
java·开发语言·日期
程序员天天困7 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn7 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull7 小时前
Spring Boot请求处理组件对比详解
java·spring boot
jun_bai7 小时前
Orthanc服务器使用java上传dicom影像文件
java·运维·服务器