优惠券省钱app高并发秒杀系统:基于Redis与消息队列的架构设计

优惠券省钱app高并发秒杀系统:基于Redis与消息队列的架构设计

大家好,我是省赚客APP研发者阿宝!

在省赚客APP中,优惠券秒杀活动是提升用户活跃度和转化率的重要手段。面对数万甚至百万级用户同时抢券的场景,传统数据库直写方案极易导致系统雪崩。为此,我们构建了一套基于Redis缓存与消息队列(如RocketMQ)的高并发秒杀架构,有效支撑了单场活动10万+ QPS的请求压力。

整体架构设计

系统采用"缓存前置 + 异步削峰 + 最终一致性"策略。核心流程如下:

  1. 用户请求进入网关层后,首先校验用户身份与活动资格;
  2. 通过Redis原子操作判断库存是否充足,并完成预扣减;
  3. 若成功,则将秒杀订单信息投递至消息队列;
  4. 后台消费者异步处理订单落库、优惠券发放等逻辑;
  5. 前端通过轮询或WebSocket获取结果。

该架构将数据库写压力从峰值转移到后台异步处理,极大提升了系统吞吐能力。

Redis库存预扣减实现

我们使用Redis的DECR命令实现原子性库存扣减。为防止超卖,需确保库存值非负。关键代码如下(Java):

java 复制代码
package juwatech.cn.seckill.service;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class SeckillStockService {

    private final StringRedisTemplate redisTemplate;

    public SeckillStockService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryDeductStock(String activityId) {
        String stockKey = "seckill:stock:" + activityId;
        Long current = redisTemplate.opsForValue().decrement(stockKey);
        return current != null && current >= 0;
    }

    public void initStock(String activityId, int stock) {
        String stockKey = "seckill:stock:" + activityId;
        redisTemplate.opsForValue().setIfAbsent(stockKey, String.valueOf(stock));
    }
}

注意:setIfAbsent确保活动库存仅初始化一次,避免重复覆盖。

防刷与限流机制

为防止恶意脚本刷券,我们在网关层集成令牌桶限流与用户行为校验。例如,每个用户每秒最多发起1次秒杀请求:

java 复制代码
package juwatech.cn.seckill.gateway;

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class UserRateLimiter {

    private final Map<String, RateLimiter> userLimiters = new ConcurrentHashMap<>();

    public boolean allowRequest(String userId) {
        RateLimiter limiter = userLimiters.computeIfAbsent(userId, k -> RateLimiter.create(1.0));
        return limiter.tryAcquire();
    }
}

此外,结合Redis记录用户已参与活动状态,防止重复下单:

java 复制代码
public boolean hasParticipated(String userId, String activityId) {
    String key = "seckill:user:" + userId + ":activity:" + activityId;
    Boolean exists = redisTemplate.hasKey(key);
    if (Boolean.FALSE.equals(exists)) {
        redisTemplate.opsForValue().set(key, "1", Duration.ofHours(24));
        return false;
    }
    return true;
}

消息队列异步处理订单

秒杀成功后,将订单信息封装为消息发送至RocketMQ:

java 复制代码
package juwatech.cn.seckill.mq;

import juwatech.cn.seckill.dto.SeckillOrderDTO;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Service;

@Service
public class SeckillMqProducer {

    private final RocketMQTemplate rocketMQTemplate;

    public SeckillMqProducer(RocketMQTemplate rocketMQTemplate) {
        this.rocketMQTemplate = rocketMQTemplate;
    }

    public void sendSeckillOrder(SeckillOrderDTO order) {
        rocketMQTemplate.convertAndSend("SECKILL_ORDER_TOPIC", order);
    }
}

消费者端异步处理:

java 复制代码
package juwatech.cn.seckill.mq.consumer;

import juwatech.cn.seckill.dto.SeckillOrderDTO;
import juwatech.cn.seckill.service.CouponGrantService;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "SECKILL_ORDER_TOPIC", consumerGroup = "seckill-group")
public class SeckillOrderConsumer implements RocketMQListener<SeckillOrderDTO> {

    private final CouponGrantService couponGrantService;

    public SeckillOrderConsumer(CouponGrantService couponGrantService) {
        this.couponGrantService = couponGrantService;
    }

    @Override
    public void onMessage(SeckillOrderDTO order) {
        try {
            couponGrantService.grantCoupon(order.getUserId(), order.getCouponId());
            // 记录订单到DB
        } catch (Exception e) {
            // 失败可重试或告警
        }
    }
}

数据一致性保障

由于采用异步模式,需确保最终一致性。我们通过以下措施:

  • Redis库存扣减成功即视为"逻辑成功",前端可立即提示用户"抢购成功";
  • 消息队列启用事务消息或本地消息表,确保订单不丢失;
  • 定时对账任务校验Redis库存与数据库已发放数量,自动修复偏差。

例如,对账任务伪代码:

java 复制代码
@Scheduled(fixedRate = 300000) // 每5分钟
public void reconcileStock() {
    List<Activity> activities = activityMapper.getActiveActivities();
    for (Activity act : activities) {
        String redisStock = redisTemplate.opsForValue().get("seckill:stock:" + act.getId());
        int dbIssued = couponRecordMapper.countByActivity(act.getId());
        int expectedStock = act.getTotalStock() - dbIssued;
        if (!String.valueOf(expectedStock).equals(redisStock)) {
            // 触发告警或自动修正
        }
    }
}

压测与优化效果

上线前,我们使用JMeter模拟10万并发用户秒杀1万张券。优化后系统表现:

  • 平均响应时间 < 80ms;
  • 成功率 > 99.5%;
  • 数据库CPU负载下降70%;
  • 无超卖、无重复发放。

本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!

相关推荐
Σίσυφος190010 小时前
PCL法向量估计 之 方向约束法向量(Orientation Guided Normal)
数据库
老毛肚10 小时前
手写mybatis
java·数据库·mybatis
海山数据库10 小时前
移动云大云海山数据库(He3DB)postgresql_anonymizer插件原理介绍与安装
数据库·he3db·大云海山数据库·移动云数据库
云飞云共享云桌面10 小时前
高性能图形工作站的资源如何共享给10个SolidWorks研发设计用
linux·运维·服务器·前端·网络·数据库·人工智能
2501_9279935311 小时前
SQL Server 2022安装详细教程(图文详解,非常详细)
数据库·sqlserver
星火s漫天11 小时前
第一篇: 使用Docker部署flask项目(Flask + DB 容器化)
数据库·docker·flask
xcLeigh11 小时前
Python 项目实战:用 Flask 实现 MySQL 数据库增删改查 API
数据库·python·mysql·flask·教程·python3
威迪斯特11 小时前
Flask:轻量级Web框架的技术本质与工程实践
前端·数据库·后端·python·flask·开发框架·核心架构
xu_yule11 小时前
Redis存储(15)Redis的应用_分布式锁_Lua脚本/Redlock算法
数据库·redis·分布式
一灰灰blog11 小时前
Spring AI中的多轮对话艺术:让大模型主动提问获取明确需求
数据库·人工智能·spring