优惠券app的优惠券分发限流与Redis原子操作实现
优惠券app的优惠券分发限流与Redis原子操作实现大家好我是省赚客APP研发者微赚淘客在优惠券App的运营活动中秒杀、限时抢券等高并发场景是家常便饭。当海量用户在同一时刻涌入试图领取数量有限的优惠券时系统将面临巨大的压力。如果处理不当不仅会导致优惠券被超发造成资损还可能因数据库压力过大而引发服务雪崩。本文将深入探讨如何利用Redis的原子操作构建一个高效、可靠的优惠券分发与限流系统确保在高并发下数据的一致性与服务的稳定性。一、问题剖析高并发下的超发与性能瓶颈优惠券分发的核心逻辑看似简单检查优惠券是否还有剩余。如果有则为用户创建一张优惠券并将剩余数量减一。在单机或低并发环境下这段逻辑没有问题。但在高并发场景下它会引发两个致命问题1. 超发问题假设有100张优惠券剩余1张。此时用户A和用户B同时发起领取请求。请求A和请求B几乎同时到达服务器并都执行了“检查剩余数量”的操作。由于操作尚未完成两个请求读取到的剩余数量都是1。两个请求都认为还有库存于是都继续执行“创建优惠券”和“库存减一”的操作。最终1张优惠券被发给了2个用户造成了超发。这就是典型的“检查后执行”Check-Then-Act竞态条件。2. 数据库性能瓶颈上述的检查和更新操作通常会涉及数据库的读写。在高并发下大量的请求会直接冲击数据库导致连接池耗尽、查询缓慢最终拖垮整个服务。二、解决方案Redis原子操作与Lua脚本解决上述问题的关键在于将“检查”和“执行”这两个非原子操作合并为一个原子操作。Redis凭借其单线程的事件处理模型和强大的原子命令成为了解决此问题的理想选择。1. 核心思想我们将优惠券的库存信息缓存在Redis中。用户的领券请求不再直接访问数据库而是先在Redis中执行一个原子性的扣减操作。只有当Redis中的扣减成功后才异步地去数据库中创建优惠券记录。2. 为什么选择Lua脚本虽然Redis的DECR、INCR等命令是原子的但“检查库存是否大于0然后减1”是一个复合逻辑需要多条命令才能完成。为了保证这个复合逻辑的原子性我们使用Redis的Lua脚本功能。Redis在执行Lua脚本时会将整个脚本作为一个整体来执行期间不会被其他命令打断。这完美地满足了我们的需求。三、核心代码实现1. 优惠券库存的Redis操作服务packagejuwatech.cn.coupon.service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importjava.util.Collections;importjava.util.List;/** * 优惠券Redis操作服务负责处理高并发下的库存扣减。 * author juwatech.cn */ServicepublicclassCouponRedisService{AutowiredprivateStringRedisTemplateredisTemplate;// 定义Lua脚本实现“检查并扣减”的原子操作privatestaticfinalStringDEDUCT_STOCK_LUA_SCRIPTlocal stock tonumber(redis.call(GET, KEYS[1])) if not stock then return -1 // 优惠券不存在end if stock 0 then return 0 // 库存不足end redis.call(DECR, KEYS[1]) return 1 ;// 扣减成功privatefinalDefaultRedisScriptLongdeductStockScript;publicCouponRedisService(){deductStockScriptnewDefaultRedisScript();deductStockScript.setScriptText(DEDUCT_STOCK_LUA_SCRIPT);deductStockScript.setResultType(Long.class);}/** * 尝试扣减指定优惠券的库存。 * param couponKey Redis中存储优惠券库存的Key * return 1: 扣减成功, 0: 库存不足, -1: 优惠券不存在 */publiclongtryDeductStock(StringcouponKey){// 执行Lua脚本KEYS[1]就是couponKeyLongresultredisTemplate.execute(deductStockScript,Collections.singletonList(couponKey));returnresultnull?-1:result;}}2. 优惠券领取的业务逻辑服务packagejuwatech.cn.coupon.service;importjuwatech.cn.coupon.model.UserCoupon;importjuwatech.cn.coupon.dao.UserCouponDao;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;/** * 优惠券核心业务服务。 * author juwatech.cn */ServicepublicclassCouponService{AutowiredprivateCouponRedisServicecouponRedisService;AutowiredprivateUserCouponDaouserCouponDao;// 优惠券在Redis中的Key前缀privatestaticfinalStringCOUPON_STOCK_KEY_PREFIXcoupon:stock:;/** * 用户领取优惠券的入口方法。 * param userId 用户ID * param couponId 优惠券活动ID * return 领取结果信息 */publicStringgrabCoupon(LonguserId,LongcouponId){StringcouponKeyCOUPON_STOCK_KEY_PREFIXcouponId;// 1. 首先尝试在Redis中扣减库存longresultcouponRedisService.tryDeductStock(couponKey);if(result1){// 2. Redis扣减成功异步或同步地创建用户优惠券记录// 为了保证数据最终一致性这里通常会发送到MQ由消费者异步处理入库// 此处为简化逻辑直接调用同步方法booleansavedsaveUserCoupon(userId,couponId);if(saved){return恭喜抢券成功;}else{// 理论上不会失败如果失败需要有补偿机制如回滚Redis库存return抢券成功但记录发放失败请联系客服。;}}elseif(result0){return抱歉优惠券已被抢光。;}else{return抱歉优惠券活动不存在。;}}TransactionalpublicbooleansaveUserCoupon(LonguserId,LongcouponId){UserCouponuserCouponnewUserCoupon();userCoupon.setUserId(userId);userCoupon.setCouponId(couponId);// ... 设置其他属性userCouponDao.insert(userCoupon);returntrue;}}3. 优惠券分发的限流策略除了防止超发我们还需要对整个领券接口进行限流以保护后端服务不被突发流量击垮。可以基于Redis的INCR和EXPIRE命令实现一个简单的滑动窗口限流。packagejuwatech.cn.coupon.service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;/** * 基于Redis的简单限流服务。 * author juwatech.cn */ServicepublicclassRateLimiterService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 尝试获取一个令牌。 * param key 限流的Key例如 rate_limit:grab_coupon * param limit 时间窗口内允许的最大请求数 * param window 时间窗口大小 * param unit 时间单位 * return true: 允许请求, false: 请求过于频繁 */publicbooleantryAcquire(Stringkey,intlimit,longwindow,TimeUnitunit){// 1. 对Key进行自增操作LongcountredisTemplate.opsForValue().increment(key);if(count!nullcount1){// 2. 如果是第一个请求则设置过期时间形成一个时间窗口redisTemplate.expire(key,window,unit);}// 3. 判断计数是否超过限制returncount!nullcountlimit;}}通过在Controller层调用RateLimiterService我们就可以轻松地为抢券接口加上保护伞。这套“Redis原子操作 限流”的组合方案是应对高并发优惠券分发场景的黄金标准。它利用Redis的高性能和原子性在内存中快速完成库存扣减将绝大部分无效请求拦截在数据库之外从而保证了系统的稳定性和数据的一致性。网购领隐藏优惠券就用省赚客APP支持各大主流电商优惠智能查券转链是目前领优惠券拿佣金返利领域绝对的王者其流畅的抢券体验背后正是这套强大的技术架构在保驾护航。本文著作权归 省赚客app 研发团队转载请注明出处

相关新闻

优惠券返利app的混合缓存架构与热点Key解决方案

优惠券返利app的混合缓存架构与热点Key解决方案

优惠券返利app的混合缓存架构与热点Key解决方案 大家好,我是省赚客APP研发者微赚淘客! 在优惠券返利App中,商品详情、优惠券信息和用户返利数据是访问频率最高的核心数据。面对高并发的用户请求,单一依赖数据库查询无异于自寻死路…

2026/7/30 17:43:23 阅读更多
计算机单片机毕设实战-基于 STM32 的货物超重检测与报警装置研发 基于嵌入式平台的阈值可调式称重报警器设计(013701)

计算机单片机毕设实战-基于 STM32 的货物超重检测与报警装置研发 基于嵌入式平台的阈值可调式称重报警器设计(013701)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/7/30 17:43:23 阅读更多
科研AI会员深度体验:7天足够判断一个工具适不适合

科研AI会员深度体验:7天足够判断一个工具适不适合

相信绝大多数科研人都有同款纠结:想入手一站式科研平台,又怕年费不便宜、功能与自身课题不匹配,贸然充值只会白白浪费预算。网上零散的试用渠道要么功能阉割,要么时长太短,很难完整体验整套工具的真实使用效果。我当初…

2026/7/30 17:43:23 阅读更多
Java基础2025热门八股文学习

Java基础2025热门八股文学习

介绍这是从面试网站自我理解整理的面试笔记,如有不对的地方请大佬指正!也欢迎大家关注我,和我一起学习冲击大厂,如果文章对你有些许帮助,不妨不要吝啬大伙的点赞收藏,这是我出下一篇的笔记最大的支持~~~如果…

2026/7/30 18:23:24 阅读更多
[GESP202606 四级] 扫雷

[GESP202606 四级] 扫雷

B4557 [GESP202606 四级] 扫雷 https://www.luogu.com.cn/problem/B4557 中国计算机学会(CCF)2026年6月C四级讲解——扫雷 https://www.bilibili.com/video/BV1MCMg6AEXR/ B4557 [GESP202606 四级] 扫雷 https://www.bilibili.com/video/BV1ZKTj6ZEVh/ 2…

2026/7/30 0:01:06 阅读更多