秒杀,几乎是Java后端面试里绕不开的“高并发试金石”。Spring Boot、Kafka、Redis 这三件套,是电商秒杀方案里最常被问到的组合。面试官只要问出“让你设计一个秒杀系统,你怎么做”,大概率就是想从流量削峰、库存扣减、数据一致性这三个维度看你有没有真实项目经验。落到技术栈上,Spring Boot 负责搭起整体工程,Redis 在前端扛住读请求和分布式锁,Kafka 在中间做异步削峰,这套组合已经是电商场景的标准答案。但标准答案和线上能稳定跑通的方案之间,隔着大量细节坑,比如 Redis 分布式锁续期、Kafka 消息顺序性、缓存与数据库一致性,每一条我都踩过。
这篇实录没有教科书式的废话,适合正准备跳槽的 Java 开发,也适合已经把 CRUD 写得很熟、想往高并发方向突破的工程师。我会从整体方案设计讲起,再落到 Spring Boot 工程里的核心代码,最后把面试官最常追问的高频问题和线上排查经验一起给你。你不需要一次全背下来,但建议先理解每层设计要解决什么问题,后面看代码和面试答案会轻松很多。
1. 电商秒杀场景的整体方案设计
1.1 秒杀为什么难:流量模型与核心矛盾
先看流量模型。普通业务接口的 QPS 可能就几百,秒杀开场那几秒,流量往往达到平时的几十倍甚至上百倍,而且用户像潮水一样涌进来。数据库连接池一般也就 50~100 个连接,一个慢 SQL 就能把连接池打满,更别说秒杀瞬间的写入压力。所以你会发现,所有秒杀方案本质上都在解决同一个矛盾:有限的数据库处理能力 vs 瞬间爆发的海量请求。
既然处理不过来,思路就变成“让请求在到达数据库之前尽量被拦截、排队或丢弃”。后端常见手段有四级:浏览器/CDN 限流、Nginx 层限流、Redis 层拦截、Kafka 层削峰。前两级是挡无效流量,后两级才是保护订单系统。秒杀结果本来就只有少数人能抢到,所以系统设计的第一原则不是“每个请求都成功”,而是“保证成功请求不丢、库存不超卖、数据最终一致”。
这里要注意一个思维转换:很多新手一上来就想着“怎么优化数据库”,其实秒杀最忌讳让数据库直接面对峰值流量。数据库在秒杀里扮演的角色应当是最终流水存储,而不是实时请求处理。把数据库往后放,前面用 Redis 和 Kafka 顶住,才是正确的架构方向。
1.2 三层削峰方案:前端限流、Redis 缓存与 Kafka 异步削峰
从用户点击“立即抢购”到最后收到订单结果,我习惯把秒杀链路分成三段来设计。
第一段是请求接入层。用户请求先进 Nginx,通过 OpenResty 的 lua-resty-limit-traffic 做令牌桶限流,每秒只放行比如 1 万请求,超过直接返回“限流中”。这一层能防住一部分脚本刷单。再往下,后端接口在 Spring Boot 层面用 Guava RateLimiter 或 Resilience4j 做单机限流,防止单节点被冲垮。很多人忽略这一层,但线上秒杀如果没有接入层限流,后面 Redis 压力再小也会被无效请求打满。
第二段是Redis 预扣库存。秒杀接口的第一步不是写数据库,而是先查 Redis 里的库存,再通过 Lua 脚本完成“检查库存 > 校验是否已购买 > 扣减库存”。Lua 脚本可以保证原子性,避免超卖。库存数据在秒杀开始前从数据库预热到 Redis,秒杀结束后再异步同步回数据库。Redis 能抗的 QPS 在十万级别,用来做瞬间拦截再合适不过。
第三段是Kafka 异步下单。Redis 预扣成功后,只说明用户“抢到了资格”,真正的订单创建、扣减数据库库存、生成支付记录等动作放到 Kafka 消息里,由订单服务异步消费。这样数据库收到的写入频率就被拉平了,从峰值每秒几万变成每秒几百上千。消费端做一定的批量处理,还能进一步提高吞吐。
这套三段式方案的好处是每一层都在做“减负”。流量到数据库之前已经过了一层层的漏斗,真正落到数据库的写操作数量级大幅下降。面试时我们讲方案,不需要把每层代码都背出来,但一定要讲清楚每一层拦截了什么流量、为什么要放在这一层。
2. Spring Boot 工程落地:接口分层与并发控制
2.1 秒杀接口的核心流程:预扣库存、校验重复、异步下单
用 Spring Boot 写一个秒杀接口,代码上其实不复杂,复杂的是并发控制。我习惯把接口拆成三步:请求校验、Redis 预扣、发送 Kafka 消息。
第一步请求校验包括:参数合法性、用户是否登录、活动是否已开始/结束。这些校验要在 Redis 预扣之前做干净,否则一个非法参数就可能浪费一次库存扣减。第二步调 Redis Lua 脚本预扣库存,这一步同时完成“库存 > 0 判断”和“扣减”两个动作,确保原子。第三步如果扣减成功,就封装一个 OrderMessage 发给 Kafka,由下游服务创建订单。
有一个非常容易被忽视的点:发送 Kafka 消息放在 Redis 扣减成功之后,但如果在发送消息前服务宕机了,用户明明抢到了资格,订单却永远不生成。所以工程上不能简单发了就完事。我这边用的方案是:预扣成功后先把一条“待创建订单”记录写入本地数据库(或者写一个订单流水表),状态是“待处理”,然后发消息;消费端处理成功后回写状态。如果发消息失败或消费失败,由一个定时任务扫描待处理订单,重新投递消息。这个“本地消息表 + 定时对账”的思路,下面讲数据一致性时会详细说。
2.2 Redis 分布式锁的正确写法与续期问题
秒杀里最容易被面试官追问的就是 Redis 分布式锁。很多网上的教程还在教 setnx + expire,但真正生产环境这样写至少有两个坑:一是 setnx 后服务崩了没有设置过期时间,锁永远不释放;二是锁过期时间太短,业务还没执行完锁就自动释放了,另一个线程进来造成并发覆盖。
正确的写法是用SET key value NX EX timeout这一条命令完成加锁和设置过期时间,value 必须是唯一标识,比如 UUID 或业务订单号。释放锁的时候不能直接 del,而要先用 Lua 比较 value 是否是自己,是自己的才 del,避免误删别人的锁。原因很简单:如果线程 A 执行时间过长锁过期了,线程 B 拿到锁开始执行,A 执行完直接 del,就会把 B 的锁删掉。
再说续期。高版本 Redis 客户端推荐用 Redisson 的 watchdog 机制,默认锁的租期是 30 秒,每 10 秒自动续期。如果业务执行完了就释放锁,不用关心续期;如果服务宕机了,锁也会因为租期到期自动释放,不会死锁。这比自己写 Timer 续期安全得多。我见过不少为了省依赖自己实现续期的,最后要么少续期导致锁提前释放,要么忘了释放导致死锁。能用 Redisson 就用 Redisson,面试时可以提一嘴看门狗机制,会很加分。
2.3 数据一致性:缓存与数据库的最终一致性
秒杀里 Redis 库存是“前置库存”,数据库库存是“最终库存”。理论上两个值最终要一致,但不可能实时一致,所以必须接受“最终一致性”。怎么保证?我的做法是:
- 扣减 Redis 时记录一条库存流水,流水表里带本次扣减的唯一订单号;
- Kafka 消费者收到消息后,在数据库本地事务里同时完成“扣减数据库库存 + 更新库存流水状态 + 创建订单”;
- 如果数据库扣减成功但 Kafka 消费失败,流水表里会有一直处于“待处理”的数据,由定时任务扫描并重发;
- 如果数据库库存不足或消费端业务失败,需要回补 Redis 库存。回补操作同样先写流水,再通过消息或直接调用来恢复 Redis。
这套“流水 + 对账”机制是一个通用保底方案,面试里一定要讲清楚,因为它同时回答了“消息丢失怎么办”“消费失败怎么办”“Redis 和数据库不一致怎么办”三个问题。至于用不用分布式事务,我的观点是:秒杀这种高并发场景,尽量规避强分布式事务,因为 XA 事务会锁资源、拖垮性能。用本地消息表和最终一致性就好。
3. Kafka 消息中间件在秒杀中的实战配置
3.1 为什么用 Kafka:削峰填谷与流量整形
秒杀场景选消息队列,Kafka 是首选。相比 RabbitMQ、RocketMQ,Kafka 的吞吐更高、分区模型更适合并行消费,而且在大促场景下有成熟的“削峰填谷”能力。所谓削峰填谷,就是生产者把高峰期的大量消息快速写入 Kafka,消费者按自己的节奏慢慢消费。从时间维度看,请求的“峰”被削掉了,数据库处理的“谷”被填平了,整体负载平稳。
Kafka 写入延迟极低,生产端吞吐可达每秒几十万条,这点对秒杀特别重要。消费者虽然默认单线程,但可以通过增加分区数和消费者实例数并行消费。每个分区只能被同一个消费组内的一个消费者实例消费,分区数是并行消费的上限。所以大促前我会把秒杀订单 topic 的分区数提前设置成 16 或 32,而不是用默认的 1。后续想扩容分区会比较麻烦,需要提前规划。
3.2 生产端与消费端的几个关键参数陷阱
先说生产端。Kafka 单条消息默认最大 1MB,topic 的 max.message.bytes 默认也是 1MB。如果订单消息里塞了完整的商品快照、优惠券详情甚至营销日志,很容易踩到“Record is too large”的坑。我的经验是:消息体里只放订单号、用户 ID、商品 ID、数量、秒杀活动 ID 等必要字段,其他详情由消费端去查缓存或数据库。实在要传大对象,需要在 broker 端调大 message.max.bytes,但带来的网络和存储开销也会变大,非必要不建议。
消费端有几个容易踩的配置:
- enable.auto.commit 默认 true,处理逻辑复杂时容易消息还没处理完就自动提交了 offset,服务重启后会丢消息。建议设为 false,手动提交,并等业务处理完成后提交。
- max.poll.interval.ms 默认 300 秒,如果单条消息处理耗时超过这个时间,消费者会被踢出消费组触发 rebalance。秒杀订单处理里要避免在消费线程里做长任务,比如远程调用、慢 SQL,尽量做成异步化或批量化。
- 消费线程数不要超过分区数。用多线程消费时,如果同一订单的重试消息落到不同分区,顺序就无法保证。要保序只能牺牲并行度:一个分区一个线程,或者按订单号哈希到固定分区。
3.3 Kafka 消息积压与消费延迟的排查实录
线上秒杀最常见的故障就是“消息积压”。现象是 Redis 已扣库存,但用户迟迟出不来订单结果。排查路径大概是:
第一步,看 Kafka 消费组 lag。用命令行工具kafka-consumer-groups.sh --describe --group order_group看每个分区的 lag 数。lag 持续增长说明消费能力跟不上生产速度,或消费者卡住了。
第二步,看消费者日志有没有频繁 rebalance。rebalance 会导致消费暂停。常见原因是 max.poll.interval.ms 设置太短、处理耗时超时,或消费者实例频繁宕机。rebalance 期间分区的 offset 可能要重新分配,也会放大延迟。
第三步,看下游数据库连接池和慢 SQL。很多时候 Kafka 不背锅,而是消费线程在等数据库连接。秒杀订单服务要把数据库连接池从默认的 10 个调大,压测时观察活跃连接数和等待线程数。我遇到过“消息积压 10 万”的报警,最后排查是消费端一个查询商品详情的 SQL 没走索引,单条处理从 5ms 变成 300ms,直接拖垮了消费速度。优化后 lag 几分钟就清掉了。
排查这种事,靠经验也靠工具。可视化工具可以装 Offset Explorer(原 Kafka Tool),看 cluster、topic、consumer group 的 lag 非常直观,不用每次都敲命令。
4. 面试高频问题汇总与避坑指南
4.1 Redis 缓存穿透、击穿、雪崩的区别与应对
这是 Java 大厂面试的基础题,但放在秒杀场景里问就成了进阶题。三者的区别简单说:
- 穿透:查一个根本不存在的数据(比如伪造的商品 ID),缓存和数据库都没有,导致每次请求都打数据库。
- 击穿:热点 key 在缓存过期的一瞬间,大量请求同时打到数据库。
- 雪崩:大量 key 在同一时间过期,或者 Redis 宕机,导致请求全部落到数据库。
秒杀场景里,击穿是重点。秒杀商品的库存 key 是绝对热点,一旦过期,瞬间所有请求都会去数据库查库,数据库必挂。应对方案有几种:热点 key 永不过期,或者设置逻辑过期时间,由后台任务异步更新;加互斥锁,缓存未命中时只让一个线程去查数据库,其他线程等待后回源;还有多级缓存,本地 Caffeine 或 Redis 副本兜底。
穿透的应对是布隆过滤器,或缓存空值并设置短过期时间。雪崩则需要加随机过期时间、做 Redis 高可用、限流降级。这些都要作为方案的一部分讲给面试官,不要只背概念,要结合秒杀链路的哪一环会出现每种问题。
4.2 Kafka 重复消费与 Redis 幂等性设计
消息队列不丢消息很难做到“恰好一次”,所以消费端必须做幂等。秒杀订单场景的重复消费主要有两种来源:一是生产者发送时服务重试导致同一条消息发了两遍,二是消费端处理成功后还没来得及提交 offset 就宕机,重启后又消费一遍。
幂等设计我常用三种方案:
- 数据库唯一键约束。订单流水表用“用户ID + 秒杀活动ID + 商品ID”建唯一索引,重复插入时数据库报 duplicate key,捕获后直接返回成功。
- Redis 幂等标记。每个用户抢购前先把用户ID写入 Redis SETNX,设置过期时间,如果已经存在则拒绝。这是业务层的重复请求拦截。
- 本地去重表 + 状态机。消费消息后先查流水表状态,已经“处理中”或“已完成”就不重复处理。
这三种可以叠加使用。面试时提到“消费端必须幂等”,然后说出具体怎么设计,比单纯喊口号强很多。
4.3 分布式事务与最终一致性:本地消息表 vs 事务消息
秒杀链路里“扣减库存、创建订单、增加积分、推送通知”分布在多个服务里,数据库没办法用本地事务一把梭,这就涉及分布式事务。我从来不用强一致方案,而是用最终一致性。两种主流实现:
一是本地消息表。在业务数据库中维护一张消息表,业务操作和消息写入在同一个本地事务里提交。然后有一个定时任务把消息表中状态为“待发送”的记录发给 MQ,成功后再更新状态。这样做的好处是简单可靠,坏处是消息表和业务表耦合,且对数据库有一定额外压力。
二是事务消息。RocketMQ 支持事务消息,Kafka 生态里对应 Exactly Once 和 Kafka Streams 更复杂,通常不直接用。如果我们强制用 Kafka 做分布式事务,一般还是“本地消息表 + Kafka”的组合。所以面试时你可以说:Kafka 生态下我更倾向本地消息表;如果架构里用了 RocketMQ,可以选用事务消息配合回查。
所谓“回查”,就是事务消息发送到 MQ 后,如果事务未提交,MQ 会反向调用生产者的接口查一下本地事务状态。这个机制保证了“本地事务成功但消息没发出去”也不会丢。能把这些区别讲明白,面试官就知道你是真写过。
4.4 面试现场如何回答“秒杀系统的设计”
很多候选人对方案很熟,但回答时没有框架,东讲一句西讲一句。我建议按下面这个顺序组织答案:
- 先说核心矛盾:瞬时流量 vs 数据库有限处理能力。
- 再说总体架构:接入层限流 -> Redis 预扣库存 -> Kafka 异步下单 -> 数据库最终落库。
- 重点讲三个关键点:如何防超卖(Redis Lua 原子扣减)、如何防重复(幂等设计)、如何保证最终一致(本地消息表/对账)。
- 最后补充高可用:Redis 哨兵/Cluster、Kafka 多副本、数据库主从、降级策略。
这个顺序符合“问题 -> 方案 -> 细节 -> 保障”的逻辑,面试官就算追问细节,你也能稳稳接住。千万不要一上来就背 Redis 命令,让人觉得你只是看过八股文。
5. 实操复盘:一个可复现的秒杀 Demo 核心代码
5.1 环境准备与基础配置
本地跑一个最小原型,推荐用 Docker 一次性把 Redis 和 Kafka 拉起来。如果你用的是 IntelliJ IDEA 社区版,也可以正常创建 Spring Boot 项目,社区版只是少了 Spring Initializr 的向导按钮,去 start.spring.io 手动生成压缩包,导入 IDEA 即可。个人开发足够用。
先用 Docker 起服务,写一个 docker-compose.yml:
version: '3' services: redis: image: redis:7-alpine ports: - "6379:6379" zookeeper: image: bitnami/zookeeper:3.8 ports: - "2181:2181" environment: - ALLOW_ANONYMOUS_LOGIN=yes kafka: image: bitnami/kafka:3.4 ports: - "9092:9092" environment: - KAFKA_BROKER_ID=1 - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 - ALLOW_PLAINTEXT_LISTENER=yes depends_on: - zookeeper本地单节点够用。如果面试或工作中要搭集群,至少三台 broker,配置 KAFKA_CFG_BROKER_ID 分别为 1、2、3,并修改 advertised.listeners 使用各自内网 IP。集群的价值在副本,默认 replication.factor 至少 2,否则一台 broker 挂了分区没有副本,生产环境会出大事。
Spring Boot 项目里引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>配置 application.yml:
spring: redis: host: localhost port: 6379 kafka: bootstrap-servers: localhost:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer acks: all retries: 3 consumer: group-id: seckill-order-group enable-auto-commit: false key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer auto-offset-reset: latest listener: ack-mode: manual_immediate这里有几个参数我解释一下。acks=all 表示生产者要等所有 ISR 副本都写入成功才返回,保证消息不丢,代价是吞吐略降,但秒杀场景下可靠优先。enable-auto-commit=false + ack-mode=manual_immediate 表示消费者手动提交 offset,拿到消息处理完后立即提交,避免处理中出问题导致 offset 丢失。auto-offset-reset=latest 表示消费者启动后从最新 offset 开始消费,秒杀场景通常不接受回放老消息,所以用 latest;如果你要做补偿扫描再单独用 earliest。
5.2 秒杀接口核心代码与解释
先定义一个 Redis Lua 脚本,做原子性的库存预扣和防重复。RedisTemplate 序列化这里有个大坑,很多人存进去是字符串,取出来却报类型错误。原因是用默认的 JdkSerializationRedisSerializer 把对象序列化成了二进制。我在配置里统一把 key 和 value 改成 StringRedisSerializer,需要存对象时再用 JSON 序列化,这样可读性也更好。
Redis 扣库存脚本如下,注意 KEYS[1] 是库存 key,KEYS[2] 是用户去重 key,ARGV[1] 是用户 ID,ARGV[2] 是扣减数量,ARGV[3] 是去重 key 的过期时间:
local stock = redis.call('get', KEYS[1]) if not stock or tonumber(stock) < tonumber(ARGV[2]) then return 0 end local bought = redis.call('sismember', KEYS[2], ARGV[1]) if bought == 1 then return 2 end redis.call('decrby', KEYS[1], ARGV[2]) redis.call('sadd', KEYS[2], ARGV[1]) redis.call('expire', KEYS[2], ARGV[3]) return 1解释一下:返回 0 表示库存不足,返回 2 表示重复秒杀,返回 1 表示扣减成功。用户去重集合单独设置过期时间,避免 key 一直占内存。这个脚本把“查库存、查重复、减库存、记录用户”放在一个 Lua 里执行,Redis 单线程保证原子性,不会出现两个请求同时读到库存 1 然后都扣减成功的情况。
接着是秒杀接口的 Service 方法核心代码:
@Service public class SeckillService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private KafkaTemplate<String, String> kafkaTemplate; private static final String STOCK_KEY = "seckill:stock:1001"; private static final String BUY_KEY = "seckill:buy:1001"; // DefaultRedisScript 初始化省略 public long seckill(Long userId, Long goodsId, Integer count) { // 前置校验省略 List<String> keys = Arrays.asList(STOCK_KEY, BUY_KEY); Object result = redisTemplate.execute( SECKILL_SCRIPT, keys, userId.toString(), count.toString(), "86400" ); long code = Long.parseLong(result.toString()); if (code == 1) { // 构建订单消息,发送到 Kafka OrderMessage message = new OrderMessage(); message.setUserId(userId); message.setGoodsId(goodsId); message.setCount(count); kafkaTemplate.send("seckill-order-topic", userId.toString(), JSON.toJSONString(message)); } return code; } }发送消息时把用户 ID 作为 key,目的是让同一个用户的订单消息始终进入同一个分区,这样消费端可以按照用户维度保证顺序。Kafka 的分区器会用 key 的哈希值选分区,这一点对后续消费顺序很重要。
消费端代码:
@Component public class SeckillOrderConsumer { @KafkaListener(topics = "seckill-order-topic", groupId = "seckill-order-group") public void onMessage(ConsumerRecord<String, String> record, Acknowledgment ack) { try { OrderMessage message = JSON.parseObject(record.value(), OrderMessage.class); // 1. 校验本地流水表是否已存在 // 2. 本地事务:创建订单、扣减数据库库存、更新流水状态 // 3. 假如流程成功,则提交 offset ack.acknowledge(); } catch (Exception e) { // 记录错误,由对账任务补偿,不要在这里无限重试 log.error("consume order message failed", e); } } }到这里,一个最小链路就通了:用户调用秒杀接口 -> Redis Lua 扣库存 -> Kafka 发消息 -> 消费者落库。你可以用 Postman 或写个 Jmeter 脚本模拟并发请求,观察 Redis 库存不会变负数、数据库订单数不超过库存数。
5.3 压测与监控:用实际数据验证方案
Demo 写完后要压测。我用 JMeter 开 1000 线程,循环 10 次,观察三个指标:Redis 的 QPS、Kafka 的消费 lag、数据库的活跃连接数。压测前先预热,否则第一次请求会大量落到数据库。压测中重点看 Redis 的info commandstats里 lua 脚本的平均耗时,如果在 1ms 以下,说明原子扣减还有余量。
监控上,Spring Boot 可以接 Spring Boot Admin 或者 Micrometer + Prometheus。如果你只是本地验证,最简单的就是直接看 Kafka 消费组的 lag。命令如下:
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group seckill-order-grouplag 为 0 说明消费速度跟得上。如果 lag 持续增大,优先查消费者日志里的 rebalance 记录,以及数据库慢查询日志。这个顺序我压测时用过几十次,不会错。
6. 踩坑实录与个人心得
6.1 线上坑 TOP5
把我在真实秒杀项目里踩过的、也是面试官最喜欢问的坑整理成一张表,方便你自查。
| 坑 | 现象 | 根因 | 解法 |
|---|---|---|---|
| Redis 库存为负 | 超卖 | 判断和扣减不是原子的 | Lua 脚本合并 |
| 锁被误删 | 并发覆盖 | 释放时没有校验持有者 | 释放前用 Lua 比较 value |
| 消息丢失 | 用户抢到无订单 | 消费者 offset 自动提交或生产者未确认 | acks=all + 手动提交 |
| 消费顺序错乱 | 同一用户订单乱序 | 多线程消费且没有分区策略 | 用用户ID做 key,固定分区 |
| 消息体积超限 | 发送失败 | 消息体塞入大字段 | 只传必要字段或调大 max.message.bytes |
第一行那个坑我印象最深。早期用 Redis 的get和decrby两条命令来判断库存,高并发下 A 请求读到库存 1,B 请求也读到库存 1,两个都执行 decrby,库存变成 -1。这就是典型的“非原子操作导致超卖”。后来换成 Lua 脚本,再也没出现过。面试现场你如果能主动讲出这个演化过程,会让面试官有共鸣。
第二行的锁误删也很经典。值用 UUID 后释放锁前必须用 Lua 判断 value 再删除。但还有一个更隐蔽的坑:如果业务只加锁不加续期,代码执行超过锁过期时间,即使加了 UUID 也可能被误删。所以要么用 Redisson,要么业务方法里尽量缩短锁内耗时。
其实这些坑背后有个共同规律:分布式场景下的每个判断和操作,都要优先考虑是不是原子的、会不会因为网络或宕机产生中间状态。这个思维方式比背具体命令更重要。
6.2 给新人的建议:从 Demo 到项目实战
如果你现在还在学习阶段,我的建议是把 Demo 跑通之后,再自己动手改造成一个完整的小项目:增加商品活动表、把库存预热做成定时任务、给订单消费加上本地消息表和定时对账,然后用压测验证超卖和消息积压问题。这样再去看大厂面经,就不会觉得“这只存在于面试官嘴里”了。
有人说秒杀系统是面试造火箭、工作拧螺丝,但我觉得秒杀场景是极少数能在一套系统里同时训练高并发、一致性和高可用的实战场景。你把这些思路吃透,再去面对日常的接口性能优化、缓存治理和消息队列问题,思维层次会完全不一样。
最后说一个我压箱底的小技巧:秒杀活动开始前,提前把商品库存和活动信息预热到 Redis,并做一次小规模压测,别等线上流量真的涌进来了才排查问题。还有,所有秒杀相关操作都要加接口幂等和用户级别限流,不然脚本一刷,你的活动就没意义了。这些听起来都是小事,但线上的大事故往往就败在这些小细节上。