前言
秒杀系统的文章网上一搜一大把,Redis 缓存、消息队列、限流熔断那一套,相信大家都能背下来了。
但是现在已经是 2025 年,Java 已经进化到 21 了,虚拟线程、结构化并发、记录模式这些新特性,能不能把老掉牙的秒杀架构玩出点新花样?
这篇文章来了,直接聊点干货。

整体架构设计
整体架构其实这些年变化不大,基本还是这几位老朋友:接入层顶流量,应用层搞逻辑,缓存层抗压力,消息队列做削峰,数据库兜底。
仍然遵循这种分层防护的思想,每一层都承担特定的防护职责。
用户层通过CDN将静态资源分发到各地,减少用户访问延迟。负载均衡器智能分发请求,避免单点故障。
接入层是系统的第一道屏障。网关集群处理请求路由、用户认证、协议转换等工作,限流中间件则像水闸一样控制流量,防止系统被瞬间涌入的请求冲垮。
应用层实现核心业务逻辑。秒杀服务集群负责库存检查和扣减,预扣库存服务处理库存预占,订单服务管理订单生命周期。
缓存层提供多级缓存服务。Redis集群存储实时库存和用户购买状态,本地缓存Caffeine提供毫秒级访问速度,大幅减少网络开销。
消息层通过RocketMQ实现系统解耦。将耗时的订单处理操作异步化,提高用户响应速度。
数据层采用主从架构保证数据安全。主库处理写操作,从库分担读压力,备份库防止数据丢失。

核心流程设计
秒杀主流程解析
一个完整的秒杀请求在系统中的流转过程,涉及多个业务组件的协同工作:
整个流程的精髓在于快速响应 和异步处理。
用户发起请求后,系统首先进行多层检查,快速过滤掉无效请求。
对于有效请求,立即进行库存扣减并返回成功消息,而复杂的订单处理则放在后台异步进行。
确保用户能在毫秒级时间内收到反馈。
库存扣减核心策略
库存扣减是整个秒杀系统的核心难点。
以某手机首发为例,假设只有100台现货,但同时有10万用户点击购买。如何确保恰好100个用户成功,而不会出现101台或者99台的情况?
这个流程采用了多级过滤的设计思想。
本地缓存预检查能拦截90%以上的无效请求,Redis分布式锁保证操作的互斥性,再结合Lua脚本确保扣减操作的原子性。
通过这种层层过滤的机制,既保证了数据的准确性,又最大化了系统的性能。
关键技术讲解
多级缓存
多级缓存的核心思想是就近访问 和逐层过滤。
单纯依赖Redis的话,在极高并发下反而容易成为瓶颈。
当100万用户同时查询库存时,即使Redis性能再强,也难以应对如此巨大的压力。
通过多级缓存将这100万次查询分层拦截,让真正需要到达Redis的请求大幅减少。
scss
@Component
public class InventoryCache {
// 本地缓存:使用Caffeine实现高性能内存缓存
private final Cache<Long, Integer> localCache = Caffeine.newBuilder()
.maximumSize(10_000) // 缓存1万个商品的库存信息
.expireAfterWrite(Duration.ofSeconds(1)) // 1秒过期保证实时性
.build();
/**
* 本地预检查:拦截大部分无效请求
* 作用:减少Redis压力,提高响应速度
* 策略:宁可误拒绝,不能误通过
*/
public boolean preCheck(Long productId, Integer quantity) {
Integer cached = localCache.getIfPresent(productId);
// 本地缓存显示库存不足时直接拒绝,避免无谓的Redis访问
return cached != null && cached >= quantity;
}
/**
* 异步刷新缓存:使用虚拟线程更新本地缓存
* 虚拟线程特点:创建成本极低,适合大量短期任务
*/
public void refreshAsync(Long productId) {
Thread.ofVirtual().name("cache-refresh-" + productId).start(() -> {
// 从Redis获取最新库存并更新本地缓存
Integer inventory = redisTemplate.opsForValue().get("inventory:" + productId);
if (inventory != null) {
localCache.put(productId, inventory);
}
});
}
}
限流
限流应该是最常见的手段,而令牌桶算法是限流的经典实现。
系统以恒定速率向桶中投放令牌,每个请求消耗一个令牌。当请求过多时,桶中令牌不足,多余请求被拒绝或排队。这种机制既能平滑处理突发流量,又能保护系统不被压垮。
实现令牌桶最简单的方式就是Guava
的线程工具,但这里我们手搓一个lua脚本,也能更清晰的了解到整个令牌桶的流程。
scss
public class DistributedRateLimiter {
/**
* Redis + Lua实现的分布式令牌桶算法
* 原子性:Lua脚本在Redis内原子执行,避免并发问题
* 一致性:全局统一的限流状态,避免单机限流的不均衡问题
*/
private static final String RATE_LIMIT_SCRIPT = """
local key = KEYS[1] -- 限流键
local capacity = tonumber(ARGV[1]) -- 桶容量
local tokens = tonumber(ARGV[2]) -- 补充速率
local interval = tonumber(ARGV[3]) -- 时间间隔
-- 获取当前桶状态
local current = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens_count = tonumber(current[1]) or capacity
local last_refill = tonumber(current[2]) or 0
-- 根据时间流逝补充令牌
local now = redis.call('time')[1]
local elapsed = math.max(0, now - last_refill)
tokens_count = math.min(capacity, tokens_count + (elapsed * tokens / interval))
-- 尝试获取令牌
if tokens_count >= 1 then
tokens_count = tokens_count - 1
redis.call('hmset', key, 'tokens', tokens_count, 'last_refill', now)
redis.call('expire', key, interval * 2)
return 1 -- 获取成功
else
return 0 -- 获取失败
end
""";
}
扣减库存
库存扣减才是秒杀系统的核心,自然也是难点所在。
最简单的做法就是数据库锁,但是一旦出现并发(甚至都不用高并发),性能很差。当然也可以用乐观锁,虽然性能相对较好,但是失败率高,比较影响用户体验。
所以高并发场景下一般会采用 Redis + Lua脚本的方案,既能保证操作的原子性,又拥有出色的性能表现。
arduino
@Service
public class InventoryService {
/**
* 库存扣减的核心Lua脚本
* 原子操作:整个脚本作为一个事务执行,不可分割
* 防重复:检查用户购买记录,避免重复下单
* 高性能:避免多次网络往返,减少延迟
*/
private static final String DEDUCT_INVENTORY_SCRIPT = """
local product_key = KEYS[1] -- 商品库存键
local user_key = KEYS[2] -- 用户购买记录键
local quantity = tonumber(ARGV[1]) -- 购买数量
local user_id = ARGV[2] -- 用户ID
-- 防重复检查:避免用户重复购买
if redis.call('exists', user_key) == 1 then
return -2 -- 重复购买错误码
end
-- 库存检查和原子扣减
local current_stock = redis.call('get', product_key)
if not current_stock or tonumber(current_stock) < quantity then
return -1 -- 库存不足错误码
end
-- 原子操作:扣减库存并标记用户
redis.call('decrby', product_key, quantity)
redis.call('setex', user_key, 3600, user_id) -- 用户购买标记,1小时有效
return tonumber(current_stock) - quantity -- 返回剩余库存
""";
public SeckillResult deductInventory(Long productId, Long userId, Integer quantity) {
String productKey = "inventory:" + productId;
String userKey = "seckill_user:" + productId + ":" + userId;
// 执行库存扣减脚本
Object result = redisTemplate.execute(deductScript,
Arrays.asList(productKey, userKey), quantity, userId.toString());
int code = (Integer) result;
return switch (code) {
case -2 -> new SeckillResult(false, "您已参与过此次秒杀");
case -1 -> new SeckillResult(false, "商品已售罄");
default -> {
// 异步创建订单,快速响应用户
createOrderAsync(productId, userId, quantity);
yield new SeckillResult(true, "抢购成功,请尽快支付");
}
};
}
}
虚拟线程
重点来了。
既然是Java21,那虚拟线程自然不能丢下。
虚拟线程的优势就在于轻量级 和高并发。每个虚拟线程只占用几KB内存,而且由JVM内部调度,避免操作系统线程切换时产生的开销。
极大意义上简化了异步编程的复杂度,对于性能的提升也有了革命性的进步。
比如下面这个例子:
kotlin
@RestController
public class SeckillController {
/**
* 虚拟线程处理秒杀请求
* 每个请求独立的虚拟线程:真正的并发处理,无需复杂的异步编程
* 分层防护:多道检查机制,逐层过滤无效请求
*/
@PostMapping("/seckill/{productId}")
public CompletableFuture<SeckillResult> seckill(
@PathVariable Long productId,
@RequestHeader("User-Id") Long userId) {
return CompletableFuture.supplyAsync(() -> {
// 本地缓存预检查:快速拦截无效请求
if (!inventoryCache.preCheck(productId, 1)) {
return new SeckillResult(false, "商品已售罄");
}
// 用户限流:保证公平性,防止恶意刷单
if (!rateLimiter.tryAcquire("user:" + userId, 10, 5, 60)) {
return new SeckillResult(false, "请求过于频繁,请稍后再试");
}
// 执行核心业务逻辑
return inventoryService.deductInventory(productId, userId, 1);
}, virtualThreadExecutor); // 使用虚拟线程执行器
}
@Bean
public Executor virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
具体的场景远不止这些,在整个链路中有不少场景都可以使用:
场景 | 传统线程劣势 | 虚拟线程优势 |
---|---|---|
接入层请求处理 | 线程池容易被瞬间流量撑爆,需严格控制池大小 | 虚拟线程极轻量,可放心"人手一个",避免拒绝请求 |
库存预扣 | 高并发下线程池竞争激烈,处理阻塞 I/O 成本高 | I/O 挂起几乎无成本,可支撑海量并发预扣请求 |
外部接口调用(风控/黑名单) | 外部调用延迟不可控,线程容易被白白占住 | 虚拟线程挂起消耗极低,能并发跑数十万请求 |
消息队列消费者 | 高并发消费需要调优线程池,容易出现 backlog | 虚拟线程消费者几乎无限扩展,削峰填谷更平滑 |
订单写库 & 回写缓存 | 数据库/缓存操作阻塞时拖慢线程池吞吐 | 同步写法更自然,挂起不浪费资源 |
超时控制 & 异常收集 | CompletableFuture 写法复杂,可读性差 | 结构化并发天然支持超时/取消,异常统一收集 |
说白了,最划算的用法,就是把那些高并发IO密集的地方交给它(比如库存预扣、外部接口调用、订单写库)。
这些环节本质上都是等IO,换成虚拟线程,挂起几乎没成本,随便开几万几十万个都行。
但注意⚠️:
- CPU 密集型逻辑(比如复杂计算、加解密)虚拟线程并不会更快,可能和普通线程差不多;
- 如果设计不当而无脑使用,也会成为新坑;
- 不能盲目,得看实质收益。
异步订单处理
秒杀成功后的订单处理是一个复杂的业务流程,涉及用户验证、商品确认、价格计算、优惠券应用等多个步骤。
如果同步处理这些操作,用户可能需要等待几秒钟才能收到响应,这在秒杀场景下是不可接受的。
异步处理的核心思想是关注点分离。
秒杀阶段专注于库存扣减的准确性和速度,订单处理阶段专注于业务逻辑的完整性和一致性。
通过消息队列将两个阶段解耦,既保证了用户体验,又确保了系统稳定性。
csharp
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderCreateListener implements RocketMQListener<OrderCreateEvent> {
@Override
public void onMessage(OrderCreateEvent event) {
try {
// 使用结构化并发处理订单创建
// Java 21特性:让并发任务管理更安全、更直观
processOrderWithStructuredConcurrency(event);
} catch (Exception e) {
// 订单创建失败,执行补偿操作
handleOrderFailure(event, e);
}
}
/**
* 结构化并发处理订单创建
* 优势:所有子任务要么全部成功,要么全部失败
* 安全性:避免了传统并发编程中的线程泄露问题
*/
private void processOrderWithStructuredConcurrency(OrderCreateEvent event) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 并发执行多个验证任务,提高处理效率
var userTask = scope.fork(() -> userService.validateUser(event.userId()));
var priceTask = scope.fork(() -> productService.getCurrentPrice(event.productId()));
var inventoryTask = scope.fork(() -> inventoryService.reconfirmInventory(event.productId()));
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 任何任务失败都会抛出异常
// 创建订单
Order order = buildOrder(event, userTask.get(), priceTask.get());
orderService.createOrder(order);
}
}
}
性能优化策略
代码写完了,现在就要来进行优化了。
JVM调优
虽然一般用不上,但是Java21带来的zgc还是值得一试的,性能也许会有飞跃。
ruby
# 垃圾收集器:ZGC专为低延迟设计,暂停时间<10ms
-XX:+UseZGC
-XX:+UnlockExperimentalVMOptions
# 内存配置:固定大小避免动态调整开销
-Xmx8g -Xms8g
-XX:MaxDirectMemorySize=2g
# 虚拟线程优化
--enable-preview
-Djdk.virtualThreadScheduler.parallelism=16
# 性能优化
-XX:+UseTransparentHugePages
-XX:+OptimizeStringConcat
数据库设计优化
合理的表结构和索引策略能让查询效率提升数倍:
sql
-- 库存表:针对高并发读写优化
CREATE TABLE inventory (
product_id BIGINT PRIMARY KEY,
available_stock INT NOT NULL DEFAULT 0,
version BIGINT NOT NULL DEFAULT 0, -- 乐观锁版本号
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_update_time (update_time)
) ENGINE=InnoDB;
-- 分表订单:分散写入压力
CREATE TABLE order_0 (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
total_price DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB;
故障处理
熔断降级策略
在分布式系统中,故障传播是常见问题。
其中某一个组件的故障可能导致整个系统崩溃。
这时候就需要熔断器发挥作用了,熔断器就是我们业务系统的保险丝,当检测到故障时自动断开,防止故障蔓延。
less
@Component
public class SeckillServiceWithFallback {
/**
* 多重保护的秒杀方法
* CircuitBreaker:熔断保护,防止故障传播
* RateLimiter:限流保护,控制请求速率
* TimeLimiter:超时保护,防止请求堆积
*/
@CircuitBreaker(name = "seckill", fallbackMethod = "seckillFallback")
@RateLimiter(name = "seckill")
@TimeLimiter(name = "seckill")
public CompletableFuture<SeckillResult> seckill(Long productId, Long userId) {
return CompletableFuture.supplyAsync(() ->
inventoryService.deductInventory(productId, userId, 1));
}
/**
* 降级处理方法
* 原则:保证核心功能,提供友好提示
* 策略:排队机制,延迟满足用户需求
*/
public CompletableFuture<SeckillResult> seckillFallback(Long productId, Long userId, Exception ex) {
return CompletableFuture.completedFuture(
new SeckillResult(false, "系统繁忙,您已进入排队队列,请稍后刷新查看结果"));
}
}
数据一致性保证
在异步处理模式下,如何保证数据的最终一致性是一个重要问题。
我们可以采用补偿机制 和重试策略来处理各种异常情况:
- 补偿机制:当下游操作失败时,自动回滚上游操作
- 重试策略:对于瞬时故障,自动重试处理
- 人工介入:对于系统无法自动处理的异常,提供人工处理接口
当然,最简单就是用Seata,或者用RocketMQ的事务消息。
写在最后
套路早就写烂了,缓存、限流、队列,一个都跑不了。
但 Java 21 把虚拟线程和结构化并发塞到我们手里,相当于直接给异步编程插上了翅膀。
以前要费劲管理线程池、写一堆回调的地方,现在一句同步代码就能跑几十万并发,写法简单,性能还更稳。
这不是说老架构就过时了,而是它现在能用上新武器。
