1. 秒杀在业务上到底在躲什么坑
1.1 超卖是怎么发生的
超卖这个问题,看起来简单,但很多人其实没想透。库存只有100件,结果1000个人同时抢,最后卖出去了150单,这显然不行。问题出在哪?出在"检查库存"和"扣减库存"这两步不是原子的。
想想看,两个线程同时读到库存还剩1件,都认为可以卖,各自执行库存减一,数据库里库存就变成-1了。Java里的count--在字节码层面是读变量、减一、写回去三步,任何一个并发场景下都会被插队。数据库层面如果用SELECT stock FROM table WHERE id = ?然后再UPDATE table SET stock = stock - 1,也一样有窗口期——两个连接都能查到库存大于0,然后各自执行更新,超卖就成了。
有人会说,那给库存字段加锁不就行了?可以,MySQL的SELECT ... FOR UPDATE确实能锁行,两个事务串行执行。但代价是:这把行锁会挡住所有抢购请求,数据库连接被占满,系统响应时间直线上升。1000个并发进来,QPS看着不高,数据库却先跪了。秒杀这种场景,本质上是在和"读多写少、瞬时流量极高"对抗,你不可能在数据库行锁这一层死磕。
1.2 一人一单:比超卖更容易漏掉的坑
超卖只是第一层坑,第二层坑是"一人一单"。秒杀活动通常限制每个用户只能购买一件或一定数量的商品,但用户完全可以多开几个窗口同时下单,或者脚本并发刷接口。
你在代码里写"先查一下这个用户有没有下过单,没有才允许下单",这在并发下照样会破。两个请求同时查到用户没下过单,都通过校验,都创建了订单——一人一单就失效了。
黑马点评里对这个问题的处理思路值得学习:在数据库层面给订单表加唯一索引,比如(user_id, voucher_id)组合唯一。这样就算业务代码判断漏了,数据库也会因为主键冲突拒绝第二条订单。这是兜底方案,也是最终防线。但光有兜底还不够,高并发下大量请求打到数据库触发唯一索引冲突,错误日志满天飞,白白消耗数据库性能。所以至少在Redis或Lua脚本里先做一层判断,把绝大多数重复请求挡在门外。
提示:唯一索引不是万能灵药,它解决的是"并发穿透"的最终一致性问题。真正的性能优化还得靠前置过滤,把无意义的请求挡在数据库之外。
1.3 "阻塞式"秒杀为什么活不过小流量
最直觉的做法是:用户点一下秒杀按钮,后端同步做"校验活动时间、校验库存、扣减库存、创建订单、返回结果"。这个链路在单机低并发下没有任何问题,但一旦流量进来,你会发现几个灾难性的现象。
第一个是请求线程被拖死。创建订单需要写数据库,数据库IO一慢,线程就卡在DB调用上,Tomcat线程池被占满,后续请求全部排队。第二个是数据库连接被打满。每个订单创建都占用一个数据库连接,连接池默认可能只有20个连接,200个并发请求排队等连接,超时直接报错。
这就像一个只有一个收银台的超市,所有顾客都挤在收银台前面排队。正确做法是让顾客先拿号,叫号了再来付款。秒杀系统也一样:请求进来先"拿号"(校验资格、预扣库存),然后异步处理真正耗时的下单操作,前端轮询结果。这也是黑马点评秒杀模块的核心思路——同步校验,异步下单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 黑马点评的库存方案:先砍单机,再上Redis Lua
2.1 乐观锁CAS:代码很短,坑不浅
先说明一点:黑马点评里早期方案里有乐观锁的对比实现。乐观锁的核心思路是"不加锁,更新时检查版本号或条件是否满足"。放到秒杀场景,最简单的写法是:
sql复制UPDATE tb_seckill_voucher
SET stock = stock - 1
WHERE voucher_id = ? AND stock > 0
这条SQL本身就是CAS(Compare And Swap)实现——只有当库存仍大于0时才执行扣减,MySQL在更新时会锁定该行,所以并发下不会出现超卖。这是很多课程里讲的"数据库层防超卖"的标准答案。
但这里的坑也很明显。一个坑是失败率爆炸。如果一开始就大量并发抢购,stock > 0这个条件会同时拦住很多人,但满足条件的那些请求中,只有一部分能成功执行UPDATE,其他请求会更新0行。你的代码如果把这个"When result == 0"直接当成秒杀失败,那会出现大量用户明明还没抢到就提示"已售罄",体验很差。更温和的做法是失败后重试几次,但重试次数有限,效果依然一般。
另一个坑是Java层面的CAS完全无用。有些人会在Service层写"先查库存,判断大于0,再更新",然后加一个@Transactional就觉得没问题了。实际上两个事务同时读到库存1,都能通过Java层判断,然后依次执行UPDATE ... SET stock = stock - 1,第二次更新时库存已经是0,但数据库不会拦你,照样更新成功变成-1。所以Java层的判断根本不能防超卖,必须在SQL或Redis中做原子操作。
黑马点评的最终实现其实没纠结在SQL乐观锁上,因为数据库的更新再快,也扛不住秒杀级的QPS。于是方案就挪到了Redis。
2.2 为什么最终选了Lua:让判断和扣减变成一件事
Redis的Lua脚本为什么适合秒杀?根本原因是Redis是单线程执行命令,而Lua脚本在执行期间不会被其他客户端命令插入。这意味着"检查库存、检查是否已购买、扣减库存、记录购买用户"这一连串操作在Lua脚本里执行是原子的,不存在并发穿插。
先看一个黑马点评风格的秒杀Lua脚本骨架:
lua复制-- KEYS[1]: 库存key 例如 seckill:stock:1
-- KEYS[2]: 用户购买记录key 例如 seckill:order:1
-- ARGV[1]: 用户id
-- 库存key不存在,说明秒杀活动未初始化或商品不存在
if (redis.call('exists', KEYS[1]) == 0) then
return -1
end
local stock = tonumber(redis.call('get', KEYS[1]))
if (stock < 1) then
return -2
end
-- 用户是否已购买
if (redis.call('sismember', KEYS[2], ARGV[1]) == 1) then
return -3
end
-- 执行扣减和记录
redis.call('decrby', KEYS[1], 1)
redis.call('sadd', KEYS[2], ARGV[1])
return 0
这段脚本把三个核心动作打包成了一个原子操作:判断库存是否充足、判断用户是否重复购买、扣减库存并记录用户。所有并发请求进到Redis后,Lua脚本会在一个执行周期内跑完,天然串行化。无论来多少请求,最终库存扣减的总量绝对不会超过库存总数,一人一单也从脚本层面挡住了。
为什么不用Redis事务(MULTI/EXEC)?因为WATCH在事务执行过程中如果key被修改会直接失败,需要重试,逻辑复杂。Lua脚本在执行前不需要WATCH,执行过程中又不会被其他命令打断,代码写起来更像"普通函数",而且可以把判断和写入放到一起,这是它最大的价值。
2.3 库存预减后的一致性风险在哪
用Lua脚本扣了Redis里的库存,但订单还没真正创建,这中间出问题怎么办?比如:Redis扣减成功,但消费者线程在处理时数据库连接池满了,订单创建失败;或者MQ消息丢失,消费者根本处理不到;再或者服务器突然宕机,Stream里积压了大量消息。
这类问题在业界最常见答案是"Redis管引流,数据库管落库,最终对账"。翻译一下:Redis里的库存只是"预占名额",真正的权威数据在数据库。订单最终创建成功与否,要以数据库订单表和库存表的操作结果为准。
具体到黑马点评,它把Redis库存和数据库库存都维护了一份。Redis库存高速扣减,MySQL库存只在实际创建订单时再扣一次(用上面的stock > 0条件更新保证不超卖)。两边的数据会有短暂不一致,但通过定时任务或者对账脚本可以最终收敛。面试时如果能主动讲出"Redis和MySQL之间是最终一致性,不是强一致",会明显加分,因为这说明你想过分布式场景下的数据问题。
3. 完整秒杀链路:从请求入口到异步建单
3.1 业务流程时序拆解
黑马点评的秒杀下单,整体流程可以拆成七步。用文字描述比图直观:
- 用户发起秒杀请求,携带userId和voucherId。
- 业务层先判断秒杀活动是否在有效时间窗口内。这一步不查库,可以提前把开始时间和结束时间放到Redis或本地缓存。
- 执行Lua脚本,完成"库存校验+一人一单校验+预扣库存+记录用户"。脚本返回0表示资格校验通过,返回负数表示不同失败原因。
- 如果脚本返回0,生成一个订单id(比如用Redis的ID生成器或雪花算法)。
- 将订单创建消息推入Redis Stream(或MQ),消息包含userId、voucherId、orderId。
- 消费者线程从Stream取出消息,调用数据库创建订单,状态为"待支付"。
- 接口立即返回"排队中"和orderId,前端定时轮询订单状态。
这里的关键设计是:第3步是同步的,用户点击后很快拿到结果;第6步是异步的,下单动作被推迟到了消息队列里。同步部分只做"资格判定",异步部分才做"真正落库"。这样做的好处是,高并发请求在Redis这一层就完成了过滤,真正打到数据库的请求数量被压缩到了可承受的范围。
为什么订单id要提前生成而不是在数据库插入时自动生成?因为消息队列里需要带着订单id才能把"预扣库存的记录"和"数据库订单"对应起来,否则消费者拿到消息后还得走一次业务逻辑生成id,不利于做幂等。订单id在入口生成,后面全程携带,最后在数据库唯一索引里兜底幂等。
3.2 用Redis Stream做异步下单队列
黑马点评教学版本用的是Redis Stream,不是RabbitMQ或者Kafka。很多人疑惑,为什么不用主流MQ?答案很实际:项目里没有额外引入消息中间件,Redis本来就是秒杀流程的核心组件,Stream是在Redis 5.0引入的原生消息队列,能解决"生产者消费者从零搭建"的问题,对中小规模项目够用,也少了一个需要运维的组件。
Stream的核心用法要掌握。生产端:
bash复制XADD stream.orders * userId 1001 voucherId 2001 orderId 778899
消费端用消费者组:
bash复制XGROUP CREATE stream.orders group1 0
# 消费者读取
XREADGROUP GROUP group1 consumer1 COUNT 10 BLOCK 2000 STREAMS stream.orders >
读出来后,要XACK确认消息消费成功:
bash复制XACK stream.orders group1 1688888888888-0
这里有几个容易踩的坑。
第一个是消息确认问题。用XREADGROUP读取消息后,如果消费者在处理过程中宕机,消息会留在Pending Entries List(PEL)里,变成"未确认"状态。下次要用XCLAIM把超时未确认的消息转移给其他消费者处理,否则消息会一直卡在那里。这其实是Stream提供的"至少一次投递"保证,代价是消费者端必须做幂等处理。
第二个坑是>参数的语义。XREADGROUP ... STREAMS stream.orders >表示只读新消息,0表示从历史消息开始读。很多初学者写错了,导致消费者一直重复消费历史消息或者永远拿不到新消息。
第三个坑是Redis Stream的消息会占用内存。如果不及时清理,大量积压消息会让Redis内存持续上涨。生产上要么控制消息条数,要么定期用XTRIM裁剪,要么干脆设置合理的消费速度报警。对黑马点评这个项目,订单量不大,风险可控,但你在复盘时要想到这一层。
3.3 前端轮询结果,后端状态机兜底
接口在Lua脚本通过后立即返回,页面显示"排队中"。这时候用户怎么知道自己的订单创建成功了?答案是前端轮询。黑马点评里的做法是:前端拿到orderId后,每隔几秒请求一次查询订单接口,如果订单状态从"待支付"变成"已支付"或"已取消",就不再轮询。
订单状态机是这个环节的核心。一张秒杀订单,生命周期大致是:
| 状态 | 含义 | 可能去向 |
|---|---|---|
| 0 | 待支付 | 支付成功变为1,超时变为2 |
| 1 | 已支付 | 终态 |
| 2 | 已取消 | 终态,库存回补 |
为什么说状态机兜底?因为消息队列的消息可能乱序,Redis Stream消费也可能重复,但有数据库订单表的状态字段做唯一事实来源,所有操作都围绕状态流转来做,即使某个环节丢了,定时扫描也能把"待支付超过30分钟"的订单修改为"已取消",同时回补库存。这一套状态机设计,是所有秒杀系统的基石。我在实际项目里见过不少订单状态混乱的情况,根因都是状态流转没有收敛到一条明确路径上,导致"支付回调"和"超时关单"同时改状态时互相覆盖。
4. 订单创建了,用户却跑了:超时关单的三种做法
4.1 Redis过期监听:最经典的坑
秒杀下单后,用户迟迟不支付,订单不能一直占着库存不放。常见做法是限时30分钟,超时自动关单释放库存。
很多人的第一反应是"给订单id在Redis里设置一个30分钟过期的key,过期事件触发关单"。这个方案用Redis的keyspace notifications(键空间通知)实现:开启notify-keyspace-events Ex,客户端订阅过期事件,收到事件后执行关单逻辑。
这个方案听起来优雅,实际坑特别多。第一,Redis过期事件不是实时触发的。key过期后,Redis的定时任务才能在后续周期内检测到并发布事件,延迟可能从几百毫秒到几十秒不等。如果秒杀库存紧张,最后几分钟内大量订单集中过期,事件风暴会让Redis处理不过来。第二,过期事件通过Pub/Sub发布,消息不持久化。消费者宕机或者重启期间,所有过期事件都会丢失,关单任务直接失效。第三,同一订单可能被重复设置多个过期key,每个key都能触发过期事件,如果你的消费者没做幂等处理,一个订单会被取消两次,库存重复回补,直接造成超卖。
结论很明确:Redis过期监听适合"提醒类、非关键性"任务,不适合做订单关单这种强一致性的资金/库存操作。黑马点评的教学方案里对这个做法进行了对比,但面试时你如果能主动指出它的缺陷,面试官会高看你一眼。
4.2 延迟队列:稍微靠谱的前提
第二种做法是延迟队列。思路是:订单创建后不立即发起定时任务,而是把订单id放进一个延迟队列,设置延迟时间30分钟,时间到了消费者才拿到这个订单id去执行关单。
黑马点评或者很多教学项目里会用Redis的zset模拟延迟队列,这是最不依赖外部组件的方案。做法:把订单id作为member,把"过期时间戳"作为score,存入zset。一个后台任务每隔几秒执行:
bash复制ZRANGEBYSCORE order:delay -inf <当前时间戳>
取出的member就是已经到期的订单id,然后去数据库执行关单和库存回补。处理成功后再从zset中删除,防止重复处理:
bash复制ZREM order:delay <订单id>
这个方案比过期监听靠谱在两点:第一,消息不会丢,数据存在Redis里,就算消费线程重启,重启后继续扫到期列表就能补处理。第二,处理时机可控,不是依赖Redis后台事件,而是自己去拉取,拉取频率决定处理延迟,项目里通常1~5秒扫一次,完全够用。
但也要说清楚它的局限:延迟时间不精确。如果Redis中积压了大量到期订单,一批一批扫出来,处理延迟会累积。如果订单量破万甚至十万,单靠zset轮询效率会下降。再往上走,就得用RocketMQ的延迟消息或者Netty时间轮算法,把"到期触发"这个动作做得更精准高效。面试被问到"如果量再大十倍怎么办"时,这个演进路径是你最有力的回答。
4.3 定时任务扫单:最土但最稳
第三种做法最朴素:定时任务扫数据库订单表。比如用Spring的@Scheduled注解,每30秒执行一次:
sql复制SELECT id FROM tb_voucher_order
WHERE status = 0
AND create_time < NOW() - INTERVAL 30 MINUTE
把查询出来的订单id逐一更新状态为"已取消",同时回补库存。为什么说它最稳?因为它的状态依据是数据库里的真实订单记录,不存在消息丢失的问题。订单表里状态为0且创建时间超过30分钟的记录,就是确凿无疑的超时单,直接处理即可。哪怕定时任务本身宕机了,恢复后重新扫描一遍,结果是一样的,天然幂等。
代价也很明显:随着订单量增长,定时扫描对数据库会有压力。优化方向也直接:给status和create_time加联合索引,每次只扫少量数据;或者把订单按天分表,只扫描当天的表;再或者用懒取消的思路,用户查询订单状态时顺便检查超时时间并修改状态,把压力分散到用户请求里。
我个人的建议是,秒杀项目中超时关单优先采用"定时扫单+库存回补"的组合。理由只有一个字:稳。你不需要为复杂度买单,数据库的状态扫描是这个场景下最不容易出错、也最容易排查问题的方案。黑马点评项目规模下,订单量几千几万,定时任务完全承担得起。延迟队列、MQ延迟消息这些都是订单量上去后的优化方向,不要一开始就把系统搞复杂。
5. 分布式环境下的隔离与session共享:面试必问的附加题
5.1 集群部署下的Session失效
秒杀接口部署了多个节点,用户登录信息如果还存Session,问题立刻暴露。节点A处理了登录请求,Session记录在节点A的内存里;用户下一个秒杀请求被负载均衡器转发到节点B,节点B的Session里根本没有这个用户的记录,用户就"被下线"了。
黑马点评的登录模块恰好就是这个经典场景的解法:Session不存用户状态,改用Redis存token。登录成功后生成一个随机token,作为key存入Redis,value是用户信息,token返回给前端。后续请求在Header里带上token,后端写一个Spring MVC拦截器或Spring Security过滤器,每次请求时根据token从Redis取用户信息,取不到就提示重新登录。
java复制// 登录成功
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("login:token:" + token, userId, 30, TimeUnit.MINUTES);
// 后续请求
String userId = redisTemplate.opsForValue().get("login:token:" + token);
这个解法的好处是用户状态不依赖具体节点,任何节点拿到token都能从共享的Redis中恢复用户身份。秒杀场景下所有请求本来就要经过Redis做库存预减和资格校验,把session也放进Redis,基础设施上是完全一致的。这个设计要能讲清楚"为什么不能用Session"——因为Session是单机内存态,分布式环境下没有共享的可能,除非做session共享中间件,但那也是基于Redis或其他共享存储实现的,绕了一圈还是一样的道理。
5.2 秒杀要不要上熔断和降级
热词里有"服务熔断和降级",很多人会觉得秒杀接口必须配Sentinel或者Hystrix才能算完整。实际做项目时,要分清楚什么场景才需要熔断降级。
熔断的前提是服务之间存在依赖调用,且下游服务可能故障。比如秒杀下单流程中调用了独立的用户服务、优惠券服务、库存服务,一旦优惠券服务响应变慢,上游秒杀接口会被拖垮,这时才需要在调用链路里加熔断器。黑马点评这个项目内部没有拆分成多个微服务,库存、订单、用户都在一个应用里,谈不上服务间熔断。
但降级思路是通用的:Redis挂了怎么办、数据库挂了怎么办?实际项目里我见过很多团队在秒杀入口做降级开关,比如"库存预减Redis异常时,直接返回'活动太火爆,请稍后再试'";"MQ堆积超过阈值时,拒绝新的秒杀请求";"数据库连接池耗尽时,返回排队等待而不是直接500"。这些开关本质上是把系统的负载控制在可承受范围内,而不是让故障无限放大。
如果你在面试中想聊熔断降级,建议这样表达:单独秒杀模块不一定要上Sentinel,但如果秒杀依赖了外部微服务,那对下游的调用一定要做熔断和降级,避免单个服务故障拖垮整个秒杀链路。这种表达既展现了全局视角,又不至于让人觉得你在堆技术名词。
5.3 压测与调优记录:连接池、行锁、JVM
用JMeter对秒杀接口做压测,是检验方案靠不靠谱的唯一方式。黑马点评这门课里大家最常跑的场景就是500个线程并发抢购,跑完看三样东西:数据库是否超卖、Redis库存是否准确、接口响应耗时。我实际跑下来发现几个典型问题。
第一个是Redis连接池被打满。默认Lettuce连接池很小,压测一上来连接就不够用,大量请求卡在获取Redis连接这一步。解决办法是调大spring.redis.lettuce.pool.max-active,同时压测前先预热连接池,避免启动后短时间内建立大量连接。
第二个是数据库行锁等待。虽然最终落库的请求被Lua脚本过滤了一大批,但数据库还承担着查询订单状态、回补库存等操作。如果订单表和库存表的数据分布不均匀,秒杀热门商品的UPDATE语句会相互等锁,慢查询飙升。优化方向是给秒杀订单表、优惠券表的所有查询条件都加上合适的索引,尤其是voucher_id和user_id,避免锁升级为表锁。
第三个是JVM GC波动。压测过程中如果接口耗时出现周期性尖刺,多半是GC停顿导致的。秒杀期间创建了大量短生命周期对象(订单DTO、消息体、响应对象),Young GC频繁,老年代如果也不稳定,就会出现响应抖动。实际调优时可以做三件事:加大新生代内存、开启-XX:+UseG1GC并设置合理的停顿时间目标、把秒杀接口产生的大对象尽量复用,减少并发峰值下的对象创建速率。压测不是一次性的,每改一个参数都要重新跑一遍,对比前后吞吐量和RT曲线,这个习惯比堆技术方案重要得多。
6. 面试怎么把黑马点评秒杀讲成自己的项目
6.1 一句话项目描述与递进表达
黑马点评这个项目在简历上出现的频率实在太高了,如果你的表达还停留在"我做了登录、秒杀、关注、点赞"这个层面,面试官很难提起兴趣。真正拉开差距的,是你能不能把秒杀这个功能讲出一条清晰的决策链。
推荐的四段式表达:
第一段讲业务。秒杀券是一个典型的限量抢购场景:活动时间有限、库存有限、每个用户限购一单、订单创建后30分钟不支付自动取消并回补库存。
第二段讲核心方案。在高并发抢购下,数据库并发扣减库存会引发超卖,因此把"库存校验+一人一单校验+扣减库存"放到Redis的Lua脚本里原子执行,再配合数据库唯一索引兜底防重复下单,最后通过Redis Stream异步创建订单,把同步阻塞改为异步削峰。
第三段讲代价。库存预减和异步下单带来了新的问题:Redis与MySQL状态可能短暂不一致,所以要在数据库状态机上做最终兜底,定时扫描超时订单并回补库存。
第四段讲演进。如果流量再提升一个量级,Redis Stream可以替换为RocketMQ或Kafka,Redis内存库存可以换成多级缓存加分布式锁,数据库订单可以分库分表,入口还可以加网关限流和服务降级。
这一段完整的表述下来,面试官能感觉到你不是背了一个项目,而是真的理解每个技术选型背后的权衡。
6.2 高频追问的应答要点
围绕秒杀这个模块,有四个追问几乎必被问到。
第一个追问:Redis扣了库存,但MySQL建单失败怎么办?回答思路:Redis库存是预占,不是最终扣减,最终以数据库订单为准。建单失败时,可以记录失败日志并定时对账回补Redis库存,或者通过消息队列的重试机制重新消费,超过重试次数则将该订单状态置为无效。
第二个追问:超卖到底被谁拦住了?回答思路:三层防线。第一层是Redis Lua脚本原子执行库存扣减,拦截绝大多数并发请求;第二层是MySQL更新库存时使用stock > 0条件,保证最终落库不会扣成负数;第三层是订单表唯一索引拦截一人多单,防止超卖订单数量超过库存。
第三个追问:为什么用Lua而不是Redis事务?回答思路:Lua脚本执行期间不会被其他命令插入,天然原子;Redis事务依赖WATCH,冲突时要重试,代码复杂,而且事务执行过程中出错不影响其他命令,无法做到"要么全做,要么全不做"的业务原子性。
第四个追问:Stream消息重复消费怎么办?回答思路:消费者处理消息前先查数据库订单是否存在,存在则跳过;处理成功后XACK确认,未确认的消息通过XCLAIM转移给其他消费者重试。幂等设计是消息队列消费端的必修课,重复消费不可怕,处理逻辑要保证重复执行不影响最终结果。
6.3 哪些话不要乱说
简历和面试中有几句常见的话,表面上很唬人,实际上最容易被深挖翻车。
第一句是"我用Redis解决了秒杀的超卖问题"。这句话表述不准确。Redis只是解决了高并发下库存扣减的性能瓶颈,超卖的最终防线在数据库的条件更新和唯一索引。如果你说Redis单方面解决了超卖,面试官追问"Redis和MySQL库存不一致怎么办"时你自己就把自己绕进去了。
第二句是"这个项目实现了分布式事务"。黑马点评项目并没有真正引入分布式事务组件,Redis库存预减和MySQL建单之间是最终一致性,不是强一致。你可以谈对账、兜底、状态机,但不要说自己实现了Seata或者TCC事务,一旦被追问细节就露馅。
第三句是"秒杀接口支持每秒十万并发"。黑马点评级别的项目压测几百并发就很不错了,没必要把数字吹上天。面试官更看重的是你面对高并发时怎么一步步优化思路,而不是一个脱离实际的数据指标。
最后说点实际的。项目代码可以照着敲,但面试表达一定要变成自己的话。建议你找一个周末下午,把秒杀这个链路的每一步都写下来:用户点按钮之后发生了什么,数据怎么流转,每一个技术组件在其中扮演什么角色,如果某个环节挂了系统会怎样。写完之后对着镜子讲一遍,能讲顺了,这题才算真过了。我见过太多人代码敲得飞快,但一被问"为什么"就卡壳,根源就是只背了流程,没有建立自己的技术判断。
