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

相关推荐
tb_first1 分钟前
SSM速通4
java·jvm·spring·tomcat·maven·mybatis
百炼成神 LV@菜哥16 分钟前
Kylin Linux V10 aarch64安装DBeaver
java·linux·服务器·kylin
有代理ip31 分钟前
成功请求的密码:HTTP 2 开头响应码深度解析
java·大数据·python·算法·php
呱呱巨基37 分钟前
c语言 文件操作
c语言·开发语言·c++·笔记·学习
好好沉淀1 小时前
ES 脚本核心语法:ctx._source [‘group_id‘]
java·elasticsearch·script
李慕婉学姐1 小时前
【开题答辩过程】以《基于Spring Boot的疗养院理疗管理系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·spring boot·后端
tb_first1 小时前
SSM速通2
java·javascript·后端
qq_12498707531 小时前
基于协同过滤算法的运动场馆服务平台设计与实现(源码+论文+部署+安装)
java·大数据·数据库·人工智能·spring boot·毕业设计·计算机毕业设计
大飞哥~BigFei1 小时前
自定义注解记录接口切面log日志入库优化
java
人道领域1 小时前
javaWeb从入门到进阶(maven高级进阶)
java·spring·maven