Redis如何实现高并发抢红包和拆红包逻辑?

来源:AI视频音频作者:柬埔寨程序员头衔:程序员
导读:本期聚焦于柬埔寨程序员创作的《Redis如何实现高并发抢红包和拆红包逻辑?》,敬请观看详情。红包系统最容易被低估的是并发一致性。用户点击红包的瞬间,服务端需要完成金额扣减、资格校验和结果记录,任何一步出现竞态都会导致超发或重复领取。Redis之所以适合这个场景,不是因为缓存快,而是因为单线程命令配合Lua脚本可以把多个操作合并成一个原子步骤。文章从金额预分配开始,讲清楚为什么红包金额要提前写入List而不是实时计算,再通过Lua脚本展示抢红包的原子操作设计,最后说明拆红包的幂等控制和异步落库方案。整个过程避免直接依赖数据库事务,把热点请求拦截在Redis层,即使大量用户同时拆同一个红包,也能保证金额队列不丢失、用户记录不重复,并且后续数据可以可靠地同步到MySQL。

抢红包和拆红包看似是同一个操作,实际上在服务端要分开处理。抢红包解决的是能否参与以及分到多少钱的问题,拆红包解决的是用户反复查看结果时如何保持幂等。Redis在这里扮演的角色是把热点请求拦截在内存层,借助丰富的数据结构和原子操作避免数据库行锁竞争。

Redis如何实现高并发抢红包和拆红包逻辑?

一、红包数据模型与金额预分配策略

红包功能的首个设计要点是金额不能实时随机生成,否则在并发场景下容易因为随机数计算和余额判断不是原子操作而出现金额超发。更稳妥的做法是提前把红包金额拆分好,放入Redis的List结构中。抢红包时只需要执行一次弹出操作,时间复杂度为O(1),也不会在运行时反复计算剩余金额。

金额预分配可以使用二倍均值法。假设总金额为100元,总个数为10个,第一个红包随机范围是1分到20元,第二个红包根据剩余金额和剩余个数继续计算。所有金额生成后依次压入Redis List,队列头部为待领金额。红包基础信息可以放在Hash中,例如保存总金额、总个数、剩余个数和过期时间。已抢用户放在Set中,便于O(1)判断重复。用户与金额的对应关系则放在另一个Hash中,供拆红包阶段读取。

public void prepareRedPacket(String redPacketId, int totalAmount, int totalCount) {
    List<Integer> amounts = new ArrayList<>();
    int remainAmount = totalAmount;
    int remainCount = totalCount;
    Random random = new Random();
    for (int i = 0; i < totalCount; i++) {
        if (remainCount == 1) {
            amounts.add(remainAmount);
            break;
        }
        int max = remainAmount / remainCount * 2;
        int amount = 1 + random.nextInt(max);
        amounts.add(amount);
        remainAmount -= amount;
        remainCount--;
    }
    String[] values = amounts.stream().map(String::valueOf).toArray(String[]::new);
    jedis.rpush("red:queue:" + redPacketId, values);
}

这里将金额提前写入red:queue:红包ID这个List,每次抢红包只需要从右侧弹出即可。如果红包队列为空,说明已经抢完。这样设计后,抢红包的核心接口不再依赖数据库事务,也不需要频繁更新红包剩余金额,从而大幅降低热点请求的压力。

二、抢红包原子操作:Lua脚本设计

抢红包不能拆成多个Redis命令依次执行。常见错误是先判断用户是否已抢,再弹出金额,再写入记录。这种分步操作在并发下会出现竞态:同一个用户的两个请求可能同时通过重复判断,最终都弹出金额。即便使用Redis事务,普通事务也没有条件回滚能力,因此必须使用Lua脚本将所有判断和写入动作合并成一个原子块。

下面是一段抢红包Lua脚本。脚本先弹出金额队列,如果没有金额则返回-1。随后检查用户是否已经存在于已抢集合,若存在说明是重复请求,需要把刚刚弹出的金额重新放回队列并返回-2。通过检查后,再把用户写入集合、金额写入详情Hash,最后返回金额。

-- KEYS[1]: 红包金额队列key
-- KEYS[2]: 已抢用户集合key
-- KEYS[3]: 红包详情key
-- ARGV[1]: 用户ID
local user_id = ARGV[1]
local amount = redis.call('RPOP', KEYS[1])
if not amount then
    return -1
end
local exists = redis.call('SISMEMBER', KEYS[2], user_id)
if exists == 1 then
    redis.call('LPUSH', KEYS[1], amount)
    return -2
end
redis.call('SADD', KEYS[2], user_id)
redis.call('HSET', KEYS[3], user_id, amount)
return amount

执行Lua脚本时可以把脚本缓存到Redis服务端,客户端通过evalsha调用脚本摘要,避免每次抢红包都传输完整脚本内容。这样既降低了网络开销,也减少了脚本解析成本。Redis单线程执行Lua脚本期间不会插入其他命令,因此金额弹出、重复判断、记录写入这三步天然具备原子性。

除了原子性,还需要注意脚本执行时间不能过长。抢红包流程只涉及RPOPSISMEMBERSADDHSET等O(1)命令,整个脚本非常短,不会阻塞Redis。即便在秒杀级流量下,单实例也能支撑数万乃至更高的吞吐。

三、拆红包幂等控制与缓存读取

拆红包通常发生在用户已经抢到红包之后,前端需要展示具体金额。拆红包接口必须保证幂等,即同一个用户无论调用多少次,返回的金额都相同,不会因为重复请求再次分配金额。由于抢红包阶段已经将用户与金额写入详情Hash,拆红包可以直接从Hash中读取,不产生任何写操作。

拆红包的读取逻辑可以分成两步:先判断用户是否存在于已抢集合,如果不存在直接返回未抢到;如果存在,再从详情Hash中取出金额并返回。这个流程只需要两次Redis读命令,可以进一步使用Pipeline合并为一次网络往返。

public Integer openRedPacket(String redPacketId, String userId) {
    Boolean exists = jedis.sismember("red:users:" + redPacketId, userId);
    if (exists == null || !exists) {
        return null;
    }
    String amount = jedis.hget("red:detail:" + redPacketId, userId);
    if (amount == null) {
        return null;
    }
    return Integer.parseInt(amount);
}

在高并发场景下,即使拆红包请求量远大于抢红包,也不会对核心金额队列产生任何影响。对于已经抢完且详情数据不再变化的热点红包,还可以在网关层或本地缓存中做短期缓存,但需要设置合理的过期时间,避免数据不一致。

四、异步落库与一致性保障

Redis虽然能支撑高并发读写,但内存数据存在丢失风险。如果红包金额只在Redis中保存,一旦节点宕机且持久化不完整,用户可能无法查到已抢到的金额。因此需要把拆红包结果异步同步到MySQL等关系型数据库,用于最终对账和长期查询。

落库可以采用定时任务扫描详情Hash,也可以将抢红包成功事件发送到消息队列,由消费者批量插入数据库。为了应对重复消费和重复插入,数据库表需要建立红包ID与用户ID的联合唯一索引。插入时使用INSERT ... ON DUPLICATE KEY UPDATE可以保证幂等。

INSERT INTO red_packet_record (red_packet_id, user_id, amount, created_at)
VALUES ('rp_1001', 'user_001', 25, NOW())
ON DUPLICATE KEY UPDATE amount = VALUES(amount);

Redis持久化策略也需要根据业务容忍度调整。如果对资金数据要求较高,可以开启AOF并设置appendfsync everysec,在性能与安全之间取得平衡。如果红包数据只是营销活动,允许极小概率丢失,则可以只开启RDB,换取更好的写入性能。实际生产中通常不建议只依赖RDB,因为RDB快照间隔内的数据无法恢复。

整体来看,Redis抢红包方案的核心在于用List预分配金额、用Lua脚本实现原子抢包、用Hash保存用户结果、用Set保证去重,再通过异步任务将数据落到MySQL。这套设计把高并发请求的核心链路压缩在内存层,避免了对数据库事务和行锁的强依赖,既保证了不超发、不重复拆,也满足了活动高峰期的性能要求。

Redis抢红包拆红包修改时间:2026-08-25 01:11:47

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。