可以在面试时深入探讨的问题集合,涵盖系统设计、架构决策、工程实践等多个维度
问题描述:如何设计一个能够支撑百万级QPS的电商秒杀系统?
考察要点:
参考答案框架:
1. 前端层面
- 静态化:活动页面CDN预热
- 限流:验证码、答题、排队机制
- 防抖:按钮置灰、请求合并
2. 接入层
- Nginx限流(漏桶/令牌桶)
- 黑名单过滤
- 请求合法性校验
3. 服务层
- 缓存预热:Redis缓存商品信息
- 异步处理:MQ削峰填谷
- 限流降级:Sentinel/Hystrix
4. 数据层
- 库存扣减:Redis原子操作 + 数据库最终一致
- 分库分表:按用户ID或商品ID分片
- 读写分离:查询走从库
5. 一致性保障
- 预扣库存:Redis预扣,异步同步DB
- 补偿机制:定时任务对账
- 幂等设计:唯一请求ID
延伸问题:
问题描述:类似bit.ly的短链接服务,如何将长URL映射为短URL?
考察要点:
核心设计:
短码生成方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自增ID+Base62 | 简单、无冲突 | 可预测、有顺序 | 内部系统 |
| 雪花算法 | 趋势递增、分布式 | 较长(12位+) | 高并发 |
| 随机字符串 | 不可预测 | 可能冲突 | 对外服务 |
| MurmurHash | 固定长度、均匀 | 哈希冲突 | 大数据量 |
推荐架构:
用户请求 -> API Gateway -> 短链服务 -> Redis(缓存) -> MySQL(持久化)
|
v
重定向服务 -> 302跳转
问题描述:如何实现微信/钉钉级别的即时通讯系统?
核心挑战:
技术方案:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 客户端 │────▶│ 接入网关 │────▶│ 路由服务 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 长连接管理 │ │ 消息队列 │
└─────────────┘ └─────────────┘
│
┌─────────────┐ ┌───────▼──────┐
│ 消息存储 │◀────│ 业务服务 │
│ (TiDB/HBase)│ └─────────────┘
└─────────────┘
消息ID设计:
uid + seq方式,每个用户维护自己的消息序列号问题描述:在分布式环境下,如何生成全局唯一且趋势递增的ID?
方案对比:
| 方案 | 趋势递增 | 性能 | 依赖 | 适用 |
|---|---|---|---|---|
| UUID | ❌ | 高 | 无 | 日志追踪 |
| 数据库自增 | ✅ | 低 | 数据库 | 单机 |
| 雪花算法 | ✅ | 高 | 无 | 推荐 |
| 号段模式 | ✅ | 极高 | 数据库 | 超高并发 |
雪花算法详解:
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
- └────────────────────时间戳(41bit)──────────────┘ └-机器─┘ └-机房─┘ └─序列号(12bit)─┘
符号位
- 时间戳:约69年
- 机器ID:32台
- 机房ID:32个
- 序列号:每毫秒4096个
时钟回拨问题解决方案:
问题描述:微服务拆多细合适?什么情况下应该合并?
拆分原则:
DDD领域驱动设计:
拆分粒度评估维度:
| 维度 | 过细的问题 | 过粗的问题 |
|---|---|---|
| 团队规模 | 沟通成本高 | 代码冲突多 |
| 部署频率 | 依赖复杂 | 发布风险大 |
| 故障隔离 | 链路长 | 爆炸半径大 |
| 资源成本 | 基础设施开销大 | 无法按需扩缩容 |
决策树:
是否需要独立演进?
├── 是 -> 是否需要独立部署?
│ ├── 是 -> 是否需要独立扩缩容?
│ │ ├── 是 -> 独立服务
│ │ └── 否 -> 共享部署单元
│ └── 否 -> 共享代码库
└── 否 -> 合并到现有服务
问题描述:新项目技术选型,关系型数据库和文档数据库如何选择?
对比分析:
| 场景 | MySQL | MongoDB |
|---|---|---|
| 强事务 | ✅ ACID | ⚠️ 4.0+支持多文档事务 |
| 复杂查询 | ✅ JOIN支持 | ❌ 需冗余设计 |
| 灵活Schema | ❌ 需DDL | ✅ 文档模型 |
| 水平扩展 | ⚠️ 分库分表复杂 | ✅ 原生分片 |
| 地理位置 | ⚠️ 需扩展 | ✅ 内置Geo索引 |
| 全文搜索 | ❌ 需ES | ✅ 内置文本索引 |
选型建议:
问题描述:Cache Aside、Read Through、Write Through、Write Behind如何选择?
策略对比:
| 策略 | 实现复杂度 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| Cache Aside | 低 | 最终一致 | 高 | 通用场景 |
| Read Through | 中 | 最终一致 | 高 | 统一缓存层 |
| Write Through | 中 | 强一致 | 低 | 写少读多 |
| Write Behind | 高 | 弱一致 | 极高 | 高吞吐写入 |
Cache Aside模式详解:
// 读操作
public Object read(String key) {
Object value = cache.get(key);
if (value == null) {
value = db.query(key);
cache.set(key, value, expire);
}
return value;
}
// 写操作
public void write(String key, Object value) {
db.update(key, value);
cache.delete(key); // 删除而非更新
}
缓存一致性保障:
问题描述:服务间通信,什么时候用RPC,什么时候用MQ?
决策矩阵:
| 维度 | RPC同步调用 | MQ异步消息 |
|---|---|---|
| 响应时效 | 实时 | 延迟 |
| 耦合度 | 强耦合 | 松耦合 |
| 可靠性 | 调用失败即失败 | 可重试 |
| 流量削峰 | 不支持 | 支持 |
| 事务一致性 | 强一致 | 最终一致 |
| 调试难度 | 低 | 高 |
使用RPC的场景:
使用MQ的场景:
问题描述:系统上线后如何快速定位问题?可观测性三大支柱如何落地?
三大支柱:
1. Metrics(指标)
黄金指标:
- 延迟(Latency):P50/P95/P99
- 流量(Traffic):QPS/TPS
- 错误(Errors):错误率/错误码分布
- 饱和度(Saturation):CPU/内存/连接数
RED方法:
- Rate:请求速率
- Errors:错误率
- Duration:请求耗时
USE方法:
- Utilization:资源使用率
- Saturation:资源饱和度
- Errors:错误计数
2. Logging(日志)
日志级别规范:
- ERROR:需要立即处理的错误
- WARN:潜在问题,需要关注
- INFO:关键业务流程记录
- DEBUG:调试信息,生产环境关闭
结构化日志:
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "ERROR",
"traceId": "abc123",
"service": "order-service",
"message": "订单创建失败",
"context": {
"orderId": "12345",
"userId": "67890",
"error": "库存不足"
}
}
3. Tracing(链路追踪)
OpenTelemetry标准:
- Trace:完整请求链路
- Span:单个操作单元
- Context:上下文传播
采样策略:
- 头部采样:固定比例
- 尾部采样:基于错误/延迟
- 自适应采样:动态调整
技术栈推荐:
问题描述:作为Reviewer,如何高效地进行代码审查?
审查清单:
功能性:
可读性:
可维护性:
性能:
安全:
Review技巧:
问题描述:从SRE角度,如何构建高稳定性系统?
稳定性建设体系:
1. 防御式编程
// 输入校验
public void processOrder(OrderRequest request) {
Preconditions.checkNotNull(request, "请求不能为空");
Preconditions.checkArgument(request.getAmount() > 0, "金额必须大于0");
// ...
}
// 超时控制
Future<Result> future = executor.submit(task);
try {
Result result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new ServiceException("处理超时");
}
// 降级策略
@HystrixCommand(
fallbackMethod = "getDefaultData",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
}
)
public Data getData() {
// ...
}
2. 容量规划
容量评估公式:
峰值QPS = 日PV × 峰值系数 / 86400
机器数量 = 峰值QPS / 单机QPS × 冗余系数(1.5)
压测指标:
- 单机最大QPS
- 99分位延迟
- 错误率拐点
- 资源瓶颈点
3. 混沌工程
故障注入类型:
- 节点级:杀进程、断网、重启
- 网络级:延迟、丢包、分区
- 应用级:CPU满载、内存溢出、磁盘满
- 依赖级:Mock超时、Mock异常
实验原则:
- 最小爆炸半径
- 可观测可回滚
- 生产环境谨慎
4. 应急响应
On-Call机制:
- 值班轮换
- 告警分级(P0/P1/P2/P3)
- 升级策略
故障处理流程:
1. 发现(监控告警/用户反馈)
2. 止血(降级/限流/回滚)
3. 定位(日志/链路/指标)
4. 修复(热修复/发版)
5. 复盘(5 Whys分析)
问题描述:如何在业务交付和技术债务之间取得平衡?
技术债务分类:
| 类型 | 描述 | 示例 | 处理优先级 |
|---|---|---|---|
| 代码债务 | 代码质量差 | 长方法、重复代码 | 中 |
| 架构债务 | 设计不合理 | 紧耦合、单点故障 | 高 |
| 测试债务 | 测试不足 | 无单元测试 | 高 |
| 文档债务 | 文档缺失 | API文档过时 | 低 |
| 运维债务 | 部署复杂 | 手工部署 | 中 |
管理策略:
1. 债务可视化
2. 偿还计划
20%法则:每个迭代预留20%时间处理技术债务
债务Sprint:每季度安排1个Sprint专门还债
随借随还:修改代码时顺手重构(童子军法则)
3. 预防机制
问题描述:HTTP/2相比HTTP/1.1有哪些改进?HTTP/3又有什么不同?
HTTP/1.1的问题:
HTTP/2核心特性:
1. 二进制分帧
HTTP/1.1: 文本格式
GET / HTTP/1.1\r\n
Host: example.com\r\n\r\n
HTTP/2: 二进制帧
┌─────────────────────────────────────┐
│ Length (24) │ Type (8) │ Flags (8) │
├─────────────────────────────────────┤
│R│ Stream Identifier (31) │
├─────────────────────────────────────┤
│ Payload │
└─────────────────────────────────────┘
2. 多路复用(Multiplexing)
3. 头部压缩(HPACK)
4. 服务器推送
HTTP/3(QUIC):
| 特性 | HTTP/2 | HTTP/3 |
|---|---|---|
| 传输层 | TCP | UDP + QUIC |
| 队头阻塞 | TCP层阻塞 | 彻底解决 |
| 连接迁移 | 不支持 | 支持(连接ID) |
| 握手延迟 | 2-3 RTT | 0-1 RTT |
| 拥塞控制 | 内核实现 | 用户空间 |
QUIC优势:
问题描述:G1和ZGC垃圾收集器有什么区别?如何选择?
垃圾收集器演进:
Serial → Parallel → CMS → G1 → ZGC/Shenandoah
(单线程) (吞吐优先) (低延迟) (平衡) (超低延迟)
G1收集器:
设计目标:在可控停顿时间(默认200ms)内获得最大吞吐量
Region分区:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Eden│ Eden│Survivor│ Old │ Old │ Old │Humongous│
│ │ │ │ │ │ │(大对象) │
└─────┴─────┴────────┴─────┴─────┴─────┴─────────┘
- 将堆划分为多个Region(1-32MB)
- 年轻代和老年代不再固定
- 根据回收价值动态调整
回收过程:
ZGC收集器:
设计目标:TB级堆内存,停顿时间<10ms
核心技术:
1. 染色指针(Colored Pointers)
64位指针结构(Linux x86_64):
┌────────┬────────┬────────┬────────┬────────────────────────┐
│Finalizable│Remapped│Marked1 │Marked0 │ Object Address │
│ (1bit) │ (1bit) │ (1bit) │ (1bit) │ (44bits) │
└────────┴────────┴────────┴────────┴────────────────────────┘
- Marked0/Marked1:标记位
- Remapped:重映射位
- Finalizable:终结器位
2. 读屏障(Load Barrier)
// 伪代码
Object* load_barrier(Object** ptr) {
Object* obj = *ptr;
if (obj & BAD_COLOR_MASK) {
obj = remap_object(obj);
}
return obj;
}
3. 并发整理
选择建议:
| 场景 | 推荐GC | 原因 |
|---|---|---|
| 小内存(<4G) | G1 | 开销低 |
| 大内存(>16G) | ZGC | 低延迟 |
| 低延迟要求(<10ms) | ZGC | 设计目标 |
| 稳定吞吐 | G1 | 成熟稳定 |
问题描述:InnoDB的B+树索引是如何工作的?什么情况下索引会失效?
B+树结构:
[10 | 30 | 50]
/ | \
┌─────────┐ ┌──┴──┐ ┌──┴──┐
[1|5|8] [12|15|18] [35|40|45] [55|60|65]
/ | \ / | \ / | \ / | \
1 5 8 12 15 18 35 40 45 55 60 65
特点:
- 非叶子节点只存键值,不存数据
- 叶子节点包含所有数据,且有序链接
- 树高度低(通常3-4层),IO次数少
聚簇索引 vs 非聚簇索引:
| 特性 | 聚簇索引 | 非聚簇索引(二级索引) |
|---|---|---|
| 数据存储 | 叶子节点存整行数据 | 叶子节点存主键值 |
| 数量 | 只能有1个 | 可以有多个 |
| 查询效率 | 高(直接定位数据) | 可能需要回表 |
| 插入性能 | 需要维护顺序 | 相对独立 |
索引失效场景:
-- 1. 违反最左前缀原则
CREATE INDEX idx_a_b_c ON t(a, b, c);
WHERE b = 1 AND c = 2; -- 失效,缺少a
WHERE a = 1 AND c = 2; -- 部分失效,c不走索引
-- 2. 对索引列做函数运算
WHERE YEAR(create_time) = 2024; -- 失效
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'; -- 有效
-- 3. 隐式类型转换
WHERE phone = 13800138000; -- phone是varchar,失效
WHERE phone = '13800138000'; -- 有效
-- 4. 使用不等于或NOT IN
WHERE status != 0; -- 可能失效(取决于数据分布)
-- 5. LIKE以通配符开头
WHERE name LIKE '%abc'; -- 失效
WHERE name LIKE 'abc%'; -- 有效
-- 6. OR条件使用不当
WHERE a = 1 OR b = 2; -- a、b都有索引才有效
索引优化技巧:
问题描述:Redis为什么快?底层数据结构是怎样的?
Redis快的原因:
底层数据结构:
1. SDS(Simple Dynamic String)
struct sdshdr {
uint32_t len; // 已使用长度
uint32_t alloc; // 分配总长度
char buf[]; // 柔性数组
};
优势:
- O(1)获取长度
- 预分配减少内存重分配
- 二进制安全(可存任意数据)
2. 跳表(Skip List)
Level 3: 1 ──────────────────────────▶ 10
Level 2: 1 ──────────▶ 5 ─────────────▶ 10
Level 1: 1 ─────▶ 3 ──▶ 5 ──▶ 7 ──▶ 9 ──▶ 10
Level 0: 1 ──▶ 2 ──▶ 3 ──▶ 5 ──▶ 7 ──▶ 9 ──▶ 10
- 平均O(logN)查询
- 实现简单,支持范围查询
- 用于Sorted Set
3. 压缩列表(ziplist)
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
- 连续内存存储
- 适合小数据量(<512个元素,每个<64字节)
- 节省内存但有连锁更新问题
4. 字典(hashtable)
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2]; // 两个哈希表,渐进式rehash
int rehashidx;
} dict;
- 链地址法解决冲突
- 渐进式rehash避免阻塞
数据类型与编码:
| 类型 | 编码方式 | 转换阈值 |
|---|---|---|
| String | int / embstr / raw | 44字节 |
| List | ziplist / linkedlist / quicklist | 512元素 |
| Hash | ziplist / hashtable | 512/64 |
| Set | intset / hashtable | 512元素 |
| ZSet | ziplist / skiplist | 128/64 |
问题描述:遇到团队成员或业务方不理解技术方案时,如何有效沟通?
推动策略:
1. 建立共识
2. 降低风险
渐进式推进:
Phase 1: 小范围试点(1个团队/1个功能)
Phase 2: 收集反馈,调整方案
Phase 3: 扩大范围,全面推广
回滚方案:
- 数据备份
- 灰度开关
- 快速回滚机制
3. 有效沟通
4. 处理冲突
技术分歧:
- 原型验证:用代码说话
- A/B测试:数据决策
- 求同存异:非核心问题妥协
资源冲突:
- 优先级排序:重要紧急矩阵
- 资源置换:用其他资源交换
- 分阶段实施:错峰执行
问题描述:作为技术负责人,如何帮助团队成员成长?
培养体系:
1. 能力模型
初级工程师 -> 中级工程师 -> 高级工程师 -> 技术专家 -> 架构师
│ │ │ │ │
执行能力 独立负责 技术攻坚 领域深度 全局视野
代码质量 方案设计 性能优化 技术规划 组织建设
学习成长 跨团队协作 指导他人 影响力 业务理解
2. 培养方式
| 方式 | 适用场景 | 注意事项 |
|---|---|---|
| 导师制 | 新人入职 | 明确目标,定期反馈 |
| Code Review | 日常提升 | 解释Why,而非只给What |
| 技术分享 | 知识传播 | 鼓励输出,而非被动接受 |
| 轮岗锻炼 | 拓展视野 | 给予足够支持 |
| 挑战性任务 | 突破成长 | 风险可控,允许失败 |
3. 1:1沟通
频率:每两周一次,每次30-60分钟
话题:
- 近期工作感受
- 遇到的困难
- 职业规划
- 需要的支持
- 对我的反馈
禁忌:
- 变成工作汇报
- 单向说教
- 只谈问题不谈成长
问题描述:技术完美主义与业务快速迭代如何平衡?
决策框架:
技术债务矩阵:
| 象限 | 特征 | 策略 |
|---|---|---|
| 高业务价值+高技术价值 | 核心架构优化 | 优先投入 |
| 高业务价值+低技术价值 | 紧急业务需求 | 快速交付 |
| 低业务价值+高技术价值 | 技术预研 | 20%时间 |
| 低业务价值+低技术价值 | 技术洁癖 | 拒绝或延后 |
时间分配建议:
理想比例:
- 业务交付:60%
- 技术优化:25%
- 技术预研:15%
实际情况:
- 业务紧急期:80/15/5
- 业务平稳期:50/30/20
- 技术债务期:40/50/10
妥协的艺术:
场景:用户反馈订单查询很慢,偶尔还会超时,如何排查?
排查思路:
Step 1: 确认现象
- 影响范围:全部用户还是部分用户?
- 发生时间:持续还是间歇?
- 错误特征:超时/报错/数据错误?
Step 2: 分层排查
客户端
↓ 网络延迟?
接入层(Nginx/Gateway)
↓ 请求是否到达?
应用层(Order Service)
↓ CPU/内存/线程池?
缓存层(Redis)
↓ 命中率?慢查询?
数据库(MySQL)
↓ 慢SQL?锁等待?
Step 3: 常用命令
# 查看当前连接
mysql> SHOW PROCESSLIST;
mysql> SELECT * FROM information_schema.processlist WHERE command != 'Sleep';
# 查看慢查询
mysql> SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
# 查看锁等待
mysql> SELECT * FROM information_schema.innodb_lock_waits;
mysql> SELECT * FROM information_schema.innodb_trx;
# Redis慢查询
redis> SLOWLOG GET 10
# JVM分析
jstack <pid> > thread_dump.txt
jmap -dump:format=b,file=heap.hprof <pid>
Step 4: 常见原因
场景:接口P99延迟从100ms上升到500ms,如何优化?
优化步骤:
1. 定位瓶颈
工具链:
- Arthas: 方法级耗时分析
- SkyWalking: 链路追踪
- Async-profiler: CPU火焰图
- JProfiler: 内存分析
2. 分层优化
| 层级 | 优化手段 | 预期收益 |
|---|---|---|
| 数据库 | 索引优化、SQL改写、分库分表 | 10-100x |
| 缓存 | 本地缓存、Redis缓存、缓存预热 | 5-50x |
| 应用 | 异步化、批量处理、算法优化 | 2-10x |
| JVM | GC优化、内存分配、JIT预热 | 1.5-3x |
| 网络 | 连接池、序列化优化、压缩 | 1.5-2x |
3. 优化案例
// 优化前:N+1查询
List<Order> orders = orderDao.findByUserId(userId);
for (Order order : orders) {
List<Item> items = itemDao.findByOrderId(order.getId()); // N次查询
}
// 优化后:批量查询
List<Order> orders = orderDao.findByUserId(userId);
List<Long> orderIds = orders.stream().map(Order::getId).collect(toList());
Map<Long, List<Item>> itemMap = itemDao.findByOrderIds(orderIds) // 1次查询
.stream().collect(groupingBy(Item::getOrderId));
场景:团队要做一个新的支付系统,作为架构师如何评审方案?
评审维度:
功能性:
可靠性:
安全性:
性能:
可维护性:
4S分析法:
沟通技巧:
本文档持续更新,如有补充建议欢迎提出。
文档信息