防止重复提交和防抖设计

背景和价值

因为网络原因,用点击提交页面没有响应,用户重复操作
因为后端性能问题,用点击提交页面,后端其实已经接收到请求,并且已经在处理请求,但是响应比较慢,用户又重复操作

前端防抖并做页面加载友好提示

前端预生成token,提交表单带上token,后端做校验 (推荐,复杂度中,适合金融支付场景)

【设计考虑】
1 用户重复提交得到拦截。【错误设计】 后端接到请求就删除redis,举个极端场景,后端接到前端token,在redis查询发现还有,删除redis, 执行业务操作,但是业务操作很慢。 那么用户看到点击没反应,又点了一次,这个时候redis的token已经被消费,绕过了 token的校验

2 后端处理业务发生异常,允许用户再次提交

【正确设计】
确保业务处理完成后再删除Token。使用三阶段

流程

一、流程图概览

sequenceDiagram participant 用户 participant 前端 participant 网关 participant 拦截器 participant 业务服务 participant Redis participant 数据库 用户 ->> 前端: 1. 进入结算页 前端 ->> 网关: 2. 请求生成Token 网关 ->> Redis: 3. 生成Token并存储 Redis -->> 网关: 4. 返回Token 网关 -->> 前端: 5. 返回Token 前端 ->> 用户: 6. 展示结算页(携带Token) 用户 ->> 前端: 7. 点击提交订单(携带Token) 前端 ->> 网关: 8. 提交订单请求(Header: Idempotency-Token) 网关 ->> 拦截器: 9. 进入幂等拦截器 拦截器 ->> Redis: 10. 检查Token状态 alt Token有效 Redis ->> 拦截器: 11. 标记为PROCESSING 拦截器 ->> 业务服务: 12. 放行请求 业务服务 ->> 数据库: 13. 执行业务操作 数据库 -->> 业务服务: 14. 返回结果 业务服务 ->> Redis: 15. 标记Token为USED(或删除) 业务服务 -->> 拦截器: 16. 返回响应 拦截器 -->> 网关: 17. 返回响应 网关 -->> 前端: 18. 返回订单结果 前端 ->> 用户: 19. 展示成功 else Token无效 Redis ->> 拦截器: 11. 返回冲突状态 拦截器 -->> 网关: 12. 返回409错误 网关 -->> 前端: 13. 返回错误提示 前端 ->> 用户: 14. 提示"请勿重复提交" end

后端分布式锁(简单,粗暴,难度度:低)

后端使用可自动延续的redis全局锁(userid+场景id)锁。

2. 安全增强措施

  • 绑定用户身份:Token必须与用户ID关联。
    // 错误示例:全局Token可能被其他用户盗用
    String token = redis.get("global_token");
    // 正确示例:绑定用户
    String token = redis.get("checkout_token:user123");
    
  • 加密传输:Token在传输时使用HTTPS + 请求签名。
  • 一次性使用:即使订单提交失败,同一Token也仅允许重试有限次数(如3次)。

Redis全局锁

以 用户ID + 业务场景ID (页面ID)作为 Redis 锁的 Key,确保同一用户在特定场景下 ​同一时间只能执行一次操作。例如:
缺点:高频请求下 Redis 压力较大

二、实现步骤

1. 获取锁(原子操作)

// 使用 SET 命令实现原子锁(推荐)
public boolean acquireLock(String userId, String sceneId, int expireSeconds) {
    String key = "lock:" + sceneId + ":user" + userId;
    // 参数说明:NX(不存在时设置)、EX(过期时间)
    String result = redisTemplate.execute(
        (RedisCallback<String>) connection -> 
            connection.set(
                key.getBytes(), 
                "1".getBytes(), 
                Expiration.seconds(expireSeconds), 
                RedisStringCommands.SetOption.SET_IF_ABSENT
            )
    );
    return "OK".equals(result);
}

2. 执行业务逻辑

@PostMapping("/submit")
public ResponseEntity<?> submitOrder(@RequestParam String userId) {
    String sceneId = "order_submit"; // 场景标识(如订单提交页)
    int lockExpire = 30; // 锁过期时间(秒)
    
    // 1. 尝试获取锁
    if (!acquireLock(userId, sceneId, lockExpire)) {
        return ResponseEntity.status(409).body("操作进行中,请勿重复提交");
    }
    
    try {
        // 2. 执行业务逻辑(如创建订单)
        Order order = orderService.create(userId);
        return ResponseEntity.ok(order);
    } finally {
        // 3. 释放锁(根据业务需求可选)
        // redisTemplate.delete("lock:order_submit:user" + userId);
    }
}

六、对比:Redis 锁 vs 幂等 Token

维度 Redis 锁 幂等 Token
复杂度 低(无需预生成Token) 中(需管理Token生命周期)
适用场景 简单并发控制(如防重提交) 严格幂等性(如支付、资金操作)
性能影响 高频请求下 Redis 压力较大 需额外存储Token,但压力更分散

参考资料

posted @ 2025-05-01 12:32  向着朝阳  阅读(77)  评论(0)    收藏  举报