南平市网站建设_网站建设公司_服务器部署_seo优化
2025/12/24 2:17:09 网站建设 项目流程

一、核心概念辨析

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 组件选型

需求场景RedisTemplateRedissonClient推荐理由
防重复点击⭐⭐⭐⭐⭐⭐⭐⭐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不混用(除非必要)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询