引言:秒杀背后的技术挑战
大家好!今天我们来聊聊电商系统中最具挑战的场景之一——秒杀系统。想象一下,1万件商品在1秒内被抢购一空,同时还要处理30分钟未支付的自动关闭订单。这不仅是业务需求,更是对技术架构的极限考验。
一、需求拆解:不只是"秒杀"那么简单
核心需求
-
秒杀活动:高并发下的库存扣减
-
订单管理:创建、支付、关闭等状态流转
-
超时关闭:30分钟未支付自动取消订单
-
库存回滚:订单关闭后库存恢复
-
用户体验:流畅的下单、支付流程
技术挑战
-
瞬间高并发:百万级QPS冲击
-
数据一致性:库存不超卖
-
系统可用性:99.99%的可靠性
-
延迟敏感:毫秒级响应
二、整体架构设计
核心设计思路是:前端限流、异步削峰、服务解耦、数据分治。
整个请求处理流程如下:
-
用户端(App/H5/小程序/PC)发起秒杀请求。
-
负载均衡层(Nginx集群)接收请求,进行初步的分发和负载均衡。
-
网关层进行统一管控:
API网关:负责路由转发、用户鉴权、请求聚合。
Sentinel:实现限流、熔断、降级,保护下游业务服务。
-
业务服务层是核心业务逻辑所在:
秒杀服务:处理秒杀资格校验、库存扣减(通常与Redis配合)。
订单服务:生成订单。
支付服务:处理支付流程。
各服务均为集群部署,保证高可用。
-
中间件层提供关键支撑:
Redis集群:缓存热点数据(如商品库存)、存放秒杀令牌,支撑高并发读和原子扣减。
RabbitMQ集群:将下单等耗时操作异步化,实现流量削峰,提升系统吞吐量。
xxl-job:执行定时任务,如活动状态同步、对账等。
-
数据存储层:
MySQL分库分表:应对海量订单数据的存储和查询。
TiDB(可选):作为分布式数据库的补充,解决分库分表带来的复杂性问题。
-
监控告警层(Prometheus + Grafana):对整个系统的性能指标、业务指标进行监控和可视化,便于及时发现问题。
三、核心模块设计
1. 秒杀服务设计
/**
* 秒杀核心服务
* 采用Redis + Lua脚本保证原子性
*/
@Component
@Slf4j
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 秒杀核心逻辑
*/
public SeckillResult seckill(Long userId, Long seckillId) {
// 1. 前置检查
if (!checkUserLimit(userId, seckillId)) {
return SeckillResult.error("已达购买限制");
}
// 2. Redis预减库存(Lua脚本保证原子性)
Long stock = decrStockByLua(seckillId);
if (stock < 0) {
// 库存不足,异步恢复用户购买资格
restoreUserLimit(userId, seckillId);
return SeckillResult.error("库存不足");
}
// 3. 发送异步消息创建订单
String orderNo = sendCreateOrderMessage(userId, seckillId);
return SeckillResult.success(orderNo);
}
/**
* Lua脚本实现原子库存扣减
*/
private Long decrStockByLua(Long seckillId) {
String luaScript =
"local stockKey = KEYS[1] " +
"local stock = redis.call('get', stockKey) " +
"if stock and tonumber(stock) > 0 then " +
" redis.call('decr', stockKey) " +
" return tonumber(stock) - 1 " +
"else " +
" return -1 " +
"end";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
return redisTemplate.execute(script,
Collections.singletonList("seckill:stock:" + seckillId));
}
/**
* 检查用户购买限制
*/
private boolean checkUserLimit(Long userId, Long seckillId) {
String key = String.format("seckill:limit:%s:%s", seckillId, userId);
RAtomicLong counter = redissonClient.getAtomicLong(key);
// 设置过期时间30分钟
if (counter.isExists()) {
counter.expire(30, TimeUnit.MINUTES);
}
return counter.incrementAndGet() <= 1; // 每个用户限购1件
}
}
2. 订单状态机设计
/**
* 订单状态机
*/
@Component
public class OrderStateMachine {
public enum OrderStatus {
CREATED(0, "已创建"),
PAID(1, "已支付"),
CANCELED(2, "已取消"),
CLOSED(3, "已关闭"),
COMPLETED(4, "已完成");
private final int code;
private final String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
/**
* 状态转移规则
*/
private static final Map<OrderStatus, Set<OrderStatus>> STATE_TRANSITIONS =
new EnumMap<>(OrderStatus.class);
static {
STATE_TRANSITIONS.put(OrderStatus.CREATED,
EnumSet.of(OrderStatus.PAID, OrderStatus.CANCELED, OrderStatus.CLOSED));
STATE_TRANSITIONS.put(OrderStatus.PAID,
EnumSet.of(OrderStatus.COMPLETED, OrderStatus.CANCELED));
STATE_TRANSITIONS.put(OrderStatus.CANCELED, EnumSet.of());
STATE_TRANSITIONS.put(OrderStatus.CLOSED, EnumSet.of());
STATE_TRANSITIONS.put(OrderStatus.COMPLETED, EnumSet.of());
}
/**
* 校验状态转移是否合法
*/
public boolean canTransition(OrderStatus from, OrderStatus to) {
Set<OrderStatus> allowed = STATE_TRANSITIONS.get(from);
return allowed != null && allowed.contains(to);
}
/**
* 执行状态转移
*/
public Order executeTransition(Order order, OrderStatus newStatus, String remark) {
if (!canTransition(order.getStatus(), newStatus)) {
throw new IllegalStateException(
String.format("状态转移非法: %s -> %s", order.getStatus(), newStatus));
}
order.setStatus(newStatus);
order.setUpdateTime(new Date());
// 记录状态变更日志
OrderStatusLog log = new OrderStatusLog();
log.setOrderId(order.getId());
log.setFromStatus(order.getStatus());
log.setToStatus(newStatus);
log.setRemark(remark);
log.setCreateTime(new Date());
return order;
}
}
3. 订单关闭的三种实现方案
方案一:定时任务扫描(简单但低效)
/**
* 定时任务扫描未支付订单
* 优点:实现简单
* 缺点:性能差,时间不精确
*/
@Component
@Slf4j
public class OrderCloseJob {
@Autowired
private OrderService orderService;
// 每5分钟执行一次
@Scheduled(fixedDelay = 5 * 60 * 1000)
public void closeExpiredOrders() {
log.info("开始扫描超时未支付订单...");
// 查询30分钟前创建的未支付订单
Date expireTime = DateUtils.addMinutes(new Date(), -30);
List<Order> expiredOrders = orderService.findExpiredOrders(expireTime, 1000);
for (Order order : expiredOrders) {
try {
orderService.closeOrder(order.getId(), "超时未支付");
log.info("关闭超时订单: {}", order.getOrderNo());
} catch (Exception e) {
log.error("关闭订单失败: {}", order.getOrderNo(), e);
}
}
}
}
方案二:延迟消息队列(推荐)
/**
* 基于RocketMQ延迟消息的实现
*/
@Component
@Slf4j
public class OrderCloseByDelayMessage {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 创建订单时发送延迟消息
*/
public void sendCloseOrderMessage(Order order) {
try {
// RocketMQ支持18个延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
int delayLevel = 16; // 对应30分钟
Message<String> message = MessageBuilder
.withPayload(order.getId().toString())
.setHeader(MessageConst.PROPERTY_KEYS, order.getOrderNo())
.build();
// 发送延迟消息
rocketMQTemplate.syncSend("ORDER_CLOSE_TOPIC", message, 3000, delayLevel);
log.info("发送订单关闭延迟消息: orderNo={}", order.getOrderNo());
} catch (Exception e) {
log.error("发送延迟消息失败", e);
// 降级方案:记录到数据库,由定时任务补偿
orderService.saveCloseTask(order.getId(), new Date());
}
}
/**
* 消费延迟消息
*/
@RocketMQMessageListener(
topic = "ORDER_CLOSE_TOPIC",
consumerGroup = "ORDER_CLOSE_CONSUMER_GROUP"
)
public class OrderCloseConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
String orderId = new String(message.getBody());
log.info("收到订单关闭消息: orderId={}", orderId);
try {
orderService.closeOrder(Long.parseLong(orderId), "超时未支付");
} catch (Exception e) {
log.error("关闭订单失败,重试...", e);
// 重试机制
throw new RuntimeException(e);
}
}
}
}
方案三:Redis过期键监听(高精度)
/**
* 基于Redis键过期事件的实现
*/
@Component
@Slf4j
public class OrderCloseByRedisExpire {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private OrderService orderService;
/**
* 创建订单时设置Redis键,30分钟后过期
*/
public void setOrderExpireKey(Order order) {
String key = String.format("order:expire:%s", order.getOrderNo());
String value = order.getId().toString();
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
log.info("设置订单过期键: key={}, expire=30m", key);
}
/**
* 监听Redis过期事件
* 需要在Redis配置中开启:notify-keyspace-events Ex
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
if (expiredKey.startsWith("order:expire:")) {
String orderNo = expiredKey.substring("order:expire:".length());
log.info("检测到订单过期: orderNo={}", orderNo);
// 异步处理,避免阻塞
CompletableFuture.runAsync(() -> {
try {
orderService.closeOrderByNo(orderNo, "超时未支付");
} catch (Exception e) {
log.error("处理过期订单失败: orderNo={}", orderNo, e);
}
});
}
}
}
}
4. 库存管理策略
/**
* 库存管理服务
* 三级库存设计:Redis -> MySQL -> 预警
*/
@Service
@Slf4j
public class StockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
// 库存同步锁
private final Lock syncLock = new ReentrantLock();
/**
* 预减库存
*/
public boolean preDeductStock(Long productId, Integer quantity) {
String key = "product:stock:" + productId;
// 使用Lua脚本保证原子性
String luaScript =
"local stock = redis.call('get', KEYS[1]) " +
"if not stock then " +
" return -2 " + // Redis中无库存数据
"end " +
"stock = tonumber(stock) " +
"if stock >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" return stock - tonumber(ARGV[1]) " +
"else " +
" return -1 " + // 库存不足
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
quantity.toString()
);
if (result == -2) {
// Redis中无数据,从数据库加载
syncStockFromDB(productId);
return preDeductStock(productId, quantity);
}
return result >= 0;
}
/**
* 关闭订单时恢复库存
*/
public void restoreStock(Long productId, Integer quantity) {
String key = "product:stock:" + productId;
// 异步恢复,避免影响主流程
CompletableFuture.runAsync(() -> {
try {
redisTemplate.opsForValue().increment(key, quantity);
log.info("恢复库存成功: productId={}, quantity={}", productId, quantity);
// 异步更新数据库
updateStockInDB(productId, quantity);
} catch (Exception e) {
log.error("恢复库存失败", e);
}
});
}
/**
* 定时同步数据库库存到Redis
*/
@Scheduled(fixedRate = 60000) // 每分钟同步一次
public void syncStockToRedis() {
if (!syncLock.tryLock()) {
return;
}
try {
List<Product> products = productMapper.selectAll();
for (Product product : products) {
String key = "product:stock:" + product.getId();
redisTemplate.opsForValue().set(key, product.getStock());
}
log.info("库存同步完成,同步{}个商品", products.size());
} finally {
syncLock.unlock();
}
}
}
四、高可用与容错设计
1. 限流与降级
/**
* 网关层限流配置
*/
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
);
}
@Bean
public RedisRateLimiter redisRateLimiter() {
// 每秒100个请求,突发200个请求
return new RedisRateLimiter(100, 200);
}
}
2. 熔断与降级
/**
* 订单服务熔断降级
*/
@Service
@Slf4j
public class OrderService {
// 创建订单的熔断器
@CircuitBreaker(name = "createOrder", fallbackMethod = "createOrderFallback")
@TimeLimiter(name = "createOrder")
public CompletableFuture<Order> createOrderAsync(OrderDTO orderDTO) {
return CompletableFuture.supplyAsync(() -> {
// 业务逻辑
return doCreateOrder(orderDTO);
});
}
// 降级方法
public CompletableFuture<Order> createOrderFallback(OrderDTO orderDTO, Exception e) {
log.warn("创建订单降级,进入排队队列", e);
// 1. 将订单请求放入队列
// 2. 返回排队中状态
// 3. 异步处理
Order order = new Order();
order.setStatus(OrderStatus.PENDING);
order.setMessage("系统繁忙,您的订单正在排队处理");
return CompletableFuture.completedFuture(order);
}
}
3. 数据一致性保障
/**
* 分布式事务解决方案
*/
@Service
@Slf4j
public class OrderTransactionService {
/**
* TCC模式解决订单创建事务
*/
@Transactional(rollbackFor = Exception.class)
public boolean createOrderWithTCC(Order order) {
try {
// Try阶段
boolean tryResult = orderTccService.tryCreateOrder(order);
if (!tryResult) {
throw new RuntimeException("Try阶段失败");
}
// Confirm阶段
orderTccService.confirmCreateOrder(order);
return true;
} catch (Exception e) {
// Cancel阶段
orderTccService.cancelCreateOrder(order);
throw e;
}
}
}
五、监控与报警
# Prometheus监控配置
spring:
application:
name: seckill-service
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
metrics:
export:
prometheus:
enabled: true
# 自定义指标
seckill:
metrics:
requests_total: "seckill_requests_total"
success_total: "seckill_success_total"
fail_total: "seckill_fail_total"
duration_seconds: "seckill_duration_seconds"
/**
* 业务指标监控
*/
@Component
public class SeckillMetrics {
private final MeterRegistry meterRegistry;
private final Counter requestCounter;
private final Counter successCounter;
private final Timer seckillTimer;
public SeckillMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.requestCounter = Counter.builder("seckill.requests.total")
.description("秒杀请求总数")
.register(meterRegistry);
this.successCounter = Counter.builder("seckill.success.total")
.description("秒杀成功总数")
.register(meterRegistry);
this.seckillTimer = Timer.builder("seckill.duration")
.description("秒杀处理耗时")
.register(meterRegistry);
}
public void recordRequest() {
requestCounter.increment();
}
public void recordSuccess() {
successCounter.increment();
}
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
public void stopTimer(Timer.Sample sample) {
sample.stop(seckillTimer);
}
}
六、压测方案
/**
* JMeter压测脚本配置示例
*/
public class SeckillLoadTest {
public static void main(String[] args) {
// 压测场景
// 1. 库存预热:1000件商品
// 2. 模拟用户:10000并发
// 3. 压测时间:5分钟
// 4. 监控指标:
// - QPS:目标5000+
// - 响应时间:P99 < 200ms
// - 错误率:< 0.1%
// - CPU使用率:< 70%
// - 内存使用率:< 80%
}
}
七、部署架构
# Docker Compose部署配置
version: '3.8'
services:
# 应用服务
seckill-service:
image: seckill-service:latest
deploy:
replicas: 10
resources:
limits:
cpus: '2'
memory: 4G
environment:
- JAVA_OPTS=-Xmx3g -Xms3g -XX:+UseG1GC
# Redis集群
redis-cluster:
image: redis:7-alpine
command: redis-server /usr/local/etc/redis/redis.conf
deploy:
mode: global
# MySQL集群
mysql-master:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
mysql-slave:
image: mysql:8.0
scale: 3
八、总结与展望
核心要点回顾
-
分层架构:清晰的系统分层,各司其职
-
异步化:消息队列解耦,提升吞吐量
-
缓存策略:多级缓存,减少数据库压力
-
限流降级:保护系统不被压垮
-
监控报警:快速发现和定位问题
优化方向
-
弹性伸缩:基于K8s的HPA自动扩缩容
-
智能库存:基于机器学习的库存预测
-
边缘计算:CDN边缘节点处理静态资源
-
服务网格:Istio实现更精细的流量控制
写给开发者的建议
-
代码可读性:复杂系统更需要清晰的代码结构
-
文档完整性:架构图、接口文档、部署手册
-
监控完备性:没有监控的系统就像"盲人摸象"
-
容错设计:总是假设任何环节都可能失败
结语
秒杀系统的设计是一个系统工程,涉及架构、算法、网络、数据库等多个领域。30分钟未支付自动关闭只是其中的一个环节,但却体现了分布式系统设计的精髓:在保证数据一致性的前提下,提供高可用、高性能的服务。
思考题: 如果你的秒杀系统要在双11支持1000万QPS,你会如何调整架构? 欢迎在评论区分享你的设计方案!