当前位置: 首页 > article >正文

分布式环境下的重复请求防护:非Redis锁替代方案全解析

目录

引言

方案一:前端防护策略

方案二:后端协同控制

方案三:流量控制与过滤

滑动窗口限流

布隆过滤器

方案四:基于框架的实践方案

多层防护策略与最佳实践

总结        


引言

        在Web应用开发中,防止用户重复点击提交是一个常见却棘手的问题。重复提交不仅会导致数据重复、资源浪费,在交易、下单等场景中甚至可能造成严重的业务异常。通常情况下,我们会使用Redis分布式锁来解决这个问题,但当Redis不可用或由于架构限制无法使用时,我们需要其他可靠的替代方案。
        本文将深入探讨几种不依赖Redis的防重复点击方案,从前端到后端,从简单到复杂,分析各自的实现原理、适用场景以及优缺点,帮助开发者根据自身业务需求选择最合适的解决方案。

方案一:前端防护策略

        最直接的防重复点击方案是在前端实现按钮防抖。当用户点击按钮后,立即将按钮禁用或置灰,防止用户进行二次点击。

// 伪代码:防抖按钮实现示例
function debounceButton(btn, time = 2000) {
  if (btn.disabled) return;
  
  // 禁用按钮
  btn.disabled = true;
  btn.classList.add('disabled');
  
  // 发送请求
  sendRequest()
    .finally(() => {
      // 请求完成后恢复按钮状态(也可以根据业务需要不恢复)
      setTimeout(() => {
        btn.disabled = false;
        btn.classList.remove('disabled');
      }, time);
    });
}

优点:

  • 实现简单,无需后端配合
  • 用户体验友好,提供直观的视觉反馈
  • 适用于大多数普通业务场景

局限性:

  • 网络延迟可能导致禁用不及时
  • 技术熟练的用户可通过浏览器开发工具绕过前端限制
  • 无法防止通过接口工具(如Postman)直接调用API的重复请求

开发经验分享:在实际项目中,我发现单纯依赖前端防抖虽然能解决80%的问题,但在支付等关键业务中,仍需结合后端验证机制,构建多层防护。

 

方案二:后端协同控制

        Token机制是一种有效的服务端防重复提交方案。核心思想是为每次操作生成唯一标识,确保同一标识只被处理一次。
工作流程:

  1. 用户访问页面时,后端生成唯一token并返回前端
  2. 用户提交请求时携带该token
  3. 后端验证token是否已被使用,未使用则标记为已使用并处理请求
  4. 如token已使用,拒绝请求并返回错误提示

 后端实现示例:

// 伪代码
@RestController
public class OrderController {
    
    private final Map<String, Boolean> tokenMap = new ConcurrentHashMap<>();
    
    // 获取token
    @GetMapping("/getToken")
    public Result getToken() {
        String token = UUID.randomUUID().toString();
        tokenMap.put(token, false); // false表示未使用
        return Result.success(token);
    }
    
    // 提交订单
    @PostMapping("/submitOrder")
    public Result submitOrder(@RequestParam String token, @RequestBody OrderDTO order) {
        // 使用数据库事务保证原子性
        return transactionTemplate.execute(status -> {
            // 查询token使用状态
            Boolean used = tokenMap.get(token);
            if (used == null) {
                return Result.error("无效的token");
            }
            if (used) {
                return Result.error("请勿重复提交");
            }
            
            // 标记token为已使用
            tokenMap.put(token, true);
            
            // 处理订单逻辑
            orderService.createOrder(order);
            return Result.success();
        });
    }
}

数据库实现方案:
        在实际生产环境中,可使用数据库存储token状态,结合事务确保原子性:

-- 创建token表
CREATE TABLE submission_token (
    token VARCHAR(36) PRIMARY KEY,
    used BOOLEAN DEFAULT FALSE,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expire_time TIMESTAMP
);

-- 验证并标记token (使用悲观锁)
BEGIN TRANSACTION;
SELECT used FROM submission_token WHERE token = ? FOR UPDATE;
-- 如果未使用,则标记为已使用
UPDATE submission_token SET used = TRUE WHERE token = ?;
COMMIT;

优点:

  • 服务端验证,安全性高
  • 可靠性强,能防止各种渠道的重复请求
  • 结合数据库事务,保证操作原子性

局限性:

  • 实现复杂度较高
  • 需要额外的存储空间管理token
  • 不适合所有场景下的性能要求

方案三:流量控制与过滤

滑动窗口限流

        滑动窗口限流是控制请求频率的有效方法,可以限制用户在指定时间窗口内的请求次数,从而防止重复提交。

直通车:高并发系统中的限流策略:滑动窗口限流与Redis实现-CSDN博客

 

//伪代码
public class SlidingWindowRateLimiter {
    // 用户请求记录: <用户ID, 请求时间列表>
    private Map<String, LinkedList<Long>> requestRecords = new ConcurrentHashMap<>();
    
    // 窗口大小(毫秒)
    private final long windowSize;
    // 窗口内允许的最大请求数
    private final int maxRequests;
    
    public SlidingWindowRateLimiter(long windowSize, int maxRequests) {
        this.windowSize = windowSize;
        this.maxRequests = maxRequests;
    }
    
    /**
     * 判断请求是否被允许
     * @param userId 用户ID
     * @return 是否允许请求
     */
    public synchronized boolean allowRequest(String userId) {
        long currentTime = System.currentTimeMillis();
        
        // 获取用户的请求记录,如不存在则创建
        LinkedList<Long> records = requestRecords.computeIfAbsent(userId, 
                k -> new LinkedList<>());
        
        // 移除窗口外的过期记录
        while (!records.isEmpty() && currentTime - records.getFirst() > windowSize) {
            records.removeFirst();
        }
        
        // 判断窗口内请求是否超过限制
        if (records.size() < maxRequests) {
            // 记录新请求
            records.addLast(currentTime);
            return true;
        }
        
        return false;
    }
}

使用示例:

// 创建限流器: 2秒内最多允许1次请求
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(2000, 1);

@PostMapping("/api/submit")
public Result submit(@RequestHeader String userId) {
    // 检查限流
    if (!limiter.allowRequest(userId)) {
        return Result.error("请求过于频繁,请稍后再试");
    }
    
    // 处理正常业务逻辑
    return businessService.process();
}

适用场景:

  • 适用于高频操作的防重复控制
  • 对性能要求较高的场景
  • 允许在短时间内丢弃部分请求的业务

布隆过滤器

        布隆过滤器是一个空间效率很高的概率型数据结构,用于判断一个元素是否存在于集合中。它可以快速进行"可能存在"或"一定不存在"的判断,适合作为防重复提交的快速过滤层。

直通车:布隆过滤器原理介绍和典型应用案例_布隆过滤器案例-CSDN博客

// 伪代码
public class BloomFilterValidator {
    private BitSet bitSet;
    private int size;
    private int hashFunctions;
    
    public BloomFilterValidator(int size, int hashFunctions) {
        this.size = size;
        this.hashFunctions = hashFunctions;
        this.bitSet = new BitSet(size);
    }
    
    // 添加元素
    public void add(String element) {
        for (int i = 0; i < hashFunctions; i++) {
            int hash = getHash(element, i);
            bitSet.set(hash);
        }
    }
    
    // 判断元素是否可能存在
    public boolean mightContain(String element) {
        for (int i = 0; i < hashFunctions; i++) {
            int hash = getHash(element, i);
            if (!bitSet.get(hash)) {
                return false; // 一定不存在
            }
        }
        return true; // 可能存在
    }
    
    // 简单哈希函数
    private int getHash(String element, int seed) {
        int hash = element.hashCode();
        hash = hash * seed % size;
        return Math.abs(hash) % size;
    }
}

应用架构:

  1. 使用布隆过滤器进行快速判断
  2. 如果过滤器返回"可能存在",则进一步查询数据库确认
  3. 如果确实是重复提交,则拒绝请求
// 伪代码
@Service
public class OrderSubmitService {
    
    private BloomFilterValidator bloomFilter = new BloomFilterValidator(10000, 3);
    private OrderRepository orderRepository;
    
    public Result submitOrder(OrderDTO orderDTO) {
        // 生成请求标识
        String requestId = generateRequestId(orderDTO);
        
        // 布隆过滤器快速检查
        if (bloomFilter.mightContain(requestId)) {
            // 可能是重复请求,进一步查询数据库确认
            if (orderRepository.existsByRequestId(requestId)) {
                return Result.error("订单已提交,请勿重复操作");
            }
        }
        
        // 处理订单并保存请求标识
        Order order = orderService.createOrder(orderDTO);
        bloomFilter.add(requestId); // 添加到布隆过滤器
        
        return Result.success(order);
    }
    
    // 生成请求唯一标识
    private String generateRequestId(OrderDTO orderDTO) {
        // 根据关键业务字段生成唯一标识
        return DigestUtils.md5Hex(orderDTO.getUserId() + orderDTO.getProductId() 
                + orderDTO.getAmount() + System.currentTimeMillis());
    }
}

优点:

  • 空间效率高,内存占用小
  • 查询速度快,适合大规模数据
  • 作为快速过滤层,降低数据库查询压力

局限性:

  • 有一定的误判率(误报)
  • 不能单独使用,需要与数据库等确切存储配合
  • 不支持删除元素,需要定期重建

方案四:基于框架的实践方案

        参考RuoYi框架的实现,RuoYi框架提供了一种基于表单信息的防重复提交方案,核心思想是将表单内容、提交时间等信息进行校验,限制相同内容在短时间内的重复提交。

/**
 * 自定义注解防止表单重复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    int interval() default 5000;
}

/**
 * 防重复提交拦截器
 */
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
    
    private final FormTokenService tokenService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null) {
                // 验证表单是否重复提交
                return validateFormRepeat(request, annotation);
            }
        }
        return true;
    }
    
    private boolean validateFormRepeat(HttpServletRequest request, RepeatSubmit annotation) {
        // 获取请求参数内容
        String formContent = getFormContent(request);
        // 获取请求路径
        String requestPath = request.getRequestURI();
        // 用户标识
        String userToken = getUserToken(request);
        
        // 生成表单唯一标识
        String formKey = DigestUtils.md5Hex(requestPath + userToken + formContent);
        
        // 检查数据库中是否存在且是否在规定时间内
        FormSubmitRecord record = formRecordRepository.findByFormKey(formKey);
        if (record != null) {
            long interval = System.currentTimeMillis() - record.getSubmitTime();
            if (interval < annotation.interval()) {
                return false; // 判定为重复提交
            }
        }
        
        // 记录本次提交
        saveFormRecord(formKey);
        
        return true;
    }
    
    // 获取表单内容
    private String getFormContent(HttpServletRequest request) {
        // 获取POST内容或GET参数,进行排序和标准化处理
        // ...具体实现略
    }
    
    // 保存表单记录
    private void saveFormRecord(String formKey) {
        FormSubmitRecord record = new FormSubmitRecord();
        record.setFormKey(formKey);
        record.setSubmitTime(System.currentTimeMillis());
        formRecordRepository.save(record);
    }
}

使用示例

@RestController
public class UserController {

    @PostMapping("/user/register")
    @RepeatSubmit(interval = 10000) // 10秒内不允许重复提交
    public Result register(@RequestBody UserRegisterForm form) {
        // 注册逻辑
        return userService.register(form);
    }
}

优点:

  • 配置简便,使用注解即可实现
  • 支持配置不同接口的防重复策略
  • 基于表单内容校验,更符合业务语义

局限性:

  • 依赖于请求内容,不适用于所有场景
  • 需要存储请求内容的哈希值
  • 配置不当可能影响用户体验

多层防护策略与最佳实践

        在实际项目中,往往需要综合使用多种防重复点击方案,构建多层防护机制。

前端第一道防线:

  • 实现按钮防抖和禁用
  • 合理设置UI反馈,提升用户体验

API网关层:

  • 实现基本的流量控制和限流
  • 对异常请求进行预警和拦截

应用服务层:

  • 实现token验证或表单校验机制
  • 使用布隆过滤器进行快速过滤

数据持久层:

  • 利用数据库约束和事务保证数据一致性
  • 实现业务层面的幂等性检查

总结        

        ​​​​​​​防止重复点击是一个需要从多角度综合考虑的问题。虽然Redis分布式锁提供了一种优雅的解决方案,但在Redis不可用的场景下,我们仍有多种替代方案可以选择。
        理想的防重复点击方案应当在安全性、可靠性和性能之间找到平衡点。在实际应用中,应根据业务特点、技术栈和性能要求等因素,选择合适的方案或组合方案。同时,也应当注意用户体验,避免过度限制影响正常操作。


http://www.kler.cn/a/599400.html

相关文章:

  • 数据不外传!通过内网穿透实现绿联NAS远程访问的安全配置方案
  • iPaaS集成平台:企业数字化转型的加速器
  • VUE2导出el-table数据为excel并且按字段分多个sheet
  • 大模型技术在商品归一和商品预测中的应用
  • Shiro框架漏洞攻略
  • AI 时代,我们需要什么样的数据库?
  • 【全队项目】智能学术海报生成系统PosterGenius(项目介绍)
  • Resource usage
  • Linux系统还可以在做一层虚拟化安装虚拟机吗
  • Web3与网络安全:如何确保去中心化应用的安全性
  • 使用 ChatGPT 套结构仿写解决写作难题
  • Leetcode刷题笔记1 图论part03
  • 【操作系统安全】任务3:Linux 网络安全实战命令手册
  • 【C语言】多进程/多线程
  • python的文件上传
  • 4、操作系统结构和发展史
  • Ubuntu给appimage创建图标启动
  • Jira story与测试用例自动化关联脚本
  • [已解决]jupyter notebook报错 500 : Internal Server Error及notebook闪退
  • [深度学习]图像分类项目-食物分类