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不混用(除非必要)

相关推荐
计算机毕设指导62 小时前
基于微信小程序的派出所业务管理系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·uniapp
程可爱2 小时前
详解Redis 中 RDB 与 AOF 的区别
redis
皮秒亿2 小时前
谷歌浏览器在线升级指南
笔记
d111111111d2 小时前
在stm32中什么是hal库,什么是标准库,二者的区别?
笔记·stm32·单片机·嵌入式硬件·学习
ohoy2 小时前
Xxl-Job实现订单30分钟未支付自动取消
java
明洞日记2 小时前
【设计模式手册022】抽象工厂模式 - 创建产品家族
java·设计模式·抽象工厂模式
用户8307196840822 小时前
Spring Boot 多数据源与事务管理深度解析:从原理到实践
java·spring boot
Yiii_x2 小时前
基于多线程机制的技术应用与性能优化
java·经验分享·笔记
uup2 小时前
包装类的 “缓存陷阱”:Integer.valueOf (128) == 128 为何为 false?
java