前言
秒杀是电商系统中最典型的高并发场景,特点是:
-
瞬时流量大:大量用户在同一时间抢购少量商品
-
库存少:通常只有几百到几千件
-
时间短:秒杀活动通常持续几秒到几分钟
本文将从零开始设计并实现一个完整的秒杀系统,涵盖:
-
秒杀系统架构设计
-
数据库表设计
-
Redis缓存预热与库存扣减
-
消息队列异步下单
-
分布式锁防重复下单
-
限流防刷策略
-
完整的Java代码实现
技术栈:Spring Boot + Redis + RabbitMQ + MySQL + Lua脚本
一、秒杀系统核心挑战
1.1 高并发带来的问题
| 超卖 | 库存扣减不一致 | Redis原子操作 + 数据库乐观锁 |
| 重复下单 | 同一用户多次下单 | 分布式锁 + 唯一索引 |
| 数据库压力 | 大量请求直接打到数据库 | Redis预扣库存 + 消息队列削峰 |
| 恶意刷单 | 脚本抢购 | 限流 + 验证码 + 设备指纹 |
| 数据一致性 | 缓存与数据库不一致 | 最终一致性方案 |
1.2 秒杀系统架构设计
┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Nginx负载均衡 │
│ (限流、静态资源缓存) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 网关层 (Spring Cloud Gateway) │
│ (全局限流、用户身份验证、请求过滤) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 秒杀服务集群 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 秒杀服务1 │ │ 秒杀服务2 │ │ 秒杀服务3 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Redis │ │ RabbitMQ │ │ MySQL │
│ (库存缓存) │ │ (异步下单) │ │ (持久化) │
└─────────────┘ └─────────────┘ └─────────────┘
1.3 核心设计原则
尽量将请求拦截在上游:限流、验证码、库存校验
异步处理:下单请求放入消息队列,异步处理
缓存优先:库存预热到Redis,减少数据库访问
最终一致性:允许短暂不一致,保证最终数据正确
二、数据库表设计
2.1 秒杀活动表
CREATE TABLE `seckill_activity` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '活动ID',
`activity_name` varchar(100) NOT NULL COMMENT '活动名称',
`product_id` bigint NOT NULL COMMENT '商品ID',
`product_name` varchar(200) NOT NULL COMMENT '商品名称',
`product_image` varchar(500) DEFAULT NULL COMMENT '商品图片',
`original_price` decimal(10,2) NOT NULL COMMENT '原价',
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价',
`total_stock` int NOT NULL COMMENT '总库存',
`available_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-已结束',
`limit_per_user` int NOT NULL DEFAULT 1 COMMENT '每人限购数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_product_id` (`product_id`),
KEY `idx_start_time` (`start_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀活动表';
2.2 秒杀订单表
CREATE TABLE `seckill_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单编号',
`activity_id` bigint NOT NULL COMMENT '活动ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`product_name` varchar(200) NOT NULL COMMENT '商品名称',
`seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价',
`quantity` int NOT NULL DEFAULT 1 COMMENT '购买数量',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待支付,1-已支付,2-已取消,3-已退款',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`) COMMENT '同一活动同一用户只能下一单',
KEY `idx_user_id` (`user_id`),
KEY `idx_activity_id` (`activity_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';
2.3 秒杀商品表
CREATE TABLE `seckill_product` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`product_name` varchar(200) NOT NULL COMMENT '商品名称',
`product_image` varchar(500) DEFAULT NULL COMMENT '商品图片',
`product_detail` text COMMENT '商品详情',
`original_price` decimal(10,2) NOT NULL COMMENT '原价',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
三、Redis缓存设计
3.1 Redis Key设计
/**
* Redis Key 常量定义
*/
public class RedisKeyConstant {
/**
* 秒杀库存Key
* 格式:seckill:stock:{activityId}
* 类型:String (Integer)
*/
public static final String SECKILL_STOCK = "seckill:stock:%s";
/**
* 秒杀活动信息Key
* 格式:seckill:activity:{activityId}
* 类型:Hash
*/
public static final String SECKILL_ACTIVITY = "seckill:activity:%s";
/**
* 用户已购数量Key
* 格式:seckill:user:buy:{activityId}:{userId}
* 类型:String (Integer)
*/
public static final String SECKILL_USER_BUY = "seckill:user:buy:%s:%s";
/**
* 秒杀订单锁Key
* 格式:seckill:order:lock:{activityId}:{userId}
* 类型:String
*/
public static final String SECKILL_ORDER_LOCK = "seckill:order:lock:%s:%s";
/**
* 秒杀商品详情Key
* 格式:seckill:product:{productId}
* 类型:Hash
*/
public static final String SECKILL_PRODUCT = "seckill:product:%s";
/**
* 秒杀活动列表Key
* 格式:seckill:activity:list
* 类型:List
*/
public static final String SECKILL_ACTIVITY_LIST = "seckill:activity:list";
}
3.2 Lua脚本:原子扣减库存
— seckill_stock_deduct.lua
— 秒杀库存扣减Lua脚本(原子操作)
— 参数:KEYS[1] = 库存Key, KEYS[2] = 用户购买记录Key
— 参数:ARGV[1] = 用户ID, ARGV[2] = 限购数量
— 1. 检查库存是否充足
local stock = tonumber(redis.call('get', KEYS[1]))
if stock == nil or stock <= 0 then
return -1 — 库存不足
end
— 2. 检查用户是否已购买
local userBuyKey = KEYS[2]
local userBuyCount = tonumber(redis.call('get', userBuyKey))
if userBuyCount ~= nil and userBuyCount >= tonumber(ARGV[2]) then
return -2 — 超过限购数量
end
— 3. 扣减库存
redis.call('decr', KEYS[1])
— 4. 增加用户购买记录
if userBuyCount == nil then
redis.call('set', userBuyKey, 1)
else
redis.call('incr', userBuyKey)
end
— 5. 设置用户购买记录过期时间(活动结束后24小时过期)
redis.call('expire', userBuyKey, 86400)
return 1 — 扣减成功
3.3 Lua脚本:库存回滚
— seckill_stock_rollback.lua
— 秒杀库存回滚Lua脚本
— 参数:KEYS[1] = 库存Key, KEYS[2] = 用户购买记录Key
— 参数:ARGV[1] = 回滚数量
— 1. 回滚库存
redis.call('incrby', KEYS[1], tonumber(ARGV[1]))
— 2. 囏少用户购买记录
local userBuyCount = tonumber(redis.call('get', KEYS[2]))
if userBuyCount ~= nil and userBuyCount > 0 then
redis.call('decr', KEYS[2])
end
return 1
3.4 Redis操作Service
package com.example.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import static com.example.constant.RedisKeyConstant.*;
/**
* Redis秒杀操作Service
*/
@Service
public class RedisSeckillService {
private static final Logger log = LoggerFactory.getLogger(RedisSeckillService.class);
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 库存扣减Lua脚本
*/
private DefaultRedisScript<Long> stockDeductScript;
/**
* 库存回滚Lua脚本
*/
private DefaultRedisScript<Long> stockRollbackScript;
@PostConstruct
public void init() {
// 初始化库存扣减脚本
stockDeductScript = new DefaultRedisScript<>();
stockDeductScript.setScriptText(STOCK_DEDUCT_LUA);
stockDeductScript.setResultType(Long.class);
// 初始化库存回滚脚本
stockRollbackScript = new DefaultRedisScript<>();
stockRollbackScript.setScriptText(STOCK_ROLLBACK_LUA);
stockRollbackScript.setResultType(Long.class);
}
/**
* 预热秒杀库存到Redis
*
* @param activityId 活动ID
* @param stock 库存数量
*/
public void warmUpStock(Long activityId, Integer stock) {
String stockKey = String.format(SECKILL_STOCK, activityId);
redisTemplate.opsForValue().set(stockKey, stock.toString());
log.info("秒杀库存预热成功,活动ID:{},库存:{}", activityId, stock);
}
/**
* 获取秒杀库存
*
* @param activityId 活动ID
* @return 库存数量
*/
public Integer getStock(Long activityId) {
String stockKey = String.format(SECKILL_STOCK, activityId);
String stock = redisTemplate.opsForValue().get(stockKey);
return stock != null ? Integer.parseInt(stock) : 0;
}
/**
* 原子扣减库存(使用Lua脚本)
*
* @param activityId 活动ID
* @param userId 用户ID
* @param limitPerUser 每人限购数量
* @return 1-成功,-1-库存不足,-2-超过限购
*/
public Long deductStock(Long activityId, Long userId, Integer limitPerUser) {
String stockKey = String.format(SECKILL_STOCK, activityId);
String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);
Long result = redisTemplate.execute(
stockDeductScript,
Arrays.asList(stockKey, userBuyKey),
userId.toString(),
limitPerUser.toString()
);
log.info("库存扣减结果,活动ID:{},用户ID:{},结果:{}", activityId, userId, result);
return result;
}
/**
* 回滚库存(使用Lua脚本)
*
* @param activityId 活动ID
* @param userId 用户ID
* @param quantity 回滚数量
*/
public void rollbackStock(Long activityId, Long userId, Integer quantity) {
String stockKey = String.format(SECKILL_STOCK, activityId);
String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);
redisTemplate.execute(
stockRollbackScript,
Arrays.asList(stockKey, userBuyKey),
quantity.toString()
);
log.info("库存回滚成功,活动ID:{},用户ID:{},数量:{}", activityId, userId, quantity);
}
/**
* 获取用户已购数量
*
* @param activityId 活动ID
* @param userId 用户ID
* @return 已购数量
*/
public Integer getUserBuyCount(Long activityId, Long userId) {
String userBuyKey = String.format(SECKILL_USER_BUY, activityId, userId);
String count = redisTemplate.opsForValue().get(userBuyKey);
return count != null ? Integer.parseInt(count) : 0;
}
/**
* 尝试获取分布式锁
*
* @param activityId 活动ID
* @param userId 用户ID
* @param timeout 超时时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(Long activityId, Long userId, Long timeout) {
String lockKey = String.format(SECKILL_ORDER_LOCK, activityId, userId);
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放分布式锁
*
* @param activityId 活动ID
* @param userId 用户ID
*/
public void unlock(Long activityId, Long userId) {
String lockKey = String.format(SECKILL_ORDER_LOCK, activityId, userId);
redisTemplate.delete(lockKey);
}
// Lua脚本常量
private static final String STOCK_DEDUCT_LUA = """
— 秒杀库存扣减Lua脚本
local stock = tonumber(redis.call('get', KEYS[1]))
if stock == nil or stock <= 0 then
return -1
end
local userBuyKey = KEYS[2]
local userBuyCount = tonumber(redis.call('get', userBuyKey))
if userBuyCount ~= nil and userBuyCount >= tonumber(ARGV[2]) then
return -2
end
redis.call('decr', KEYS[1])
if userBuyCount == nil then
redis.call('set', userBuyKey, 1)
else
redis.call('incr', userBuyKey)
end
redis.call('expire', userBuyKey, 86400)
return 1
""";
private static final String STOCK_ROLLBACK_LUA = """
— 秒杀库存回滚Lua脚本
redis.call('incrby', KEYS[1], tonumber(ARGV[1]))
local userBuyCount = tonumber(redis.call('get', KEYS[2]))
if userBuyCount ~= nil and userBuyCount > 0 then
redis.call('decr', KEYS[2])
end
return 1
""";
}
四、消息队列异步下单
4.1 RabbitMQ配置
package com.example.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置类
*/
@Configuration
public class RabbitMQConfig {
/**
* 秒杀订单队列
*/
public static final String SECKILL_ORDER_QUEUE = "seckill.order.queue";
/**
* 秒死信队列(处理失败订单)
*/
public static final String SECKILL_ORDER_DEAD_QUEUE = "seckill.order.dead.queue";
/**
* 秒杀订单交换机
*/
public static final String SECKILL_ORDER_EXCHANGE = "seckill.order.exchange";
/**
* 秒杀订单路由键
*/
public static final String SECKILL_ORDER_ROUTING_KEY = "seckill.order.create";
/**
* 定义秒杀订单队列
*/
@Bean
public Queue seckillOrderQueue() {
return QueueBuilder.durable(SECKILL_ORDER_QUEUE)
// 绑定死信队列
.withArgument("x-dead-letter-exchange", SECKILL_ORDER_EXCHANGE)
.withArgument("x-dead-letter-routing-key", "seckill.order.dead")
// 队列过期时间(可选)
.withArgument("x-message-ttl", 300000) // 5分钟
.build();
}
/**
* 定义死信队列
*/
@Bean
public Queue seckillOrderDeadQueue() {
return QueueBuilder.durable(SECKILL_ORDER_DEAD_QUEUE).build();
}
/**
* 定义交换机
*/
@Bean
public DirectExchange seckillOrderExchange() {
return new DirectExchange(SECKILL_ORDER_EXCHANGE);
}
/**
* 绑定队列到交换机
*/
@Bean
public Binding seckillOrderBinding() {
return BindingBuilder.bind(seckillOrderQueue())
.to(seckillOrderExchange())
.with(SECKILL_ORDER_ROUTING_KEY);
}
/**
* 绑定死信队列到交换机
*/
@Bean
public Binding seckillOrderDeadBinding() {
return BindingBuilder.bind(seckillOrderDeadQueue())
.to(seckillOrderExchange())
.with("seckill.order.dead");
}
/**
* 消息转换器(JSON格式)
*/
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 配置RabbitTemplate
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
// 设置消息确认回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
// 消息发送失败,记录日志或重试
System.err.println("消息发送失败:" + cause);
}
});
return rabbitTemplate;
}
}
4.2 秒杀订单消息体
package com.example.model;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 秒杀订单消息体
*/
@Data
public class SeckillOrderMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 活动ID
*/
private Long activityId;
/**
* 用户ID
*/
private Long userId;
/**
* 商品ID
*/
private Long productId;
/**
* 商品名称
*/
private String productName;
/**
* 秒杀价
*/
private BigDecimal seckillPrice;
/**
* 购买数量
*/
private Integer quantity;
/**
* 订单创建时间
*/
private Long createTime;
}
4.3 消息生产者
package com.example.service;
import com.example.config.RabbitMQConfig;
import com.example.model.SeckillOrderMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 秒杀消息生产者
*/
@Service
public class SeckillMessageProducer {
private static final Logger log = LoggerFactory.getLogger(SeckillMessageProducer.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送秒杀订单消息
*
* @param message 订单消息
*/
public void sendSeckillOrderMessage(SeckillOrderMessage message) {
try {
rabbitTemplate.convertAndSend(
RabbitMQConfig.SECKILL_ORDER_EXCHANGE,
RabbitMQConfig.SECKILL_ORDER_ROUTING_KEY,
message
);
log.info("秒杀订单消息发送成功,活动ID:{},用户ID:{}",
message.getActivityId(), message.getUserId());
} catch (Exception e) {
log.error("秒杀订单消息发送失败,活动ID:{},用户ID:{}",
message.getActivityId(), message.getUserId(), e);
throw new RuntimeException("消息发送失败", e);
}
}
}
4.4 消息消费者
package com.example.consumer;
import com.example.entity.SeckillOrder;
import com.example.mapper.SeckillOrderMapper;
import com.example.model.SeckillOrderMessage;
import com.example.service.RedisSeckillService;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 秒杀订单消息消费者
*/
@Service
public class SeckillOrderConsumer {
private static final Logger log = LoggerFactory.getLogger(SeckillOrderConsumer.class);
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisSeckillService redisSeckillService;
/**
* 处理秒杀订单消息
*
* @param message 消息体
* @param channel RabbitMQ Channel
* @param msg 原始消息
*/
@RabbitListener(queues = "seckill.order.queue")
@Transactional(rollbackFor = Exception.class)
public void handleSeckillOrder(SeckillOrderMessage message, Channel channel, Message msg) {
log.info("开始处理秒杀订单消息,活动ID:{},用户ID:{}",
message.getActivityId(), message.getUserId());
try {
// 1. 检查用户是否已下单(数据库层面)
Integer existOrder = seckillOrderMapper.countByActivityIdAndUserId(
message.getActivityId(), message.getUserId());
if (existOrder > 0) {
log.warn("用户已下单,忽略消息,活动ID:{},用户ID:{}",
message.getActivityId(), message.getUserId());
// 确认消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
return;
}
// 2. 扣减数据库库存(乐观锁)
int updateResult = seckillOrderMapper.deductStock(message.getActivityId());
if (updateResult <= 0) {
log.warn("数据库库存不足,活动ID:{}", message.getActivityId());
// 回滚Redis库存
redisSeckillService.rollbackStock(
message.getActivityId(),
message.getUserId(),
message.getQuantity());
// 确认消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
return;
}
// 3. 创建订单
SeckillOrder order = new SeckillOrder();
order.setOrderNo(generateOrderNo());
order.setActivityId(message.getActivityId());
order.setUserId(message.getUserId());
order.setProductId(message.getProductId());
order.setProductName(message.getProductName());
order.setSeckillPrice(message.getSeckillPrice());
order.setQuantity(message.getQuantity());
order.setTotalAmount(message.getSeckillPrice().multiply(new BigDecimal(message.getQuantity())));
order.setStatus(0); // 待支付
order.setCreateTime(LocalDateTime.now());
seckillOrderMapper.insert(order);
// 4. 确认消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
log.info("秒杀订单创建成功,订单号:{},用户ID:{}", order.getOrderNo(), message.getUserId());
} catch (Exception e) {
log.error("处理秒杀订单消息异常,活动ID:{},用户ID:{}",
message.getActivityId(), message.getUserId(), e);
try {
// 消息处理失败,拒绝消息并重新入队
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, true);
} catch (Exception ex) {
log.error("消息Nack失败", ex);
}
}
}
/**
* 生成订单号
*/
private String generateOrderNo() {
return "SK" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8);
}
}
五、秒杀核心Service
5.1 秒杀活动Service
package com.example.service;
import com.example.entity.SeckillActivity;
import com.example.mapper.SeckillActivityMapper;
import com.example.model.SeckillOrderMessage;
import com.example.vo.SeckillResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
/**
* 秒杀活动Service
*/
@Service
public class SeckillActivityService {
private static final Logger log = LoggerFactory.getLogger(SeckillActivityService.class);
@Autowired
private SeckillActivityMapper seckillActivityMapper;
@Autowired
private RedisSeckillService redisSeckillService;
@Autowired
private SeckillMessageProducer messageProducer;
/**
* 获取秒杀活动详情
*
* @param activityId 活动ID
* @return 活动信息
*/
public SeckillActivity getActivity(Long activityId) {
return seckillActivityMapper.selectById(activityId);
}
/**
* 预热秒杀活动到Redis
*
* @param activityId 活动ID
*/
public void warmUpActivity(Long activityId) {
SeckillActivity activity = seckillActivityMapper.selectById(activityId);
if (activity == null) {
throw new RuntimeException("活动不存在");
}
// 预热库存
redisSeckillService.warmUpStock(activityId, activity.getAvailableStock());
log.info("秒杀活动预热成功,活动ID:{},库存:{}", activityId, activity.getAvailableStock());
}
/**
* 执行秒杀
*
* @param activityId 活动ID
* @param userId 用户ID
* @return 秒杀结果
*/
public SeckillResult executeSeckill(Long activityId, Long userId) {
// 1. 参数校验
if (activityId == null || userId == null) {
return SeckillResult.fail("参数错误");
}
// 2. 获取活动信息
SeckillActivity activity = seckillActivityMapper.selectById(activityId);
if (activity == null) {
return SeckillResult.fail("活动不存在");
}
// 3. 校验活动状态
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime())) {
return SeckillResult.fail("活动未开始");
}
if (now.isAfter(activity.getEndTime())) {
return SeckillResult.fail("活动已结束");
}
if (activity.getStatus() != 1) {
return SeckillResult.fail("活动状态异常");
}
// 4. 获取分布式锁(防止重复提交)
boolean locked = redisSeckillService.tryLock(activityId, userId, 10L);
if (!locked) {
return SeckillResult.fail("请勿重复提交");
}
try {
// 5. 扣减Redis库存(Lua脚本原子操作)
Long deductResult = redisSeckillService.deductStock(
activityId, userId, activity.getLimitPerUser());
if (deductResult == -1) {
return SeckillResult.fail("库存不足");
}
if (deductResult == -2) {
return SeckillResult.fail("超过限购数量");
}
// 6. 发送MQ消息,异步创建订单
SeckillOrderMessage message = new SeckillOrderMessage();
message.setActivityId(activityId);
message.setUserId(userId);
message.setProductId(activity.getProductId());
message.setProductName(activity.getProductName());
message.setSeckillPrice(activity.getSeckillPrice());
message.setQuantity(1);
message.setCreateTime(System.currentTimeMillis());
messageProducer.sendSeckillOrderMessage(message);
// 7. 返回成功
return SeckillResult.success("秒杀成功,请尽快支付");
} catch (Exception e) {
log.error("秒杀执行异常,活动ID:{},用户ID:{}", activityId, userId, e);
// 回滚库存
redisSeckillService.rollbackStock(activityId, userId, 1);
return SeckillResult.fail("系统繁忙,请稍后重试");
} finally {
// 8. 释放锁
redisSeckillService.unlock(activityId, userId);
}
}
/**
* 获取秒杀活动状态
*
* @param activityId 活动ID
* @return 活动状态信息
*/
public SeckillActivityStatus getActivityStatus(Long activityId) {
SeckillActivity activity = seckillActivityMapper.selectById(activityId);
if (activity == null) {
return null;
}
SeckillActivityStatus status = new SeckillActivityStatus();
status.setActivityId(activityId);
status.setStatus(activity.getStatus());
status.setStartTime(activity.getStartTime());
status.setEndTime(activity.getEndTime());
// 获取Redis中的实时库存
Integer stock = redisSeckillService.getStock(activityId);
status.setAvailableStock(stock);
// 计算剩余时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(activity.getStartTime())) {
// 未开始,计算倒计时
long seconds = ChronoUnit.SECONDS.between(now, activity.getStartTime());
status.setCountdown(seconds);
status.setPhase("NOT_START");
} else if (now.isAfter(activity.getEndTime())) {
// 已结束
status.setCountdown(0L);
status.setPhase("ENDED");
} else {
// 进行中
long seconds = ChronoUnit.SECONDS.between(now, activity.getEndTime());
status.setCountdown(seconds);
status.setPhase("IN_PROGRESS");
}
return status;
}
/**
* 秒杀活动状态内部类
*/
@lombok.Data
public static class SeckillActivityStatus {
private Long activityId;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer availableStock;
private Long countdown;
private String phase;
}
}
5.2 秒杀结果VO
package com.example.vo;
import lombok.Data;
/**
* 秒杀结果
*/
@Data
public class SeckillResult {
/**
* 状态码:200-成功,500-失败
*/
private Integer code;
/**
* 消息
*/
private String message;
/**
* 数据(可选)
*/
private Object data;
public static SeckillResult success(String message) {
SeckillResult result = new SeckillResult();
result.setCode(200);
result.setMessage(message);
return result;
}
public static SeckillResult fail(String message) {
SeckillResult result = new SeckillResult();
result.setCode(500);
result.setMessage(message);
return result;
}
public static SeckillResult success(String message, Object data) {
SeckillResult result = new SeckillResult();
result.setCode(200);
result.setMessage(message);
result.setData(data);
return result;
}
}
六、限流与防刷策略
6.1 限流注解
package com.example.annotation;
import java.lang.annotation.*;
/**
* 限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流key(支持SpEL表达式)
*/
String key() default "";
/**
* 限流次数
*/
int count() default 100;
/**
* 限流时间窗口(秒)
*/
int time() default 1;
/**
* 限流类型
*/
LimitType limitType() default LimitType.IP;
/**
* 限流类型枚举
*/
enum LimitType {
/**
* 按IP限流
*/
IP,
/**
* 按用户限流
*/
USER,
/**
* 按接口限流
*/
GLOBAL
}
}
6.2 限流切面
package com.example.aspect;
import com.example.annotation.RateLimit;
import com.example.exception.RateLimitException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Collections;
/**
* 限流切面
*/
@Aspect
@Component
public class RateLimitAspect {
private static final Logger log = LoggerFactory.getLogger(RateLimitAspect.class);
@Autowired
private StringRedisTemplate redisTemplate;
private DefaultRedisScript<Long> rateLimitScript;
@PostConstruct
public void init() {
rateLimitScript = new DefaultRedisScript<>();
rateLimitScript.setScriptText(RATE_LIMIT_LUA);
rateLimitScript.setResultType(Long.class);
}
@Before("@annotation(rateLimit)")
public void doBefore(JoinPoint point, RateLimit rateLimit) {
// 获取限流key
String key = getLimitKey(point, rateLimit);
// 执行限流Lua脚本
Long count = redisTemplate.execute(
rateLimitScript,
Collections.singletonList(key),
String.valueOf(rateLimit.count()),
String.valueOf(rateLimit.time())
);
if (count == null || count > rateLimit.count()) {
log.warn("触发限流,key:{},当前次数:{}", key, count);
throw new RateLimitException("请求过于频繁,请稍后重试");
}
}
/**
* 生成限流key
*/
private String getLimitKey(JoinPoint point, RateLimit rateLimit) {
StringBuilder key = new StringBuilder("rate_limit:");
// 根据限流类型生成key
switch (rateLimit.limitType()) {
case IP:
key.append(getIpAddress()).append(":");
break;
case USER:
// 从请求中获取用户ID(根据实际项目调整)
key.append(getUserId()).append(":");
break;
case GLOBAL:
key.append("global:");
break;
}
// 获取方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
key.append(method.getDeclaringClass().getName())
.append(".")
.append(method.getName());
return key.toString();
}
/**
* 获取请求IP地址
*/
private String getIpAddress() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return "unknown";
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理时取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
/**
* 获取用户ID(需要根据实际项目实现)
*/
private String getUserId() {
// TODO: 从SecurityContext或Token中获取用户ID
return "anonymous";
}
/**
* 限流Lua脚本
*/
private static final String RATE_LIMIT_LUA = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local count = tonumber(redis.call('get', key) or "0")
if count >= limit then
return count + 1
end
count = redis.call('incr', key)
if count == 1 then
redis.call('expire', key, expire)
end
return count
""";
}
6.3 验证码防刷
package com.example.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 验证码服务
*/
@Service
public class CaptchaService {
private static final Logger log = LoggerFactory.getLogger(CaptchaService.class);
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 验证码过期时间(秒)
*/
private static final long CAPTCHA_EXPIRE_SECONDS = 300;
/**
* 验证码Redis key前缀
*/
private static final String CAPTCHA_KEY_PREFIX = "captcha:seckill:";
/**
* 生成验证码
*
* @param userId 用户ID
* @return 验证码图片Base64和验证码ID
*/
public CaptchaResult generateCaptcha(Long userId) {
// 生成随机验证码
String captchaCode = generateRandomCode(4);
// 生成验证码ID
String captchaId = String.valueOf(System.currentTimeMillis());
// 保存到Redis
String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
redisTemplate.opsForValue().set(redisKey, captchaCode, CAPTCHA_EXPIRE_SECONDS, TimeUnit.SECONDS);
// 生成验证码图片
String base64Image = generateCaptchaImage(captchaCode);
log.info("验证码生成成功,用户ID:{},验证码ID:{}", userId, captchaId);
CaptchaResult result = new CaptchaResult();
result.setCaptchaId(captchaId);
result.setCaptchaImage(base64Image);
return result;
}
/**
* 验证验证码
*
* @param captchaId 验证码ID
* @param captchaCode 用户输入的验证码
* @return 是否验证成功
*/
public boolean verifyCaptcha(String captchaId, String captchaCode) {
if (captchaId == null || captchaCode == null) {
return false;
}
String redisKey = CAPTCHA_KEY_PREFIX + captchaId;
String storedCode = redisTemplate.opsForValue().get(redisKey);
if (storedCode == null) {
log.warn("验证码已过期或不存在,captchaId:{}", captchaId);
return false;
}
// 验证成功后删除验证码
if (storedCode.equalsIgnoreCase(captchaCode)) {
redisTemplate.delete(redisKey);
log.info("验证码验证成功,captchaId:{}", captchaId);
return true;
}
log.warn("验证码错误,captchaId:{},输入:{},正确:{}", captchaId, captchaCode, storedCode);
return false;
}
/**
* 生成随机验证码
*/
private String generateRandomCode(int length) {
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
/**
* 生成验证码图片
*/
private String generateCaptchaImage(String code) {
int width = 120;
int height = 40;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 24));
// 绘制干扰线
Random random = new Random();
g.setColor(Color.LIGHT_GRAY);
for (int i = 0; i < 20; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 绘制验证码
for (int i = 0; i < code.length(); i++) {
g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
g.drawString(String.valueOf(code.charAt(i)), 20 + i * 25, 28);
}
g.dispose();
// 转换为Base64
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "PNG", baos);
byte[] bytes = baos.toByteArray();
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
log.error("生成验证码图片失败", e);
return "";
}
}
/**
* 验证码结果内部类
*/
@lombok.Data
public static class CaptchaResult {
private String captchaId;
private String captchaImage;
}
}
七、秒杀Controller
package com.example.controller;
import com.example.annotation.RateLimit;
import com.example.entity.SeckillActivity;
import com.example.service.CaptchaService;
import com.example.service.SeckillActivityService;
import com.example.service.SeckillActivityService.SeckillActivityStatus;
import com.example.service.RedisSeckillService;
import com.example.vo.SeckillResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 秒杀Controller
*/
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
private static final Logger log = LoggerFactory.getLogger(SeckillController.class);
@Autowired
private SeckillActivityService seckillActivityService;
@Autowired
private CaptchaService captchaService;
@Autowired
private RedisSeckillService redisSeckillService;
/**
* 获取秒杀活动详情
*/
@GetMapping("/activity/{activityId}")
public Map<String, Object> getActivity(@PathVariable Long activityId) {
Map<String, Object> result = new HashMap<>();
try {
SeckillActivity activity = seckillActivityService.getActivity(activityId);
if (activity == null) {
result.put("code", 404);
result.put("message", "活动不存在");
return result;
}
result.put("code", 200);
result.put("data", activity);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "查询失败");
}
return result;
}
/**
* 获取秒杀活动状态(实时库存、倒计时等)
*/
@GetMapping("/activity/{activityId}/status")
public Map<String, Object> getActivityStatus(@PathVariable Long activityId) {
Map<String, Object> result = new HashMap<>();
try {
SeckillActivityStatus status = seckillActivityService.getActivityStatus(activityId);
if (status == null) {
result.put("code", 404);
result.put("message", "活动不存在");
return result;
}
result.put("code", 200);
result.put("data", status);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "查询失败");
}
return result;
}
/**
* 获取秒杀验证码
*/
@GetMapping("/captcha")
@RateLimit(count = 10, time = 60, limitType = RateLimit.LimitType.IP)
public Map<String, Object> getCaptcha(@RequestParam Long userId) {
Map<String, Object> result = new HashMap<>();
try {
CaptchaService.CaptchaResult captcha = captchaService.generateCaptcha(userId);
result.put("code", 200);
result.put("data", captcha);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "获取验证码失败");
}
return result;
}
/**
* 执行秒杀
*
* @param activityId 活动ID
* @param userId 用户ID
* @param captchaId 验证码ID
* @param captchaCode 验证码
*/
@PostMapping("/execute")
@RateLimit(count = 5, time = 1, limitType = RateLimit.LimitType.USER)
public SeckillResult executeSeckill(@RequestParam Long activityId,
@RequestParam Long userId,
@RequestParam String captchaId,
@RequestParam String captchaCode) {
// 1. 验证验证码
boolean captchaValid = captchaService.verifyCaptcha(captchaId, captchaCode);
if (!captchaValid) {
return SeckillResult.fail("验证码错误或已过期");
}
// 2. 执行秒杀
return seckillActivityService.executeSeckill(activityId, userId);
}
/**
* 预热秒杀活动(管理员接口)
*/
@PostMapping("/activity/{activityId}/warmup")
public Map<String, Object> warmUpActivity(@PathVariable Long activityId) {
Map<String, Object> result = new HashMap<>();
try {
seckillActivityService.warmUpActivity(activityId);
result.put("code", 200);
result.put("message", "预热成功");
} catch (Exception e) {
result.put("code", 500);
result.put("message", "预热失败:" + e.getMessage());
}
return result;
}
}
八、前端秒杀页面示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>秒杀活动</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.seckill-card {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.product-info {
display: flex;
gap: 20px;
}
.product-image {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 5px;
}
.product-detail {
flex: 1;
}
.product-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
}
.price-info {
margin: 15px 0;
}
.original-price {
text-decoration: line-through;
color: #999;
font-size: 16px;
}
.seckill-price {
color: #ff4444;
font-size: 28px;
font-weight: bold;
margin-left: 10px;
}
.stock-info {
color: #666;
margin-bottom: 20px;
}
.countdown {
background: #ff4444;
color: white;
padding: 10px 20px;
border-radius: 5px;
display: inline-block;
margin-bottom: 20px;
}
.countdown span {
font-weight: bold;
font-size: 20px;
}
.seckill-btn {
background: #ff4444;
color: white;
border: none;
padding: 15px 40px;
font-size: 18px;
border-radius: 5px;
cursor: pointer;
width: 100%;
}
.seckill-btn:hover {
background: #ff2222;
}
.seckill-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.captcha-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
.captcha-content {
background: white;
width: 350px;
padding: 30px;
border-radius: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.captcha-image {
width: 120px;
height: 40px;
cursor: pointer;
vertical-align: middle;
}
.captcha-input {
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 5px;
width: 150px;
}
.result-message {
text-align: center;
padding: 20px;
font-size: 18px;
}
.success {
color: #52c41a;
}
.fail {
color: #ff4444;
}
</style>
</head>
<body>
<div class="container">
<div class="seckill-card">
<div class="product-info">
<img src="product.jpg" alt="商品图片" class="product-image">
<div class="product-detail">
<h2 class="product-name">iPhone 15 Pro Max 256GB</h2>
<div class="price-info">
<span class="original-price">¥9999</span>
<span class="seckill-price">¥7999</span>
</div>
<div class="stock-info">
剩余库存:<span id="stock">100</span> 件
</div>
<div class="countdown">
距离开始:<span id="countdown">00:00:00</span>
</div>
<button class="seckill-btn" id="seckillBtn" οnclick="showCaptcha()">
立即秒杀
</button>
</div>
</div>
</div>
</div>
<!– 验证码弹窗 –>
<div class="captcha-modal" id="captchaModal">
<div class="captcha-content">
<h3 style="margin-bottom: 20px;">请输入验证码</h3>
<div style="margin-bottom: 15px;">
<img id="captchaImage" class="captcha-image" οnclick="refreshCaptcha()" title="点击刷新">
<input type="text" id="captchaCode" class="captcha-input" placeholder="请输入验证码" maxlength="4">
</div>
<div style="text-align: center;">
<button οnclick="executeSeckill()" style="padding: 10px 30px; margin-right: 10px;">确认</button>
<button οnclick="closeCaptcha()" style="padding: 10px 30px;">取消</button>
</div>
</div>
</div>
<!– 结果弹窗 –>
<div class="captcha-modal" id="resultModal">
<div class="captcha-content">
<div class="result-message" id="resultMessage"></div>
<div style="text-align: center; margin-top: 20px;">
<button οnclick="closeResult()" style="padding: 10px 30px;">确定</button>
</div>
</div>
</div>
<script>
let captchaId = '';
const userId = 10001; // 当前登录用户ID
const activityId = 1; // 秒杀活动ID
// 倒计时功能
function updateCountdown() {
// 这里应该从后端获取实际的倒计时
// 简化示例:模拟倒计时
const countdownElement = document.getElementById('countdown');
let timeStr = countdownElement.textContent;
let parts = timeStr.split(':');
let seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
if (seconds > 0) {
seconds–;
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
countdownElement.textContent =
String(hours).padStart(2, '0') + ':' +
String(mins).padStart(2, '0') + ':' +
String(secs).padStart(2, '0');
}
}
setInterval(updateCountdown, 1000);
// 显示验证码弹窗
function showCaptcha() {
document.getElementById('captchaModal').style.display = 'block';
refreshCaptcha();
}
// 关闭验证码弹窗
function closeCaptcha() {
document.getElementById('captchaModal').style.display = 'none';
}
// 刷新验证码
async function refreshCaptcha() {
try {
const response = await fetch(`/api/seckill/captcha?userId=${userId}`);
const data = await response.json();
if (data.code === 200) {
captchaId = data.data.captchaId;
document.getElementById('captchaImage').src = data.data.captchaImage;
}
} catch (error) {
console.error('获取验证码失败', error);
}
}
// 执行秒杀
async function executeSeckill() {
const captchaCode = document.getElementById('captchaCode').value;
if (!captchaCode) {
alert('请输入验证码');
return;
}
try {
const response = await fetch('/api/seckill/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `activityId=${activityId}&userId=${userId}&captchaId=${captchaId}&captchaCode=${captchaCode}`
});
const data = await response.json();
closeCaptcha();
showResult(data.code === 200, data.message);
if (data.code === 200) {
// 秒杀成功,更新库存显示
const stockElement = document.getElementById('stock');
stockElement.textContent = parseInt(stockElement.textContent) – 1;
}
} catch (error) {
console.error('秒杀请求失败', error);
showResult(false, '网络错误,请重试');
}
}
// 显示结果
function showResult(success, message) {
const resultMessage = document.getElementById('resultMessage');
resultMessage.className = success ? 'result-message success' : 'result-message fail';
resultMessage.textContent = message;
document.getElementById('resultModal').style.display = 'block';
}
// 关闭结果弹窗
function closeResult() {
document.getElementById('resultModal').style.display = 'none';
document.getElementById('captchaCode').value = '';
}
// 定时刷新库存
setInterval(async () => {
try {
const response = await fetch(`/api/seckill/activity/${activityId}/status`);
const data = await response.json();
if (data.code === 200) {
document.getElementById('stock').textContent = data.data.availableStock;
}
} catch (error) {
console.error('刷新库存失败', error);
}
}, 5000);
</script>
</body>
</html>
九、压力测试
9.1 JMeter测试计划
使用JMeter进行秒杀压力测试:
线程组配置
-
线程数:1000
-
Ramp-Up时间:1秒
-
循环次数:1
HTTP请求配置
-
方法:POST
-
路径:/api/seckill/execute
-
参数:
-
activityId: 1
-
userId: ${__Random(1,10000)}
-
captchaId: ${captchaId}
-
captchaCode: ${captchaCode}
-
查看结果树
-
监听器:查看结果树、聚合报告
9.2 预期测试结果
| 并发数 | 1000 |
| 响应时间 | < 100ms |
| TPS | > 500 |
| 错误率 | < 1% |
| 库存扣减准确性 | 100% |
十、常见问题及解决方案
10.1 超卖问题
问题描述:多个请求同时扣减库存,导致库存为负数。
解决方案:
Redis Lua脚本原子操作(本文采用)
数据库乐观锁:UPDATE stock SET count = count – 1 WHERE id = ? AND count > 0
分布式锁(性能较差)
10.2 重复下单
问题描述:同一用户多次点击秒杀按钮,产生多个订单。
解决方案:
分布式锁(本文采用)
数据库唯一索引:UNIQUE KEY (activity_id, user_id)
Redis用户购买记录
10.3 缓存与数据库不一致
问题描述:Redis库存与数据库库存不一致。
解决方案:
异步消息保证最终一致性(本文采用)
定时任务校对库存
数据库操作失败时回滚Redis
10.4 恶意刷单
问题描述:脚本批量请求,影响正常用户。
解决方案:
验证码(本文采用)
IP限流
用户行为分析
设备指纹
十一、优化建议
11.1 性能优化
使用本地缓存:Caffeine缓存活动信息,减少Redis访问
连接池优化:Redis、MySQL连接池配置
异步日志:使用Logback异步Appender
JVM调优:合理的堆大小和GC策略
11.2 架构优化
服务拆分:秒杀服务独立部署,不影响主站
多级缓存:Nginx缓存 → 本地缓存 → Redis → 数据库
CDN加速:静态资源使用CDN
弹性扩容:K8s自动扩容
11.3 业务优化
错峰秒杀:不同商品不同时间开始
排队机制:超过库存的请求进入排队
候补机制:未支付订单释放后,候补用户获得机会
十二、总结
本文实现了一个完整的秒杀系统,核心设计包括:
| 库存预热 | Redis | 减少数据库访问 |
| 库存扣减 | Redis + Lua | 原子操作,防止超卖 |
| 异步下单 | RabbitMQ | 削峰填谷,保护数据库 |
| 重复下单 | 分布式锁 + 唯一索引 | 保证一人一单 |
| 限流防刷 | 注解 + Lua | 保护系统稳定性 |
| 验证码 | Redis + 图片 | 防止脚本刷单 |
关键技术点:
Redis Lua脚本保证原子性
消息队列异步处理
分布式锁防并发
多级限流保护系统
参考资料
-
Redis官方文档
-
RabbitMQ官方文档
-
Spring Boot官方文档
-
高并发秒杀系统设计





