2025年基于Java21的的秒杀系统要怎么设计?来点干货

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

大家好,欢迎来到IT知识分享网。

前言

秒杀系统的文章网上一搜一大把,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 的请求大幅减少。

@Component public class InventoryCache { private final Cache<Long, Integer> 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 脚本的方案,既能保证操作的原子性,又拥有出色的性能表现。

@Service public 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) < 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 内部调度,避免操作系统线程切换时产生的开销。

极大意义上简化了异步编程的复杂度,对于性能的提升也有了革命性的进步。

比如下面这个例子:

@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 密集型逻辑 (比如复杂计算、加解密)虚拟线程并不会更快,可能和普通线程差不多;
  • 如果设计不当而无脑使用,也会成为新坑;
  • 不能盲目,得看实质收益。

异步订单处理

秒杀成功后的订单处理是一个复杂的业务流程,涉及用户验证、商品确认、价格计算、优惠券应用等多个步骤。

如果同步处理这些操作,用户可能需要等待几秒钟才能收到响应,这在秒杀场景下是不可接受的。

异步处理的核心思想是 关注点分离

秒杀阶段专注于库存扣减的准确性和速度,订单处理阶段专注于业务逻辑的完整性和一致性。

通过消息队列将两个阶段解耦,既保证了用户体验,又确保了系统稳定性。

@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer") public class OrderCreateListener implements RocketMQListener<OrderCreateEvent> { @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); } } } 

性能优化策略

代码写完了,现在就要来进行优化了。

JVM调优

虽然一般用不上,但是 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; 

故障处理

熔断降级策略

在分布式系统中,故障传播是常见问题。

其中某一个组件的故障可能导致整个系统崩溃。

这时候就需要熔断器发挥作用了,熔断器就是我们业务系统的保险丝,当检测到故障时自动断开,防止故障蔓延。

@Component public class SeckillServiceWithFallback { @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 把虚拟线程和结构化并发塞到我们手里,相当于直接给异步编程插上了翅膀。

以前要费劲管理线程池、写一堆回调的地方,现在一句同步代码就能跑几十万并发,写法简单,性能还更稳。

这不是说老架构就过时了,而是它现在能用上新武器。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/188562.html

(0)
上一篇 2025-09-21 09:33
下一篇 2025-09-21 10:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信