设计一个支持万人同时抢购商品的秒杀系统?

一、系统架构设计

1. 分层架构

markdown 复制代码
客户端层 → 接入层 → 业务服务层 →  数据层
     ↓       ↓         ↓          ↓
    限流    缓存       队列      数据库

2. 具体组件

  • 客户端:静态资源CDN、倒计时校准、防重复提交
  • 接入层:Nginx+Lua/OpenResty,做第一层限流和缓存
  • 业务层
    • 秒杀服务集群(无状态)
    • 消息队列(Kafka/RocketMQ)
    • 缓存集群(Redis Cluster)
  • 数据层
    • 主从数据库(读写分离)
    • 分库分表(按商品/时间)

二、核心问题解决方案

1. 超卖问题

解决方案一:Redis原子操作

ini 复制代码
# 使用Redis的DECR原子操作扣减库存
def deduct_stock(product_id, user_id):
    stock_key = f"stock:{product_id}"

# Lua脚本保证原子性
lua_script = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
"""

result = redis.eval(lua_script, 1, stock_key)
return result == 1

解决方案二:数据库乐观锁

ini 复制代码
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = ? AND stock > 0 AND version = ?

解决方案三:预扣库存

arduino 复制代码
// 先预扣Redis库存,再异步同步到DB
public boolean preDeductStock(String productId, int count) {
    String key = "seckill:stock:" + productId;
    Long remaining = redisTemplate.opsForValue().decrement(key, count);

    if (remaining >= 0) {
        // 发送MQ消息异步扣减数据库
        sendStockDeductMessage(productId, count);
        return true;
    } else {
        // 库存不足,回滚
        redisTemplate.opsForValue().increment(key, count);
        return false;
    }
}

2. 高并发请求处理

2.1 流量削峰

kotlin 复制代码
// 使用消息队列缓冲请求
@Component
public class SeckillService {
    @Autowired
    private RocketMQTemplate mqTemplate;

    public SeckillResult seckill(SeckillRequest request) {
        // 1. 校验用户和商品状态
        if (!validate(request)) {
            return SeckillResult.fail("校验失败");
        }

        // 2. 生成唯一请求ID
        String requestId = generateRequestId(request);

        // 3. 请求入队,立即返回
        mqTemplate.sendOneWay("seckill-topic", 
                              MessageBuilder.withPayload(request).build());

        // 4. 返回排队中状态,前端轮询结果
        return SeckillResult.processing(requestId);
    }
}

2.2 分层过滤

复制代码
所有请求 → 合法性校验 → 库存校验 → 频率控制 → 实际下单
   ↓           ↓          ↓         ↓         ↓
 100万        50万       10万       5万       1万

3. 系统性能优化

3.1 缓存策略

makefile 复制代码
# 多级缓存配置
缓存层级:
  一级: JVM本地缓存 (Caffeine) - 热点商品
  二级: Redis集群 - 库存信息
  三级: 数据库 - 最终一致性

3.2 读多写少优化

scss 复制代码
// 商品信息缓存预热
@Service
public class CacheWarmUpService {

    @PostConstruct
    public void warmUpSeckillProducts() {
        List<Product> hotProducts = loadHotProducts();

        for (Product product : hotProducts) {
            // 库存信息
            redisTemplate.opsForValue().set(
                "stock:" + product.getId(),
                product.getStock()
            );

            // 商品详情
            redisTemplate.opsForValue().set(
                "product:" + product.getId(),
                JSON.toJSONString(product)
            );

            // 使用布隆过滤器存储可售商品ID
            bloomFilter.add(product.getId());
        }
    }
}

4. 详细实现方案

4.1 秒杀流程

ruby 复制代码
class SeckillSystem:
    def process_seckill(self, user_id, product_id):
        # 1. 恶意请求拦截
        if not self.check_risk(user_id):
            return {"code": 403, "msg": "访问过于频繁"}

        # 2. 布隆过滤器快速判断
        if not bloom_filter.contains(product_id):
            return {"code": 404, "msg": "商品不存在"}

        # 3. 内存标记(已售罄的商品直接返回)
        if sold_out_flags.get(product_id):
            return {"code": 400, "msg": "已售罄"}

        # 4. Redis原子扣减库存
        if not self.deduct_stock_in_redis(product_id):
            sold_out_flags[product_id] = True
            return {"code": 400, "msg": "库存不足"}

        # 5. 生成订单ID(雪花算法)
        order_id = snowflake.generate()

        # 6. 订单信息入队
        mq.send({
            "order_id": order_id,
            "user_id": user_id,
            "product_id": product_id,
            "time": time.time()
        })

        # 7. 返回排队中
        return {
            "code": 200,
            "msg": "排队中",
            "order_id": order_id,
            "queue_position": get_queue_position(order_id)
        }

4.2 库存同步方案

scss 复制代码
@Component
@Slf4j
public class StockSyncService {

    // 数据库最终扣减
    @Transactional
    public void syncStockToDB(String productId, int count) {
        try {
            // 数据库扣减(带重试机制)
            boolean success = productDAO.deductStock(productId, count);

            if (success) {
                // 更新Redis中的最终库存状态
                redisTemplate.opsForValue().set(
                    "stock_final:" + productId,
                    getDBStock(productId)
                );

                // 删除售罄标记
                soldOutCache.remove(productId);
            }
        } catch (Exception e) {
            log.error("库存同步失败", e);
            // 记录异常,人工介入处理
            alertService.sendAlert(e);
        }
    }

    // 库存对账任务
    @Scheduled(cron = "0 */5 * * * ?")
    public void stockReconciliation() {
        List<Product> products = productDAO.getAllSeckillProducts();

        for (Product product : products) {
            Integer redisStock = getRedisStock(product.getId());
            Integer dbStock = product.getStock();

            if (!Objects.equals(redisStock, dbStock)) {
                log.warn("库存不一致: productId={}, redis={}, db={}", 
                         product.getId(), redisStock, dbStock);
                // 自动修复或报警
                fixStockInconsistency(product.getId(), dbStock);
            }
        }
    }
}

三、高可用保障

1. 限流降级策略

makefile 复制代码
# 多维度限流配置
限流规则:
  用户维度: 每个用户10次/分钟
  IP维度: 每个IP 1000次/分钟
  商品维度: 每个商品 10000次/分钟
  总QPS: 系统最大承受50000 QPS

2. 熔断降级

less 复制代码
@RestController
@Slf4j
public class SeckillController {

    @GetMapping("/seckill/{productId}")
    @HystrixCommand(
        fallbackMethod = "seckillFallback",
        commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20")
        }
    )
    public Response seckill(@PathVariable String productId, 
                            @RequestParam String userId) {
        return seckillService.process(userId, productId);
    }

    // 降级方法
    public Response seckillFallback(String productId, String userId) {
        return Response.error("系统繁忙,请稍后重试");
    }
}

四、监控与告警

1. 关键监控指标

  • 系统层面:QPS、RT、错误率、CPU/内存使用率
  • 应用层面:库存扣减成功率、消息堆积量
  • 业务层面:抢购成功率、用户排队时长

2. 监控实现

arduino 复制代码
@Component
public class SeckillMonitor {

    private final MeterRegistry meterRegistry;

    // 记录关键指标
    public void recordSeckill(String productId, boolean success, long cost) {
        // QPS监控
        meterRegistry.counter("seckill.requests.total").increment();

        if (success) {
            meterRegistry.counter("seckill.success.total").increment();
        } else {
            meterRegistry.counter("seckill.fail.total").increment();
        }

        // 耗时分布
        meterRegistry.timer("seckill.process.time")
        .record(cost, TimeUnit.MILLISECONDS);

        // 库存变化
        meterRegistry.gauge("seckill.stock." + productId, 
                            getCurrentStock(productId));
    }
}

五、部署与扩展

1. 弹性扩展策略

  • 水平扩展:无状态服务可快速扩容
  • 自动伸缩:基于CPU使用率或QPS自动扩缩容
  • 异地多活:重要业务支持多机房部署

2. 压测方案

makefile 复制代码
压测场景:
  场景1: 库存预热,10万用户同时抢1万商品
  场景2: 持续高压,5万QPS持续5分钟
  场景3: 峰值冲击,瞬间20万QPS
  
压测目标:
  成功率: >99.9%
  平均RT: <100ms
  错误率: <0.1%

六、安全考虑

  1. 防刷机制
    • 验证码(峰值时降级)
    • 设备指纹
    • 行为分析
  1. 数据安全
    • 关键数据加密
    • 操作日志记录
    • 防篡改校验

总结要点

  1. 架构核心:分层过滤 + 异步处理 + 最终一致
  2. 库存核心:Redis原子操作 + 消息队列 + 数据库乐观锁
  3. 性能核心:缓存预热 + 流量削峰 + 读写分离
  4. 稳定核心:熔断降级 + 限流隔离 + 快速失败

面试回答

首先,架构设计上要动静分离、分层削峰。我会把系统分为:

  1. 静态资源分离:商品图片、描述页等提前推送到CDN,请求直接走边缘节点,不给后端压力。
  2. 网关层限流:在入口用Nginx或网关(如Sentinel)做恶意请求拦截和总流量限制,比如对同一UID限速,超过阈值直接返回"请求频繁"。
  3. 业务逻辑后置,请求队列化 :秒杀的核心------"下单扣库存"这个最重要的逻辑,绝不放在前台实时处理。用户点击"抢购"后,前端直接返回"排队中",请求进入一个消息队列 (比如RabbitMQ、Kafka或RocketMQ)。这样一来,海量并发就被平滑成顺序处理的流量,后端服务按照自己的能力从队列里慢慢消费,实现削峰填谷
  4. 服务独立部署:把秒杀相关的功能(验资格、扣库存)单独做成一个微服务,避免影响商城其他正常功能(如浏览、普通下单)。

其次,针对如何解决超卖、库存扣减和高并发请求这三个核心问题,我的解决方案是:

  1. 解决超卖和库存扣减:这是秒杀的核心。我的方案是:
    • 预扣库存 :活动开始前,把商品的库存从主库加载到Redis中。Redis是单线程内存操作,可以保证原子性。
    • 原子化操作 :在Redis里,使用 DECRLUA 脚本来扣减库存。DECR 命令会直接返回扣减后的值,如果返回值小于0,就说明库存没了,后续流程直接返回售罄。LUA脚本可以打包多个操作(检查库存、扣减),确保整个过程原子性,彻底杜绝超卖。
    • 最终同步:后台服务从队列消费,成功扣减Redis库存后,生成一个订单ID(但状态是"未支付"),再异步去更新数据库的库存。这里数据库的库存更多是用于后续对账和长尾查询。
  1. 应对高并发请求
    • 限流:除了网关层的总限流,在秒杀服务本身也要做限流,比如用信号量或令牌桶控制处理线程数,只服务自己能承受的流量,多的直接拒绝,快速失败。
    • 无状态化与扩容:秒杀服务做成无状态的,方便用K8s或云服务快速横向扩容,扛过峰值后再缩容,控制成本。
    • 热点数据隔离 :对于"爆款"商品,它的库存Key在Redis里是热点Key。可以做两件事:一是提前对它进行Key散列,把压力分散到多个Redis节点;二是使用Redis集群模式,并开启读写分离。

最后,还有一些关键的细节和兜底策略

  • 防刷与验证:前端加入计算型验证码或答题,防止机器人;下单前必须校验用户资格(是否登录、地址完善等)。
  • 异步下单与结果轮询:用户提交后,服务端返回一个"排队ID",前端用这个ID轮询后端,查询最终结果(成功、失败或等待)。用户体验上是"排队等待",而不是一直卡住或报错。
  • 数据一致性对账:因为用了Redis和消息队列,可能出现极端情况下的数据不一致(比如Redis扣成功,但下游服务挂了,订单没生成)。需要有一个定时对账任务,核对Redis、数据库库存和订单状态,进行修复。
  • 降级与熔断:如果Redis或数据库访问慢,要有熔断机制,防止服务被拖垮。比如可以快速降级到"返回售罄"的静态页面。

总结一下 ,我的设计思路是:前端限流拦截,请求队列削峰;Redis原子扣减防超卖;服务无状态化应对高并发;再通过异步、对账等手段保证最终一致性和用户体验

相关推荐
L***d6702 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
37手游后端团队2 小时前
gorm回读机制溯源
后端·面试·github
古城小栈2 小时前
Rust 的 validator 库
开发语言·后端·rust
C雨后彩虹2 小时前
竖直四子棋
java·数据结构·算法·华为·面试
疾风sxp2 小时前
nl2sql技术实现自动sql生成之langchain4j SqlDatabaseContentRetriever
java·人工智能·langchain4j
一勺菠萝丶2 小时前
PDF24 转图片出现“中间横线”的根本原因与终极解决方案(DPI 原理详解)
java
姓蔡小朋友3 小时前
Unsafe类
java
上进小菜猪3 小时前
基于 YOLOv8 的昆虫智能识别工程实践 [目标检测完整源码]
后端
一只专注api接口开发的技术猿3 小时前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring