本文目录导读:

我来为你设计一个高并发秒杀系统的完整案例,这是一个经典的分布式系统设计难题,我会从架构到代码给出完整的解决方案。
系统需求分析
核心业务场景
- 商品限量抢购(如1000件商品,10万人抢购)
- 高并发访问(瞬时QPS可达10万+)
- 需要防止超卖和重复购买
核心挑战
- 高并发:瞬间流量巨大
- 超卖问题:库存不能为负数
- 限购问题:同一用户限购1件
- 响应延迟:用户体验要求高
系统架构设计
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 客户端/APP │────▶│ 负载均衡 │────▶│ 静态资源CDN │
└─────────────┘ │ (Nginx) │ └──────────────┘
└──────────────┘
│
┌──────▼──────┐
│ 网关层 │
│ (限流/降级) │
└──────┬──────┘
│
┌──────▼────────────────────────────────┐
│ 应用层 (集群部署) │
│ 秒杀接口 ─ 缓存预减 ─ 异步下单 │
└──────┬────────────────────────────────┘
│
┌────────────┼─────────────┐
│ │ │
┌────────▼───┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Redis集群 │ │ 消息队列 │ │ 数据库集群 │
│ (库存预减) │ │ (RabbitMQ)│ │ (最终库存) │
└────────────┘ └───────────┘ └───────────┘
核心代码实现
秒杀接口设计(Controller层)
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private RedisService redisService;
/**
* 秒杀接口
* @param userId 用户ID
* @param goodsId 商品ID
*/
@PostMapping("/execute")
public Result<String> seckill(@RequestParam Long userId,
@RequestParam Long goodsId) {
// 1. 前置校验(限流拦截)
if (!seckillService.preCheck(userId, goodsId)) {
return Result.error("请求过于频繁或已参加过秒杀");
}
// 2. 执行秒杀
boolean success = seckillService.executeSeckill(userId, goodsId);
return success ? Result.success("秒杀成功") : Result.error("库存不足");
}
}
秒杀核心服务(Service层)
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private GoodsMapper goodsMapper;
private static final String STOCK_PREFIX = "seckill:stock:";
private static final String USER_PREFIX = "seckill:user:";
/**
* 秒杀执行(核心逻辑)
*/
@Override
public boolean executeSeckill(Long userId, Long goodsId) {
// 1. 检查用户是否已经秒杀过(防止重复秒杀)
Boolean isExists = redisTemplate.hasKey(USER_PREFIX + goodsId + ":" + userId);
if (Boolean.TRUE.equals(isExists)) {
return false;
}
// 2. Redis库存预减(原子操作)
Long stock = redisTemplate.opsForValue().decrement(STOCK_PREFIX + goodsId);
if (stock < 0) {
// 库存不足,回补库存
redisTemplate.opsForValue().increment(STOCK_PREFIX + goodsId);
return false;
}
// 3. 记录用户已秒杀(防止重复)
redisTemplate.opsForValue().set(USER_PREFIX + goodsId + ":" + userId,
String.valueOf(System.currentTimeMillis()),
24, TimeUnit.HOURS);
// 4. 发送异步消息,生成订单
try {
SeckillMessage message = new SeckillMessage(userId, goodsId);
rocketMQTemplate.convertAndSend("seckill-order-topic", message);
return true;
} catch (Exception e) {
log.error("发送秒杀消息失败", e);
// 消息发送失败,需要回滚
rollbackStock(userId, goodsId);
return false;
}
}
/**
* 回滚库存
*/
private void rollbackStock(Long userId, Long goodsId) {
redisTemplate.opsForValue().increment(STOCK_PREFIX + goodsId);
redisTemplate.delete(USER_PREFIX + goodsId + ":" + userId);
}
/**
* 前置校验
*/
public boolean preCheck(Long userId, Long goodsId) {
// 检查商品是否在秒杀窗口期
SeckillGoods goods = goodsMapper.selectById(goodsId);
if (goods == null) {
return false;
}
long current = System.currentTimeMillis();
if (current < goods.getStartTime() || current > goods.getEndTime()) {
return false;
}
return true;
}
}
Redis库存初始化
@Component
public class StockInitializer implements InitializingBean {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private GoodsMapper goodsMapper;
private static final String STOCK_PREFIX = "seckill:stock:";
@Override
public void afterPropertiesSet() {
// 系统启动时,将数据库库存加载到Redis
List<Goods> goodsList = goodsMapper.selectAll();
for (Goods goods : goodsList) {
if (goods.getStock() > 0) {
redisTemplate.opsForValue().set(
STOCK_PREFIX + goods.getId(),
String.valueOf(goods.getStock())
);
}
}
}
}
消息队列消费者(异步下单)
@Component
@RocketMQMessageListener(topic = "seckill-order-topic",
consumerGroup = "seckill-order-group")
@Slf4j
public class SeckillOrderConsumer implements RocketMQListener<SeckillMessage> {
@Autowired
private OrderService orderService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public void onMessage(SeckillMessage message) {
Long userId = message.getUserId();
Long goodsId = message.getGoodsId();
// 生成订单
try {
Order order = new Order();
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setOrderNo(generateOrderNo());
order.setCreateTime(new Date());
orderService.createOrder(order);
// 订单成功后,更新数据库库存
boolean updated = orderService.decreaseStock(goodsId);
if (!updated) {
// 数据库中库存不足,补偿
log.error("数据库库存更新失败: goodsId={}", goodsId);
}
} catch (Exception e) {
log.error("创建订单异常", e);
// 可以加入死信队列或重试机制
}
}
private String generateOrderNo() {
return "ORDER" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
}
限流器实现(防止刷单)
@Component
public class RateLimiter {
// 使用Guava RateLimiter或自研令牌桶
private final Map<Long, RateLimiter> goodsRateLimiters = new ConcurrentHashMap<>();
/**
* 基于IP和用户ID的限流
*/
public boolean tryAcquire(Long userId, String ip) {
// 1. 用户级别限流:同一用户1秒内只能请求1次
String userKey = "user:" + userId;
if (!allow("rate:user:" + userId, 1, 1)) {
return false;
}
// 2. IP级别限流:同一IP 1秒内只能请求10次
if (!allow("rate:ip:" + ip, 10, 1)) {
return false;
}
// 3. 商品级别限流:控制瞬时并发
RateLimiter goodsLimiter = goodsRateLimiters.computeIfAbsent(
goodsId, k -> RateLimiter.create(1000)); // 每秒1000个请求
return goodsLimiter.tryAcquire();
}
/**
* 基于Redis的滑动窗口限流
*/
private boolean allow(String key, int limit, int windowSeconds) {
long currentTime = System.currentTimeMillis();
String member = String.valueOf(currentTime);
// 使用ZSET实现滑动窗口
Long count = redisTemplate.opsForZSet().count(key,
currentTime - windowSeconds * 1000, currentTime);
if (count < limit) {
redisTemplate.opsForZSet().add(key, member, currentTime);
// 清理过期数据
redisTemplate.expire(key, windowSeconds + 1, TimeUnit.SECONDS);
return true;
}
return false;
}
}
数据库表设计
-- 秒杀商品表 CREATE TABLE `seckill_goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `goods_name` varchar(100) NOT NULL COMMENT '商品名称', `stock` int(11) NOT NULL COMMENT '库存', `price` decimal(10,2) NOT NULL COMMENT '秒杀价格', `start_time` datetime NOT NULL COMMENT '秒杀开始时间', `end_time` datetime NOT NULL COMMENT '秒杀结束时间', `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='秒杀商品表'; -- 订单表 CREATE TABLE `seckill_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(50) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `goods_id` bigint(20) NOT NULL COMMENT '商品ID', `status` tinyint(4) DEFAULT '0' COMMENT '订单状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_goods` (`user_id`,`goods_id`) -- 唯一约束防止重复下单 ) ENGINE=InnoDB COMMENT='秒杀订单表';
性能优化方案
多级缓存策略
// 一级缓存:本地缓存(Caffeine) // 二级缓存:Redis分布式缓存 // 三级缓存:数据库
数据库优化
-- 使用乐观锁防止超卖
UPDATE seckill_goods
SET stock = stock - 1, version = version + 1
WHERE id = #{goodsId}
AND stock > 0
AND version = #{version};
-- 如果更新失败,说明库存不足或版本冲突
异步订单处理
// 秒杀成功后,不立即创建订单 // 而是发送MQ消息,后台异步处理 // 用户可以在"我的订单"中查看结果
部署架构建议
生产环境建议:
├── 4台Nginx(负载均衡)
├── 4台前端服务器(静态资源)
├── 4台应用服务器(部署秒杀服务)
├── 1个Redis集群(3主3从)
├── 1个RocketMQ集群(2个节点)
└── 数据库集群(主从复制,读写分离)
实战提示
- 压测准备:使用JMeter进行压力测试
- 监控告警:使用Prometheus + Grafana
- 降级方案:秒杀结束/异常时,快速返回错误
- 数据一致性:Redis和数据库的最终一致性
- 限流策略:防止恶意刷接口
这个案例涵盖了秒杀系统的核心设计思路,你可以根据实际业务场景调整和优化,如果你有具体问题(比如某个模块的实现细节),我可以进一步详细讲解。