一、核心概念辨析
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不混用(除非必要)