当前位置: 首页 > news >正文

Redis防重复点击与分布式锁

在生产环境中,我们经常会遇到两个需求:

  1. 限制用户在N秒内不能重复操作(如连续点击导出按钮)

  2. 确保同一时间只有一个线程能操作共享资源(如扣减库存)

很多开发者习惯用Redisson的RLock来解决这两个问题,但这其实是一种语义错位。今天我们来聊聊为什么"防重复点击"不应该用分布式锁。


一、防重复点击:设置一个"冷却标记"

1.1 业务本质

防重复点击的核心需求是:在用户操作后,设置一个N秒后自动消失的"冷却标记"。这个时间与业务执行时长无关,纯粹是业务规则限制。

1.2 正确实现(RedisTemplate)

@Service public class DuplicateCheckService { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 防重复点击检查 * @param bizType 业务类型(如export、submit) * @param userId 用户ID * @param cooldown 冷却时间(秒) */ public void checkDuplicate(String bizType, Long userId, long cooldown) { String key = String.format("duplicate:%s:%d", bizType, userId); // 核心:SET NX EX - 不存在才设置,并自动过期 Boolean success = redisTemplate.opsForValue() .setIfAbsent(key, "1", cooldown, TimeUnit.SECONDS); if (Boolean.FALSE.equals(success)) { Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS); throw new BusinessException(ttl + "秒内不可重复操作"); } } }

关键点

  • setIfAbsent= Redis的SET NX命令,原子性判断+设置

  • 自动过期:Redis会在cooldown秒后删除Key,无需手动清理

  • 无锁竞争:失败时直接返回,不等待

1.3 使用示例

@Transactional public Result exportData(ExportRequest request, User user) { // 检查60秒内是否重复点击 duplicateCheckService.checkDuplicate("export", user.getId(), 60); // 执行导出逻辑(在事务内) return doExport(request); }

二、分布式锁:确保"排他性访问"

2.1 业务本质

分布式锁的核心需求是:保护共享资源,确保同一时间只有一个线程能修改它。必须手动释放锁,否则会造成死锁。

2.2 正确实现(Redisson)

@Service public class DistributedLockService { @Autowired private RedissonClient redissonClient; /** * 带锁执行业务逻辑 * @param lockKey 锁标识 * @param waitTime 获取锁最大等待时间(秒) * @param leaseTime 锁自动释放时间(秒,防死锁) */ public void executeWithLock(String lockKey, long waitTime, long leaseTime, Runnable businessLogic) { RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁(可重入) if (!lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) { throw new BusinessException("获取锁失败,请稍后重试"); } // 执行业务逻辑 businessLogic.run(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException("操作被中断"); } finally { // ⚠️ 必须手动释放! if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }

关键点

  • tryLock:尝试获取,失败时等待

  • 手动释放:必须在finallyunlock(),否则死锁

  • 看门狗:未指定leaseTime时会自动续期

2.3 使用示例

public void deductStock(Long productId, int quantity) { String lockKey = "stock:" + productId; // 保护库存扣减操作 lockService.executeWithLock(lockKey, 3, 10, () -> { int stock = getStockFromDB(productId); if (stock < quantity) { throw new BusinessException("库存不足"); } updateStock(productId, stock - quantity); }); }

三、核心区别对比

对比维度防重复点击分布式锁
业务语义冷却计时器(N秒后自动解除)互斥信号(必须手动释放)
Redis命令SET key value NX EX secondsSET key value+ 续期+手动DEL
生命周期自动过期(与业务无关)手动释放(与业务强相关)
失败策略直接拒绝(不等待)可选等待或失败
性能极高(单次O(1)操作)较高(有竞争开销)
代码复杂度极低(3行代码)较高(try-finally+异常处理)
事务兼容性✅ 完美兼容(无状态)⚠️ 需分离锁与事务
适用场景防重、限流、短信冷却库存扣减、并发写文件

四、致命误区:用锁实现防重复点击

❌ 错误代码(最常见)

// 误区1:finally立即释放(锁无效) @Transactional public Result export() { lock.tryLock(0, 60, TimeUnit.SECONDS); try { return doExport(); } finally { lock.unlock(); // ⚠️ 业务还没结束,锁就没了 } } // 误区2:不释放等过期(用户体验差) public Result export() { lock.tryLock(0, 60, TimeUnit.SECONDS); return doExport(); // ⚠️ 导出5秒完成,用户必须等55秒 } // 误区3:与事务冲突 @Transactional public Result export() { lock.lock(); // 事务提交前释放锁 → 脏读 // 事务回滚后释放锁 → 锁已失效 return doExport(); }

问题根源:分布式锁的生命周期必须人为控制,而防重复点击需要的是"设置后不管"的临时标记。


五、决策指南:何时用哪个?

5.1 选择流程图

需求:限制操作频率 ↓ 是"限制同一用户N秒内不能重复操作"? ↓ 是 使用 RedisTemplate.setIfAbsent()(防重复点击) ↓ 否 → 是"保护共享资源,防止并发修改"? ↓ 是 使用 Redisson RLock(分布式锁) ↓ 否 → 其他方案(如限流器RateLimiter)

5.2 一句话总结

当你想"限制用户在N秒内不能操作"时,用带过期时间的标记;当你想"确保只有一个线程能操作"时,才用分布式锁。


六、生产环境最佳实践

6.1 Key设计规范

// 防重复点击:用户级粒度 String key = "duplicate:export:" + userId; // 分布式锁:资源级粒度 String key = "lock:stock:" + productId;

6.2 冷却时间设置建议

  • 导出类:60-300秒(防止频繁生成大文件)

  • 提交类:5-10秒(防止表单重复提交)

  • 短信类:60秒(运营商普遍限制)

6.3 锁时长设置建议

  • leaseTime:必须大于业务最大执行时间

  • waitTime:根据业务容忍度设置,避免长时间阻塞


七、总结

防重复点击和分布式锁是两种完全不同的语义,但开发者常因"都用到Redis"而混用。记住:

  • 防重复点击 = 冷却计时器:用SET NX EX,自动过期,无需释放

  • 分布式锁 = 互斥信号:用Redisson RLock,手动释放,保护资源

选错工具不仅代码复杂,还会引入死锁、性能下降、用户体验差等隐患。希望这篇文章能帮你避开这个90%开发者都踩过的坑。

http://www.jsqmd.com/news/131363/

相关文章:

  • H7-TOOL隔离再出二代升级版,支持UART和SWD接口隔离,支持由TOOL或者目标板为隔离侧供电
  • 7、打造魅力应用:搜索与筛选功能全解析
  • 【期末复习03】单选+判断作业
  • 金属废料再造优质3D打印粉末,中体新材已打造行业应用的标杆
  • 8、打造迷人应用:共享与设置功能全解析
  • 虚拟主播后台支持:直播内容即时应答
  • 20、Windows Server 备份与恢复全攻略
  • 5、使用用户组组织用户账户的全面指南
  • 51、系统错误与崩溃问题的排查与解决
  • 1、Windows 8 开发入门与环境搭建
  • Java SpringBoot+Vue3+MyBatis 高校就业招聘系统系统源码|前后端分离+MySQL数据库
  • 2025年中间件厂商品牌全景报告:趋势、评估与选型指南
  • 飞象说口语剑桥评分报告:破解KP备考与教学痛点,提分与效率双提升
  • 21、服务器备份与恢复操作指南
  • Multisim仿真电路图实例中OTL功放的完整示例
  • 跨部门知识共享平台:打破企业信息壁垒
  • 三极管在电源控制中的角色:操作指南与安全设计
  • 2、Windows 8 开发项目模板与模拟器使用指南
  • 19、深入理解组策略管理与应用
  • 52、软件故障与性能问题排查案例解析
  • 3、构建首个项目全攻略
  • Vivado 2019.1安装空间与依赖要求解析
  • 电源完整性在PCB布局中的实现:深度剖析
  • 4、构建首个项目:XAML 应用开发与 MVVM 模式实战
  • 设备操作手册查询:一线工人随身AI指导员
  • Keil5安装教程详细步骤:项目应用导向的环境部署方案
  • 基于SpringBoot+Vue的和BS架构宠物健康咨询系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】
  • 22、Windows Server 2012:备份恢复与高级文件服务指南
  • 5、Windows应用开发中的数据处理与SQLite应用实践
  • 亲测2025年主流AI漫剧工具:帮作者打通小说转漫剧链路