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

前言

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

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

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

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

相关推荐
猿究院-陆昱泽29 分钟前
Redis 五大核心数据结构知识点梳理
redis·后端·中间件
yuriy.wang1 小时前
Spring IOC源码篇五 核心方法obtainFreshBeanFactory.doLoadBeanDefinitions
java·后端·spring
咖啡教室3 小时前
程序员应该掌握的网络命令telnet、ping和curl
运维·后端
你的人类朋友4 小时前
Let‘s Encrypt 免费获取 SSL、TLS 证书的原理
后端
老葱头蒸鸡4 小时前
(14)ASP.NET Core2.2 中的日志记录
后端·asp.net
李昊哲小课4 小时前
Spring Boot 基础教程
java·大数据·spring boot·后端
码事漫谈4 小时前
C++内存越界的幽灵:为什么代码运行正常,free时却崩溃了?
后端
Swift社区5 小时前
Spring Boot 3.x + Security + OpenFeign:如何避免内部服务调用被重复拦截?
java·spring boot·后端
90后的晨仔5 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端
码事漫谈5 小时前
AI智能体平台选型指南:从技术架构到商业落地的全景洞察
后端