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

霸王餐券预扣库存场景下,如何防止同一用户多设备并发“薅羊毛”

霸王餐券预扣库存场景下,如何防止同一用户多设备并发“薅羊毛”

在“霸王餐”等高价值营销活动中,黑产团伙常利用脚本控制同一用户的多个设备或会话,发起高并发请求试图突破“每人限领一张”的限制。传统的数据库唯一索引校验在高并发下会导致大量锁竞争,严重影响性能;而简单的 Redis 键值检查若缺乏原子性保障,极易在“查 - 判 - 写”的时间窗口内被并发请求绕过,导致库存超发和用户重复领取。本文将探讨如何利用 Redis Lua 脚本结合预扣库存机制,构建一道坚不可摧的防并发“薅羊毛”防线。

并发漏洞分析与防御核心

在多设备并发场景下,攻击者会在毫秒级时间内发送数十个请求。若系统逻辑为:先查询 Redis 中是否存在用户领取记录,若不存在则扣减库存并写入记录。此时,两个并发请求可能同时通过“不存在记录”的检查,进而同时执行扣减和写入操作,导致用户成功领取多张券。

防御的核心在于将“资格校验”、“库存预扣”和“记录写入”这三个动作合并为一个原子操作。Redis 的 Lua 脚本机制允许我们在服务端执行一段完整的逻辑,期间不会被其他命令插入,从而彻底消除竞态条件。此外,引入“预扣库存”状态,可以在用户未完成最终下单前暂时锁定库存,防止资源被无效占用,同时确保并发请求只能有一个成功进入预扣状态。

基于 Lua 脚本的原子防重实现

我们设计一段 Lua 脚本,入参包括活动 ID、用户 ID 和设备指纹。脚本内部首先校验用户是否已领取(包括已完成和预扣状态),若未领取则尝试扣减活动总库存。若库存充足,则写入预扣记录并设置过期时间(防止用户放弃支付导致库存死锁),返回成功标记;否则返回相应的失败码。

-- KEYS[1]: 用户领取记录 Key (user:claim:activityId)-- KEYS[2]: 活动库存 Key (stock:activityId)-- ARGV[1]: 用户唯一标识 (userId)-- ARGV[2]: 设备指纹 (deviceId)-- ARGV[3]: 预扣过期时间 (ttlSeconds)-- 返回值: 1-成功, 0-库存不足, 2-重复领取, 3-系统异常localuserRecordKey=KEYS[1]localstockKey=KEYS[2]localuserId=ARGV[1]localdeviceId=ARGV[2]localttl=tonumber(ARGV[3])-- 1. 检查用户是否已领取或正在预扣中-- 只要 Key 存在,无论状态如何,都视为该用户已占用名额ifredis.call('EXISTS',userRecordKey..':'..userId)==1thenreturn2end-- 2. 获取当前剩余库存localcurrentStock=tonumber(redis.call('GET',stockKey))ifnotcurrentStockorcurrentStock<=0thenreturn0end-- 3. 原子扣减库存redis.call('DECR',stockKey)-- 4. 写入预扣记录,包含设备指纹用于审计,并设置 TTL-- 格式:userId:deviceId:timestamplocalrecordValue=userId..':'..deviceId..':'..os.time()redis.call('SET',userRecordKey..':'..userId,recordValue,'EX',ttl)return1

Java 服务层集成与异常处理

在 Java 端,我们需要封装 RedisTemplate 调用上述 Lua 脚本,并根据返回值进行精确的业务逻辑分支处理。包名严格遵循com.baodanbao.com.cn规范。

packagecom.baodanbao.com.cn.coupon.service;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importcom.baodanbao.com.cn.coupon.exception.CouponClaimException;importcom.baodanbao.com.cn.coupon.model.ClaimResult;importjava.util.Arrays;importjava.util.Collections;importjava.util.List;@ServicepublicclassAntiFraudClaimService{privatefinalStringRedisTemplateredisTemplate;privatefinalDefaultRedisScript<Long>claimScript;publicAntiFraudClaimService(StringRedisTemplateredisTemplate){this.redisTemplate=redisTemplate;this.claimScript=newDefaultRedisScript<>(loadLuaScript(),Long.class);}/** * 执行防并发领取逻辑 * @param activityId 活动ID * @param userId 用户ID * @param deviceId 设备指纹 * @return 领取结果 */publicClaimResulttryClaimCoupon(StringactivityId,StringuserId,StringdeviceId){StringuserRecordKey="user:claim:"+activityId;StringstockKey="stock:"+activityId;List<String>keys=Arrays.asList(userRecordKey,stockKey);// 预扣库存过期时间设为 15 分钟,用户需在此期间完成下单,否则库存释放intttlSeconds=900;try{LongresultCode=redisTemplate.execute(claimScript,keys,userId,deviceId,String.valueOf(ttlSeconds));if(resultCode==1){// 成功:生成待支付订单,异步通知下游returnnewClaimResult(true,"预扣成功,请尽快下单");}elseif(resultCode==2){// 重复领取:可能是同一用户多设备并发,或之前已领取thrownewCouponClaimException("您已领取过该优惠券,请勿重复操作");}elseif(resultCode==0){// 库存不足thrownewCouponClaimException("手慢了,优惠券已被抢光");}else{thrownewCouponClaimException("系统繁忙,请稍后重试");}}catch(Exceptione){// 记录日志,抛出统一业务异常thrownewCouponClaimException("领取失败:"+e.getMessage());}}privateStringloadLuaScript(){// 实际项目中应从文件加载,此处为简化直接嵌入return"local userRecordKey = KEYS[1] "+"local stockKey = KEYS[2] "+"local userId = ARGV[1] "+"local deviceId = ARGV[2] "+"local ttl = tonumber(ARGV[3]) "+"if redis.call('EXISTS', userRecordKey .. ':' .. userId) == 1 then return 2 end "+"local currentStock = tonumber(redis.call('GET', stockKey)) "+"if not currentStock or currentStock <= 0 then return 0 end "+"redis.call('DECR', stockKey) "+"redis.call('SET', userRecordKey .. ':' .. userId, userId .. ':' .. deviceId .. ':' .. os.time(), 'EX', ttl) "+"return 1";}}

后续流程与库存回补

当用户成功预扣后,系统生成一个状态为“待支付”的订单。若用户在 TTL 时间内完成支付,则将预扣记录转为永久领取记录,并触发真正的库存扣减确认(此时 Redis 库存已预减,DB 做最终一致性核对)。若超时未支付,通过 Redis Key 过期监听或定时任务扫描,执行库存回补逻辑:删除预扣记录并将活动库存INCR

此方案利用 Lua 脚本的原子性,将并发冲突控制在 Redis 单线程模型内解决,彻底杜绝了多设备并发导致的重复领取问题。同时,预扣机制有效平衡了用户体验与库存安全,即使在高并发流量下也能保持系统的稳定与公平。

本文著作权归 俱美开放平台 ,转载请注明出处!

http://www.cnnetsun.cn/news/1261954.html

相关文章:

  • 力扣解题-200. 岛屿数量
  • 国内代理 IP 的类型:住宅 IP、机房 IP、移动 4G/5G IP 区别
  • AI 内容导出乱、格式崩、公式变?我开发了这只鸭子帮我全解决了
  • 智能楼道灯控制器(有完整资料)
  • AI安全应用实战复盘:一场2小时的深度交流,我们聊了什么?
  • esp网络时钟
  • 【无线通信】基于matlab统计信道的低复杂度旋转和位置优化为6D可移动天线无线通信【含Matlab源码 15160期】
  • 2026年,一周就能背完的web前端八股文!(春招最新附答案)!
  • 根据文章举报数量确定推荐
  • NineData 社区版 V4.10.0 发布:新增 9 条异构数据库复制链路,DevOps 能力再升级!
  • Redis知识点
  • 简单 的部分背包问题(贪心算法)
  • Kmeans聚类与肘部法确定聚类个数:Matlab实战
  • COMSOL 激光烧蚀3D体热源引力场温度场仿真探索
  • 使用Springboot实现MQTT通信
  • 多智能体事件触发一致性
  • 类内部方法内 写创建对象
  • GraphRAG 完全指南
  • 非计算机专业如何顺利转行网络安全领域?_如何转行网络安全工程师
  • 基于自适应PI的构网型变流器预同步控制策略复现报告
  • 【跨模态一致性增强模块+开源代码】定位与验证:一个提升深度伪造检测的双流网络
  • 如何从bode图得到单位阶跃的趋势
  • 大数据领域 ETL 中的元数据管理策略
  • 企业微信外部客户群自动化管理:建群+群发+踢人一体化
  • 2200 行 Python 桌面应用拆解:架构设计与关键实现细节(周报收取统计)
  • 开源OpenClaw部署指南
  • 广州信号灯厂家售后
  • 2026 年数据库变更审批工具怎么选?NineData 凭什么排进第一梯队
  • 硬核!高精度高可靠性位置检测解决方案 | 编码器/电感式/方案分享
  • 鱼眼相机标定矫正详细步骤