Skip to content

不用Redis分布式锁,如何防止用户重复点击?

在不使用Redis分布式锁的情况下,防止用户重复点击需要结合 前端防重、后端幂等性设计、数据库约束 等多种手段。以下是系统性解决方案:


1. 前端防重(客户端层)

  • 按钮防抖(Debounce)
  • 点击后禁用按钮(如置灰),并在一定时间(如2秒)后恢复,防止用户连续点击。
    javascript
    // Vue示例:使用lodash防抖
    <button @click="debouncedSubmit">提交</button>
    methods: {
      debouncedSubmit: _.debounce(function() {
        this.submitForm();
      }, 2000, { leading: true, trailing: false })
    }
  • 请求指纹(Request Fingerprint)
    为每次请求生成唯一标识(如时间戳+随机数),前端缓存已发送的指纹,重复指纹直接拦截。
    javascript
    const requestId = Date.now() + Math.random().toString(36).substr(2);
    if (localStorage.getItem(requestId)) return;
    localStorage.setItem(requestId, 'pending');

2. 后端幂等性设计(服务端层)

  • 唯一请求ID(Idempotency Key)
    客户端在请求头或参数中携带唯一ID(如UUID),服务端校验该ID是否已处理过。
    java
    // 示例:Spring Boot接口幂等性校验
    @PostMapping("/submit")
    public Response submit(@RequestHeader("Idempotency-Key") String key) {
        if (cache.contains(key)) {  // 使用本地缓存或数据库
            return Response.duplicate();  // 返回"请求已处理"
        }
        cache.add(key, EXPIRATION_SECONDS);
        // 处理业务逻辑
    }
  • 数据库唯一约束
    利用数据库唯一索引,确保同一业务唯一标识(如订单号)仅能插入一次。
    sql
    CREATE TABLE orders (
      id INT PRIMARY KEY,
      order_no VARCHAR(64) UNIQUE,  -- 唯一订单号
      ...
    );
    • 插入时若触发唯一键冲突,直接返回错误或返回已存在的订单数据。

3. 数据库锁与状态机(数据层)

  • 乐观锁(CAS机制)
    基于版本号或状态字段,确保数据更新时的原子性。
    sql
    UPDATE account 
    SET balance = balance - 100, version = version + 1 
    WHERE user_id = 123 AND version = 1;  -- 版本号校验
    • 若影响行数为0,说明数据已被修改,需提示用户重新操作。
  • 状态机约束
    定义业务状态流转规则(如"待支付 → 已支付"),拒绝非法状态变更。
    java
    public void payOrder(String orderId) {
        Order order = orderDao.findById(orderId);
        if (!order.getStatus().equals(OrderStatus.PENDING)) {
            throw new IllegalStateException("订单已处理");
        }
        order.setStatus(OrderStatus.PAID);
        orderDao.update(order);
    }

4. 请求限流与熔断(系统层)

  • 滑动窗口限流
    针对用户或接口维度限制单位时间内的请求次数(如5秒内仅允许1次)。
    java
    // 使用Guava RateLimiter
    RateLimiter limiter = RateLimiter.create(0.2);  // 每秒0.2次(5秒1次)
    if (limiter.tryAcquire()) {
        processRequest();
    } else {
        throw new RateLimitException();
    }
  • 熔断降级
    当短时间内重复请求超过阈值,触发熔断机制(如Hystrix),直接拒绝后续请求。
    java
    @HystrixCommand(
      fallbackMethod = "fallback",
      commandProperties = {
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="3")  // 3次失败触发熔断
      }
    )

5. 本地锁与队列(单机环境)

  • JVM本地锁
    在单机部署时,使用synchronizedReentrantLock锁定用户ID或业务键。
    java
    private static final Map<String, Object> userLocks = new ConcurrentHashMap<>();
    
    public void submitOrder(String userId) {
        Object lock = userLocks.computeIfAbsent(userId, k -> new Object());
        synchronized (lock) {  // 锁定当前用户
            // 处理业务逻辑
        }
    }
  • 内存队列串行化
    将请求按用户ID哈希到内存队列,单线程消费,避免并发冲突。
    java
    private static final Map<String, LinkedBlockingQueue<Runnable>> queues = new ConcurrentHashMap<>();
    
    public void submitRequest(String userId, Runnable task) {
        LinkedBlockingQueue<Runnable> queue = queues.computeIfAbsent(userId, k -> new LinkedBlockingQueue<>());
        queue.offer(task);
        // 单线程消费队列
        new Thread(() -> {
            while (!queue.isEmpty()) {
                queue.poll().run();
            }
        }).start();
    }

6. 实际应用示例

  • 场景:用户提交订单时因网络延迟重复点击。
  • 解决方案组合
    1. 前端:按钮点击后置灰2秒。
    2. 后端:生成唯一订单号,利用数据库唯一索引防重。
    3. 限流:同一用户5秒内仅允许1次提交。
    4. 结果:首次请求成功,后续重复请求被前端拦截或后端幂等校验拒绝。

总结

不用Redis分布式锁时,可通过以下组合方案防止重复点击:

  1. 前端防抖 + 请求指纹减少无效请求。
  2. 幂等性设计(唯一ID、数据库唯一键)确保业务唯一性。
  3. 乐观锁/状态机保证数据一致性。
  4. 限流熔断防止系统过载。
  5. 单机锁/队列在非分布式场景下简化实现。
    根据业务场景选择合适方案(如高并发优先限流+幂等性,低频场景可用数据库唯一键)。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我