【面试场景题】电商秒杀系统的库存管理设计实战

发布于:2025-08-07 ⋅ 阅读:(18) ⋅ 点赞:(0)

题目

场景描述

某电商平台计划开展“秒杀活动”,一款限量1000件的商品将在指定时间开放抢购,预计参与用户达100万+,瞬时请求峰值可能超过10万QPS。活动规则为:每个用户限购1件,先到先得,售罄即止。

当前系统基础:

  • 后端技术栈为Java(Spring Boot),数据库使用MySQL(单库,库存表结构简化为:goods_id[商品ID]、stock[剩余库存])。
  • 服务器配置:应用服务器10台(每台8核16GB内存),数据库服务器2台(主从架构,8核32GB内存)。

核心问题

在高并发抢购场景下,需确保:

  1. 库存准确性:绝对不允许超卖(最终下单数 ≤ 1000),也不允许少卖(库存未售罄时不提前结束)。
  2. 系统稳定性:顶住10万QPS的瞬时流量,避免服务器宕机或响应超时。
  3. 用户体验:抢购结果实时反馈(成功/失败),避免用户长时间等待。

问题

  1. 请设计整体技术方案,阐述如何通过架构设计、技术选型解决上述核心问题。
  2. 数据库层面如何保证库存操作的原子性?可能遇到哪些问题(如幻读、锁冲突),如何解决?
  3. 如何利用缓存减轻数据库压力?缓存与数据库的一致性如何保证(例如:缓存中的库存与数据库库存不一致时怎么办)?
  4. 如何防止恶意抢购(如脚本刷单、同一用户多账号抢购)?
  5. 若秒杀过程中数据库突然宕机,如何保证未完成的订单不影响最终库存准确性?

考察点

  • 高并发场景下的系统设计能力(流量削峰、资源隔离)
  • 分布式环境下的数据一致性保证(锁机制、事务设计)
  • 缓存策略与数据库优化(读写分离、索引设计)
  • 系统容错与降级方案(限流、熔断、降级)
  • 业务安全防护(防刷、限流规则)

这道题贴近真实电商场景,既考察候选人对技术细节(如数据库锁、缓存机制)的掌握,也关注其对高并发系统全局架构的理解,能有效区分“理论派”和“工程实践派”。

解决方案

针对电商秒杀系统的库存管理设计,需从流量控制、数据一致性、系统容错三个核心维度构建方案,结合分层架构和技术手段解决高并发场景下的库存准确性与系统稳定性问题。以下是具体解决方案:

一、整体架构设计(分层抗冲击)

采用“流量削峰→核心逻辑隔离→数据分层存储”的架构,将10万QPS的瞬时流量逐层过滤,最终只让有效请求到达数据库。整体架构分为5层:

用户端 → 接入层 → 应用层 → 缓存层 → 数据库层

1. 接入层:拦截无效流量,控制入口

  • 限流熔断
    用Nginx作为接入层,通过limit_req模块设置单机限流(如每台Nginx限制1万QPS),10台应用服务器总限流10万QPS(与预期峰值匹配)。超过阈值的请求直接返回“系统繁忙”,避免冲击后端。
    示例配置:limit_req zone=seckill burst=2000 nodelay;(允许2000个突发请求排队,超过则拒绝)。

  • 用户身份初步校验
    拦截未登录用户(通过Cookie/Token验证),避免匿名请求浪费资源;同时对请求IP进行初步频率限制(如单IP 10秒内最多5次请求),拦截简单脚本刷单。

2. 应用层:核心逻辑隔离,异步化处理

  • 服务隔离
    将秒杀服务独立部署(与其他业务服务物理隔离),使用单独的线程池(如核心线程数80,最大线程数160),避免被其他业务耗尽资源。通过Spring Cloud的Hystrix实现线程池隔离,防止级联失败。

  • 请求过滤与排队

  • 对通过接入层的请求,先检查用户是否已抢购(基于Redis的set结构,user_seckill:{goodsId}存储已抢购用户ID,O(1)判断),已抢购用户直接返回“重复抢购”。
  • 未抢购用户的请求放入Redis队列(如seckill_queue:{goodsId}),队列长度上限设为库存数(1000)+ 冗余量(如200),超过则直接返回“已抢完”(提前拦截,减少后续处理)。

3. 缓存层:库存操作前置,减轻DB压力

  • 库存预热
    活动开始前10分钟,将商品库存(1000件)从数据库加载到Redis,用seckill_stock:{goodsId}存储(值为1000)。同时设置Redis过期时间(如2小时),避免缓存永久有效。

  • Redis原子扣减
    Lua脚本执行库存扣减(保证原子性),逻辑为:

-- 检查库存是否充足
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) <= 0 then
    return 0  -- 库存不足,返回失败
end
-- 扣减库存
redis.call('decr', KEYS[1])
-- 记录用户已抢购
redis.call('sadd', KEYS[2], ARGV[1])
return 1  -- 扣减成功

说明:KEYS[1]为seckill_stock:{goodsId},KEYS[2]为user_seckill:{goodsId},ARGV[1]为用户ID。Lua脚本确保“扣减库存+记录用户”的原子性,避免并发导致的超卖。

4. 数据库层:最终一致性保障,防超卖兜底

  • 库存表设计优化
    库存表seckill_goods增加version字段(乐观锁)和stock字段,结构:
CREATE TABLE seckill_goods (
    goods_id BIGINT PRIMARY KEY,
    stock INT NOT NULL DEFAULT 0,  -- 剩余库存
    version INT NOT NULL DEFAULT 0  -- 版本号,用于乐观锁
);
  • 原子性扣减SQL
    Redis扣减成功后,通过异步线程(如Spring的@Async)将订单信息写入数据库,同时执行库存扣减:
UPDATE seckill_goods 
SET stock = stock - 1, version = version + 1 
WHERE goods_id = ? AND stock > 0 AND version = ?;

说明:

  • stock > 0确保扣减时库存充足,避免超卖;
  • version = ?实现乐观锁,防止并发更新覆盖(若版本号不匹配,说明其他线程已修改,本次更新失败);
  • 若更新影响行数为0,说明数据库库存已不足,需回滚Redis(加回库存,删除用户记录),保证最终一致性。

5. 订单处理:异步化与超时回滚

  • 异步创建订单
    Redis扣减成功后,不直接在主线程创建订单,而是将订单信息(用户ID、商品ID)发送到Kafka队列,由专门的订单服务消费并写入数据库(seckill_orders表)。主线程立即返回“抢购成功,订单生成中”,提升响应速度。

  • 超时未支付处理
    订单创建后设置15分钟支付超时,用Redis的expire设置过期键(order_expire:{orderId}),结合定时任务(如Quartz)或Redis的Key过期事件,检测超时未支付订单:

  • 数据库中更新订单状态为“已取消”;
  • 回补库存:Redis执行incr seckill_stock:{goodsId},数据库执行UPDATE seckill_goods SET stock = stock + 1 WHERE goods_id = ?
  • user_seckill:{goodsId}中移除用户ID,允许用户重新抢购(可选,根据业务规则)。

二、关键问题解决方案

1. 数据库层面:保证库存原子性与解决锁冲突

  • 原子性保证
    UPDATE ... WHERE stock > 0代替“先查后改”(SELECT stock; UPDATE stock = stock - 1),避免“查库存时足够,但更新时已被抢光”的超卖问题(“查改”非原子操作,高并发下必现超卖)。

  • 解决锁冲突

  • MySQL的InnoDB引擎对UPDATE语句会加行锁(where条件带主键goods_id,锁粒度为行级),避免锁表;
  • 乐观锁(version)减少锁等待:若并发更新冲突,失败的请求通过Redis回滚,无需等待锁释放,适合高并发场景(悲观锁会导致线程阻塞,降低吞吐量)。
  • 防幻读
    秒杀场景中库存是单条记录(goods_id唯一),不存在“读取范围数据”的情况,幻读不影响;若需批量操作,可加表锁(LOCK TABLES seckill_goods WRITE),但会降低性能,不推荐。

2. 缓存与数据库一致性保证

  • 更新策略
    采用“先更新数据库,再更新缓存”的最终一致性方案:
  1. Redis扣减成功后,异步更新数据库库存;
  2. 数据库更新成功后,删除Redis缓存(而非直接更新),下次查询时从数据库加载最新库存(避免缓存与DB不一致)。
  • 不一致修复
    定时任务(如每1分钟)对比Redis库存与数据库库存,若不一致则以数据库为准同步到Redis(解决网络延迟、异步失败等极端情况)。

3. 防止恶意抢购(脚本刷单、多账号)

  • 用户唯一性校验
  • 绑定手机号/身份证,限制“同一身份信息最多1单”(数据库user_info表关联身份信息,下单时校验);
  • Redis的user_seckill:{goodsId}集合记录已抢购用户ID,O(1)判断重复请求。
  • 行为验证码
    抢购按钮点击前弹出滑块验证码(如极验),前端验证通过后才允许发送请求,拦截无交互的脚本。

  • 设备指纹
    收集用户设备信息(浏览器指纹、手机IMEI哈希),通过Redis记录device_seckill:{goodsId},限制单设备最多1单,防止同一设备多账号抢购。

4. 数据库宕机的容错处理

  • 缓存临时接管
    数据库宕机时,Redis仍可正常处理库存扣减(基于内存操作),并将订单信息暂存到Kafka(持久化消息队列),保证请求不丢失。

  • 恢复后数据同步
    数据库恢复后,Kafka消费者重新消费未处理的订单消息,执行数据库库存扣减和订单创建;同时通过定时任务对比Redis与数据库的库存差异,补全未同步的数据。

  • 降级策略
    若数据库长时间宕机,触发降级:关闭抢购入口,返回“活动临时维护”,避免用户长时间等待。

三、性能与用户体验优化

  1. 前端优化
  • 活动开始前显示倒计时,前端预加载静态资源(按钮、图标);
  • 抢购按钮点击后立即置灰,防止重复提交;
  • 用WebSocket/长轮询实时推送库存状态(如“剩余200件”),减少无效刷新。
  1. 后端优化
  • Redis使用集群模式(3主3从),分担读写压力,避免单点故障;
  • 数据库主从分离,读库存操作走从库,写操作走主库,减少主库压力;
  • 订单表按user_id分表(如分1024张表),避免单表数据量过大导致的查询缓慢。

总结

该方案通过“接入层限流→应用层排队→缓存层预扣减→数据库层兜底”的分层设计,既保证了10万QPS下的系统稳定性,又通过Redis原子操作+数据库乐观锁解决了超卖问题。同时,结合异步化、容错机制和防刷策略,兼顾了用户体验与业务安全,可满足秒杀场景的核心需求。


网站公告

今日签到

点亮在社区的每一天
去签到