两个高频问题。第一个考的是「你知不知道保证在哪一层」,第二个考的是「你有没有被术语绕晕」。
20. 十万并发抢票,如何做到绝不超卖?
先明确一件事:前面所有手段都不是保证
限流、验证码、Redis 预扣、消息队列——这些全都是削峰,目的是保护系统不被打垮。它们一条都不能保证「绝不超卖」。
「绝不超卖」这个保证,只能由数据库那一层的约束提供。想清楚这一点,这道题就答对了一半。
漏斗:每层拦掉一个数量级
上面演示的第一个标签就是这个结构。拖动并发量会发现一个关键现象:无论前面并发多大,最后落库的写请求数被库存量封顶。
十万并发 ↓ 网关限流 / 验证码 / 黑名单 拦掉 ~80%2 万 ↓ 本地库存标记(JVM 内存,售罄直接拒) 拦掉 ~75%5 千 ↓ Redis Lua 原子预扣 只放过 ≈ 库存量1 千 ↓ MQ 异步削峰 平滑写入1 千 → DB 落库本地库存标记这层最容易被漏掉,但性价比最高。 每台机器内存里放一个 volatile boolean soldOut,Redis 返回售罄后就地翻转,后续请求连 Redis 都不用查,直接失败返回。一个进程内的布尔判断,成本几乎为零,却能拦掉售罄后的绝大部分无效流量——而抢票场景里,售罄后的流量往往比售罄前还多。
Redis 预扣必须用 Lua
分成两条命令就是竞态:
# ❌ 经典错误:check-then-actif redis.get("stock") > 0: # 线程 A 和 B 都读到 1 redis.decr("stock") # 两个都扣,变成 -1GET 和 DECR 之间有时间窗口,高并发下必然被命中。正确做法是用 Lua 把判断和扣减放进同一次原子执行:
-- KEYS[1] = 库存 key, ARGV[1] = 用户 idif redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -2 -- 已抢过,幂等endlocal n = tonumber(redis.call('GET', KEYS[1]) or "0")if n <= 0 then return -1 end -- 售罄redis.call('DECR', KEYS[1])redis.call('SADD', KEYS[2], ARGV[1]) -- 记录已购,防重复return n - 1Redis 单线程执行 Lua,整个脚本原子——这是 Redis 预扣能成立的全部依据。
顺带把一人一单也在这个脚本里解决了:用 Set 记录已购用户,同一个脚本内判断。如果放到脚本外面单独判断,又是一个 check-then-act。
最终防线:一条 SQL
这是整道题的核心。判断和扣减必须合并成一条语句,由数据库的行锁保证原子性:
UPDATE ticket_stock SET cnt = cnt - 1, version = version + 1 WHERE ticket_id = #{id} AND cnt >= 1; -- 关键:条件写在 WHERE 里然后检查影响行数:
int affected = stockMapper.deduct(ticketId);if (affected == 0) { throw new SoldOutException(); // 没扣成,直接失败}演示页第二个标签把这个对比动画化了——同样两个线程、同样只剩一张票,check-then-act 写法库存被扣成 -1,原子条件更新则第二个线程的 affected 为 0,直接失败。
再加一层兜底,让超卖在物理上不可能:
-- MySQL:无符号列,扣成负数直接报错cnt INT UNSIGNED NOT NULL-- 或者 CHECK 约束ALTER TABLE ticket_stock ADD CONSTRAINT chk_cnt CHECK (cnt >= 0);代码可能有 bug,约束不会。 这一层是给未来那个不知道这段历史的同事准备的。
分清两个目标:不超卖 vs 不少卖
这是很多人答漏的一点:
| 要求 | 手段 | |
|---|---|---|
| 不超卖 | 硬保证,绝不妥协 | DB 条件更新 + 列约束 |
| 不少卖 | 尽力而为,最终一致 | 对账 + 补偿 |
Redis 扣了但 MQ 消息丢了、或者用户下单后超时未支付——这些会导致少卖。少卖不可怕,可以接受短暂不一致,靠定时对账把库存回补:
定时任务:比对 Redis 预扣数 与 DB 实际订单数差额 = 预扣但未成单 → 回补库存超时未支付的订单用延迟队列自动取消并回库。把「不超卖」和「不少卖」混成一个问题,是这道题最常见的思路错误——前者要强一致,后者要最终一致,手段完全不同。
热点行才是真瓶颈
所有请求都去更新同一行,行锁串行化,单行的更新 TPS 大概就几千。库存量大的时候这会成为瓶颈。
分段库存:把 1000 张票拆成 10 段,每段 100 张,请求按用户 ID 哈希路由到不同段:
stock_0: 100 stock_1: 100 ... stock_9: 100锁竞争被摊到 10 行上。代价是可能出现「某段售罄但总库存还有」,需要做段间借调或者允许重试其他段——复杂度上来了,所以库存量不大的时候不要提前做这个优化。
完整链路
用户请求 → 网关限流 + 验证码 削峰 → 本地售罄标记 快速失败 → Redis Lua(预扣 + 一人一单) 原子,抗住十万并发 → MQ 异步削峰,平滑写入 → DB 条件更新 + 列约束 ★ 绝不超卖的保证在这里 → 延迟队列取消未支付订单 不少卖 → 定时对账补偿 最终一致面试时如果只能说一句话:前面所有层都是性能优化,正确性的保证只有最后那条 WHERE cnt >= 1。
21. 分布式和微服务有什么区别?
一句话
分布式解决「一台机器不够用」(技术驱动),微服务解决「一个团队 / 一个代码库不够用」(组织驱动)。
包含关系是单向的
微服务一定是分布式的,分布式不一定是微服务。
三个反例足以说明问题:
- 单体应用部署 10 个副本挂负载均衡 —— 这是分布式,但完全不是微服务。同一份代码、同一个仓库、同一个发布流程,只是跑了多份。
- HDFS、Elasticsearch、Kafka —— 典型的分布式系统,但它们不是微服务架构。
- Redis Cluster —— 分布式,同样不是微服务。
两个维度的对比
| 分布式 | 微服务 | |
|---|---|---|
| 是什么 | 部署形态 | 架构风格 |
| 回答的问题 | 怎么把一件事分到多台机器上做 | 按什么边界拆分代码和团队 |
| 驱动力 | 容量、可用性、性能 | 团队自治、独立发布、技术异构 |
| 拆分依据 | 数据分片、计算分片 | 业务领域(DDD 限界上下文) |
| 各节点代码 | 通常相同(同一份代码多副本) | 各不相同(不同服务不同代码库) |
| 典型代表 | HDFS、Redis Cluster、Nginx 集群 | 订单服务、库存服务、用户服务 |
「各节点代码是否相同」是最快的判别方法。 十个节点跑同一份代码 = 分布式;十个节点跑十份不同代码、各有各的仓库和发布节奏 = 微服务。
微服务的代价,是为组织自治付的钱
微服务带来的问题清单很长:
- 分布式事务(本地事务没了,要上 Saga / TCC / 最终一致)
- 链路追踪(一个请求横跨 8 个服务,出问题不知道在哪)
- 服务治理(注册发现、熔断、降级、限流)
- 数据一致性(数据库拆了,JOIN 没了)
- 运维复杂度(20 个服务的部署、监控、日志聚合)
- 本地开发困难(起一个功能要启动 6 个服务)
这些代价换来的是什么?团队能独立开发、独立发布、独立选型。
所以判断标准很直接:团队规模到不了那个量级,付了钱买不到东西。 三个人维护 15 个微服务,是在用分布式系统的全部复杂度,换一个根本不存在的组织协作问题。
Martin Fowler 那句「你必须足够高才能微服务」(You must be this tall to use microservices)说的就是这个——先具备自动化部署、监控、快速故障定位的能力,再谈拆分。
那单体就是落后的吗
不是。模块化单体(Modular Monolith)在很多场景下是更优解:代码内部按领域清晰分模块、有明确的模块边界和依赖规则,但部署成一个进程。
这样既有清晰的边界(将来真要拆,沿着模块边界拆就行),又没有分布式的代价。
推荐的演进路径:
模块化单体 → 边界稳定后 → 按需拆出压力最大 / 变更最频繁的模块先拆逻辑边界,再拆物理部署。 反过来做——边界还没想清楚就先拆服务——是微服务实践失败的主要原因:边界划错了之后,服务间会产生大量的同步调用和分布式事务,比单体还难维护。
一个常见的混淆
「我们用了 Spring Cloud,所以我们是微服务」——框架不等于架构。
用了注册中心和网关,但所有服务共用同一个数据库、任何改动都要一起发布、拆分依据是技术分层(controller 服务、service 服务、dao 服务)——这是分布式单体(Distributed Monolith),公认的最差架构:既有微服务的全部代价,又没有微服务的任何收益。
判断是不是分布式单体,问三个问题:
- 能独立发布吗? 改一个服务要不要协调其他服务一起发?
- 数据库独立吗? 多个服务直连同一个库、甚至同一张表?
- 拆分依据是业务还是技术分层? 按 controller/service/dao 拆是技术分层,不是微服务。
三个里有两个答错,那就是分布式单体。
小结
两道题其实有个共同点:都在问「保证 / 收益到底来自哪一层」。
抢票那题,削峰的手段有五层,但正确性只来自最后的数据库约束——把 Redis 预扣当成防超卖手段,是把性能优化误认为正确性保证。
微服务那题,分布式带来的是容量和可用性,微服务带来的是组织自治——把两者混为一谈,就会在三人小团队里为了「高可用」拆出十五个服务,然后被分布式事务和链路追踪拖垮。
先想清楚要买什么,再看这个代价值不值得付。






