Skip to content

一、自我介绍(1.5 分钟)

面试官您好,我叫李明,有 5年 Java 后端开发经验,之前是在一家日活百万的 B2C 电商平台担任核心开发工程师。

我主要负责订单中心、购物车服务、库存管理以及秒杀系统的架构设计与开发工作。技术栈以 Spring Boot + Spring Cloud+MyBatis-Plus 为主,数据库使用 MySQL 8.0,缓存层采用 Redis 集群,消息中间件是 RabbitMQ,服务治理使用 Nacos + Sentinel,分布式事务使用的是Seata, 链路追踪用 SkyWalking。

在过去两年中,我主导了多个高并发场景的重构,比如将秒杀系统的 QPS 从 800 提升到 6500+,并实现了“零超卖”;还优化了订单创建链路,平均响应时间从 800ms 降到 200ms 以内。

我对分布式系统的设计、性能调优和稳定性保障有较深的理解,今天很荣幸能和您交流,谢谢!

二、项目背景介绍(1 分钟)

我们平台是一个综合类电商平台,SKU 超过 50 万,日均订单量约 18 万,DAU 在 80 万左右。大促期间(如双11、618),瞬时流量可达日常的 20 倍以上,尤其是秒杀活动,经常出现 5000+ QPS 的峰值请求。

系统采用微服务架构,按业务域拆分为:商品服务、库存服务、订单服务、购物车服务、用户服务、支付服务等,通过 Dubbo 进行 RPC 调用,网关层使用 Spring Cloud Gateway 统一入口。

我重点参与并主导了以下几个核心模块的开发与优化:

模块我的角色核心挑战
秒杀系统主导重构高并发、防超卖、系统稳定性
订单中心核心开发状态机、超时关闭、幂等性
购物车服务设计与实现多端同步、性能优化
库存服务深度参与扣减准确性、分布式事务

接下来,我想以秒杀系统为例,详细说明我是如何设计和落地这个高并发场景的。

三、模块详解:秒杀系统设计与实现(4 分钟)

1. 业务背景与目标

我们每月都会做一次品牌秒杀活动,某次活动预热了 1 万台某型号手机,原价 2999,秒杀价 999,限量每人限购 1 台。

业务目标

  • 支持 5000+ 并发请求;
  • 实现 0 超卖;
  • 用户抢购成功后 30 分钟内完成支付,否则释放库存;
  • 响应时间 < 500ms。

2. 架构设计原则

我们遵循“分层削峰、资源隔离、快速失败”三大原则:

  • 分层削峰:在不同层级拦截无效流量;
  • 资源隔离:秒杀独立部署,不干扰主站;
  • 快速失败:尽早拒绝非法请求,减少后端压力。

3. 具体实现方案

(1)前端 & 网关层:前置拦截

  • 按钮置灰:秒杀未开始时按钮不可点击,避免用户频繁刷新;
  • 验证码机制:活动开始瞬间弹出滑动验证码,有效拦截脚本和机器人;
  • Nginx 限流:基于 IP 限流,单 IP 每秒最多 3 次请求,使用 limit_req 模块;
  • 接口隐藏:秒杀接口 URL 动态生成(带 Token),Token 通过登录态 + 商品 ID + 时间戳生成,防止被提前抓包。

(2)服务层:缓存预减 + 内存标记

  • Redis 预减库存:

    • 活动开始前,通过定时任务将商品库存加载到 Redis,Key 为 seckill:stock:1001,Value 为库存数;

    • 扣减使用 DECR 命令,原子操作,避免超卖;

    • 示例代码:

      java
      Long result = redisTemplate.opsForValue().decrement("seckill:stock:1001");
      if (result >= 0) {
          // 扣减成功,进入下单流程
      } else {
          // 库存不足,直接返回
      }
  • 内存标记(本地缓存):

    • 使用 ConcurrentHashMap<String, Boolean> 记录商品是否已售罄,键位秒杀商品id,值为库存

    • 每次请求先查内存,若已售罄直接返回,避免频繁查 Redis;

    • 示例:

      java
      if (stockEmptyMap.get("1001")) {
          return Result.fail("已售罄");
      }

(3)异步化:消息队列削峰

  • 秒杀成功后,不立即创建订单,而是发送消息到 RabbitMQ
  • 消息内容包含:用户 ID、商品 ID、Sku_id、购买数量(我们这里要考虑一人只能买一件的问题)、秒杀价格这些关键字段;
  • 消费者服务异步处理要做的操作有:
    1. 创建订单(写数据库);
    2. 扣减真实库存(DB);
    3. 发送支付提醒短信;
    4. 更新用户秒杀记录;
  • 这样前端响应时间控制在 200ms 内,用户体验好。
  • 这里涉及到多个表的操作,我们使用分布式事务seata来实现,主要用的是AT模式(自行准备)

(4)数据库:读写分离 + 分库分表

  • 因为订单量比较大,我们做了分库分表,使用的是MyCat(也可以使用sharding-jdbc),订单表 t_orderuser_id模8 分成 8 个库,每个库再按 order_id % 8 分成 8 个表,共 64 张表;
  • 主库负责写,3 个从库负责读,通过 MyCat 中间件路由;
  • 索引优化:user_idorder_statuscreate_time 建联合索引,提升查询效率。

4. 性能压测与结果

在发布前,我们也做了性能和压力测试

  • 我们使用 JMeter 模拟 1000 个用户并发请求,持续 5 分钟;
  • 秒杀接口平均响应时间:180ms
  • 系统最大吞吐量:6500 QPS
  • 数据库 CPU 使用率 < 60%,Redis 命中率 98%;
  • 实现 0 超卖,所有订单库存准确。

四、技术问题延伸(预判并主动回答)

刚才提到我们用 Redis 预减库存,但面试官可能会问:“如果 Redis 扣减成功,但 MQ 消息丢失,导致订单没创建,怎么办?

回答:

这是一个典型的最终一致性问题。我们通过以下机制保障:

1. 消息可靠性投递

  • RocketMQ 开启 sendSync 同步发送,确保消息写入 Broker;
  • Broker 配置 同步刷盘 + 主从复制,防止宕机丢失;
  • 生产者收到 SEND_OK 才认为成功。

2. 消费者幂等性

  • 数据库层面:消费者使用 数据库唯一索引 防重:订单表 user_id + product_id 建唯一键;
  • 使用 Redis SETNX 记录处理状态,避免重复下单。
  • 或者也可以使用token机制,第一次请求产生token返回给客户端,第二次请求时带上token去验证,如果有token就消费,没有就不能消费,消费完删除token

3. 对账补偿机制

  • 每天凌晨定时跑对账任务:
    • 统计 Redis 扣减总数;
    • 统计数据库实际下单数;
    • 若有差异,自动触发补偿流程或告警通知运维。

刚才提到我们每天凌晨跑对账任务,但面试官可能会问:“你们是怎么做定时任务的呢?定时任务的设计与实现是怎么做的呢?"

是的,我们每天凌晨 2 点会执行一个全量对账任务,用于校验秒杀活动中 Redis 预减库存和数据库实际下单数量是否一致,发现差异时自动触发补偿或告警通知。

针对这个需求,我们采用了 XXL-JOB 作为分布式任务调度平台,而不是简单的 @Scheduled 或 Quartz,原因如下:

1. 为什么不用 @Scheduled
  • @Scheduled 是 Spring 提供的轻量级定时任务注解,但在分布式环境下存在重复执行问题;
  • 比如我们有 4 个订单服务实例,如果每个实例都配置了 @Scheduled(cron = "0 0 2 * * ?"),那么这个任务会被执行 4 次,导致数据重复处理;
  • 而且它缺乏可视化界面、失败重试、报警、执行日志追踪等企业级功能。
2. 为什么选择 XXL-JOB 而不是 Quartz?
  • Quartz 虽然支持持久化任务到数据库,但原生 Quartz 配置复杂,没有统一控制台;
  • XXL-JOB 是基于 Quartz 的二次封装,提供了:
    • 可视化调度平台:可以查看任务状态、执行日志、触发时间;
    • 动态调度:支持在线修改 cron 表达式,无需重启服务;
    • 失败重试 + 告警通知:任务失败后可配置重试次数,并通过企业微信/邮件通知运维;
    • 分片广播机制:支持大数据量任务的分布式处理(后面会提到);
    • 任务依赖管理:比如“先跑商品同步,再跑库存校验”。

所以我们最终选择了 XXL-JOB + MySQL 存储任务信息 + 企业微信告警集成 的方案。

3. 对账任务的具体实现流程

我们的对账任务逻辑如下:

Java
@XxlJob("seckillReconciliationJob")
public void execute() throws Exception {
    // 1. 获取昨日秒杀活动列表
    List<SeckillActivity> activities = activityService.getTodayActivities();

    for (SeckillActivity activity : activities) {
        Long productId = activity.getProductId();

        // 2. 查询 Redis 中该商品的预扣总量(通过 counter 记录)
        Long redisDeductCount = redisTemplate.opsForValue().get("seckill:deduct:total:" + productId);

        // 3. 查询数据库中该商品的实际下单成功数量
        Long dbOrderCount = orderMapper.countByProductAndStatus(productId, OrderStatus.PAID);

        // 4. 比较差异
        if (Math.abs(redisDeductCount - dbOrderCount) > 5) {  // 容忍小误差
            log.warn("对账异常:商品{},Redis扣减{},DB下单{}", productId, redisDeductCount, dbOrderCount);
            
            // 5. 触发补偿机制 or 发送告警
            alarmService.send("秒杀对账异常", String.format("商品ID:%d, 差异:%d", productId, Math.abs(redisDeductCount - dbOrderCount)));
        }
    }
}
  • 我们在每次 Redis 扣减时,会同步累加一个 seckill:deduct:total:1001 的计数器,便于对账;
  • 数据库查询走索引:product_id + order_status 联合索引,避免全表扫描。

4. 如何应对大数据量?——使用 分片广播机制

当我们的秒杀活动越来越多,单机查询所有商品对账数据压力很大。于是我们引入了 XXL-JOB 的 分片广播 功能。

  • 在调度平台配置任务为“广播模式”,并设置 4 个分片(对应 4 个服务实例);
  • 每个实例启动时知道自己是第几个分片(XxlJobContext.getShardIndex())和总分片数(XxlJobContext.getShardTotal());
  • 查询时按 product_id % shardTotal == shardIndex 分片处理:
java
int shardIndex = XxlJobContext.get().getShardIndex();
int shardTotal = XxlJobContext.get().getShardTotal();

List<Product> products = productMapper.selectByShard(shardIndex, shardTotal); // 分片查询

这样原来 1 个任务处理 1000 个商品,现在变成 4 个实例各处理 ~250 个,并行执行,提升效率


5. 稳定性保障措施

为了确保定时任务稳定运行,我们做了以下几点:

措施说明
执行超时控制设置任务超时时间为 30 分钟,防止卡死
失败重试 3 次网络抖动或数据库慢查询时自动重试
企业微信告警任务失败、对账异常都实时通知到运维群
执行日志持久化XXL-JOB 自动记录每次执行日志,便于排查
灰度发布机制新任务先在测试环境验证,再上线

6. 当然也有一些替代方案对比(体现技术选型能力)
方案优点缺点适用场景
@Scheduled简单易用分布式重复执行单机小项目
Quartz支持持久化无控制台,配置复杂已有系统集成
XXL-JOB有控制台、分片、告警需额外部署中大型分布式系统 ✅
Elastic-Job功能强大,当当开源学习成本高大厂复杂场景
Kubernetes CronJob云原生友好不适合复杂逻辑容器化环境

所以我们选择 XXL-JOB 是在开发效率、维护成本、功能完整性之间的平衡。

另一个常见问题是:“如何防止用户重复秒杀?

回答:

我们做了三重防护:

  1. 前端限制:按钮点击后置灰,30 秒内不可重复提交;
  2. Redis 标记seckill:uid:1001,用户抢过该商品后写入,有效期与活动一致;
  3. 数据库唯一索引:订单表 user_id + product_id 唯一,防止重复下单。