设计一个支持万人同时抢购商品的秒杀系统需要解决高并发、超卖、库存扣减一致性等问题,同时保证系统的性能、稳定性和可扩展性。以下是一个完整的秒杀系统设计方案,重点解决超卖、库存扣减和高并发请求问题,并提供关键代码示例。


一、秒杀系统设计目标

  1. 高并发:支持万人甚至更高并发请求,确保系统不崩溃。
  2. 防止超卖:保证库存扣减准确,库存不会出现负数。
  3. 一致性:库存扣减和订单生成保持一致,数据无冲突。
  4. 高性能:低延迟响应用户请求,提升用户体验。
  5. 可扩展性:支持水平扩展,应对更大规模的流量。

二、系统架构设计

1. 整体架构

秒杀系统通常采用分层架构,结合分布式技术和缓存优化:

  • 前端层:静态页面(CDN 加速)、流量控制(限流、验证码)。
  • 网关层:负载均衡(Nginx)、流量分发、限流。
  • 应用层:业务逻辑处理、异步削峰、接口优化。
  • 缓存层:Redis 存储库存和热点数据,减少数据库压力。
  • 数据库层:MySQL 存储订单和最终库存,确保数据一致性。
  • 消息队列:Kafka 或 RocketMQ 用于异步下单和削峰。
  • 分布式锁:Redis 或 ZooKeeper 实现库存扣减的并发控制。
2. 关键流程
  1. 用户请求秒杀(带用户 ID 和商品 ID)。
  2. 前端限流(验证码、JS 控制请求频率)。
  3. 网关限流(Nginx 或 Sentinel 限制 QPS)。
  4. 应用层校验(用户资格、活动时间、库存状态)。
  5. Redis 扣减库存(原子操作)。
  6. 异步下单(消息队列写入订单)。
  7. 数据库持久化订单和最终库存。

三、解决核心问题

1. 防止超卖

问题:高并发下,多个线程可能同时读取库存,导致库存扣减不准确,出现超卖。 解决方案

  • Redis 原子操作:使用 Redis 的原子操作(如 DECR 或 Lua 脚本)扣减库存,确保库存不会被超扣。
  • 分布式锁:在 Redis 或 ZooKeeper 上加锁,确保同一时间只有一个线程能扣减库存。
  • 乐观锁:在数据库层面使用版本号或库存字段的 CAS(Compare And Set)机制,防止并发更新。

实现示例(Redis Lua 脚本):

local key = KEYS[1] -- 库存键
local requested = tonumber(ARGV[1]) -- 请求扣减数量
local current = tonumber(redis.call('GET', key) or 0)
if current >= requested then
    redis.call('DECRBY', key, requested)
    return 1 -- 扣减成功
else
    return 0 -- 库存不足
end

调用方式:

EVAL "script_content" 1 "seckill:stock:{itemId}" 1
2. 库存扣减

问题:高并发下,库存扣减可能导致数据库压力过大,且一致性难以保证。 解决方案

  • 热点数据缓存:将秒杀商品的库存加载到 Redis,扣减操作在 Redis 完成,减少数据库访问。
  • 异步落库:扣减成功后,将订单写入消息队列,异步写入数据库,确保一致性。
  • 最终一致性:通过定时任务或日志补偿机制,定期检查 Redis 和数据库库存一致性。

实现示例(Java + Redis 扣减库存):

import redis.clients.jedis.Jedis;
import org.springframework.stereotype.Service;

@Service
public class SeckillService {
    private static final String REDIS_KEY = "seckill:stock:%s";
    
    public boolean deductStock(String itemId) {
        try (Jedis jedis = jedisPool.getResource()) {
            String script = "local key = KEYS[1]\n" +
                           "local requested = tonumber(ARGV[1])\n" +
                           "local current = tonumber(redis.call('GET', key) or 0)\n" +
                           "if current >= requested then\n" +
                           "    redis.call('DECRBY', key, requested)\n" +
                           "    return 1\n" +
                           "else\n" +
                           "    return 0\n" +
                           "end";
            Object result = jedis.eval(script, 1, String.format(REDIS_KEY, itemId), "1");
            return "1".equals(result.toString());
        }
    }
}
3. 高并发请求

问题:万人同时请求可能导致系统过载,响应延迟甚至宕机。 解决方案

  • 流量削峰
    • 前端限流:通过 JS 限制请求频率,或要求用户输入验证码。
    • 网关限流:使用 Nginx 的 limit_req 模块或 Sentinel 限制 QPS。
    • 令牌桶算法:在应用层使用 Guava RateLimiter 或 Redis 实现分布式限流。
  • 异步处理:将秒杀请求写入消息队列(如 Kafka),异步处理下单逻辑,降低瞬时压力。
  • 热点隔离
    • 将秒杀商品的库存和请求独立存储在 Redis 集群的热点节点。
    • 使用 CDN 加速静态页面,减少后端压力。
  • 分布式架构:部署多个应用服务节点,使用负载均衡分发请求。

四、系统优化点

1. 前端优化
  • 静态化:秒杀页面静态化,部署到 CDN,减少动态请求。
  • 防刷机制:通过验证码、IP 限制或用户行为分析防止恶意请求。
  • 请求合并:前端批量发送请求,减少后端压力。
2. 后端优化
  • 热点缓存:将秒杀商品信息(如价格、库存)预加载到 Redis。
  • 异步下单:扣减库存后,订单生成通过消息队列异步处理。
  • 数据库分库分表:订单表按商品 ID 或用户 ID 分片,降低单表压力。
3. 数据库优化
  • 读写分离:主库写入订单,从库查询订单状态。
  • 索引优化:为订单表和库存表建立适当索引。
  • 事务简化:避免长事务,库存扣减和订单生成分开处理。
4. 可扩展性
  • Redis 集群:使用 Redis Cluster 或哨兵模式,支持高可用和水平扩展。
  • 服务拆分:将秒杀服务独立部署,与其他业务隔离。
  • 弹性扩容:使用 Kubernetes 或 Docker Swarm 动态扩展应用节点。

五、完整流程示例

  1. 用户请求:用户通过前端页面发起秒杀请求,携带用户 ID 和商品 ID。
  2. 网关校验:Nginx 限流,过滤非法请求。
  3. 应用层校验
    • 检查活动是否开始、用户是否有资格。
    • 查询 Redis 库存,调用 Lua 脚本扣减。
  4. 扣减成功
    • 将订单信息写入 Kafka。
    • 异步消费者从 Kafka 读取消息,生成订单并写入 MySQL。
  5. 扣减失败:返回库存不足提示。
  6. 最终一致性:定时任务比对 Redis 和 MySQL 库存,修复异常数据。
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class OrderConsumer {
    @KafkaListener(topics = "seckill_orders")
    public void processOrder(ConsumerRecord<String, String> record) {
        // 解析消息,生成订单
        String[] data = record.value().split(",");
        String userId = data[0];
        String itemId = data[1];
        // 写入数据库
        saveOrderToDatabase(userId, itemId);
    }

    private void saveOrderToDatabase(String userId, String itemId) {
        // 数据库操作:插入订单记录
        // 使用乐观锁确保库存一致性
    }
}

六、注意事项

  1. 库存预热:秒杀开始前,将库存加载到 Redis,避免冷启动问题。
  2. 防重入:通过 Redis 记录用户秒杀状态,防止同一用户重复抢购。
  3. 监控与报警:监控 Redis 内存、QPS、数据库连接数,设置报警机制。
  4. 压测准备:使用 JMeter 或 Locust 模拟高并发,优化系统瓶颈。

七、总结

  • 防止超卖:通过 Redis 原子操作(如 Lua 脚本)和分布式锁确保库存扣减准确。
  • 库存扣减:Redis 缓存热点库存,异步落库保证一致性。
  • 高并发处理:通过前端限流、网关限流、异步处理和分布式架构削峰填谷。
  • 优化与扩展:结合 CDN、Redis 集群、消息队列和弹性扩容提升性能和稳定性。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐