目录

分享万人级秒杀系统,如何设计高并发、防超卖、低成本落地方案

一、系统架构设计(分层削峰,低成本起步,高扩展迭代)

1. 分层架构(层层拦截,逐级减负,最小化资源消耗)

2. 具体组件(各司其职,开源为主,成本可控)

二、核心问题解决方案(完整Java代码,防超卖+高并发+低成本)

1. 超卖问题(秒杀核心痛点,三种Java方案兜底,兼顾性能与成本)

解决方案一:Redis原子操作(首选,高性能+低成本,开源Redis即可支撑)

解决方案二:数据库乐观锁(最终一致性兜底,无额外部署成本)

解决方案三:预扣库存(高性能+异步同步,兼顾成本与一致性)

2. 高并发请求处理(低成本削峰,避免资源浪费)

2.1 流量削峰(基于开源MQ,零额外成本,高效缓冲)

2.2 分层过滤(逐级拦截无效请求,最小化资源消耗,低成本核心)

3. 系统性能优化(高性能+低成本,鱼和熊掌兼得)

3.1 多级缓存策略(本地缓存+Redis,降低分布式缓存成本,提升性能)

3.2 读多写少优化(缓存预热+读写分离,降低数据库成本)

4. 详细实现方案(完整Java流程,低成本落地)

4.1 完整秒杀流程(全链路Java实现,层层优化,成本可控)

4.2 库存同步与对账方案(低成本兜底,保证数据一致性,避免损失)

三、高可用保障(低成本方案,避免服务崩溃,减少损失)

1. 限流降级策略(开源组件,零额外成本,保护系统不被压垮)

2. 熔断降级(Resilience4j,开源免费,低成本,避免服务级联崩溃)

四、监控与告警(开源组件,低成本,及时发现问题)

1. 关键监控指标(全链路监控,掌握系统状态,降低故障排查成本)

2. 监控实现(基于Micrometer+Prometheus+Grafana,低成本落地)

五、部署与扩展(弹性伸缩,低成本扩容,避免资源闲置)

1. 弹性扩展策略(按需扩容,兼顾性能与成本,核心原则:「峰谷适配」)

2. 压测方案(提前验证,规避线上风险,降低故障损失成本)

压测场景(覆盖核心场景,低成本,高效验证)

压测目标(兼顾性能与成本,合理设定,避免过度优化)

压测工具与成本优化

六、安全考虑(低成本方案,避免恶意攻击,减少经济损失)

总结要点(高性能+低成本,落地核心)

面试回答精简版(突出低成本+高可用,贴合企业实际需求)


✨ 作为Java后端高频面试天花板、电商核心场景的秒杀系统,不仅要搞定万人同时抢购的高并发,还要解决超卖、性能瓶颈,更要兼顾企业级低成本落地!今天这份全攻略,从架构设计到完整Java代码,从性能优化到成本控制,手把手教你搭建可落地、可扩展、低成本的秒杀系统,收藏这篇就够了!

一、系统架构设计(分层削峰,低成本起步,高扩展迭代)

1. 分层架构(层层拦截,逐级减负,最小化资源消耗)

客户端层 → 接入层 → 业务服务层 → 数据层 ↓ ↓ ↓ ↓ 前端防刷 缓存限流 队列异步 分库分表(按需启用)

设计亮点:低成本优先,非核心环节不投入昂贵资源,比如接入层优先用Nginx(开源免费),而非商业网关;缓存优先用Redis Cluster(开源),按需扩容节点,避免初期过度部署。

2. 具体组件(各司其职,开源为主,成本可控)

层级组件选择核心作用成本优化点
客户端静态资源CDN(阿里云/腾讯云按量付费)、前端原生校验减轻后端压力,拦截无效请求静态资源提前预热到边缘节点,按量付费避免闲置成本
接入层Nginx+Lua/OpenResty(开源)、Sentinel(开源)第一层限流、静态缓存、请求转发、恶意拦截单台Nginx可支撑10万+QPS,初期无需集群,按需扩容
业务层秒杀服务集群(Spring Boot/Spring Cloud)、RocketMQ(开源)、Redis Cluster(开源)、Caffeine(开源)核心业务处理、流量削峰、库存原子操作、热点缓存服务无状态化,初期单节点部署,峰值时临时扩容;MQ单机模式起步,高并发时再集群
数据层MySQL主从(开源)、MyCat(开源,分库分表按需启用)数据持久化、读写分离、缓解单库压力初期无需分库分表,主从架构满足需求;只读节点按需添加,降低主库压力同时控制成本

成本核心原则:按需部署,弹性伸缩,初期用最小资源满足业务需求,峰值时临时扩容,峰值过后缩容,避免闲置资源浪费。

二、核心问题解决方案(完整Java代码,防超卖+高并发+低成本)

1. 超卖问题(秒杀核心痛点,三种Java方案兜底,兼顾性能与成本)

超卖的本质是并发场景下库存操作的原子性无法保证,以下三种方案从高性能到兜底,既杜绝超卖,又最大化控制部署成本。

解决方案一:Redis原子操作(首选,高性能+低成本,开源Redis即可支撑)

利用Redis单线程特性,通过Lua脚本打包「查询库存+扣减库存+库存下限判断」操作,保证原子性,杜绝并发超卖。相比Redis分布式锁,Lua脚本无需额外维护锁资源,更轻量、更低成本(减少Redis交互次数,降低资源消耗)。

@Component
public class RedisStockService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 定义Lua脚本,保证库存扣减原子性
    private static final String STOCK_DEDUCT_LUA_SCRIPT = """
            local stockKey = KEYS[1]
            local deductCount = tonumber(ARGV[1])
            -- 获取当前库存
            local currentStock = tonumber(redis.call('GET', stockKey))
            if not currentStock or currentStock < deductCount then
                -- 库存不足,返回0
                return 0
            end
            -- 库存充足,扣减库存
            redis.call('DECRBY', stockKey, deductCount)
            -- 返回扣减后的库存
            return tonumber(redis.call('GET', stockKey))
            """;

    /**
     * 原子扣减Redis库存
     * @param productId 商品ID
     * @param deductCount 扣减数量(默认1)
     * @return 扣减结果:true-成功,false-失败
     */
    public boolean deductStock(String productId, int deductCount) {
        if (deductCount <= 0) {
            return false;
        }
        String stockKey = "seckill:stock:" + productId;
        // 执行Lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(STOCK_DEDUCT_LUA_SCRIPT);
        redisScript.setResultType(Long.class);

        // 传入参数:KEYS[1] = 库存key,ARGV[1] = 扣减数量
        Long result = stringRedisTemplate.execute(
                redisScript,
                Collections.singletonList(stockKey),
                String.valueOf(deductCount)
        );

        // result >= 0 说明扣减成功(库存充足),result == null 说明库存key不存在
        return result != null && result >= 0;
    }

    /**
     * 初始化商品Redis库存(缓存预热使用,低成本初始化)
     */
    public boolean initStock(String productId, int totalStock) {
        if (totalStock < 0) {
            return false;
        }
        String stockKey = "seckill:stock:" + productId;
        // setnx保证只有一个线程初始化库存,避免重复写入,降低Redis资源消耗
        Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(
                stockKey,
                String.valueOf(totalStock),
                24,
                TimeUnit.HOURS
        );
        // 若库存已存在,返回true(无需重复初始化)
        return setIfAbsent != null && (setIfAbsent || stringRedisTemplate.hasKey(stockKey));
    }
}
解决方案二:数据库乐观锁(最终一致性兜底,无额外部署成本)

无需额外引入中间件,基于现有MySQL即可实现,作为Redis方案的兜底,保证数据最终一致性,零额外部署成本,适合低并发场景或小型电商秒杀。

@Service
@Transactional(rollbackFor = Exception.class)
public class DbStockService {
    @Autowired
    private SeckillProductMapper seckillProductMapper;

    /**
     * 乐观锁扣减数据库库存
     * @param productId 商品ID
     * @param deductCount 扣减数量
     * @return 扣减结果:true-成功,false-失败
     */
    public boolean deductStockByOptimisticLock(String productId, int deductCount) {
        if (deductCount <= 0) {
            return false;
        }
        // 1. 查询商品当前信息(获取版本号和库存)
        SeckillProduct product = seckillProductMapper.selectById(productId);
        if (product == null || product.getStock() < deductCount || product.getStatus() != 1) {
            return false;
        }
        // 2. 乐观锁更新库存(版本号匹配才更新)
        int affectedRows = seckillProductMapper.updateStockByOptimisticLock(
                productId,
                deductCount,
                product.getVersion(),
                product.getStock()
        );
        // 3. 受影响行数>0说明更新成功
        return affectedRows > 0;
    }
}

对应的Mapper XML(MyBatis):

<update id="updateStockByOptimisticLock">
    UPDATE t_seckill_product
    SET stock = stock - #{deductCount},
        version = version + 1,
        update_time = NOW()
    WHERE id = #{productId}
      AND stock = #{currentStock}
      AND version = #{version}
      AND status = 1
</update>
解决方案三:预扣库存(高性能+异步同步,兼顾成本与一致性)

先在Redis中预扣库存(高性能),再通过消息队列异步同步到数据库(降低数据库瞬时压力),无需让用户等待数据库操作,既提升用户体验,又减少数据库资源消耗(低成本),是高并发秒杀的主流方案。

@Component
public class PreDeductStockService {
    @Autowired
    private RedisStockService redisStockService;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 秒杀库存同步MQ主题(统一主题,降低MQ集群部署成本)
    private static final String SECKILL_STOCK_SYNC_TOPIC = "seckill-stock-sync-topic";

    /**
     * 预扣Redis库存,异步同步到数据库
     * @param productId 商品ID
     * @param userId 用户ID
     * @return 扣减结果:true-成功,false-失败
     */
    public SeckillResult preDeductStock(String productId, String userId) {
        // 1. 预扣Redis库存(扣减1件,默认秒杀每人限购1件)
        boolean deductSuccess = redisStockService.deductStock(productId, 1);
        if (!deductSuccess) {
            // 库存不足,标记商品售罄(JVM本地缓存,避免重复访问Redis,降低成本)
            markProductSoldOut(productId);
            return SeckillResult.fail("商品已售罄,秒杀失败");
        }

        // 2. 生成秒杀订单前置信息(雪花算法生成唯一订单号,无额外中间件,低成本)
        String orderNo = SnowflakeUtil.generateOrderNo();
        SeckillStockSyncMessage syncMessage = SeckillStockSyncMessage.builder()
                .orderNo(orderNo)
                .productId(productId)
                .userId(userId)
                .deductCount(1)
                .createTime(new Date())
                .build();

        // 3. 发送MQ消息,异步同步到数据库(OneWay模式,无需等待响应,提升性能,降低MQ资源消耗)
        try {
            rocketMQTemplate.sendOneWay(
                    SECKILL_STOCK_SYNC_TOPIC,
                    MessageBuilder.withPayload(JSON.toJSONString(syncMessage)).build()
            );
        } catch (Exception e) {
            // MQ发送失败,回滚Redis库存(避免数据不一致)
            redisStockService.deductStock(productId, -1);
            return SeckillResult.fail("系统繁忙,请稍后再试");
        }

        // 4. 返回排队中结果,前端轮询订单状态
        return SeckillResult.processing(orderNo, "已进入排队,正在为您生成订单");
    }

    /**
     * JVM本地缓存标记商品售罄(无需访问Redis,降低Redis压力和成本)
     */
    private final Map<String, Boolean> soldOutMarkMap = new ConcurrentHashMap<>();
    private void markProductSoldOut(String productId) {
        soldOutMarkMap.put(productId, true);
    }

    /**
     * 检查商品是否已售罄(优先查本地缓存,低成本高性能)
     */
    public boolean isProductSoldOut(String productId) {
        return soldOutMarkMap.getOrDefault(productId, false);
    }
}

配套MQ消费者(异步同步数据库,削峰填谷,降低数据库压力):

@Component
@RocketMQMessageListener(
        topic = "seckill-stock-sync-topic",
        consumerGroup = "seckill-stock-sync-consumer-group",
        consumeMode = ConsumeMode.ORDERLY, // 按商品ID有序消费,避免同一商品库存更新冲突
        messageModel = MessageModel.CLUSTERING
)
public class SeckillStockSyncConsumer implements RocketMQListener<String> {
    @Autowired
    private DbStockService dbStockService;
    @Autowired
    private SeckillOrderMapper seckillOrderMapper;
    @Slf4j
    @Override
    public void onMessage(String message) {
        try {
            // 1. 解析消息
            SeckillStockSyncMessage syncMessage = JSON.parseObject(message, SeckillStockSyncMessage.class);
            // 2. 乐观锁扣减数据库库存
            boolean dbDeductSuccess = dbStockService.deductStockByOptimisticLock(
                    syncMessage.getProductId(),
                    syncMessage.getDeductCount()
            );
            if (!dbDeductSuccess) {
                log.error("订单{}同步数据库库存失败,库存不足", syncMessage.getOrderNo());
                // 可记录死信队列,后续人工对账,避免数据丢失
                return;
            }
            // 3. 生成正式订单(状态为未支付)
            SeckillOrder order = SeckillOrder.builder()
                    .orderNo(syncMessage.getOrderNo())
                    .productId(syncMessage.getProductId())
                    .userId(syncMessage.getUserId())
                    .payStatus(0) // 0-未支付,1-已支付
                    .createTime(syncMessage.getCreateTime())
                    .build();
            seckillOrderMapper.insert(order);
            log.info("订单{}生成成功,库存同步完成", syncMessage.getOrderNo());
        } catch (Exception e) {
            log.error("秒杀库存同步失败,消息内容:{}", message, e);
            // 抛出异常,让MQ重试(默认重试3次,避免单次异常导致数据不一致)
            throw new RuntimeException(e);
        }
    }
}

2. 高并发请求处理(低成本削峰,避免资源浪费)

2.1 流量削峰(基于开源MQ,零额外成本,高效缓冲)

秒杀的核心痛点是「瞬间海量并发」,直接冲击后端服务会导致资源耗尽、服务崩溃。利用开源RocketMQ做流量缓冲,将「瞬间爆发流量」转化为「平稳串行流量」,后端服务按需消费,既保证服务稳定,又避免资源闲置浪费(低成本核心)。

成本优化点:初期采用RocketMQ单机模式,支撑10万+消息堆积,无需集群部署;峰值时临时扩容Broker节点,峰值过后缩容,降低运维和服务器成本。

完整Java代码已在「预扣库存」方案中体现,补充核心设计思路:

  1. 用户请求到达后,仅做简单校验,立即异步入队,不阻塞用户请求(提升用户体验)

  2. 后端消费者按自身处理能力消费消息(可配置消费线程数),避免被压垮

  3. 采用有序消费(按商品ID分片),避免同一商品库存更新冲突,无需额外分布式锁(降低成本)

2.2 分层过滤(逐级拦截无效请求,最小化资源消耗,低成本核心)

从入口到核心业务,层层过滤无效请求,将百万级请求压缩至后端可承受范围,减少不必要的资源占用,降低服务器、Redis、数据库的负载成本。

@Component
public class SeckillRequestFilterService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private BloomFilter<String> seckillProductBloomFilter;

    // 1. 第一层:用户请求频率限制(Redis计数器,开源免费,低成本)
    private static final String USER_REQUEST_LIMIT_KEY = "seckill:limit:user:";
    private static final int USER_REQUEST_LIMIT = 10; // 每分钟最多10次请求
    private static final int LIMIT_EXPIRE_SECONDS = 60;
    public boolean checkUserRequestLimit(String userId) {
        String limitKey = USER_REQUEST_LIMIT_KEY + userId;
        // Redis自增计数器,原子操作,无需额外锁
        Long increment = stringRedisTemplate.opsForValue().increment(limitKey);
        if (increment == 1) {
            // 第一次请求,设置过期时间
            stringRedisTemplate.expire(limitKey, LIMIT_EXPIRE_SECONDS, TimeUnit.SECONDS);
        }
        // 超过限制,返回false
        return increment != null && increment <= USER_REQUEST_LIMIT;
    }

    // 2. 第二层:布隆过滤器快速判断商品是否存在(Caffeine实现,本地缓存,零部署成本)
    public boolean checkProductExist(String productId) {
        // 布隆过滤器快速判断,误判率可控制,无需访问数据库/Redis,低成本高性能
        return seckillProductBloomFilter.contains(productId);
    }

    // 3. 第三层:商品售罄标记判断(JVM本地缓存,零成本)
    @Autowired
    private PreDeductStockService preDeductStockService;
    public boolean checkProductSoldOut(String productId) {
        return !preDeductStockService.isProductSoldOut(productId);
    }

    // 4. 第四层:商品秒杀状态校验(Redis缓存,避免访问数据库,降低成本)
    private static final String PRODUCT_STATUS_KEY = "seckill:product:status:";
    public boolean checkProductStatus(String productId) {
        String statusKey = PRODUCT_STATUS_KEY + productId;
        String status = stringRedisTemplate.opsForValue().get(statusKey);
        // 1-秒杀中,0-未开始,2-已结束
        return "1".equals(status);
    }

    /**
     * 全链路请求过滤(逐级拦截,返回第一个不通过的结果)
     */
    public SeckillResult filterRequest(String userId, String productId) {
        // 1. 用户请求频率限制
        if (!checkUserRequestLimit(userId)) {
            return SeckillResult.fail("请求过于频繁,请1分钟后再试");
        }
        // 2. 商品是否存在
        if (!checkProductExist(productId)) {
            return SeckillResult.fail("秒杀商品不存在");
        }
        // 3. 商品是否已售罄
        if (!checkProductSoldOut(productId)) {
            return SeckillResult.fail("商品已售罄,秒杀失败");
        }
        // 4. 商品秒杀是否正在进行
        if (!checkProductStatus(productId)) {
            return SeckillResult.fail("商品秒杀未开始或已结束");
        }
        // 过滤通过
        return SeckillResult.success("请求合法,进入秒杀流程");
    }
}

分层过滤效果(资源消耗逐级递减,成本可控): 所有请求 → 用户频率过滤 → 商品存在过滤 → 售罄状态过滤 → 秒杀状态过滤 → 实际下单 ↓ ↓ ↓ ↓ ↓ ↓ 100万 80万 50万 10万 5万 1万

成本优化点:优先使用本地缓存(Caffeine、JVM Map)和开源中间件(Redis、布隆过滤器),无需商业组件,减少服务器部署数量,降低硬件和运维成本。

3. 系统性能优化(高性能+低成本,鱼和熊掌兼得)

3.1 多级缓存策略(本地缓存+Redis,降低分布式缓存成本,提升性能)

采用「JVM本地缓存(Caffeine)→ Redis集群 → 数据库」的多级缓存架构,优先使用本地缓存,减少Redis访问次数,降低Redis集群压力和部署成本,同时兼顾性能与数据一致性。

@Component
public class SeckillMultiLevelCacheService {
    // 一级缓存:JVM本地缓存(Caffeine,开源免费,高性能,零部署成本)
    private final Cache<String, SeckillProduct> localCache = Caffeine.newBuilder()
            .maximumSize(1000) // 缓存1000个热点商品,足够支撑大部分秒杀场景
            .expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期,保证数据新鲜度
            .build();

    // 二级缓存:Redis集群(开源,按需扩容)
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private static final String PRODUCT_DETAIL_KEY = "seckill:product:detail:";

    // 三级缓存:数据库(最终一致性兜底)
    @Autowired
    private SeckillProductMapper seckillProductMapper;

    /**
     * 获取秒杀商品详情(多级缓存查询,优先本地缓存,低成本高性能)
     */
    public SeckillProduct getSeckillProductDetail(String productId) {
        // 1. 查询一级缓存:JVM本地缓存(无网络开销,高性能,零成本)
        SeckillProduct product = localCache.getIfPresent(productId);
        if (product != null) {
            return product;
        }

        // 2. 查询二级缓存:Redis(分布式缓存,支撑高并发,低成本)
        String productJson = stringRedisTemplate.opsForValue().get(PRODUCT_DETAIL_KEY + productId);
        if (productJson != null && !productJson.isEmpty()) {
            product = JSON.parseObject(productJson, SeckillProduct.class);
            // 写入本地缓存,提升下次查询性能
            localCache.put(productId, product);
            return product;
        }

        // 3. 查询三级缓存:数据库(最终兜底,仅当缓存未命中时访问,降低数据库压力)
        product = seckillProductMapper.selectById(productId);
        if (product == null) {
            return null;
        }

        // 4. 写入二级缓存和一级缓存,预热下次查询
        stringRedisTemplate.opsForValue().set(
                PRODUCT_DETAIL_KEY + productId,
                JSON.toJSONString(product),
                1,
                TimeUnit.HOURS
        );
        localCache.put(productId, product);

        return product;
    }
}

成本优化点:本地缓存承载大部分热点商品查询,减少Redis的网络开销和访问压力,使得Redis集群无需大规模部署,初期单节点即可支撑高并发,降低硬件成本。

3.2 读多写少优化(缓存预热+读写分离,降低数据库成本)

秒杀场景属于典型的「读多写少」,活动开始前提前将热点商品数据加载到缓存,避免活动期间大量请求穿透到数据库;同时采用MySQL主从架构,读写分离,降低主库压力,减少数据库扩容成本。

@Service
public class SeckillCacheWarmUpService {
    @Autowired
    private SeckillProductMapper seckillProductMapper;
    @Autowired
    private RedisStockService redisStockService;
    @Autowired
    private SeckillMultiLevelCacheService multiLevelCacheService;
    @Autowired
    private BloomFilter<String> seckillProductBloomFilter;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 缓存预热(项目启动后执行,或秒杀活动开始前30分钟定时执行,零人工成本)
    @PostConstruct
    @Scheduled(cron = "0 30 * * * ?") // 每小时30分执行一次,保证缓存新鲜度
    public void warmUpSeckillCache() {
        log.info("开始执行秒杀商品缓存预热");
        // 1. 查询即将开始或正在进行的秒杀商品(从MySQL从库查询,降低主库压力,低成本)
        List<SeckillProduct> seckillProducts = seckillProductMapper.selectSeckillProducts();
        if (seckillProducts.isEmpty()) {
            log.info("暂无秒杀商品需要缓存预热");
            return;
        }

        // 2. 批量预热缓存(减少单次Redis/数据库交互,降低资源消耗)
        for (SeckillProduct product : seckillProducts) {
            String productId = product.getId();
            // 2.1 预热Redis库存
            redisStockService.initStock(productId, product.getStock());
            // 2.2 预热商品详情缓存(自动写入Redis和本地缓存)
            multiLevelCacheService.getSeckillProductDetail(productId);
            // 2.3 预热布隆过滤器(商品ID)
            seckillProductBloomFilter.add(productId);
            // 2.4 预热商品秒杀状态
            stringRedisTemplate.opsForValue().set(
                    "seckill:product:status:" + productId,
                    String.valueOf(product.getStatus()),
                    24,
                    TimeUnit.HOURS
            );
        }

        log.info("秒杀商品缓存预热完成,共预热{}个商品", seckillProducts.size());
    }
}

成本优化点:

  1. 从MySQL从库查询商品数据,避免冲击主库,主库仅负责写入操作,无需高频扩容

  2. 批量预热缓存,减少Redis和数据库的交互次数,降低网络开销和资源消耗

  3. 定时自动预热,无需人工干预,降低运维成本

4. 详细实现方案(完整Java流程,低成本落地)

4.1 完整秒杀流程(全链路Java实现,层层优化,成本可控)
@Service
public class SeckillCoreService {
    @Autowired
    private SeckillRequestFilterService requestFilterService;
    @Autowired
    private PreDeductStockService preDeductStockService;
    @Autowired
    private SeckillMultiLevelCacheService multiLevelCacheService;
    @Slf4j

    /**
     * 完整秒杀处理流程
     * @param userId 用户ID
     * @param productId 商品ID
     * @return 秒杀结果
     */
    public SeckillResult processSeckill(String userId, String productId) {
        long startTime = System.currentTimeMillis();
        try {
            // 第一步:全链路请求过滤(逐级拦截无效请求,降低后续资源消耗)
            SeckillResult filterResult = requestFilterService.filterRequest(userId, productId);
            if (!filterResult.isSuccess()) {
                return filterResult;
            }

            // 第二步:获取商品详情(多级缓存,低成本高性能)
            SeckillProduct product = multiLevelCacheService.getSeckillProductDetail(productId);
            if (product == null) {
                return SeckillResult.fail("秒杀商品不存在");
            }

            // 第三步:预扣Redis库存,异步同步数据库(核心步骤,防超卖+低成本)
            return preDeductStockService.preDeductStock(productId, userId);
        } catch (Exception e) {
            log.error("秒杀处理异常,userId={}, productId={}", userId, productId, e);
            return SeckillResult.fail("系统繁忙,请稍后再试");
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            // 记录处理耗时(监控用,便于后续优化成本和性能)
            log.info("秒杀请求处理完成,userId={}, productId={}, 耗时={}ms", userId, productId, costTime);
        }
    }
}

配套控制层(轻量实现,避免复杂逻辑,降低资源消耗):

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    @Autowired
    private SeckillCoreService seckillCoreService;
    @Autowired
    private SeckillOrderMapper seckillOrderMapper;

    // 秒杀请求接口(POST方式,防止GET请求缓存,轻量实现)
    @PostMapping("/process")
    public SeckillResult processSeckill(
            @RequestParam String userId,
            @RequestParam String productId) {
        // 简单参数校验(避免无效请求进入核心流程)
        if (userId == null || userId.isEmpty() || productId == null || productId.isEmpty()) {
            return SeckillResult.fail("参数不能为空");
        }
        return seckillCoreService.processSeckill(userId, productId);
    }

    // 订单状态查询接口(前端轮询,轻量查询,优先Redis缓存)
    @GetMapping("/order/query")
    public SeckillResult queryOrderStatus(@RequestParam String orderNo) {
        if (orderNo == null || orderNo.isEmpty()) {
            return SeckillResult.fail("订单号不能为空");
        }
        // 优先查询Redis缓存(订单状态,降低数据库压力)
        String orderStatus = stringRedisTemplate.opsForValue().get("seckill:order:status:" + orderNo);
        if (orderStatus != null) {
            return "1".equals(orderStatus) ? SeckillResult.success("秒杀成功,订单已生成,请尽快支付")
                    : SeckillResult.processing(orderNo, "正在排队中,请稍后再查");
        }
        // 查询数据库
        SeckillOrder order = seckillOrderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            return SeckillResult.fail("订单不存在或秒杀失败");
        }
        // 写入Redis缓存,提升下次查询性能
        stringRedisTemplate.opsForValue().set(
                "seckill:order:status:" + orderNo,
                String.valueOf(order.getPayStatus()),
                1,
                TimeUnit.HOURS
        );
        return order.getPayStatus() == 1 ? SeckillResult.success("秒杀成功,订单已生成,请尽快支付")
                : SeckillResult.processing(orderNo, "订单已生成,请尽快支付");
    }
}
4.2 库存同步与对账方案(低成本兜底,保证数据一致性,避免损失)

由于采用异步处理,可能出现Redis库存与数据库库存不一致的情况(如MQ消息丢失、消费者异常),通过定时对账任务兜底,无需额外中间件,基于现有Redis和数据库实现,零额外部署成本,保证数据一致性,避免企业经济损失。

@Component
@Slf4j
public class SeckillStockReconciliationService {
    @Autowired
    private SeckillProductMapper seckillProductMapper;
    @Autowired
    private RedisStockService redisStockService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 每5分钟执行一次库存对账(低频率,降低数据库和Redis压力,低成本)
    @Scheduled(cron = "0 */5 * * * ?")
    public void stockReconciliation() {
        log.info("开始执行秒杀库存对账任务");
        // 1. 查询所有秒杀商品
        List<SeckillProduct> seckillProducts = seckillProductMapper.selectSeckillProducts();
        if (seckillProducts.isEmpty()) {
            log.info("暂无秒杀商品需要对账");
            return;
        }

        // 2. 逐一对账Redis库存与数据库库存
        for (SeckillProduct product : seckillProducts) {
            String productId = product.getId();
            // 2.1 获取Redis库存
            String redisStockStr = stringRedisTemplate.opsForValue().get("seckill:stock:" + productId);
            Integer redisStock = redisStockStr == null ? 0 : Integer.parseInt(redisStockStr);
            // 2.2 获取数据库库存
            Integer dbStock = product.getStock();
            // 2.3 对比库存(允许少量偏差,避免频繁修复)
            if (Math.abs(redisStock - dbStock) > 0) {
                log.warn("库存不一致,productId={}, Redis库存={}, 数据库库存={}",
                        productId, redisStock, dbStock);
                // 3. 库存修复(以数据库库存为准,保证数据最终一致性)
                fixStockInconsistency(productId, dbStock);
            }
        }

        log.info("秒杀库存对账任务执行完成");
    }

    /**
     * 修复库存不一致(低成本,无需人工干预)
     */
    private void fixStockInconsistency(String productId, Integer dbStock) {
        // 3.1 更新Redis库存,与数据库保持一致
        stringRedisTemplate.opsForValue().set("seckill:stock:" + productId, String.valueOf(dbStock));
        // 3.2 重置售罄标记(若数据库库存>0,清除售罄标记)
        if (dbStock > 0) {
            preDeductStockService.soldOutMarkMap.remove(productId);
        } else {
            preDeductStockService.markProductSoldOut(productId);
        }
        // 3.3 记录对账日志,便于后续追溯
        log.info("库存修复完成,productId={}, 修复后库存={}", productId, dbStock);
    }
}

三、高可用保障(低成本方案,避免服务崩溃,减少损失)

1. 限流降级策略(开源组件,零额外成本,保护系统不被压垮)

采用「多维度限流+降级兜底」,基于开源Sentinel和Redis实现,无需商业组件,低成本高可用,精准控制流量,避免单一维度限流的局限性,同时在服务异常时快速返回兜底结果,避免资源耗尽。

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    @Autowired
    private SeckillCoreService seckillCoreService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 基于Sentinel限流(开源免费,低成本,支持多维度限流)
    @GetMapping("/process")
    @SentinelResource(value = "seckillProcess", fallback = "seckillFallback")
    public SeckillResult processSeckill(
            @RequestParam String userId,
            @RequestParam String productId) {
        if (userId == null || userId.isEmpty() || productId == null || productId.isEmpty()) {
            return SeckillResult.fail("参数不能为空");
        }
        return seckillCoreService.processSeckill(userId, productId);
    }

    // 限流降级兜底方法(快速返回,避免用户等待,降低服务压力)
    public SeckillResult seckillFallback(String userId, String productId, Throwable e) {
        log.error("秒杀请求被限流或降级,userId={}, productId={}", userId, productId, e);
        // 兜底结果:返回系统繁忙,引导用户稍后重试,无需访问核心服务,低成本
        return SeckillResult.fail("当前秒杀人数过多,请稍后再试");
    }

    // 补充:Redis分布式限流(无中间件依赖,低成本,适合小型系统)
    private static final String SECKILL_TOTAL_LIMIT_KEY = "seckill:limit:total:";
    private static final int TOTAL_REQUEST_LIMIT = 50000; // 总QPS限制5万
    public boolean checkTotalRequestLimit() {
        String limitKey = SECKILL_TOTAL_LIMIT_KEY + LocalDate.now().toString();
        Long increment = stringRedisTemplate.opsForValue().increment(limitKey);
        if (increment == 1) {
            stringRedisTemplate.expire(limitKey, 24, TimeUnit.HOURS);
        }
        return increment != null && increment <= TOTAL_REQUEST_LIMIT;
    }
}

限流规则(多维度,低成本,覆盖核心场景):

  • 用户维度:每个用户10次/分钟(防止单个用户恶意刷请求,Redis计数器实现)

  • IP维度:每个IP 1000次/分钟(防止同一网段恶意攻击,Sentinel实现)

  • 商品维度:每个商品10000次/分钟(匹配商品库存上限,Sentinel实现)

  • 总QPS:系统最大承受50000 QPS(根据服务器配置调整,Redis/Sentinel实现)

成本优化点:优先使用Redis计数器实现限流(无额外中间件),大型系统再引入Sentinel,避免初期过度部署,降低运维和服务器成本。

2. 熔断降级(Resilience4j,开源免费,低成本,避免服务级联崩溃)

使用Resilience4j替代Hystrix(轻量、开源、低成本),实现服务熔断降级,当后端服务(如Redis、数据库)出现异常或响应超时,快速返回兜底结果,防止服务被拖垮,零额外部署成本,易于集成。

@Service
public class SeckillCircuitBreakerService {
    @Autowired
    private RedisStockService redisStockService;

    // 配置熔断规则(5秒内超过10次请求失败,熔断5秒,低成本,轻量)
    private final CircuitBreaker circuitBreaker = CircuitBreaker.of("seckillStockService",
            CircuitBreakerConfig.custom()
                    .failureRateThreshold(50) // 失败率阈值50%
                    .waitDurationInOpenState(Duration.ofSeconds(5)) // 熔断状态持续5秒
                    .slidingWindowSize(10) // 滑动窗口大小10
                    .build()
    );

    /**
     * 带熔断的Redis库存扣减
     */
    public boolean deductStockWithCircuitBreaker(String productId, int deductCount) {
        // 执行熔断包装的方法
        return Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker,
                () -> redisStockService.deductStock(productId, deductCount)))
                .recover(ex -> {
                    log.error("库存扣减服务熔断,productId={}", productId, ex);
                    // 熔断兜底:返回false,标记商品售罄,避免重复请求
                    preDeductStockService.markProductSoldOut(productId);
                    return false;
                })
                .get();
    }
}

成本优化点:Resilience4j是轻量级组件,无需独立部署,直接集成在Spring Boot项目中,零额外服务器成本,运维简单。

四、监控与告警(开源组件,低成本,及时发现问题)

1. 关键监控指标(全链路监控,掌握系统状态,降低故障排查成本)

分为三个层面,覆盖系统、应用、业务,无需商业监控工具,基于开源组件实现,低成本:

  • 系统层面:QPS、响应时间(RT)、错误率、CPU/内存/磁盘使用率、网络吞吐量(Prometheus+Grafana,开源免费)

  • 应用层面:库存扣减成功率、MQ消息堆积量、缓存命中率、数据库读写耗时(Micrometer,开源免费)

  • 业务层面:抢购成功率、用户排队时长、订单生成率、支付转化率(自定义日志+ELK,开源免费)

2. 监控实现(基于Micrometer+Prometheus+Grafana,低成本落地)

通过Micrometer记录关键指标,暴露给Prometheus采集,最终在Grafana中可视化展示,异常时触发告警(邮件/钉钉),零商业成本,易于扩展。

@Component
public class SeckillMonitorService {
    private final MeterRegistry meterRegistry;

    public SeckillMonitorService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    /**
     * 记录秒杀请求关键指标(低成本,无额外资源消耗)
     */
    public void recordSeckillMetric(String productId, boolean success, long costTime) {
        // 1. 总请求数计数
        meterRegistry.counter("seckill.requests.total", "productId", productId).increment();
        // 2. 成功/失败请求数计数
        String result = success ? "success" : "fail";
        meterRegistry.counter("seckill.requests.result", "productId", productId, "result", result).increment();
        // 3. 处理耗时统计(直方图,便于分析响应时间分布)
        meterRegistry.timer("seckill.process.time", "productId", productId).record(costTime, TimeUnit.MILLISECONDS);
        // 4. 库存剩余量监控(仪表盘,便于实时查看)
        Integer currentStock = getCurrentRedisStock(productId);
        meterRegistry.gauge("seckill.stock.remaining", "productId", productId, currentStock);
        // 5. 缓存命中率监控(降低Redis和数据库压力,成本优化关键指标)
        recordCacheHitRate();
    }

    /**
     * 记录缓存命中率(低成本,提升系统性能,降低资源消耗)
     */
    private static long cacheHitCount = 0;
    private static long cacheMissCount = 0;
    public void recordCacheHit(boolean hit) {
        if (hit) {
            cacheHitCount++;
        } else {
            cacheMissCount++;
        }
    }

    private void recordCacheHitRate() {
        long total = cacheHitCount + cacheMissCount;
        if (total == 0) {
            return;
        }
        double hitRate = (double) cacheHitCount / total;
        meterRegistry.gauge("seckill.cache.hit.rate", hitRate);
    }

    /**
     * 获取当前Redis库存(轻量查询,低成本)
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private Integer getCurrentRedisStock(String productId) {
        String stockKey = "seckill:stock:" + productId;
        String stockStr = stringRedisTemplate.opsForValue().get(stockKey);
        return stockStr == null ? 0 : Integer.parseInt(stockStr);
    }
}

成本优化点:监控组件均为开源免费,无需商业授权;指标采集轻量级,不会对系统性能造成明显影响,降低服务器资源消耗。

五、部署与扩展(弹性伸缩,低成本扩容,避免资源闲置)

1. 弹性扩展策略(按需扩容,兼顾性能与成本,核心原则:「峰谷适配」)

  • 水平扩展:秒杀服务无状态化设计,基于Docker/K8s实现快速横向扩容,峰值时临时增加节点,峰值过后缩容,避免资源闲置,降低服务器成本

  • 自动伸缩:基于K8s HPA(Horizontal Pod Autoscaler),根据CPU使用率(>80%)或QPS(>40000)自动扩容,无需人工干预,降低运维成本

  • 异地多活:核心服务(秒杀服务、Redis、数据库)按需部署在多个机房,初期可单机房部署,业务增长后再扩展,避免初期过度投入,降低硬件和运维成本

成本核心原则:资源复用,按需分配,避免为了峰值流量而长期部署大量闲置资源。

2. 压测方案(提前验证,规避线上风险,降低故障损失成本)

压测是秒杀系统上线前的必要环节,基于开源JMeter实现,零商业成本,提前发现性能瓶颈和资源不足,避免线上故障导致的经济损失。

压测场景(覆盖核心场景,低成本,高效验证)
  1. 库存预热场景:10万用户同时抢1万商品,验证库存扣减准确性与系统响应能力

  2. 持续高压场景:5万QPS持续5分钟,验证系统稳定性与资源瓶颈

  3. 峰值冲击场景:瞬间20万QPS,验证流量削峰与熔断降级效果

  4. 降级恢复场景:手动熔断Redis服务,验证降级兜底效果与服务恢复能力

压测目标(兼顾性能与成本,合理设定,避免过度优化)
  • 秒杀请求成功率:>99.9%

  • 平均响应时间:<100ms

  • 系统错误率:<0.1%

  • 无超卖、无少卖问题

  • 熔断降级生效及时,服务恢复正常无数据不一致

压测工具与成本优化
  • 压测工具:JMeter(开源免费,支持分布式压测,低成本)

  • 压测环境:使用云服务器按量付费,压测完成后释放资源,避免长期占用,降低服务器成本

  • 压测数据:使用模拟数据,无需真实数据,降低数据准备成本和隐私泄露风险

六、安全考虑(低成本方案,避免恶意攻击,减少经济损失)

  1. 防刷机制(开源组件,零额外成本):

  • 计算型验证码(Google reCAPTCHA,开源免费),峰值时可降级为按钮置灰,降低后端压力,低成本

  • 设备指纹校验(前端收集设备信息,后端哈希存储,Redis实现,开源免费),防止同一设备多账号刷请求

  • 行为分析(自定义日志+ELK,开源免费),拦截异常行为(如短时间内大量请求不同商品)

  • 布隆过滤器(Caffeine实现,本地缓存),拦截无效商品请求,降低Redis和数据库压力,低成本

  1. 数据安全(原生方案,零额外成本):

  • 用户敏感信息加密存储(Spring Security加密,开源免费),如手机号、地址,采用AES加密,防止数据泄露,降低法律风险和经济损失

  • 订单操作日志全记录(MyBatis拦截器,开源免费),便于追溯和对账,降低故障排查成本

  • 接口防篡改校验(签名验证,原生Java实现),请求参数+时间戳+密钥生成签名,防止恶意篡改请求,低成本

  1. 权限控制(Spring Security,开源免费,低成本):

  • 登录态校验(JWT,无状态,开源免费),避免匿名用户参与秒杀

  • 用户秒杀资格校验(Redis缓存,开源免费),避免重复参与秒杀

  • 接口访问权限控制(Spring Security,开源免费),防止恶意调用内部接口

成本优化点:所有安全方案均基于开源组件和原生Java实现,无需商业安全工具,零额外部署成本,同时有效避免恶意攻击,减少经济损失。

总结要点(高性能+低成本,落地核心)

  1. 架构核心:分层过滤 + 异步处理 + 最终一致性,层层削峰,避免后端承压,最小化资源消耗,降低部署成本

  2. 库存核心:Redis原子操作 + 消息队列 + 数据库乐观锁,三重保障,杜绝超卖,无需商业中间件,低成本落地

  3. 性能核心:多级缓存 + 缓存预热 + 读写分离,提升响应速度,降低数据库压力,优先使用本地缓存,减少分布式缓存成本

  4. 成本核心:按需部署 + 弹性伸缩 + 开源组件,避免资源闲置,初期最小资源起步,业务增长后再扩容,兼顾性能与成本

  5. 稳定核心:熔断降级 + 多维度限流 + 定时对账,快速失败,兜底数据一致性,降低故障排查成本和经济损失

面试回答精简版(突出低成本+高可用,贴合企业实际需求)

在设计万人级秒杀系统时,我的核心思路是「前端拦截、中间削峰、后端兜底、成本可控」,既保证高并发、防超卖,又兼顾企业低成本落地,具体分为四步:

  1. 架构分层,层层削峰,低成本起步:首先做静态资源CDN分离(按量付费,降低闲置成本),减轻后端压力;然后通过Nginx(开源免费)做入口限流,拦截恶意请求;最后将秒杀请求入开源RocketMQ(单机起步,峰值扩容),异步处理,把瞬间高并发转化为平稳流量,避免后端服务被压垮。同时,秒杀服务独立部署且无状态化,支持K8s弹性伸缩(峰值扩容,谷值缩容),控制服务器成本。

  2. 核心防超卖,三重保障,零额外成本:采用「Redis原子扣减」作为核心方案(开源Redis,Lua脚本保证原子性,杜绝超卖);再用「数据库乐观锁」作为兜底(基于现有MySQL,零额外部署成本);同时引入「预扣库存+异步同步」,活动前缓存预热(多级缓存,优先本地缓存,降低Redis成本),活动中异步更新数据库,兼顾性能与数据一致性。

  3. 高可用兜底,开源组件,低成本落地:一方面通过多维度限流(Redis计数器+开源Sentinel)、熔断降级(开源Resilience4j),防止服务级联崩溃,快速返回兜底结果,降低故障损失;另一方面通过定时对账任务(基于现有Redis和数据库,零额外成本),修复Redis与数据库的库存偏差,保证最终一致性。

  4. 全链路优化,成本可控,贴合企业实际:优先使用本地缓存(Caffeine)和开源组件,避免商业中间件;监控采用Prometheus+Grafana(开源免费),及时发现问题;部署采用弹性伸缩,按需分配资源,避免闲置浪费。同时,做好防刷、数据安全等细节,兼顾系统性能、用户体验与企业落地成本,最终实现高并发、高可用、低成本、无超卖的秒杀系统。


💡 请期待更多低成本优化技巧(热点Key分散、Redis单机转集群平滑迁移、K8s弹性伸缩配置),助力企业快速落地秒杀系统,降低研发与运维成本!

Logo

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

更多推荐