摘要:sequenceDiagram participant U as 用户 participant G as 网关 participant S as 秒杀服务 participant R as Redis participant MQ as RocketMQ pa
秒杀系统的文章网上一搜一大把,Redis 缓存、消息队列、限流熔断那一套,相信大家都能背下来了。
但是现在已经是 2025 年,Java 已经进化到 21 了,虚拟线程、结构化并发、记录模式这些新特性,能不能把老掉牙的秒杀架构玩出点新花样?
这篇文章来了,直接聊点干货。
整体架构其实这些年变化不大,基本还是这几位老朋友:接入层顶流量,应用层搞逻辑,缓存层抗压力,消息队列做削峰,数据库兜底。
仍然遵循这种分层防护的思想,每一层都承担特定的防护职责。
graph TB subgraph "用户层" A[用户浏览器] --> B[CDN静态资源] A --> C[负载均衡器] end subgraph "接入层" C --> D[网关集群] D --> E[限流中间件] end subgraph "应用层" E --> F[秒杀服务集群] F --> G[预扣库存服务] F --> H[订单服务] end subgraph "缓存层" I[Redis集群-库存] J[Redis集群-用户状态] K[本地缓存Caffeine] end subgraph "消息层" L[RocketMQ集群] end subgraph "数据层" M[(MySQL主库)] N[(MySQL从库)] O[(备份库)] end F --> I F --> J F --> K G --> L L --> H H --> M M --> N M --> O#技术分享用户层 通过 CDN 将静态资源分发到各地,减少用户访问延迟。负载均衡器智能分发请求,避免单点故障。
接入层 是系统的第一道屏障。网关集群处理请求路由、用户认证、协议转换等工作,限流中间件则像水闸一样控制流量,防止系统被瞬间涌入的请求冲垮。
应用层 实现核心业务逻辑。秒杀服务集群负责库存检查和扣减,预扣库存服务处理库存预占,订单服务管理订单生命周期。
缓存层 提供多级缓存服务。Redis 集群存储实时库存和用户购买状态,本地缓存 Caffeine 提供毫秒级访问速度,大幅减少网络开销。
消息层 通过 RocketMQ 实现系统解耦。将耗时的订单处理操作异步化,提高用户响应速度。
数据层 采用主从架构保证数据安全。主库处理写操作,从库分担读压力,备份库防止数据丢失。
一个完整的秒杀请求在系统中的流转过程,涉及多个业务组件的协同工作:
sequenceDiagram participant U as 用户 participant G as 网关 participant S as 秒杀服务 participant R as Redis participant MQ as RocketMQ participant O as 订单服务 participant DB as 数据库 U->>G: 秒杀请求 G->>G: 用户限流检查 G->>S: 转发请求 S->>R: 检查用户购买资格 alt 已购买 R-->>S: 返回已购买 S-->>U: 重复购买提示 else 未购买 S->>R: 预扣库存(Lua脚本) alt 库存不足 R-->>S: 扣减失败 S-->>U: 商品已抢完 else 扣减成功 R-->>S: 扣减成功,返回token S->>R: 标记用户已购买 S->>MQ: 发送订单创建消息 S-->>U: 抢购成功,等待支付 MQ->>O: 异步处理订单 O->>DB: 创建订单记录 O->>R: 更新最终库存 end end整个流程的精髓在于 快速响应 和 异步处理 。
用户发起请求后,系统首先进行多层检查,快速过滤掉无效请求。
对于有效请求,立即进行库存扣减并返回成功消息,而复杂的订单处理则放在后台异步进行。
确保用户能在毫秒级时间内收到反馈。
库存扣减是整个秒杀系统的核心难点。
以某手机首发为例,假设只有100台现货,但同时有10万用户点击购买。如何确保恰好100个用户成功,而不会出现101台或者99台的情况?
flowchart TD A[收到秒杀请求] --> B{本地缓存预检查} B -->|库存不足| C[返回售罄] B -->|有库存| D{Redis分布式锁} D -->|获锁失败| E[返回系统繁忙] D -->|获锁成功| F[Lua脚本原子扣减] F --> G{扣减结果} G -->|失败| H[返回库存不足] G -->|成功| I[生成预订单token] I --> J[异步创建订单] J --> K[返回成功结果]这个流程采用了 多级过滤 的设计思想。
本地缓存预检查能拦截90%以上的无效请求,Redis 分布式锁保证操作的互斥性,再结合 Lua 脚本确保扣减操作的原子性。
通过这种层层过滤的机制,既保证了数据的准确性,又最大化了系统的性能。
多级缓存的核心思想是 就近访问 和 逐层过滤 。
单纯依赖 Redis 的话,在极高并发下反而容易成为瓶颈。
当100万用户同时查询库存时,即使 Redis 性能再强,也难以应对如此巨大的压力。
通过多级缓存将这100万次查询分层拦截,让真正需要到达 Redis 的请求大幅减少。
@Componentpublic class InventoryCache { private final Cache localCache = Caffeine.newbuilder .maximumSize(10_000) .expireAfterWrite(Duration.ofSeconds(1)) .build; public boolean preCheck(Long productId, Integer quantity) { Integer cached = localCache.getIfPresent(productId); return cached != null && cached >= quantity; } public void refreshAsync(Long productId) { Thread.ofVirtual.name("cache-refresh-" + productId).start( -> { Integer inventory = redisTemplate.opsForValue.get("inventory:" + productId); if (inventory != null) { localCache.put(productId, inventory); } }); }}限流应该是最常见的手段,而令牌桶算法是限流的经典实现。
系统以恒定速率向桶中投放令牌,每个请求消耗一个令牌。当请求过多时,桶中令牌不足,多余请求被拒绝或排队。这种机制既能平滑处理突发流量,又能保护系统不被压垮。
实现令牌桶最简单的方式就是 Guava 的线程工具,但这里我们手搓一个 lua 脚本,也能更清晰的了解到整个令牌桶的流程。
public class DistributedRateLimiter { 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 脚本的方案,既能保证操作的原子性,又拥有出色的性能表现。
@Servicepublic class InventoryService { 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) new SeckillResult(false, "您已参与过此次秒杀"); case -1 -> new SeckillResult(false, "商品已售罄"); default -> { createOrderAsync(productId, userId, quantity); yield new SeckillResult(true, "抢购成功,请尽快支付"); } }; }}极大意义上简化了异步编程的复杂度,对于性能的提升也有了革命性的进步。
比如下面这个例子:
@RestControllerpublic class SeckillController { @PostMapping("/seckill/{productId}") public CompletableFuture 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 密集型逻辑 (比如复杂计算、加解密)虚拟线程并不会更快,可能和普通线程差不多;如果设计不当而无脑使用,也会成为新坑;不能盲目,得看实质收益。秒杀成功后的订单处理是一个复杂的业务流程,涉及用户验证、商品确认、价格计算、优惠券应用等多个步骤。
如果同步处理这些操作,用户可能需要等待几秒钟才能收到响应,这在秒杀场景下是不可接受的。
异步处理的核心思想是 关注点分离 。
秒杀阶段专注于库存扣减的准确性和速度,订单处理阶段专注于业务逻辑的完整性和一致性。
通过消息队列将两个阶段解耦,既保证了用户体验,又确保了系统稳定性。
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")public class OrderCreateListener implements RocketMQListener { @Override public void onMessage(OrderCreateEvent event) { try { 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); } }}虽然一般用不上,但是 Java21带来的 zgc 还是值得一试的,性能也许会有飞跃。
-XX:+UseZGC-XX:+UnlockExperimentalVMOptions-Xmx8g -Xms8g -XX:MaxDirectMemorySize=2g--enable-preview -Djdk.virtualThreadScheduler.parallelism=16-XX:+UseTransparentHugePages -XX:+OptimizeStringConcat合理的表结构和索引策略能让查询效率提升数倍:
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;在分布式系统中,故障传播是常见问题。
其中某一个组件的故障可能导致整个系统崩溃。
这时候就需要熔断器发挥作用了,熔断器就是我们业务系统的保险丝,当检测到故障时自动断开,防止故障蔓延。
@Componentpublic class SeckillServiceWithFallback { @CircuitBreaker(name = "seckill", fallbackMethod = "seckillFallback") @RateLimiter(name = "seckill") @TimeLimiter(name = "seckill") public CompletableFuture seckill(Long productId, Long userId) { return CompletableFuture.supplyAsync( ->inventoryService.deductInventory(productId, userId, 1)); } public CompletableFuture seckillFallback(Long productId, Long userId, Exception ex) { return CompletableFuture.completedFuture( new SeckillResult(false, "系统繁忙,您已进入排队队列,请稍后刷新查看结果")); } }在异步处理模式下,如何保证数据的最终一致性是一个重要问题。
补偿机制 :当下游操作失败时,自动回滚上游操作重试策略 :对于瞬时故障,自动重试处理人工介入 :对于系统无法自动处理的异常,提供人工处理接口套路早就写烂了,缓存、限流、队列,一个都跑不了。
但 Java 21 把虚拟线程和结构化并发塞到我们手里,相当于直接给异步编程插上了翅膀。
以前要费劲管理线程池、写一堆回调的地方,现在一句同步代码就能跑几十万并发,写法简单,性能还更稳。
这不是说老架构就过时了,而是它现在能用上新武器。
来源:墨码行者