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

相关推荐
葡萄成熟时 !8 小时前
JAVA 常用API学习笔记
java·笔记·学习
DevOpenClub8 小时前
个人公开市场研究笔记功能需求文档
笔记
Rain的Java大神之路9 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
吴声子夜歌9 小时前
Java面试题——基础(一)
java·开发语言
南城以南溫暖如初14710 小时前
外卖CPS实战指南:从技术选型到运营落地全流程解析
java·spring boot·mysql·架构·vue
傻啦嘿哟10 小时前
英雄联盟皮肤爬虫:爬取全皮肤价格与特效,做比价工具
java·c++·爬虫
码匠许师傅11 小时前
【C++ 面试真题】24. 聊聊 C++ 的时间日期处理
java·c++·面试
mqiqe12 小时前
AgentScope Java 2.0 Agent 状态存储(AgentStateStore)深度解析:构建可恢复、可扩展的智能体运行时
java·运维·网络
zhifou12345612 小时前
java 17升级安装
java·开发语言
用户37215742613512 小时前
如何使用 Java 将 Markdown 转换为 PDF(含自定义设置)
java