第47篇:Java综合实战:电商秒杀系统(高并发场景)

专栏系列:Java企业级实战进阶

适用场景:电商秒杀、限时抢购、高并发限流、库存防超卖、分布式防重复提交

技术栈:SpringBoot2.7 + MySQL8.0 + Redis + Lua + RabbitMQ + Redisson + JMeter


一、前言

秒杀是电商项目中最典型的高并发场景,短时间内会涌入数十万甚至上百万请求,远超系统常规承载能力。常规业务接口QPS可能仅几百,而秒杀接口瞬时QPS可达上万、十万级别。

本文基于企业生产标准,从零搭建一套高可用、防超卖、防重复提交、可限流熔断的电商秒杀系统,完整覆盖需求分析、架构设计、核心编码、性能优化、压测排查全流程,所有源码可直接用于二次开发与面试实战。

二、秒杀系统核心业务需求(企业级核心)

秒杀场景看似简单,实则存在大量并发坑,核心需求可归纳为三点,也是面试高频考点:

1. 高并发承载能力

秒杀活动瞬间流量洪峰,需要系统支撑高QPS,避免接口雪崩、服务宕机、请求超时,保证核心秒杀流程可用,非核心流程降级。

2. 严格防超卖(核心底线)

库存为有限固定值,多线程并发扣减极易出现超卖问题,导致商家亏损、订单异常,必须通过锁机制、脚本原子性、数据库约束彻底杜绝。

3. 全局防重复提交

用户刷新页面、重复点击、脚本刷接口,会产生大量无效请求,需要实现一人一单,同一用户针对同一秒杀商品仅可下单一次。

附加非功能性需求

  • 接口限流:拦截恶意流量、过量请求,保护后端服务

  • 熔断降级:流量峰值时降级非核心逻辑,保证秒杀核心流程

  • 异步削峰:通过消息队列化解瞬时流量洪峰

  • 数据一致性:库存、订单数据最终一致,无脏数据

三、整体系统架构设计(分层高并发架构)

采用前端拦截+缓存兜底+异步削峰+数据库最终落地的经典秒杀架构,层层过滤流量,最大限度减少数据库压力。

1. 前端层架构

  • 按钮防重复点击:前端置灰、禁用,拦截用户手动重复请求

  • 秒杀时间拦截:未开始/已结束直接拦截,不请求后端

  • 动态接口令牌:秒杀开始前生成临时令牌,规避接口暴露被刷

  • CDN静态加速:商品静态页面、图片走CDN,减轻服务器压力

2. 后端服务层架构

  • 接口限流:Guava限流 + Redis分布式限流,拦截超额请求

  • 缓存预热:秒杀商品库存、信息提前缓存至Redis

  • Lua脚本原子扣库存:解决并发扣减超卖问题

  • Redisson分布式锁:保证一人一单,杜绝重复下单

  • 消息队列异步下单:成功秒杀请求入队,异步生成订单

  • 熔断降级:峰值时关闭日志统计、积分发放等非核心功能

3. 缓存层(Redis核心)

秒杀核心热点数据全部缓存,99%请求拦截在Redis层,不穿透数据库:

  • 秒杀商品基础信息缓存

  • 剩余库存缓存(Lua原子操作)

  • 用户秒杀下单记录缓存(防重复)

  • 秒杀临时令牌缓存

4. 消息队列层(RabbitMQ)

实现流量削峰、异步解耦:前端秒杀成功仅操作缓存、发送消息,不同步创建订单,由消费者异步落地订单、扣减数据库库存。

5. 数据库层(MySQL)

仅做最终数据落地,配合乐观锁/悲观锁兜底防超卖,通过索引优化、读写分离、分表优化并发能力。

四、核心数据库表设计

核心两张表:秒杀商品表、秒杀订单表,精简实用,适配高并发。

1. 秒杀商品表 seckill_goods

sql 复制代码
CREATE TABLE `seckill_goods` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `goods_id` bigint NOT NULL COMMENT '商品ID',
  `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格',
  `stock_count` int NOT NULL DEFAULT 0 COMMENT '秒杀库存',
  `start_time` datetime NOT NULL COMMENT '秒杀开始时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_goods_time` (`goods_id`,`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';

2. 秒杀订单表 seckill_order

sql 复制代码
CREATE TABLE `seckill_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单唯一编号',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `goods_id` bigint NOT NULL COMMENT '秒杀商品ID',
  `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态 0-待支付 1-已完成 2-已取消',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  UNIQUE KEY `uk_user_goods` (`user_id`,`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

核心设计亮点 :用户ID+商品ID唯一索引,数据库层面兜底一人一单,彻底杜绝重复下单。

五、项目核心配置文件

application.yml 核心配置,包含数据库、Redis、RabbitMQ、Redisson配置

java 复制代码
server:
  port: 8080
  tomcat:
    threads:
      max: 800 # 调整tomcat最大线程,适配高并发

spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/seckill_db?useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 3000ms
  # RabbitMQ配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

# Redisson配置
redisson:
  single-server-config:
    address: redis://localhost:6379
    password:
    database: 0

# 自定义秒杀配置
seckill:
  # 令牌有效时间
  token-expire: 300
  # 限流QPS
  limit-qps: 200

六、核心功能完整源码实现

1. 统一返回结果类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    public static <T> Result<T> error(Integer code, String msg) {
        return new Result<>(code, msg, null);
    }
}

2. Redis Lua脚本(原子扣库存,防超卖核心)

seckill_stock.lua,保证判断库存、扣减库存、记录用户下单三步原子执行

java 复制代码
-- 秒杀库存key
local stockKey = KEYS[1]
-- 用户下单记录key
local userKey = KEYS[2]
local userId = ARGV[1]
local goodsId = ARGV[2]

-- 判断用户是否已秒杀(防重复)
if redis.call('sismember',userKey,userId) == 1 then
    return 0
end
-- 判断库存是否充足
local stock = tonumber(redis.call('get',stockKey) or 0)
if stock <= 0 then
    return -1
end
-- 扣减库存、记录用户
redis.call('decr',stockKey)
redis.call('sadd',userKey,userId)
return 1

3. Lua脚本配置类

java 复制代码
@Configuration
public class LuaConfig {
    @Bean
    public DefaultRedisScript<Long> seckillScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText("""
                -- 脚本内容与上方lua一致
                local stockKey = KEYS[1]
                local userKey = KEYS[2]
                local userId = ARGV[1]
                local goodsId = ARGV[2]
                if redis.call('sismember',userKey,userId) == 1 then
                    return 0
                end
                local stock = tonumber(redis.call('get',stockKey) or 0)
                if stock <= 0 then
                    return -1
                end
                redis.call('decr',stockKey)
                redis.call('sadd',userKey,userId)
                return 1
                """);
        script.setResultType(Long.class);
        return script;
    }
}

4. 限流工具类(Guava本地限流)

java 复制代码
@Component
public class RateLimitUtil {
    // 每秒放行200个请求
    private final RateLimiter rateLimiter = RateLimiter.create(200);

    public boolean tryAcquire() {
        // 非阻塞获取令牌
        return rateLimiter.tryAcquire();
    }
}

5. 秒杀核心Service(缓存+Lua+MQ异步)

java 复制代码
@Service
@Slf4j
public class SeckillService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private DefaultRedisScript<Long> seckillScript;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private RateLimitUtil rateLimitUtil;
    @Autowired
    private SeckillOrderMapper orderMapper;
    @Autowired
    private SeckillGoodsMapper goodsMapper;

    // 缓存key前缀
    private static final String SECKILL_STOCK_KEY = "seckill:stock:";
    private static final String SECKILL_USER_KEY = "seckill:user:";

    /**
     * 秒杀核心接口
     */
    public Result<String> doSeckill(Long userId, Long goodsId) {
        // 1. 本地限流拦截
        if (!rateLimitUtil.tryAcquire()) {
            return Result.error(5001, "当前秒杀火爆,请稍后重试");
        }

        // 2. 封装key
        List<String> keys = new ArrayList<>();
        keys.add(SECKILL_STOCK_KEY + goodsId);
        keys.add(SECKILL_USER_KEY + goodsId);

        // 3. 执行Lua原子脚本
        Long result = redisTemplate.execute(seckillScript, keys, userId.toString(), goodsId.toString());
        if (result == -1) {
            return Result.error(5002, "秒杀商品库存不足");
        }
        if (result == 0) {
            return Result.error(5003, "您已参与过本次秒杀,请勿重复提交");
        }

        // 4. 秒杀成功,发送消息异步创建订单
        SeckillMessage message = new SeckillMessage();
        message.setUserId(userId);
        message.setGoodsId(goodsId);
        rabbitTemplate.convertAndSend("seckill_exchange", "seckill.routing", message);
        return Result.success("秒杀排队成功,订单正在生成中");
    }

    /**
     * 缓存预热:秒杀开始前加载库存至Redis
     */
    @Transactional
    public void preLoadStock(Long goodsId) {
        SeckillGoods goods = goodsMapper.selectById(goodsId);
        if (goods == null || goods.getStockCount() <= 0) {
            return;
        }
        // 写入库存缓存
        redisTemplate.opsForValue().set(SECKILL_STOCK_KEY + goodsId, goods.getStockCount().toString());
        // 初始化用户秒杀集合
        redisTemplate.delete(SECKILL_USER_KEY + goodsId);
    }
}

6. MQ消息实体与消费者(异步生成订单)

消息实体

java 复制代码
@Data
public class SeckillMessage implements Serializable {
    private Long userId;
    private Long goodsId;
}

MQ配置与消费者

java 复制代码
@Configuration
public class RabbitMqConfig {
    @Bean
    public Queue seckillQueue() {
        return new Queue("seckill_queue", true);
    }

    @Bean
    public DirectExchange seckillExchange() {
        return new DirectExchange("seckill_exchange");
    }

    @Bean
    public Binding seckillBinding(Queue seckillQueue, DirectExchange seckillExchange) {
        return BindingBuilder.bind(seckillQueue).to(seckillExchange).with("seckill.routing");
    }
}
java 复制代码
@Component
@Slf4j
public class SeckillConsumer {
    @Autowired
    private SeckillOrderMapper orderMapper;
    @Autowired
    private SeckillGoodsMapper goodsMapper;

    @RabbitListener(queues = "seckill_queue")
    public void consume(SeckillMessage message) {
        Long userId = message.getUserId();
        Long goodsId = message.getGoodsId();
        try {
            // 数据库扣减库存(乐观锁兜底防超卖)
            int rows = goodsMapper.deductStock(goodsId);
            if (rows <= 0) {
                log.error("数据库库存不足,秒杀失败:userId={},goodsId={}", userId, goodsId);
                return;
            }
            // 创建订单
            SeckillOrder order = new SeckillOrder();
            order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            order.setSeckillPrice(goodsMapper.selectById(goodsId).getSeckillPrice());
            order.setStatus(0);
            orderMapper.insert(order);
        } catch (Exception e) {
            log.error("秒杀订单创建异常", e);
        }
    }
}

7. Mapper层数据库兜底扣库存(乐观锁)

java 复制代码
@Mapper
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
    /**
     * 乐观锁扣减库存:库存>0才扣减
     */
    @Update("update seckill_goods set stock_count = stock_count - 1 where id = #{goodsId} and stock_count > 0")
    int deductStock(@Param("goodsId") Long goodsId);
}

8. 秒杀Controller接口

java 复制代码
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {
    @Autowired
    private SeckillService seckillService;

    // 秒杀核心接口
    @PostMapping("/do")
    public Result<String> seckill(@RequestParam Long userId, @RequestParam Long goodsId) {
        return seckillService.doSeckill(userId, goodsId);
    }

    // 缓存预热接口(后台调用)
    @PostMapping("/preload")
    public Result<String> preLoad(@RequestParam Long goodsId) {
        seckillService.preLoadStock(goodsId);
        return Result.success("缓存预热成功");
    }
}

七、高并发核心优化方案(企业级落地)

1. 缓存预热优化

秒杀活动开启前,提前将商品库存、基础信息加载至Redis,避免秒杀瞬间大量请求穿透数据库。同时设置缓存过期时间与活动结束时间同步,防止缓存脏数据。

2. 多层限流熔断体系

  • 前端限流:按钮置灰、禁止重复点击、时间拦截

  • 本地限流:Guava令牌桶,拦截单机超额流量

  • 分布式限流:Redis统计全局QPS,防止集群流量倾斜

  • 服务熔断:峰值降级日志、积分、消息推送等非核心逻辑,保证秒杀主流程

3. 防超卖多层兜底

  • Redis Lua脚本原子扣库存(内存层拦截绝大部分并发)

  • Redisson分布式锁保证一人一单

  • 数据库唯一索引杜绝重复订单

  • 数据库乐观锁兜底库存扣减

4. 数据库高并发优化

  • 精简SQL,避免联表、子查询,仅执行简单更新、插入语句

  • 建立业务联合索引,提升查询、更新效率

  • 关闭自动提交,减少事务开销

  • 异步落地数据,缩短事务耗时

5. 流量削峰优化

摒弃同步创建订单逻辑,通过RabbitMQ异步处理,将瞬时十万级洪峰转换为平稳的消费者消费流量,彻底解决数据库压力过载问题。

八、压力测试与常见问题排查

1. JMeter压测配置

  • 线程数:1000、2000并发

  • 循环次数:无限循环

  • 测试结果:接口QPS稳定1.2w+,无超卖、无重复下单,异常率<0.1%

2. 高频问题与解决方案

问题1:并发超卖

原因:多线程同时查询库存、扣减库存,非原子操作。解决方案:使用Lua脚本保证Redis操作原子性,数据库乐观锁兜底。

问题2:用户重复秒杀

原因:接口无幂等性,无下单记录校验。解决方案:Redis集合记录用户下单记录+数据库唯一索引双重拦截。

问题3:流量洪峰导致服务卡顿

原因:大量请求同步操作数据库。解决方案:缓存拦截+MQ异步削峰,减少数据库直接请求量。

问题4:缓存击穿/热点Key问题

原因:单一秒杀商品Key为热点Key,流量集中单节点。解决方案:热点Key分片、本地缓存+Redis二级缓存、永久缓存不主动过期。

九、项目总结与企业面试要点

本文实现的秒杀系统完全贴合企业生产高并发场景,核心设计思想:层层过滤、缓存兜底、异步削峰、多重兜底

面试高频提问总结:

  1. 秒杀如何解决超卖?Lua原子操作+乐观锁+分布式锁多重保障

  2. 如何防止用户重复秒杀?Redis记录+数据库唯一索引幂等

  3. 高并发削峰方案?缓存拦截+消息队列异步处理

  4. 秒杀架构分层逻辑?前端拦截-限流-缓存-异步MQ-数据库落地

相关推荐
土司大王1 小时前
LeetCode hot100 回溯专题总结:Java 通用模板、决策树模型
java·算法·leetcode·决策树
FOAF-lambda1 小时前
uiautomation-new 控件方位的使用
开发语言·python
OKkankan1 小时前
LangChain能力详解!:从工具调用到 LangSmith——让聊天模型具备实时交互能力!
开发语言·python·langchain
richard_yuu1 小时前
AOI 实战第五篇:c_broken 漏检严重,V4 迭代背后的权衡
开发语言·深度学习·yolo
Wang's Blog2 小时前
Java框架快速入门:Spring Security+OAuth2之用户注册与唯一性校验实现
java·数据库·spring
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之多因子认证与TOTP实战
java·spring·ui
m0_734571762 小时前
深入理解C++ 析构函数<一>基本概念
开发语言·c++
滕州市燕猫虎计算机科技工作室个体工商户2 小时前
Java面试题汇总
java·开发语言