Java中,使用token+Redis实现接口幂等性,防止订单重复提交

一、什么是幂等性?

幂等一般指的是方法被多次重复执行的时候,所产生的影响和第一次执行的影响是相同的。

二、技术场景中的幂等性

操作类型示例实现方式
创建资源订单提交Token机制、唯一ID
更新资源设置用户状态为"已激活"乐观锁、条件更新
删除资源取消订单先查状态再删除
金融操作账户扣款事务+唯一流水号

三、HTTP方法的幂等性

方法幂等性说明
GET读取操作不影响资源状态
PUT全量更新(如更新用户信息)
DELETE删除一次后再删除返回404
POST每次调用创建新资源(如订单)
PATCH⚠️部分更新,需自行实现幂等

⚠️ 实际业务中需通过设计将POST/PATCH转为幂等操作

四、接口会被重复请求的情况

场景1:用户的重复提交或者用户的恶意攻击,导致请求被多次重复执行。

核心思路

  1. 生成唯一Token:在用户进入订单提交页时,后端生成唯一Token并存储到Redis
  2. 前端携带Token:提交订单时,前端将Token附加到请求中(Header/Param)
  3. 验证并删除Token:后端通过Redis原子操作验证Token存在性,并立即删除
  4. 拒绝重复请求:若Token不存在/重复,则判定为重复提交

执行流程

在这里插入图片描述

实现步骤

  1. 生成Token接口(供前端获取)
@RestController
public class TokenController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/generateToken")
    public Response<String> generateToken() {
        String token = UUID.randomUUID().toString();
        // 存储到Redis,有效期5分钟(根据业务调整)
        redisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES);
        return Response.success(token);
    }
}
  1. 订单提交接口(幂等校验)
@RestController
public class OrderController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @PostMapping("/submitOrder")
    public Response<String> submitOrder(@RequestHeader("X-Idempotent-Token") String token, 
                                        @RequestBody Order order) {
        
        // 1. 校验Token有效性(原子操作:查询+删除)
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(
                "if redis.call('get', KEYS[1]) then return redis.call('del', KEYS[1]) else return 0 end", 
                Long.class
            ),
            Collections.singletonList(token)
        );

        // 2. 处理验证结果
        if (result == 0) {
            // Token不存在或已使用(重复请求)
            return Response.fail("重复提交或无效请求");
        }

        // 3. 正常处理业务(创建订单等)
        orderService.createOrder(order);
        return Response.success("订单创建成功");
    }
}

关键点解析

  1. 原子操作:使用Lua脚本保证GET+DEL操作的原子性,避免并发问题:
if redis.call('get', KEYS[1]) then 
    return redis.call('del', KEYS[1]) 
else 
    return 0 
end
  1. Token传递方式

    • Token绑定用户ID:order_{userId}_{uuid} 防止Token被其他用户盗用

    • 推荐使用请求头:@RequestHeader(“X-Idempotent-Token”)

    • 可选请求参数:@RequestParam(“token”)

  2. Redis配置

    • Key:Token字符串(如 “c6a9d6c7-3d4f-4eaa-bb7e-123456789abc”)
    • Value:任意值(比如"1")
    • TTL:根据业务设置(通常5-30分钟)
  3. 前端配合

    • 进入订单页时调用/generateToken获取Token
    • 提交订单时在Header中添加X-Idempotent-Token:

增强方案

  1. 添加业务标识(防不同订单复用Token)
// 生成Token时加入用户ID
String token = "ORDER_" + userId + "_" + UUID.randomUUID();
  1. 自定义注解简化流程
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String header() default "X-Idempotent-Token"; // 指定Token位置
}

// 使用AOP统一处理
@Around("@annotation(idempotent)")
public Object checkIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
    // 从Header获取Token并验证
    // ...
}
  1. 异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IdempotentException.class)
    public ResponseEntity<String> handleDuplicate(IdempotentException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage());
    }
}

场景2:在分布式架构中,RPC接口远程调用,上游服务调用下游服务接口超时通常会重试,此时有可能会重复调用。

核心思路

  1. 上游服务生成唯一ID,下游服务通过状态机保证幂等

执行流程

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/d30ce4d5d71a4dfcb6693905fd9d500d.png

实现步骤

  1. 设计幂等处理器
// 幂等处理器
@Component
public class IdempotentProcessor {
    
    @Autowired
    private IdempotentRecordRepository recordRepository;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "IDEMPOTENT_LOCK:";

    public <T> T process(String requestId, Supplier<T> businessLogic) {
        // 1. 检查是否已处理过
        IdempotentRecord record = recordRepository.findByRequestId(requestId);
        if (record != null) {
            return (T) JSON.parseObject(record.getResponse(), businessLogic.get().getClass());
        }
        
        // 2. 获取分布式锁(防止并发请求)
        String lockKey = LOCK_PREFIX + requestId;
        boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
        
        if (!locked) {
            throw new IdempotentException("系统处理中,请稍后");
        }
        
        try {
            // 3. 双重检查(获取锁后再次查询)
            record = recordRepository.findByRequestId(requestId);
            if (record != null) {
                return (T) JSON.parseObject(record.getResponse(), businessLogic.get().getClass());
            }
            
            // 4. 执行业务逻辑
            T result = businessLogic.get();
            
            // 5. 保存处理结果(事务中)
            saveIdempotentRecord(requestId, result);
            
            return result;
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    }
    
    @Transactional
    private <T> void saveIdempotentRecord(String requestId, T result) {
        IdempotentRecord record = new IdempotentRecord();
        record.setRequestId(requestId);
        record.setStatus(IdempotentStatus.COMPLETED);
        record.setResponse(JSON.toJSONString(result));
        record.setCreatedAt(LocalDateTime.now());
        recordRepository.save(record);
    }
}

// 业务服务使用示例
@Service
public class OrderService {
    
    @Autowired
    private IdempotentProcessor idempotentProcessor;
    
    public Order createOrder(CreateOrderCommand command) {
        // 从上游获取的请求ID(由调用方生成)
        String requestId = command.getRequestId();
        
        return idempotentProcessor.process(requestId, () -> {
            // 真正的业务逻辑
            Order order = new Order();
            // ...订单创建逻辑
            return orderRepository.save(order);
        });
    }
}
  1. 数据库设计,新建幂等记录表
CREATE TABLE idempotent_record (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    request_id VARCHAR(64) NOT NULL COMMENT '唯一请求ID',
    status TINYINT NOT NULL COMMENT '状态:1-处理中,2-成功,3-失败',
    response TEXT COMMENT '响应结果(JSON)',
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uniq_request_id (request_id)
) COMMENT='幂等记录表';

双场景统一方案

对于同时需要处理两种场景的系统,可结合两种方案:

@PostMapping("/orders")
public ResponseEntity<?> createOrder(
        @RequestHeader(value = "X-Idempotent-Token", required = false) String token,
        @RequestBody OrderRequest request) {
    
    // 场景1:来自前端的请求(带Token)
    if (token != null) {
        if (!validateToken(token)) {
            return ResponseEntity.status(409).body("重复请求");
        }
        return processOrder(request);
    }
    
    // 场景2:来自服务的请求(带RequestID)
    if (request.getRequestId() != null) {
        return idempotentProcessor.process(request.getRequestId(), 
            () -> processOrder(request));
    }
    
    // 无防护的请求
    return ResponseEntity.badRequest().body("缺少幂等标识");
}

方案对比总结:

场景方案核心组件优点适用接口
用户重复提交Token+Redis一次性令牌前端友好,简单易实现用户触发的写操作
分布式系统重试请求ID+状态机唯一ID+状态记录服务间通用,支持结果缓存RPC接口/消息队列消费
混合场景双方案结合网关路由识别全场景覆盖统一入口的API

最佳实践建议

  1. 前端场景:
    • Token有效期不宜过长(建议5-30分钟)
    • 结合验证码防止机器人攻击
    • 敏感操作添加二次确认
  2. 服务间场景:
    • 请求ID生成规则:服务标识+业务ID+UUID
    • 状态机设计明确状态流转
    • 设置合理的记录保留时间(建议72小时)
  3. 通用原则:
    在这里插入图片描述
  4. 监控告警:
    • 记录重复请求率
    • 监控幂等表数据量增长
    • 设置异常重复告警阈值

通过以上方案,可有效解决用户重复提交和分布式系统重试导致的幂等问题,同时保证系统的高可用性和数据一致性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值