Redis防重复点击与分布式锁实现方案对比笔记

一、核心概念辨析

1.1 业务场景本质

场景 核心需求 正确类比
防重复点击 设置临时冷却标记,N秒内禁止重复操作 计时器(N秒后自动解除)
分布式锁 排他性资源访问,同一时间只允许一个线程操作 互斥信号量(手动释放)

1.2 技术选型对比

组件 抽象层次 适用场景 依赖
RedisTemplate 底层命令操作 防重复点击(推荐) Spring Data Redis
RedissonClient 高级分布式对象 分布式锁(推荐)、防重复点击(可用) Redisson

二、防重复点击实现方案

2.1 RedisTemplate实现(推荐⭐⭐⭐⭐⭐)

复制代码
@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
 * 防重复点击 - Redis标记方案
 * @param key 业务唯一标识
 * @param cooldownSeconds 冷却时间(秒)
 */
public void checkDuplicateRequest(String key, long cooldownSeconds) {
    Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", cooldownSeconds, TimeUnit.SECONDS);
    
    if (Boolean.FALSE.equals(success)) {
        Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        throw new BusinessException(ttl + "秒内不可重复操作");
    }
}

// 使用示例
public Result exportData(User user) {
    String key = "export:" + user.getId();
    checkDuplicateRequest(key, 60L); // 60秒内禁止重复导出
    // 执行导出逻辑...
}

✅ 优点

  • 语义精准:SET NX EX 完美匹配"冷却"需求

  • 自动过期:无需手动清理

  • 性能最优:单次Redis操作

  • 无死锁风险

  • 与事务完美兼容

2.2 RedissonClient实现

复制代码
@Autowired
private RedissonClient redissonClient;

/**
 * 防重复点击 - Redisson RBucket方案
 * @param key 业务唯一标识
 * @param cooldownSeconds 冷却时间(秒)
 */
public void checkDuplicateRequest(String key, long cooldownSeconds) {
    RBucket<String> bucket = redissonClient.getBucket(key);
    boolean success = bucket.trySet("1", cooldownSeconds, TimeUnit.SECONDS);
    
    if (!success) {
        long ttl = bucket.remainTimeToLive() / 1000;
        throw new BusinessException(ttl + "秒内不可重复操作");
    }
}

// 使用示例
public Result exportData(User user) {
    String key = "export:" + user.getId();
    checkDuplicateRequest(key, 60L);
    // 执行导出逻辑(注意:不要在finally中释放)
}

⚠️ 注意 :虽然可用,但Redisson的RBucket看门狗机制可能导致行为不可控,不推荐


三、分布式锁实现方案

3.1 典型场景(必须使用锁)

  • 库存扣减

  • 并发写同一文件

  • 分布式任务调度

  • 缓存重建防击穿

3.2 RedissonClient实现(推荐⭐⭐⭐⭐⭐)

复制代码
@Autowired
private RedissonClient redissonClient;

/**
 * 分布式锁执行模板
 * @param key 锁标识
 * @param waitTime 获取锁最大等待时间(秒)
 * @param leaseTime 锁自动释放时间(秒)
 */
public <T> T executeWithLock(String key, long waitTime, long leaseTime, 
                             Supplier<T> businessLogic) {
    RLock lock = redissonClient.getLock(key);
    boolean isLocked = false;
    try {
        // 尝试获取锁
        isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        if (!isLocked) {
            throw new BusinessException("获取锁失败,请稍后重试");
        }
        
        // 执行业务逻辑
        return businessLogic.get();
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new BusinessException("操作被中断");
    } finally {
        // 必须手动释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

// 使用示例:库存扣减
public void deductStock(Long productId, int quantity) {
    String lockKey = "stock:" + productId;
    
    executeWithLock(lockKey, 3L, 10L, () -> {
        // 查询库存
        int stock = getStockFromDB(productId);
        if (stock < quantity) {
            throw new BusinessException("库存不足");
        }
        // 扣减库存
        updateStock(productId, stock - quantity);
        return null;
    });
}

✅ 优点

  • 可重入锁:同一线程可多次获取

  • 看门狗机制:自动续期防死锁

  • 公平锁/非公平锁可选

  • 支持RedLock算法

3.3 RedisTemplate实现(不推荐)

复制代码
// ❌ 不推荐:需自己处理死锁、续期、可重入等复杂逻辑
public boolean tryLock(String key, String value, long expireTime) {
    Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

public void unlock(String key, String value) {
    // 需用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(key), value);
}

四、关键对比总结

4.1 防重复点击 vs 分布式锁

维度 防重复点击 分布式锁
核心语义 冷却计时器 互斥访问
生命周期 自动过期(无需手动) 必须手动释放
性能 极高(单次操作) 较高(需竞争)
代码复杂度 极低(3行) 较高(try-finally)
事务兼容性 ✅ 完美 ⚠️ 需分离锁与事务
适用场景 防重、限流 资源竞争、排他操作

4.2 组件选型

需求场景 RedisTemplate RedissonClient 推荐理由
防重复点击 ⭐⭐⭐⭐⭐ ⭐⭐⭐ Template语义更直接,无看门狗干扰
分布式锁 ⭐⭐ ⭐⭐⭐⭐⭐ Redisson提供完整锁实现,无需造轮子
复杂数据结构 ⭐⭐⭐ ⭐⭐⭐⭐⭐ Redisson封装了RMap、RQueue等高级对象

五、最佳实践建议

5.1 防重复点击(最终版)

复制代码
@Service
public class DuplicateCheckService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 通用防重检查
     * @param bizType 业务类型(如:export、submit)
     * @param userId 用户ID
     * @param cooldown 冷却时间(秒)
     */
    public void check(String bizType, Long userId, long cooldown) {
        String key = String.format("duplicate:%s:%d", bizType, userId);
        
        Boolean flag = redisTemplate.opsForValue()
                .setIfAbsent(key, "1", cooldown, TimeUnit.SECONDS);
        
        if (Boolean.FALSE.equals(flag)) {
            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            throw new BusinessException(String.format("操作太频繁,请%d秒后再试", ttl));
        }
    }
}

5.2 分布式锁(最终版)

复制代码
@Service
public class DistributedLockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 带锁执行业务逻辑
     * @param lockKey 锁Key
     * @param businessLogic 业务逻辑(无返回值)
     */
    public void execute(String lockKey, Runnable businessLogic) {
        execute(lockKey, 3L, 10L, () -> {
            businessLogic.run();
            return null;
        });
    }
    
    /**
     * 带锁执行业务逻辑(带返回值)
     */
    public <T> T execute(String lockKey, long waitTime, long leaseTime, 
                         Supplier<T> businessLogic) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (!lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }
            return businessLogic.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("操作中断");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

六、常见陷阱与避坑指南

❌ 陷阱1:用锁实现防重复点击

复制代码
// 错误!
lock.tryLock(0, 60, SECONDS);
// 不释放 → 看门狗续期永不释放
// finally释放 → 锁无效

❌ 陷阱2:锁与事务范围错误

复制代码
@Transactional
public void method() {
    lock.lock();  // 事务提交前释放锁 → 脏读
    // ...
}
// 正确:锁范围 > 事务范围

❌ 陷阱3:锁Key粒度错误

复制代码
// 租户级Key(误锁所有用户)
"export:" + tenantId

// 用户级Key(正确)
"export:" + userId

✅ 检查清单

  • 防重复点击用setIfAbsent + 过期时间

  • 分布式锁必须try-finally释放

  • 锁范围必须大于事务范围

  • Key粒度确认是用户级而非租户级

  • RedisTemplate和Redisson不混用(除非必要)

相关推荐
长谷深风1111 分钟前
Agent 跑了 30 分钟宕机,如何从断点继续?
java·大数据·人工智能·ai·大模型·memory·aiagent
慧都小妮子1 分钟前
DevExpress Java 文档处理 API 免费 CTP:PDF、PowerPoint、条码与跨平台部署
java·pdf·powerpoint·devexpress·文档处理·条码生成
泡海椒4 分钟前
告别反射低效:JQuick-Java ASM动态调用链性能优化实战
java·python·性能优化
土司大王14 分钟前
LeetCode 17 电话号码的字母组合:Java 回溯模板、多叉决策树与复杂度分析
java·leetcode·决策树
liaozhilong8815 分钟前
【技术笔记】让CodeMaker自动分析dump
笔记
自小吃多17 分钟前
PCB布线叠层与阻抗设计笔记
笔记·嵌入式硬件
CoderYanger27 分钟前
前端基础——JavaScript(WebAPI)代码案例
java·开发语言·前端·javascript·css·前端框架·html5
Nuanyt38 分钟前
JUC常见核心知识梳理02 AQS ReentrantLock CAS 原子类 线程池
java
CodeStats41 分钟前
【Java数据类型】Java底层数据类型原理(上):基本类型、转换与封装核心
java·数据类型·string·封装·转换
陈皮波比茶1 小时前
SpringCloudAlibaba
java·spring cloud·微服务