防止重复提交和防抖设计
目录
背景和价值
因为网络原因,用点击提交页面没有响应,用户重复操作
因为后端性能问题,用点击提交页面,后端其实已经接收到请求,并且已经在处理请求,但是响应比较慢,用户又重复操作
前端防抖并做页面加载友好提示
前端预生成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,但压力更分散 |

浙公网安备 33010602011771号