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

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

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

相关推荐
方圆想当图灵3 小时前
《生产微服务》评估清单 CheckList
后端·微服务
服务端技术栈3 小时前
历时 1 个多月,我的第一个微信小程序「图片转 Excel」终于上线了!
前端·后端·微信小程序
计算机毕业设计指导3 小时前
基于Spring Boot的幼儿园管理系统
spring boot·后端·信息可视化
yanlele4 小时前
前端面试第 78 期 - 2025.09.07 更新 Nginx 专题面试总结(12 道题)
前端·javascript·面试
年轻的麦子4 小时前
Go 框架学习之:go.uber.org/fx项目实战
后端·go
小蒜学长4 小时前
django全国小米su7的行情查询系统(代码+数据库+LW)
java·数据库·spring boot·后端
听风同学5 小时前
RAG的灵魂-向量数据库技术深度解析
后端·架构
yh云想5 小时前
《Java线程池面试全解析:从原理到实践的高频问题汇总》
jvm·面试·职场和发展
橙序员小站5 小时前
搞定系统面试题:如何实现分布式Session管理
java·后端·面试