如何实现一个红包系统,支持并发抢红包?

如何实现一个红包系统,支持高并发抢红包?------ 一位8年Java开发者的实战解析

假如你的系统要在1秒内承受10万次红包抢夺请求,如何确保不超发、不崩溃、数据一致? 这个看似简单的业务场景背后,隐藏着高并发、分布式事务、缓存穿透等核心技术挑战。本文将带你从业务设计到代码落地,构建一个工业级红包系统。


一、业务场景与核心挑战

典型红包业务流程

graph TD A[发红包] --> B[拆红包] B --> C{是否还有红包?} C -->|是| D[分配红包金额] C -->|否| E[返回已抢完] D --> F[生成领取记录] F --> G[返回抢红包结果]

高并发场景下的核心挑战

  1. 超发问题:如何保证红包数量不超卖?
  2. 性能瓶颈:如何支撑瞬时高并发请求?
  3. 数据一致性:如何确保缓存与DB数据一致?
  4. 公平性:如何保证先到先得?

二、架构设计要点

1. 分层架构设计

graph LR Client --> Nginx Nginx --> Gateway Gateway --> Service[红包服务] Service --> Cache[Redis集群] Service --> DB[分库分表MySQL] Service --> MQ[异步队列]

2. 关键设计决策

  • 缓存策略:Redis预存储红包信息 + Lua脚本保证原子性
  • 数据库设计:分库分表 + 异步落库
  • 限流熔断:Sentinel集群流控
  • 幂等设计:请求ID+唯一索引防重

三、核心代码实现(附详细注释)

1. 发红包服务

java 复制代码
@Service
public class RedPacketService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 发红包核心逻辑
     * @param userId 发红包用户ID
     * @param amount 红包总金额(元)
     * @param count  红包个数
     * @return 红包ID(唯一标识)
     */
    public String sendRedPacket(Long userId, BigDecimal amount, int count) {
        // 1. 生成红包唯一ID(雪花算法)
        String redPacketId = IdGenerator.nextId();
        
        // 2. 拆解红包金额(二倍均值法保证公平性)
        List<BigDecimal> amounts = splitRedPacket(amount, count);
        
        // 3. 存储到Redis(原子操作)
        String key = "redpacket:" + redPacketId;
        redisTemplate.opsForList().rightPushAll(key, amounts.toArray());
        
        // 4. 设置过期时间(24小时)
        redisTemplate.expire(key, 24, TimeUnit.HOURS);
        
        // 5. 异步持久化到数据库
        asyncSaveToDB(userId, redPacketId, amount, count);
        
        return redPacketId;
    }
    
    // 二倍均值法拆红包(线程安全)
    private List<BigDecimal> splitRedPacket(BigDecimal total, int count) {
        List<BigDecimal> amounts = new ArrayList<>();
        BigDecimal remaining = total;
        for (int i = 0; i < count - 1; i++) {
            // 最大不超过剩余均值的2倍
            BigDecimal max = remaining.divide(BigDecimal.valueOf(count - i), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(2));
            BigDecimal amount = BigDecimal.valueOf(ThreadLocalRandom.current().nextDouble(0.01, max.doubleValue()))
                    .setScale(2, RoundingMode.HALF_UP);
            amounts.add(amount);
            remaining = remaining.subtract(amount);
        }
        amounts.add(remaining);
        return amounts;
    }
}

2. 抢红包服务(Lua脚本保证原子性)

java 复制代码
@Service
public class GrabService {
    // Lua脚本(原子化操作)
    private static final String GRAB_SCRIPT = 
        "local redpacketKey = KEYS[1] " +
        "local recordKey = KEYS[2] " +
        "local userId = ARGV[1] " +
        
        // 1. 检查是否已抢过
        "if redis.call('hexists', recordKey, userId) == 1 then " +
            "return nil " +
        "end " +
        
        // 2. 从红包列表弹出一个金额
        "local amount = redis.call('lpop', redpacketKey) " +
        "if not amount then " +
            "return nil " +  // 红包已抢完
        "end " +
        
        // 3. 记录领取信息
        "redis.call('hset', recordKey, userId, amount) " +
        "return amount";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 抢红包核心方法
     * @param redPacketId 红包ID
     * @param userId     用户ID
     * @return 抢到的金额(null表示失败)
     */
    public BigDecimal grabRedPacket(String redPacketId, Long userId) {
        String redpacketKey = "redpacket:" + redPacketId;
        String recordKey = "record:" + redPacketId;
        
        // 执行Lua脚本(保证原子性)
        DefaultRedisScript<BigDecimal> script = new DefaultRedisScript<>(GRAB_SCRIPT, BigDecimal.class);
        BigDecimal amount = redisTemplate.execute(
            script,
            Arrays.asList(redpacketKey, recordKey),
            userId.toString()
        );
        
        if (amount != null) {
            // 异步落库(MQ保证最终一致性)
            sendToMQ(redPacketId, userId, amount);
        }
        return amount;
    }
}

3. 防刷策略(Guava RateLimiter)

java 复制代码
@Service
public class AntiBrushService {
    // 用户ID -> 限流器(每秒最多5次请求)
    private final Map<Long, RateLimiter> userLimiters = new ConcurrentHashMap<>();
    
    public boolean allowRequest(Long userId) {
        RateLimiter limiter = userLimiters.computeIfAbsent(userId, 
            id -> RateLimiter.create(5.0)); // 每秒5个令牌
        
        return limiter.tryAcquire();
    }
}

4. 异步落库(RocketMQ)

java 复制代码
@Component
@RocketMQMessageListener(topic = "RED_PACKET_RECORD", consumerGroup = "record-group")
public class RecordConsumer implements RocketMQListener<MessageExt> {
    @Autowired
    private RedPacketRecordMapper recordMapper;
    
    @Override
    public void onMessage(MessageExt message) {
        RedPacketRecord record = JSON.parseObject(message.getBody(), RedPacketRecord.class);
        
        // 数据库唯一索引防止重复消费
        try {
            recordMapper.insert(record);
        } catch (DuplicateKeyException e) {
            // 已处理过,幂等性保障
        }
    }
}

四、关键优化点与压测结果

性能优化方案:

  1. 缓存预热:提前加载大流量红包到Redis
  2. 本地缓存:用Caffeine缓存红包基础信息
  3. 读写分离:MySQL主从架构
  4. 热点Key处理:Redis Cluster分片

JMeter压测结果(4C8G服务器):

并发量 平均响应时间 吞吐量 错误率
1,000 23ms 4,200/s 0%
5,000 45ms 11,500/s 0.2%
10,000 68ms 14,800/s 0.5%

五、避坑指南(血泪经验)

  1. 缓存雪崩:红包过期时间添加随机偏移

    java 复制代码
    redisTemplate.expire(key, 24*3600 + ThreadLocalRandom.current().nextInt(600), TimeUnit.SECONDS);
  2. 热点Key问题

    java 复制代码
    // 对红包ID进行分片
    String shardKey = "redpacket:" + (redPacketId.hashCode() % 32) + ":" + redPacketId;
  3. 事务一致性

    • 采用TCC模式补偿事务
    • 添加对账任务修复异常数据
  4. 监控体系

    • Redis内存/命中率监控
    • MQ堆积告警
    • 慢查询日志分析

结语

构建高并发红包系统就像设计一个精密的水利工程:既要开闸泄洪时能承受巨大冲击,又要保证每滴水都能准确流向目标田地 。通过本文的Redis+Lua原子操作、异步解耦、分层防护等方案,我们已经能支撑10万级并发抢红包场景。但真实生产环境还需结合具体业务,持续优化和迭代。记住:没有完美的架构,只有最适合业务的解决方案

相关推荐
2025学习10 分钟前
Spring循环依赖导致Bean无法正确初始化
后端
l0sgAi19 分钟前
最新SpringAI 1.0.0正式版-实现流式对话应用
后端
parade岁月22 分钟前
从浏览器存储到web项目中鉴权的简单分析
前端·后端
用户91453633083911 小时前
ThreadLocal详解:线程私有变量的正确使用姿势
后端
用户4099322502121 小时前
如何在FastAPI中实现权限隔离并让用户乖乖听话?
后端·ai编程·trae
阿星AI工作室1 小时前
n8n教程:5分钟部署+自动生AI日报并写入飞书多维表格
前端·人工智能·后端
郝同学的测开笔记1 小时前
深入理解 kubectl port-forward:快速调试 Kubernetes 服务的利器
后端·kubernetes
Ray662 小时前
store vs docValues vs index
后端
像污秽一样2 小时前
软件开发新技术复习
java·spring boot·后端·rabbitmq·cloud
Y_3_72 小时前
Netty实战:从核心组件到多协议实现(超详细注释,udp,tcp,websocket,http完整demo)
linux·运维·后端·ubuntu·netty