# 系统设计面试题
# 如何设计一个秒杀场景?
秒杀场景的核心特点是高并发、低库存、短时间爆发式访问 。因此,设计时需要解决以下几个问题:
- 高并发处理 :如何应对大量用户同时访问?
- 库存一致性 :如何保证库存不会超卖或少卖?
- 用户体验 :如何减少用户等待时间,避免页面崩溃?
- 防刷机制 :如何防止恶意用户利用脚本抢购商品?
面对上面这些问题,可以针对每一层做一些设计:

1、前端层:
- 静态资源分离 :将秒杀页面的静态资源(如HTML、CSS、JS)部署到CDN(内容分发网络),减轻服务器压力。
- 请求拦截:活动未开始时,前端按钮置灰;通过验证码、点击频率限制。
2、网关层:
- 流量拦截 :使用API网关对请求进行初步过滤,例如IP限流、黑名单拦截等。
- 身份验证 :通过Token或签名验证用户身份,防止未登录用户直接访问秒杀接口。
3、缓存层:
- Redis缓存库存 :将商品库存信息存储在Redis中,利用其高性能特性处理库存扣减操作。
- 预热数据 :在秒杀活动开始前,将商品信息和库存数据加载到Redis中,减少数据库压力。
4、消息队列:
- 削峰填谷 :使用消息队列(如Kafka)将用户的秒杀请求异步化,避免直接冲击后端服务。
- 订单处理 :将成功的秒杀请求放入队列,由后台服务异步生成订单,提高系统吞吐量。
5、数据库层
- 乐观锁:在库存扣减时,使用乐观锁确保库存一致性。
- 读写分离 :通过主从复制实现数据库的读写分离,提升查询性能。
关键的核心业务逻辑实现,库存防超卖方案采用:Redis原子操作 + 异步扣减数据库。

具体流程如下:
- 秒杀请求达到:用户发起秒杀请求,系统接收到请求后,首先进行一些基础校验(如用户身份验证、活动是否开始等)。如果校验通过,进入库存扣减逻辑。
- Redis库存扣减:在Redis中检查商品库存是否充足。例如,使用
GET命令获取当前库存数量。如果库存不足,直接返回失败,结束流程。如果库存充足,使用Redis的原子操作(如DECR或Lua脚本)扣减库存。 - 异步更新数据库:如果Redis库存扣减成功,生成一个秒杀成功的消息,并将其放入消息队列
- 后台服务消费消息:后台服务从消息队列中消费秒杀成功的消息,执行以下操作:
- 1、为用户创建订单记录;
- 2、使用乐观锁将数据库中的库存数量减少1;
- 3、通过唯一标识(如用户ID+商品ID+时间戳)防止重复消费。
- 最终一致性校验:在Redis库存扣减和数据库库存更新之间,可能会存在短暂的不一致状态。为了保证最终一致性,可以采取以下措施:
- 1、定期将Redis中的库存数据与数据库进行同步。
- 2、如果发现Redis和数据库库存不一致,触发补偿逻辑(如回滚订单或调整库存)。
# 设计题:订单到了半个小时,半个小时未支付就取消
有多种实现订单超时自动取消的技术方案,包括定时轮询、JDK的延迟队列、时间轮算法、Redis实现以及MQ消息队列中的延迟队列和死信队列。
定时轮询:基于SpringBoot的Scheduled实现,通过定时任务扫描数据库中的订单。优点是实现简单直接,但缺点是会给数据库带来持续压力,处理效率受任务执行间隔影响较大,且在高并发场景下可能引发并发问题和资源浪费。
JDK的延迟队列(DelayQueue):基于优先级队列实现,减少数据库访问,提供高效的任务处理。优点是内部数据结构高效,线程安全。缺点是所有待处理订单需保留在内存中,可能导致内存消耗大,且无持久化机制,系统崩溃时可能丢失数据。
时间轮算法:通过时间轮结构实现定时任务调度,能高效处理大量定时任务,提供精确的超时控制。优点是实现简单,执行效率高,且有成熟实现库。缺点同样是内存占用和崩溃时数据丢失的问题。
Redis实现:
有序集合(Sorted Set):利用有序集合的特性,定时轮询查找已超时的任务。优点是查询效率高,适用于分布式环境,减少数据库压力。缺点是依赖定时任务执行频率,处理超时订单的实时性受限,且在处理事务一致性方面需要额外努力。
Key过期监听:利用Redis键过期事件自动触发订单取消逻辑。优点是实时性好,资源消耗少,支持高并发。缺点是对Redis服务的依赖性强,极端情况下处理能力可能成为瓶颈,且键过期有一定的不确定性。
MQ消息队列:
延迟队列(如RabbitMQ的rabbitmq_delayed_message_exchange插件):实现消息在指定延迟后送达处理队列。优点是处理高效,异步执行,易于扩展,模块化程度高。缺点是高度依赖消息队列服务,配置复杂度增加,可能涉及消息丢失或延迟风险,以及消息队列与数据库操作一致性问题。
死信队列:通过设置队列TTL将超时订单转为死信,由监听死信队列的消费者处理。优点是能捕获并隔离异常消息,实现业务逻辑分离,资源保护良好,方便追踪和分析问题。缺点是相比延迟队列,处理超时不够精确,配置复杂,且同样存在消息处理完整性及一致性问题。
不同方案各有优劣,实际应用中应根据系统的具体需求、资源状况以及技术栈等因素综合评估,选择最适合的方案。在许多现代大型系统中,通常会选择消息队列的延迟队列或死信队列方案,以充分利用其异步处理、资源优化和扩展性方面的优势。
# 如果做一个大流量的网站,单Redis无法承压了如何解决?
- 读写分离:部署多个 Redis 从节点(Slave),主节点(Master)负责写操作,从节点负责读操作。主节点将数据同步到从节点,从节点可以处理大量的读请求,减轻主节点的压力。
- 构建集群:部署 Redis Cluster 集群,Redis Cluster 将数据自动划分为 16384 个槽(slots),每个槽都可以存储键值对。这些槽会被分配到多个 Redis 节点上,通过哈希函数将键映射到相应的槽,再由槽映射到具体的 Redis 节点。例如,使用
CRC16(key) % 16384来确定键属于哪个槽,然后根据槽与节点的映射关系将键值对存储到相应节点。通过数据分片,将数据和请求分散到多个节点,避免单个节点的负载过高。不同节点负责不同的槽,各自处理一部分请求,实现负载均衡。
# 如何设计一个可重入的分布式锁,用什么结构设计?
可重入锁允许同一线程在持有锁的情况下多次获得锁而不会产生死锁。当线程请求锁时,如果它已经拥有了该锁,则可以直接获得锁,锁的计数器会增加。在释放锁时,计数器会减少,只有当计数器为零时,锁才会真正释放。
在分布式系统中,通常会使用诸如 Redis、Zookeeper 或 etcd 等组件来实现锁的管理。下面以 Redis 为例,简单描述如何设计一个可重入的分布式锁。
设计要素:
- 锁的标识符(Lock ID):用于标识锁的唯一性。
- 持有者标识(Owner ID):线程或进程的唯一标识符,通常是线程的 ID 或进程的 ID。
- 计数器:记录当前持有锁的次数。
- 过期时间:为了避免死锁,锁需要设定一个合理的超时时间。
我们可以在 Redis 中使用一个哈希表或简单的键值对来存储锁的状态。使用一个 key(如 lock:<resource>)来表示锁,包含以下字段:
owner: 当前持有锁的线程/进程标识符count: 当前计数器,表示获得锁的次数expires_at: 锁的到期时间,用于防止死锁
示例数据结构
{
"key": "lock:resource_1",
"value": {
"owner": "thread_id_or_process_id",
"count": 3,
"expires_at": "2023-10-01T12:00:00Z"
}
}
以下是围绕这个锁结构的基本操作:
- 获取锁 (Lock Acquisition):尝试设置 lock: 的值,如果这个 key 不存在(即没有锁),那么可以创建该 key,并设置 owner 为当前线程/进程的唯一ID,count 设为 1,expires_at 设为当前时间加上锁的过期时间。 如果该 key 已存在且 owner 是当前线程/进程的 ID,则增加 count 并更新 expires_at。
- 释放锁 (Lock Release):检查当前的 owner 是否是当前线程/进程的 ID。如果是,减少 count。如果 count 减少到 0,则删除该 key。 如果在持有锁的情况下,锁已过期,系统会根据 expires_at 检查锁是否可释放,避免死锁。
- 锁续期 (Lock Renewal):如果当前线程在执行过程中需要继续持有锁,可以在逻辑处理中重新设置 expires_at.
- 超时与故障恢复:可以设置锁的自动超时时间,例如,设定一个最大持锁时间,超时后锁自动释放。这可以在一定程度上防止死锁的情况。
整体流程示例:
获取锁:
线程A调用获取锁的 API。如果成功,返回锁的状态。
如果线程B也尝试获取同一把锁,则会返回锁已被占用。
释放锁:
线程A在完成任务时调用释放锁的 API,递减
count字段。如果
count达到 0,删除锁并释放资源。
续约锁:
- 线程A在处理过程中可以根据需要续约锁,更新
expires_at。
- 线程A在处理过程中可以根据需要续约锁,更新
# 你有看过一些负载均衡的一些方案吗
- 硬件负载均衡:使用专用的硬件设备(如负载均衡器)来分配流量,比如F5设备,优点是性能强大,支持高并发。缺点是成本太高,通常一个专业级的硬件设备,都需要百万级别的价格。
- 软件负载均衡:使用软件应用程序来实现负载均衡,可以运行在普通服务器上,比如 Nginx 支持反向代理和负载均衡,优点灵活性高,成本低,易于修改和扩展,缺点是性能不如硬件负载均衡,但是软件负载均衡的性能也足够应对大多数场景的并发量了。
- DNS 负载均衡:通过 DNS 服务器将流量分配到多个服务器上,不同的客户端请求可能获得不同的 IP 地址。优点是简单易用,不需要额外的硬件或软件。缺点是无法智能感知服务器的实时状态,缓存问题可能导致不均匀负载。
- 内容分发网络 (CDN):CDN 是一种分布式的网络结构,可以将内容分发到离用户最近的节点上。优点减少延迟,提高用户体验。缺点主要适用于静态内容,动态请求仍需其他形式的负载均衡。
# 10w顾客抢购一个只有10个库存的商品如何设计?
这个问题其实就是典型的“秒杀”场景。核心目标其实就两个:第一,系统别被 10 万并发直接打崩;第二,库存绝对不能超卖,用户体验也别太差。
我的整体思路是分三层:先在入口把流量“削平”,再在中间用一个足够快的组件做原子扣减,最后把下单这种重活异步化落库。
首先入口层我一定会做限流和隔离。像 Nginx / 网关层做全局限流、按 IP/用户限流,防刷和机器人校验也要上,比如验证码、滑块、设备指纹之类的。然后接口本身要做到极简,只干一件事:拿资格。因为如果每个请求都直接打到数据库去查库存,10 万并发数据库必死。
然后真正防超卖的关键我会放在 Redis 或者类似的原子计数里做。最常见的做法是把库存预热到 Redis,用 Lua 脚本做「检查库存>0并扣减」的原子操作,同时做一人一单的校验,比如用 SETNX userId 或者在 Lua 里顺便判断用户是否已抢过。这样一个请求过来,要么直接返回成功拿到资格,要么失败,整个过程完全在 Redis 内完成,性能扛得住,而且原子性保证不会超卖。
拿到资格之后我不会立刻同步走创建订单、扣数据库库存这种重流程,而是把它写进消息队列,比如 Kafka/RocketMQ。这样前端能很快收到“你已抢到资格,订单处理中”的结果,系统后面慢慢消费消息去真正创建订单、落库、扣真实库存。
数据库层我会用「最终一致」的方式兜底:比如库存表用乐观锁 update ... set stock=stock-1 where id=? and stock>0,就算 Redis 那边有极端情况,也能在 DB 这层再挡一次超卖。
再往后,我会补两个关键的工程细节。
- 一个是「订单有效期」,因为抢到资格的人不一定付款,所以要给订单一个超时时间,比如 15 分钟不支付就自动取消,并把库存归还到 Redis 和 DB,这块也通常靠延迟消息或者定时任务做。
- 另一个是「重复请求和幂等」,因为用户会狂点、网络会重试,所以资格发放和下单消费都要有幂等键,比如以 userId+skuId 作为唯一键,保证不会重复创建订单。
总结一下就是:入口限流抗压,Redis 原子扣减防超卖,MQ 异步落库抗峰值,DB 乐观锁兜底一致性,再加上超时释放库存和幂等,基本就能把 10 万抢 10 个这种场景稳住。
# 微服务架构有了解吗?你认为什么是微服务架构?
简单说微服务就是把原来一个大的单体应用拆分成多个小的独立服务,每个服务负责一块具体的业务功能,可以独立开发、独立部署、独立扩展。

微服务最大的好处就是灵活。比如电商系统,我们可以把用户服务、订单服务、商品服务、支付服务都拆开,如果双十一订单量暴增,我只需要把订单服务多部署几个实例就行,不用整个系统都扩容,这样既省资源又快。而且不同团队可以并行开发不同的服务,用自己熟悉的技术栈,发版也互不影响,开发效率会高很多。

但微服务也不是银弹,它带来的复杂度其实挺高的。首先服务之间要互相调用,这就需要服务注册发现,我们一般用Nacos或者Eureka这种注册中心,让服务启动时把自己注册上去,调用方从注册中心拉取服务列表。然后调用方式上可以用HTTP的RESTful或者RPC框架像Dubbo、gRPC,这些都要考虑负载均衡、超时重试、熔断降级这些容错机制。

链路追踪也很重要,一个请求可能要经过五六个服务才完成,出了问题不好排查,所以我们会用Skywalking或者Zipkin把整个调用链路串起来,每个请求都带一个TraceID,方便追溯。配置管理也是个问题,微服务多了之后配置文件一大堆,我们用配置中心统一管理,修改配置可以动态下发不用重启服务。
网关是微服务的入口,像Spring Cloud Gateway或者Nginx,负责路由转发、鉴权限流这些统一处理的逻辑。服务间通信如果都是同步调用,一个服务慢了会拖累整条链路,所以有些场景我们会用消息队列做异步解耦,比如下单后发个消息让库存服务去扣减,不用同步等待。
数据管理也比较头疼,微服务一般提倡每个服务有自己的数据库,不能直接访问别人的表,这样能做到服务自治。但这也带来分布式事务的问题,一个业务操作涉及多个服务的数据修改,要保证一致性就得用Seata这种分布式事务框架,或者用最终一致性方案像TCC、SAGA模式。
我觉得微服务适合业务复杂、团队规模大、需要快速迭代的场景。如果就是个小项目,团队就三五个人,其实单体应用更合适,不要为了微服务而微服务。架构选型一定要结合实际情况,微服务能解决很多问题,但也会引入新的复杂度,这个要权衡好。
# 对于你的项目(单体),怎么拆分成微服务架构呢?
拆分微服务我觉得不能一上来就全拆,得循序渐进。首先我会梳理现有系统的业务模块和依赖关系,画个模块依赖图,看哪些模块相对独立、哪些耦合比较重。
然后按照业务领域来划分服务边界,遵循领域驱动设计的思想。比如电商系统,用户、商品、订单、支付、库存这些是不同的业务领域,职责比较清晰。我会优先拆那些业务独立、变更频繁或者需要单独扩展的模块,比如支付模块调用量大、对安全性要求高,就很适合先独立出来。

拆的时候我会用绞杀者模式,新功能用微服务开发,老功能逐步迁移。先拆一个相对简单的模块试水,把注册中心、配置中心、网关、监控这些基础设施搭起来,等第一个服务跑稳了再陆续拆其他模块,这样风险可控。
数据库拆分是个难点。我会先做逻辑拆分,不同服务访问不同的schema或表,但物理上还在一个库。等服务边界稳定了再做物理拆分,把数据迁到各自的数据库。原来用事务能解决的一致性问题,拆开后可能要用分布式事务或者最终一致性方案,有些强一致的场景我会考虑暂时不拆。

服务间调用方面,原来直接方法调用,拆开后要走网络,我会用Dubbo或Feign做远程调用,加上超时控制、熔断降级。如果调用链路长,还可以用消息队列做异步解耦。
公共代码要么抽成SDK让各服务依赖,要么单独做成基础服务。监控和日志也要升级,用ELK做日志收集,加上链路追踪方便排查问题。
整个过程我会保持小步快跑,每拆一个服务都充分测试,做好灰度发布和回滚预案。我觉得拆分核心是业务驱动,真正能解决单体应用的扩展性和开发效率问题,而不是为了拆而拆。
# 对外提供一个api服务,客户说请求接口超时了,怎么排查?
我第一步会先问清楚基本情况,是偶发还是持续超时?能不能提供请求ID或者具体时间点?是所有客户还是个别客户?这些信息能帮我快速判断问题范围。

然后我会立刻看监控大盘,看接口的响应时间、QPS、错误率有没有异常,同时看服务器的CPU、内存、网络IO这些基础指标有没有打满。如果整体都慢那可能是系统级问题,如果只是个别请求慢就要具体分析。
接下来我会带着请求ID去查日志,看请求链路上哪个环节慢了。是数据库查询慢?还是调用下游服务慢?还是某个业务逻辑耗时长?如果用了分布式追踪工具像Skywalking,能更直观看到每一跳的耗时。
数据库这块我会重点关注,查一下慢查询日志,看有没有SQL走错索引或者连接池满了。如果用了Redis缓存,还要看缓存命中率有没有下降,缓存失效可能导致大量请求打到数据库。
下游依赖也是重灾区,我会确认调用的其他服务有没有故障或者变更。网络层面也要排查,看有没有丢包、DNS解析慢、或者跨地域网络抖动这些问题。

如果最近有过发布,我会特别注意是不是新代码引入的问题,比如加了耗时操作或者配置改错了。还要看JVM有没有频繁Full GC,容器有没有被限流。
找到原因后我会快速处理,该加索引加索引,该扩容扩容,该熔断降级就降级,优先保证服务恢复。处理完后我会做复盘,加监控告警,完善降级策略,避免再次发生。
我觉得排查超时的核心是从全局到局部,先看监控定范围,再查日志找细节,同时要关注数据库、下游依赖、网络、代码变更这几个高频问题点,这样能更快定位问题。
最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

← 分布式面试题 Linux命令面试题 →
