ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Web开发中的接口防抖与防重复提交实践

Web开发中的接口防抖与防重复提交实践 1. 为什么我们需要接口防抖在Web开发中接口防抖Debounce和防重复提交Duplicate Submission Prevention是保障系统稳定性的重要手段。想象这样一个场景用户在电商平台点击提交订单按钮时由于网络延迟或手抖可能在短时间内连续点击多次。如果没有防护措施系统就会创建多个相同的订单导致库存扣减异常、支付重复等问题。接口防抖的核心目标是在指定时间窗口内对相同业务含义的请求只处理一次。这与前端防抖如按钮点击防抖不同后端防抖需要更全面的考虑业务一致性防止重复数据导致业务逻辑混乱系统负载避免无效请求消耗服务器资源用户体验减少因误操作导致的意外结果2. 基于请求参数的防重方案2.1 请求指纹生成策略最直观的方案是通过请求内容识别重复请求。我们需要为每个请求生成唯一指纹Fingerprint通常包含public String generateRequestFingerprint(HttpServletRequest request) { // 1. 获取关键参数如用户ID业务类型业务ID String userId request.getParameter(userId); String bizType request.getParameter(bizType); String bizId request.getParameter(bizId); // 2. 参数排序后拼接 String[] params {userId, bizType, bizId}; Arrays.sort(params); // 避免参数顺序影响 // 3. 使用MD5生成指纹 return DigestUtils.md5DigestAsHex(String.join(|, params).getBytes()); }注意不要对所有参数进行指纹计算应选择能标识业务唯一性的关键字段。例如订单创建接口只需用户ID商品ID忽略时间戳等可变参数。2.2 基于Redis的防重实现生成指纹后可以使用Redis实现原子性防重public boolean isDuplicateRequest(String fingerprint, int expireSeconds) { String key req:dup: fingerprint; // setIfAbsent SETNX EXPIRE 的原子操作 Boolean result redisTemplate.opsForValue() .setIfAbsent(key, 1, expireSeconds, TimeUnit.SECONDS); return result null || !result; }实际使用时PostMapping(/createOrder) public Result createOrder(RequestBody OrderDTO dto, HttpServletRequest request) { String fingerprint generateFingerprint(request); if (isDuplicateRequest(fingerprint, 30)) { return Result.fail(请勿重复提交); } // 正常业务逻辑 }参数选择经验过期时间通常设置30秒可根据业务调整Key设计添加前缀如req:dup:避免冲突存储选择Redis比数据库更适合高频读写3. Token令牌机制详解3.1 令牌生成与校验流程Token方案是另一种常见防重手段流程如下前端获取Token// 页面加载时获取Token axios.get(/api/token).then(res { this.submitToken res.data.token })后端生成TokenGetMapping(/token) public Result getToken() { String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set(token: token, 1, 5, TimeUnit.MINUTES); return Result.success(token); }提交时携带TokenPostMapping(/submit) public Result submit(RequestParam String token) { String key token: token; Long delete redisTemplate.delete(key); if (delete null || delete 0) { return Result.fail(无效或已使用的Token); } // 处理业务 }3.2 令牌方案的优缺点优势实现简单不依赖请求参数解析可防止网络延迟导致的重复提交适合表单类场景局限需要前后端配合每个Token只能使用一次不适用于API接口调用场景实际项目中我们会在拦截器中统一处理Token校验避免每个接口重复编码。4. 基于AOP的全局防重方案4.1 自定义防重注解通过AOP可以实现声明式防重Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface PreventDuplicate { int expire() default 30; // 默认防重时间窗口 String[] includeParams() default {}; // 参与指纹计算的参数 }4.2 切面逻辑实现Aspect Component public class DuplicatePreventAspect { Around(annotation(preventDuplicate)) public Object checkDuplicate(ProceedingJoinPoint joinPoint, PreventDuplicate preventDuplicate) throws Throwable { // 1. 获取请求上下文 HttpServletRequest request ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); // 2. 生成指纹 String fingerprint generateFingerprint(request, preventDuplicate.includeParams()); // 3. Redis防重检查 String key prevent: fingerprint; if (!redisTemplate.opsForValue().setIfAbsent( key, 1, preventDuplicate.expire(), TimeUnit.SECONDS)) { throw new BusinessException(操作过于频繁); } // 4. 执行原方法 return joinPoint.proceed(); } }使用示例PreventDuplicate(expire 60, includeParams {userId, productId}) PostMapping(/purchase) public Result purchase(RequestBody OrderDTO dto) { // 业务逻辑 }4.3 AOP方案优化建议异常处理在finally中清理Redis键如需参数解析支持从JSON body中提取字段白名单添加IP/用户维度的频率限制监控记录防重触发日志用于分析5. 分布式环境下的特殊考量在微服务架构中防重设计需要额外注意5.1 分布式锁的应用当服务部署多个实例时需要使用分布式锁public boolean tryLock(String lockKey, int expireTime) { String requestId UUID.randomUUID().toString(); return redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS); } public void unlock(String lockKey, String requestId) { // 使用Lua脚本保证原子性 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute( new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); }5.2 数据库唯一约束对于核心业务如支付可在数据库层添加唯一约束ALTER TABLE orders ADD UNIQUE INDEX uk_order_no (order_no);配合业务代码try { orderRepository.save(order); } catch (DataIntegrityViolationException e) { log.warn(重复订单: {}, order.getOrderNo()); throw new BusinessException(订单已存在); }5.3 幂等设计模式真正的终极解决方案是实现接口幂等性客户端生成唯一IDPostMapping(/pay) public Result pay(RequestParam String requestId) { // 先查询是否已处理 Payment payment paymentRepo.findByRequestId(requestId); if (payment ! null) { return Result.success(payment); } // 处理支付 }状态机校验if (order.getStatus() ! OrderStatus.INIT) { throw new BusinessException(订单状态异常); }6. 性能优化与监控6.1 防重组件性能影响在高并发场景下防重检查可能成为瓶颈Redis集群使用集群模式分散压力本地缓存结合Caffeine做二级缓存Key设计避免大Key和热Key6.2 监控指标设计建议监控以下指标指标名称监控方式报警阈值防重触发率Redis计数器定时上报10%持续5分钟防重检查耗时Micrometer计时器P9950msRedis连接池活跃数Druid监控80%持续10分钟6.3 动态调整策略根据监控结果动态调整PreventDuplicate( expire #{duplicateConfig.getExpireTime(order)}, includeParams #{duplicateConfig.getIncludeParams(order)} )7. 实际案例电商下单防重结合电商场景完整实现方案前端按钮点击后禁用请求时携带页面Token错误时恢复按钮状态网关层基于IP和用户ID限流校验必要参数业务层PreventDuplicate( expire 30, includeParams {userId, productId, skuCode} ) Transactional public OrderResult createOrder(OrderRequest request) { // 1. 参数校验 // 2. 库存检查 // 3. 创建订单 // 4. 扣减库存 }数据层订单表添加唯一索引使用乐观锁更新库存8. 不同方案的对比选型根据业务特点选择合适方案方案适用场景实现复杂度可靠性参数指纹Redis通用API接口中高Token机制表单提交低中AOP注解需要灵活配置的场景高高数据库唯一约束核心业务如支付低最高幂等设计金融级业务最高最高在SpringBoot项目中我通常会组合使用AOP注解和数据库约束。对于关键业务路径添加幂等设计同时在前端做基础防护形成多层次的防重体系。
返回列表