一、秒杀系统的挑战

电商秒杀场景具有高并发、短时高负载、库存精确扣减等特点,瞬间流量可达平时百倍甚至千倍。主要挑战包括:

  • 流量爆发:短时间内海量请求涌入,远超系统承载能力

  • 资源竞争:对同一商品库存的并发写操作,易出现超卖/少卖

  • 响应延迟:用户对秒杀结果敏感,需要快速反馈成功/失败

  • 恶意行为:脚本抢购、黄牛刷单、重复下单等

二、整体架构设计

采用分层解耦+削峰填谷+快速失败的分布式设计思路:

text

客户端 → CDN/静态化 → 负载均衡(Nginx) → 网关层 → 秒杀业务层 → 消息队列 → 订单/库存服务
                                              ↓
                                           Redis(库存预扣)
                                              ↓
                                           数据库(最终一致性)

核心设计原则

  1. 尽量将请求拦截在上游:通过限流、过滤无效请求

  2. 异步化处理:用MQ削峰,将同步写转化为异步写

  3. 无状态化:业务节点可水平扩展

  4. 热点数据隔离:秒杀商品数据独立处理

三、关键核心技术实现

1. 前端与接入层优化

页面静态化 + CDN加速

  • 将秒杀页面HTML、CSS、JS、图片等静态资源托管至CDN

  • 动态数据通过独立API接口获取,减少服务器压力

动静分离:Nginx直接返回静态页面,动态请求反向代理到后端

2. 流量削峰 - 令牌桶/漏桶限流

java

// 基于Guava RateLimiter的本地限流
@Controller
public class SecKillController {
    // 每秒最多放行1000个请求
    private final RateLimiter rateLimiter = RateLimiter.create(1000.0);
    
    @PostMapping("/seckill/{productId}")
    @ResponseBody
    public String seckill(@PathVariable Long productId, Long userId) {
        if (!rateLimiter.tryAcquire(0, TimeUnit.SECONDS)) {
            return "当前拥挤,请稍后再试";
        }
        // 执行后续秒杀逻辑
    }
}

分布式限流:使用Redis + Lua脚本实现更精确的全局限流

lua

-- 滑动窗口限流Lua脚本
local key = KEYS[1]  -- 限流key
local limit = tonumber(ARGV[1])  -- 限流阈值
local window = tonumber(ARGV[2]) -- 时间窗口(ms)
local now = tonumber(ARGV[3])    -- 当前时间戳

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local current = redis.call('ZCARD', key)
if current < limit then
    redis.call('ZADD', key, now, now)
    return 1
else
    return 0
end

3. 库存预热与原子扣减 - Redis

预热:秒杀开始前将库存加载到Redis

java

@PostConstruct
public void initStock() {
    // 商品id -> 剩余库存
    stringRedisTemplate.opsForValue().set("stock:1001", "100");
}

原子扣减:使用Lua脚本保证减库存和校验的原子性

lua

-- 扣减库存Lua脚本
local stockKey = KEYS[1]   -- 库存key
local userIdKey = KEYS[2]  -- 用户购买记录key
local userId = ARGV[1]
local limit = tonumber(ARGV[2])  -- 单用户限购数量

-- 检查用户是否已秒杀过
if redis.call('SISMEMBER', userIdKey, userId) == 1 then
    return -2  -- 重复秒杀
end

local stock = tonumber(redis.call('GET', stockKey))
if stock == nil or stock <= 0 then
    return -1  -- 无库存
end

if stock >= limit then
    redis.call('DECRBY', stockKey, limit)
    redis.call('SADD', userIdKey, userId)
    return 1   -- 扣减成功
end
return 0

调用示例:

java

public boolean tryReduceStock(Long productId, Long userId, int quantity) {
    String stockKey = "stock:" + productId;
    String userKey = "user:seckill:" + productId;
    
    String luaScript = " —— 上面Lua脚本内容 —— ";
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
    Long result = stringRedisTemplate.execute(redisScript, 
        Arrays.asList(stockKey, userKey), 
        userId.toString(), String.valueOf(quantity));
    return result != null && result == 1;
}

4. 异步下单 - 消息队列解耦

扣减Redis库存成功后,发送MQ消息异步创建订单:

java

@Service
public class SecKillService {
    @Autowired
    private RocketMQTemplate mqTemplate;
    
    public void processSeckill(Long productId, Long userId) {
        // 1. Redis预扣库存,成功后继续
        if (!tryReduceStock(productId, userId, 1)) {
            throw new BizException("秒杀失败");
        }
        
        // 2. 发送消息创建订单
        SeckillOrderMessage msg = new SeckillOrderMessage();
        msg.setProductId(productId);
        msg.setUserId(userId);
        msg.setCreateTime(System.currentTimeMillis());
        
        mqTemplate.syncSend("seckill_order_topic", msg);
        // 立即返回"排队中",前端轮询订单状态
    }
}

MQ消费端:执行真正的事务操作(扣数据库库存、创建订单)

java

@RocketMQMessageListener(topic = "seckill_order_topic", consumerGroup = "order_group")
@Component
public class SeckillOrderConsumer implements RocketMQListener<SeckillOrderMessage> {
    
    @Transactional(rollbackFor = Exception.class)
    public void onMessage(SeckillOrderMessage msg) {
        // 使用数据库行锁或乐观锁防止超卖
        int updated = productMapper.decreaseStock(msg.getProductId(), msg.getQuantity());
        if (updated <= 0) {
            // 库存不足,回滚Redis库存补偿
            redisTemplate.opsForValue().increment("stock:" + msg.getProductId(), msg.getQuantity());
            return;
        }
        // 创建订单
        orderMapper.insert(order);
    }
}

5. 数据库层面防超卖 - 乐观锁

sql

-- 商品表增加version字段
UPDATE product SET stock = stock - #{quantity}, version = version + 1 
WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version}

6. 安全防护

用户限购

java

// 使用Redis记录用户秒杀次数
String limitKey = "user:rate:" + userId;
Long count = redisTemplate.opsForValue().increment(limitKey);
if (count == 1) {
    redisTemplate.expire(limitKey, 5, TimeUnit.SECONDS);
}
if (count > 5) {
    throw new BizException("操作过于频繁");
}

验证码/脚本防护:秒杀前要求输入验证码(如算术题、滑块),增加抢购门槛

隐藏秒杀地址

  • 秒杀开始前不暴露真实下单接口

  • 动态生成token,每次请求携带token验证

java

@GetMapping("/seckill/path/{productId}")
public String getSeckillPath(@PathVariable Long productId, Long userId) {
    String path = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    // 存储到Redis,有效期60秒
    redisTemplate.opsForValue().set("seckill_path:" + productId + ":" + userId, path, 60, TimeUnit.SECONDS);
    return path;
}

四、降级与兜底策略

场景 降级方案
Redis故障 直接返回"系统繁忙",不再穿透到DB
MQ积压 增加消费者数量,限制订单创建速率
数据库压力过大 启用读写分离,订单写入可临时降级为只记录日志后异步处理
超出预估流量 启用Hystrix熔断,快速失败返回提示

五、压测与监控

  • JMeter/Locust:模拟多用户并发,重点测试Redis扣减和MQ吞吐

  • 监控指标:QPS、RT(响应时间)、Redis命中率、MQ积压数量、数据库连接池使用率

  • 全链路压测:提前压测到系统拐点,设置合理的限流阈值

六、完整业务流程时序

text

用户 -> 前端点击 -> 网关限流 -> 验证Token/验证码 -> 
Redis预扣库存(原子Lua) -> 生成秒杀订单消息(MQ) -> 立即返回"排队中" -> 
前端轮询订单状态 -> MQ消费者落库 -> 更新数据库库存&订单 -> 
轮询接口返回"成功/失败"

七、总结

电商秒杀的核心是在保证数据一致性的前提下尽可能提升系统吞吐量。通过上述设计:

  1. Redis扛住绝大部分读和预扣库存的压力

  2. MQ削峰填谷,让数据库从洪峰变为平稳流

  3. 限流+降级保护系统不被突发流量击垮

  4. Lua脚本保证Redis操作的原子性

这套架构经过大促场景的反复验证,可以支撑百万级QPS的秒杀需求。实际落地时需要根据自身业务量级和机器配置,调整限流阈值、消息队列批量设置、Redis集群模式等细节。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐