一、什么是秒杀?
秒杀是电商、零售等行业常见的营销活动形式:平台在特定时间发布限量低价商品,用户需在极短时间内完成抢购,最终只有少数用户能成功下单。其核心特征可概括为三点:
- 瞬时高并发:活动开始后几秒内,用户请求量会呈数十倍甚至上百倍激增(例如 10 万用户争抢 100 件商品);
- 资源稀缺性:商品库存有限,需严格控制超卖,同时要防止恶意刷单;
- 低延迟要求:用户抢购体验直接影响活动效果,响应时间需控制在百毫秒级。
秒杀的本质是 “高并发下的资源竞争与流量控制”,其技术挑战远超普通业务场景,需从架构设计、代码实现、运维配置等多维度协同优化。
二、秒杀系统需解决的核心问题
1. 超卖问题(数据一致性)
秒杀的核心风险是 “超卖”(实际下单量超过商品库存),这会导致平台经济损失和用户信任危机。根本原因是并发场景下库存检查与扣减的原子性无法保证,例如:
// 错误示例:非原子操作导致超卖 if (productStock > 0) { // 1.检查库存 // 并发时多个线程同时通过库存检查 productStock--; // 2.扣减库存 createOrder(); // 3.创建订单 }2. 高并发流量冲击
秒杀请求集中爆发时,直接穿透到数据库会导致连接池耗尽、SQL 执行超时,最终系统雪崩。例如:10 万 QPS 直接命中 MySQL,单库单表根本无法承载。
3. 恶意请求与刷单
恶意用户可能通过脚本、爬虫高频请求,占用正常用户的抢购名额,同时浪费系统资源。
4. 接口幂等性
用户网络抖动或重复点击时,可能导致重复下单(例如同一用户多次抢到同一件商品),需保证接口 “一次请求仅生效一次”。
5. 数据一致性(库存与订单)
库存扣减、订单创建、支付状态同步需保证一致性,避免 “有订单无库存” 或 “扣减库存未下单” 的情况
三、秒杀系统整体设计
架构分层设计(从外到内)
各层核心职责:
- 用户层:Web/H5/APP 客户端,提供秒杀入口、倒计时、下单按钮(需防重复点击);
- 接入层:Nginx + 网关(Gateway/Spring Cloud Gateway),负责限流、黑名单过滤、静态资源缓存;
- 应用层:Spring Boot 微服务,包含秒杀资格校验、库存预扣减、订单创建等核心业务;
- 缓存层:Redis,缓存商品库存、秒杀状态、用户抢购资格(核心抗并发组件);
- 数据层:MySQL,存储商品信息、订单数据、用户数据(最终一致性落地);
- 中间件:RabbitMQ/Kafka,异步处理订单创建、消息通知(削峰填谷)。
核心流程时序图
用户 → 前端防重 → 网关限流 → Redis预扣库存 → 消息队列入队 → 异步创建订单 → 库存最终扣减 → 结果返回四、Java 秒杀系统实现(核心代码)
技术栈选型
- 核心框架:Spring Boot 2.7.x
- 缓存:Redis 6.x(Redisson 分布式锁)
- 消息队列:RabbitMQ 3.9.x
- 数据库:MySQL 8.0(InnoDB)
- 网关:Spring Cloud Gateway
- 限流:Redis + Lua 脚本
1. 数据库设计(核心表)
商品表(t_seckill_product)
CREATE TABLE `t_seckill_product` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID', `name` varchar(255) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '原价', `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价', `stock` int NOT NULL COMMENT '秒杀库存', `start_time` datetime NOT NULL COMMENT '活动开始时间', `end_time` datetime NOT NULL COMMENT '活动结束时间', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-未开始,1-进行中,2-已结束', PRIMARY KEY (`id`), KEY `idx_time` (`start_time`,`end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';订单表(t_seckill_order)
CREATE TABLE `t_seckill_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID', `user_id` bigint NOT NULL COMMENT '用户ID', `product_id` bigint NOT NULL COMMENT '商品ID', `price` decimal(10,2) NOT NULL COMMENT '下单价格', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待支付,1-已支付,2-已取消', PRIMARY KEY (`id`), UNIQUE KEY `idx_user_product` (`user_id`,`product_id`) COMMENT '防止同一用户重复下单' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';2. 核心配置(application.yml)
spring: # 数据库配置 datasource: url: jdbc:mysql://localhost:3306/seckill_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # Redis配置 redis: host: localhost port: 6379 password: lettuce: pool: max-active: 16 max-idle: 8 # RabbitMQ配置 rabbitmq: host: localhost port: 5672 username: guest password: guest virtual-host: / listener: simple: acknowledge-mode: manual # 手动ACK concurrency: 5 # 消费者并发数 # 秒杀配置 seckill: redis: stock-key-prefix: "seckill:stock:" # 库存缓存前缀 user-lock-key-prefix: "seckill:user:lock:" # 用户抢购锁前缀 rabbitmq: queue-name: "seckill_order_queue" # 订单队列名称 exchange-name: "seckill_order_exchange" # 订单交换机名称 routing-key: "seckill.order" # 路由键3. 核心组件实现
(1)Redis 工具类(缓存库存与限流)
@Component public class RedisSeckillUtil { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; // 库存缓存前缀 @Value("${seckill.redis.stock-key-prefix}") private String stockKeyPrefix; // 用户抢购锁前缀 @Value("${seckill.redis.user-lock-key-prefix}") private String userLockKeyPrefix; /** * 初始化商品库存到Redis(活动开始前调用) */ public void initStock(Long productId, Integer stock) { String key = stockKeyPrefix + productId; redisTemplate.opsForValue().set(key, stock.toString()); } /** * Redis预扣减库存(Lua脚本保证原子性) */ public boolean deductStock(Long productId) { String key = stockKeyPrefix + productId; // Lua脚本:检查库存>0,然后扣减1 String luaScript = "if tonumber(redis.call('get', KEYS[1])) > 0 then " + "return redis.call('decr', KEYS[1]) " + "else " + "return -1 " + "end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList(key) ); return result != null && result >= 0; } /** * 检查用户是否已抢购(防止重复下单) */ public boolean checkUserHasSeckilled(Long userId, Long productId) { String key = userLockKeyPrefix + productId + ":" + userId; Boolean hasKey = redisTemplate.hasKey(key); return Boolean.TRUE.equals(hasKey); } /** * 锁定用户抢购资格(设置过期时间,防止内存泄漏) */ public void lockUser(Long userId, Long productId) { String key = userLockKeyPrefix + productId + ":" + userId; // 过期时间设置为活动结束时间+1小时,避免长期占用 redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.HOURS); } }(2)消息队列配置(削峰填谷)
@Configuration public class RabbitMQConfig { @Value("${seckill.rabbitmq.queue-name}") private String queueName; @Value("${seckill.rabbitmq.exchange-name}") private String exchangeName; @Value("${seckill.rabbitmq.routing-key}") private String routingKey; /** * 声明队列(持久化) */ @Bean public Queue seckillOrderQueue() { return QueueBuilder.durable(queueName).build(); } /** * 声明交换机(Direct) */ @Bean public DirectExchange seckillOrderExchange() { return ExchangeBuilder.directExchange(exchangeName).durable(true).build(); } /** * 绑定队列与交换机 */ @Bean public Binding seckillOrderBinding() { return BindingBuilder.bind(seckillOrderQueue()) .to(seckillOrderExchange()) .with(routingKey); } /** * 消息序列化配置(使用JSON) */ @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }(3)秒杀核心服务(资格校验 + 库存预扣减)
@Service @Transactional public class SeckillService { @Autowired private RedisSeckillUtil redisSeckillUtil; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private SeckillProductMapper seckillProductMapper; @Value("${seckill.rabbitmq.exchange-name}") private String exchangeName; @Value("${seckill.rabbitmq.routing-key}") private String routingKey; /** * 秒杀核心方法(入口) */ public Result<String> doSeckill(Long userId, Long productId) { // 1. 校验活动状态 SeckillProduct product = seckillProductMapper.selectById(productId); if (product == null) { return Result.fail("商品不存在"); } if (product.getStatus() != 1) { return Result.fail("秒杀活动未开始或已结束"); } // 2. 校验用户是否已抢购(防重复) if (redisSeckillUtil.checkUserHasSeckilled(userId, productId)) { return Result.fail("您已抢购过该商品,不可重复下单"); } // 3. Redis预扣减库存(原子操作) boolean deductSuccess = redisSeckillUtil.deductStock(productId); if (!deductSuccess) { return Result.fail("商品已抢完,手慢无~"); } // 4. 锁定用户抢购资格 redisSeckillUtil.lockUser(userId, productId); // 5. 发送消息到MQ,异步创建订单 SeckillOrderDTO orderDTO = new SeckillOrderDTO() .setUserId(userId) .setProductId(productId) .setPrice(product.getSeckillPrice()) .setCreateTime(new Date()); rabbitTemplate.convertAndSend(exchangeName, routingKey, orderDTO); return Result.success("抢购成功,请尽快支付"); } }(4)消息消费者(异步创建订单)
@Component public class SeckillOrderConsumer { @Autowired private SeckillOrderMapper seckillOrderMapper; @Autowired private SeckillProductMapper seckillProductMapper; /** * 消费MQ消息,创建订单(最终扣减库存) */ @RabbitListener(queues = "${seckill.rabbitmq.queue-name}") public void createSeckillOrder(SeckillOrderDTO orderDTO, Channel channel, Message message) throws IOException { try { // 1. 幂等性校验(防止MQ重复投递) SeckillOrder existOrder = seckillOrderMapper.selectByUserIdAndProductId( orderDTO.getUserId(), orderDTO.getProductId()); if (existOrder != null) { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // 2. 创建订单 SeckillOrder order = new SeckillOrder() .setUserId(orderDTO.getUserId()) .setProductId(orderDTO.getProductId()) .setPrice(orderDTO.getPrice()) .setCreateTime(orderDTO.getCreateTime()) .setStatus(0); seckillOrderMapper.insert(order); // 3. 最终扣减数据库库存(使用乐观锁防止超卖) int row = seckillProductMapper.deductStockByVersion(orderDTO.getProductId()); if (row == 0) { // 乐观锁失败,说明库存已为0,需要回滚Redis库存 redisSeckillUtil.initStock(orderDTO.getProductId(), 0); throw new RuntimeException("数据库库存扣减失败"); } // 4. 手动ACK channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { // 消费失败,拒绝消息并重新入队(或死信队列) channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); log.error("创建秒杀订单失败", e); } } }(5)网关限流配置(Spring Cloud Gateway)
@Configuration public class GatewayRateLimitConfig { @Bean public KeyResolver userKeyResolver() { // 基于用户ID限流(实际可改为IP/Token) return exchange -> Mono.just( exchange.getRequest().getHeaders().getFirst("X-User-ID") ); } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() // 秒杀接口限流配置:100QPS/用户 .route("seckill_route", r -> r.path("/api/seckill/doSeckill") .filters(f -> f.requestRateLimiter(c -> c .setRateLimiter(redisRateLimiter()) .setKeyResolver(userKeyResolver()) )) .uri("lb://seckill-service")) .build(); } @Bean public RedisRateLimiter redisRateLimiter() { // 令牌桶算法:100个令牌/秒,最大缓存100个令牌 return new RedisRateLimiter(100, 100); } }4. 秒杀活动管理(配置、开启、关闭)
(1)活动配置与初始化
@Service public class SeckillActivityManager { @Autowired private SeckillProductMapper seckillProductMapper; @Autowired private RedisSeckillUtil redisSeckillUtil; @Autowired private ScheduledExecutorService scheduledExecutorService; /** * 配置秒杀活动(后台管理接口) */ public Result<String> configActivity(SeckillProduct product) { // 1. 保存活动信息到数据库 seckillProductMapper.insert(product); // 2. 预约活动开启任务(设置库存到Redis) long delay = product.getStartTime().getTime() - System.currentTimeMillis(); scheduledExecutorService.schedule(() -> { // 活动开始:初始化库存到Redis + 更新活动状态 redisSeckillUtil.initStock(product.getId(), product.getStock()); seckillProductMapper.updateStatus(product.getId(), 1); }, delay, TimeUnit.MILLISECONDS); // 3. 预约活动结束任务 long endDelay = product.getEndTime().getTime() - System.currentTimeMillis(); scheduledExecutorService.schedule(() -> { // 活动结束:更新活动状态 seckillProductMapper.updateStatus(product.getId(), 2); }, endDelay, TimeUnit.MILLISECONDS); return Result.success("秒杀活动配置成功,将在" + product.getStartTime() + "开启"); } /** * 手动开启活动(应急接口) */ public Result<String> startActivity(Long productId) { SeckillProduct product = seckillProductMapper.selectById(productId); if (product == null) { return Result.fail("活动不存在"); } if (product.getStatus() == 1) { return Result.fail("活动已开启"); } // 初始化库存到Redis + 更新状态 redisSeckillUtil.initStock(productId, product.getStock()); seckillProductMapper.updateStatus(productId, 1); return Result.success("活动手动开启成功"); } /** * 手动关闭活动(应急接口) */ public Result<String> stopActivity(Long productId) { SeckillProduct product = seckillProductMapper.selectById(productId); if (product == null) { return Result.fail("活动不存在"); } if (product.getStatus() == 2) { return Result.fail("活动已结束"); } // 更新状态 + 清空Redis库存(防止继续抢购) seckillProductMapper.updateStatus(productId, 2); redisSeckillUtil.initStock(productId, 0); return Result.success("活动手动关闭成功"); } }(2)前端控制层(API 接口)
@RestController @RequestMapping("/api/seckill") public class SeckillController { @Autowired private SeckillService seckillService; @Autowired private SeckillActivityManager activityManager; // 普通用户抢购接口 @PostMapping("/doSeckill") public Result<String> doSeckill(@RequestHeader("X-User-ID") Long userId, @RequestParam Long productId) { return seckillService.doSeckill(userId, productId); } // 管理员配置活动接口 @PostMapping("/config") public Result<String> configActivity(@RequestBody SeckillProduct product) { return activityManager.configActivity(product); } // 管理员开启活动接口 @PostMapping("/start/{productId}") public Result<String> startActivity(@PathVariable Long productId) { return activityManager.startActivity(productId); } // 管理员关闭活动接口 @PostMapping("/stop/{productId}") public Result<String> stopActivity(@PathVariable Long productId) { return activityManager.stopActivity(productId); } }五、秒杀系统关键优化点
1. 前端优化
- 按钮置灰:活动未开始时禁用下单按钮,防止无效请求;
- 防重复点击:点击后立即禁用按钮,避免用户快速多次点击;
- 静态资源 CDN:秒杀页面的图片、CSS、JS 等通过 CDN 分发,减轻源站压力。
2. 接入层优化
- Nginx 缓存:缓存秒杀活动页面(设置 Cache-Control),避免请求穿透到应用层;
- 黑名单过滤:通过 Nginx 或网关拦截恶意 IP(例如短时间内请求次数过多的 IP);
- 限流降级:网关层对秒杀接口限流,超过阈值直接返回 “请求过忙”。
3. 应用层优化
- 无状态设计:服务集群部署,支持水平扩容;
- 异步化处理:订单创建、消息通知等非核心流程通过 MQ 异步处理;
- 分布式锁:使用 Redisson 防止并发问题(例如库存初始化、活动状态更新)。
4. 缓存层优化
- 库存预热:活动开始前将商品库存加载到 Redis,避免缓存穿透;
- Lua 脚本:保证库存检查与扣减的原子性,减少 Redis 网络往返;
- 缓存过期时间:合理设置 key 的过期时间,避免内存泄漏。
5. 数据层优化
- 索引优化:订单表添加用户 + 商品的唯一索引,防止重复下单;
- 乐观锁:数据库库存扣减使用版本号,避免悲观锁导致的性能下降;
- 分库分表:高并发场景下,对订单表按用户 ID 哈希分表,分散数据库压力。
六、总结
Java 秒杀系统的设计核心是 “流量分层过滤” 与 “数据一致性保障”:通过前端、网关、应用层的多层限流,将绝大多数无效请求拦截在门外;通过 Redis 缓存承接高并发库存操作,再通过消息队列削峰填谷,最终异步落地到数据库。
实际落地时,需重点关注:
- 超卖问题:通过 Redis 原子操作 + 数据库乐观锁双重保障;
- 并发性能:合理配置 Redis、MQ、数据库的参数,支持水平扩容;
- 异常处理:完善降级策略(例如活动异常时关闭抢购入口)和监控告警。
通过本文的设计方案与代码实现,可支撑万级 QPS 的秒杀活动,同时保证系统稳定性与数据一致性。实际业务中,可根据流量规模进一步优化架构(例如引入分布式缓存集群、数据库主从分离等)。