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` + 过期时间

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

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

相关推荐
凯子坚持 c10 分钟前
CANN 生态中的分布式训练利器:深入 `collective-ops` 项目实现高效多卡协同
分布式
泉-java12 分钟前
第56条:为所有导出的API元素编写文档注释 《Effective Java》
java·开发语言
zfoo-framework40 分钟前
帧同步和状态同步
java
charlotte1024102442 分钟前
高并发:关于在等待学校教务系统选课时的碎碎念
java·运维·网络
亓才孓1 小时前
[JDBC]PreparedStatement替代Statement
java·数据库
觉醒大王1 小时前
哪些文章会被我拒稿?
论文阅读·笔记·深度学习·考研·自然语言处理·html·学习方法
方安乐1 小时前
科普:股票 vs 债券的区别
笔记
_F_y1 小时前
C++重点知识总结
java·jvm·c++
打工的小王1 小时前
Spring Boot(三)Spring Boot整合SpringMVC
java·spring boot·后端
毕设源码-赖学姐1 小时前
【开题答辩全过程】以 高校体育场馆管理系统为例,包含答辩的问题和答案
java·spring boot