并发Redis
- 来源标题
- 去哪儿面经
- 题目数
- 8
- 已沉淀
- 0
- 来源类型
- interview_experience
- 更新时间
- 2026/04/01 20:04
面经摘要
面经中记录了并发、线程池、Redis 分布式锁、分库分表、接口性能优化和问题排查等题目。
关联题目(8)
面经原题待沉淀Java 并发
volatile 能避免 ABA 问题吗?
并发JavaJava 并发
不能。volatile 只能保证变量的可见性和一定程度上的有序性,不能保证比较并交换过程中值没有经历过“从 A 变成 B 又变回 A”的变化。ABA 问题本质上出现在 CAS 场景里:线程只比较当前值是否还是旧值 A,如果中间被改成过 B 又改回 A,CAS 仍然会成功,但实际数据已经发生过变化,所以 volatile 不能避免 ABA。
查看原文 QA
Question 1: volatile 能避免 ABA 问题吗?
Answer: 不能。volatile 只能保证变量的可见性和一定程度上的有序性,不能保证比较并交换过程中值没有经历过“从 A 变成 B 又变回 A”的变化。ABA 问题本质上出现在 CAS 场景里:线程只比较当前值是否还是旧值 A,如果中间被改成过 B 又改回 A,CAS 仍然会成功,但实际数据已经发生过变化,所以 volatile 不能避免 ABA。
相关题库题
面经原题待沉淀Java 并发
如何避免 ABA 问题?
并发JavaJava 并发
常见做法是给数据增加版本号或时间戳,把“值比较”升级为“值 + 版本”的一起比较。这样即使值又回到了 A,只要版本变了,CAS 就会失败。在 Java 里常用 AtomicStampedReference 或 AtomicMarkableReference 来解决,其中 AtomicStampedReference 最典型,它会在更新引用的同时更新版本号。 如果是在业务场景里,也可以通过以下方式规避: 1. 给记录增加 version 字段,更新时带上 version 做乐观锁控制; 2. 对关键操作加锁,避免并发 CAS 带来的 ABA; 3. 对链表、栈等无锁数据结构,使用带版本的指针或其它安全回收机制配合处理。 所以本质思路就是:不要只比较值本身,而要比较值是否在这段时间内被修改过。
查看原文 QA
Question 2: 如何避免 ABA 问题?
Answer: 常见做法是给数据增加版本号或时间戳,把“值比较”升级为“值 + 版本”的一起比较。这样即使值又回到了 A,只要版本变了,CAS 就会失败。在 Java 里常用 AtomicStampedReference 或 AtomicMarkableReference 来解决,其中 AtomicStampedReference 最典型,它会在更新引用的同时更新版本号。 如果是在业务场景里,也可以通过以下方式规避: 1. 给记录增加 version 字段,更新时带上 version 做乐观锁控制; 2. 对关键操作加锁,避免并发 CAS 带来的 ABA; 3. 对链表、栈等无锁数据结构,使用带版本的指针或其它安全回收机制配合处理。 所以本质思路就是:不要只比较值本身,而要比较值是否在这段时间内被修改过。
相关题库题
面经原题待沉淀Java 并发
动态线程池应该如何配置?
并发系统设计JavaJava 并发Spring
动态线程池的核心不是一开始把参数写死,而是把线程池配置做成可观测、可调整、可回滚。面试里我一般会从“参数设计 + 动态下发 + 监控告警 + 风险控制”几个方面回答。 首先是线程池参数本身,要根据任务类型区分: 1. CPU 密集型任务:核心线程数通常接近 CPU 核数; 2. IO 密集型任务:线程数可以适当放大,但不能无限增加; 3. 队列长度要结合流量峰值和任务耗时评估; 4. 拒绝策略要根据业务选择,比如快速失败、调用方执行、丢弃旧任务等。 其次是动态化配置。通常会把 corePoolSize、maximumPoolSize、keepAliveTime、队列阈值、拒绝策略等参数放到配置中心,比如 Apollo、Nacos、Spring Cloud Config。应用启动时初始化线程池,并监听配置变更事件,在配置更新后调用线程池的 setCorePoolSize、setMaximumPoolSize 等方法动态生效。这里要注意修改顺序,例如最大线程数不能小于核心线程数。 再就是监控和告警。动态线程池如果没有监控,调参很容易把系统调坏。一般要采集这些指标: 1. 当前线程数、活跃线程数; 2. 队列积压长度; 3. 任务执行耗时、拒绝次数; 4. 最大线程数触顶次数; 5. 线程池异常和任务失败率。 通过这些指标判断是该扩容、限流,还是优化业务逻辑。 最后是风险控制。动态调参要设置上下限,避免误操作把线程数调得过大导致 CPU 抖动、上下文切换严重,或者把队列调得过长导致延迟飙升。最好支持灰度发布和回滚;对于核心业务,还要区分不同线程池,避免一个线程池把所有任务拖垮。 总结一下,动态线程池的配置重点不是“线程数设多少”,而是:根据任务类型合理初始化参数,结合配置中心实现动态调整,再通过监控告警和边界保护保证线程池调优是安全可控的。
查看原文 QA
Question 3: 动态线程池应该如何配置?
Answer: 动态线程池的核心不是一开始把参数写死,而是把线程池配置做成可观测、可调整、可回滚。面试里我一般会从“参数设计 + 动态下发 + 监控告警 + 风险控制”几个方面回答。 首先是线程池参数本身,要根据任务类型区分: 1. CPU 密集型任务:核心线程数通常接近 CPU 核数; 2. IO 密集型任务:线程数可以适当放大,但不能无限增加; 3. 队列长度要结合流量峰值和任务耗时评估; 4. 拒绝策略要根据业务选择,比如快速失败、调用方执行、丢弃旧任务等。 其次是动态化配置。通常会把 corePoolSize、maximumPoolSize、keepAliveTime、队列阈值、拒绝策略等参数放到配置中心,比如 Apollo、Nacos、Spring Cloud Config。应用启动时初始化线程池,并监听配置变更事件,在配置更新后调用线程池的 setCorePoolSize、setMaximumPoolSize 等方法动态生效。这里要注意修改顺序,例如最大线程数不能小于核心线程数。 再就是监控和告警。动态线程池如果没有监控,调参很容易把系统调坏。一般要采集这些指标: 1. 当前线程数、活跃线程数; 2. 队列积压长度; 3. 任务执行耗时、拒绝次数; 4. 最大线程数触顶次数; 5. 线程池异常和任务失败率。 通过这些指标判断是该扩容、限流,还是优化业务逻辑。 最后是风险控制。动态调参要设置上下限,避免误操作把线程数调得过大导致 CPU 抖动、上下文切换严重,或者把队列调得过长导致延迟飙升。最好支持灰度发布和回滚;对于核心业务,还要区分不同线程池,避免一个线程池把所有任务拖垮。 总结一下,动态线程池的配置重点不是“线程数设多少”,而是:根据任务类型合理初始化参数,结合配置中心实现动态调整,再通过监控告警和边界保护保证线程池调优是安全可控的。
相关题库题
面经原题待沉淀分布式系统
Redis 分布式锁需要考虑哪些问题?
并发系统设计锁Redis
Redis 分布式锁要重点考虑“加锁是否正确、解锁是否安全、锁是否会过期、业务执行超时怎么办、以及高可用下是否可靠”这几个问题。 第一,必须保证加锁的原子性。通常使用 SET key value NX PX expireTime,一条命令同时完成“只有不存在时才加锁”和“设置过期时间”,避免先 set 再 expire 导致死锁。 第二,必须保证解锁的安全性。不能直接 delete key,因为可能出现线程 A 的锁过期后,线程 B 拿到了同一个锁,这时线程 A 再执行 delete 就会把线程 B 的锁删掉。正确做法是给锁设置唯一标识,比如 UUID,解锁时通过 Lua 脚本原子校验 value 是否还是自己的,再决定是否删除。 第三,要考虑锁过期时间怎么设置。过期时间太短,业务还没执行完锁就释放了;太长,又会影响并发和故障恢复。所以过期时间要结合业务执行时间设置,并预留一定冗余。 第四,要考虑锁续期问题。如果业务执行时间不稳定,可以引入看门狗机制,在持锁线程仍然存活且业务未完成时自动续期,避免任务未完成锁先过期。 第五,要考虑获取锁失败后的策略。是立即失败、重试、自旋等待,还是进入降级逻辑,要根据业务场景决定,避免大量线程无脑重试把 Redis 打挂。 第六,要考虑可重入性、中断响应和公平性。如果业务代码有嵌套加锁需求,可能需要支持可重入;如果是框架级封装,还要考虑线程中断和超时控制。 第七,要考虑 Redis 主从切换或集群场景下的一致性问题。比如主节点刚写入锁还没同步到从节点就宕机,从节点提升为主后可能导致另一个客户端再次加锁成功,出现并发问题。所以 Redis 分布式锁适合做最终一致性要求没那么极端的业务互斥;如果是强一致场景,通常要评估 ZooKeeper、etcd 这类更适合协调的组件。 第八,要考虑业务层的幂等性。分布式锁只能降低并发冲突概率,不能替代业务兜底,关键操作仍然应该有幂等、防重和状态校验。 如果让我落地实现,我会用 SET NX PX + 唯一 value + Lua 安全释放,必要时加续期、重试退避、监控告警,并结合业务幂等一起保障。
查看原文 QA
Question 4: Redis 分布式锁需要考虑哪些问题?
Answer: Redis 分布式锁要重点考虑“加锁是否正确、解锁是否安全、锁是否会过期、业务执行超时怎么办、以及高可用下是否可靠”这几个问题。 第一,必须保证加锁的原子性。通常使用 SET key value NX PX expireTime,一条命令同时完成“只有不存在时才加锁”和“设置过期时间”,避免先 set 再 expire 导致死锁。 第二,必须保证解锁的安全性。不能直接 delete key,因为可能出现线程 A 的锁过期后,线程 B 拿到了同一个锁,这时线程 A 再执行 delete 就会把线程 B 的锁删掉。正确做法是给锁设置唯一标识,比如 UUID,解锁时通过 Lua 脚本原子校验 value 是否还是自己的,再决定是否删除。 第三,要考虑锁过期时间怎么设置。过期时间太短,业务还没执行完锁就释放了;太长,又会影响并发和故障恢复。所以过期时间要结合业务执行时间设置,并预留一定冗余。 第四,要考虑锁续期问题。如果业务执行时间不稳定,可以引入看门狗机制,在持锁线程仍然存活且业务未完成时自动续期,避免任务未完成锁先过期。 第五,要考虑获取锁失败后的策略。是立即失败、重试、自旋等待,还是进入降级逻辑,要根据业务场景决定,避免大量线程无脑重试把 Redis 打挂。 第六,要考虑可重入性、中断响应和公平性。如果业务代码有嵌套加锁需求,可能需要支持可重入;如果是框架级封装,还要考虑线程中断和超时控制。 第七,要考虑 Redis 主从切换或集群场景下的一致性问题。比如主节点刚写入锁还没同步到从节点就宕机,从节点提升为主后可能导致另一个客户端再次加锁成功,出现并发问题。所以 Redis 分布式锁适合做最终一致性要求没那么极端的业务互斥;如果是强一致场景,通常要评估 ZooKeeper、etcd 这类更适合协调的组件。 第八,要考虑业务层的幂等性。分布式锁只能降低并发冲突概率,不能替代业务兜底,关键操作仍然应该有幂等、防重和状态校验。 如果让我落地实现,我会用 SET NX PX + 唯一 value + Lua 安全释放,必要时加续期、重试退避、监控告警,并结合业务幂等一起保障。
相关题库题
面经原题待沉淀Java 并发
你在项目中是怎么配置线程池的?
并发系统设计JavaJava 并发MySQLRedis
在项目里我一般不会只用默认线程池,而是按业务场景拆分线程池,并根据任务类型来配置。 首先,我会先区分任务是 CPU 密集型还是 IO 密集型。CPU 密集型任务,比如复杂计算、规则判断,线程数通常设置为接近 CPU 核数,避免过多线程切换;IO 密集型任务,比如调用下游接口、查库、发消息,线程数会适当放大,因为线程会有较多阻塞时间。 其次,我会按业务隔离线程池,而不是所有异步任务共用一个线程池。比如下单、库存、消息推送、报表任务会拆开配置,这样某一个业务积压时不会拖垮其它业务。 参数上我通常重点看这几个: 1. corePoolSize:根据机器核数和任务类型设置基础并发; 2. maximumPoolSize:设置线程池扩容上限,防止流量突增时扛不住; 3. workQueue:根据任务是否允许堆积来选队列和容量,核心链路不会无限堆积; 4. keepAliveTime:控制非核心线程空闲回收; 5. RejectedExecutionHandler:根据业务选择拒绝策略,核心链路更倾向于快速失败、降级或限流,而不是无脑堆积。 另外我会配套监控,关注活跃线程数、队列长度、任务耗时、拒绝次数等指标。如果发现队列长期积压,我不会只靠加线程解决,而是先判断瓶颈到底在 CPU、数据库、Redis 还是下游接口。 在线上落地时,我通常还会把线程池参数接入配置中心,支持动态调整,并限制上下限,防止误调。线程池名称、线程工厂、异常日志、MDC 透传这些细节也会处理,方便排查问题。 总结来说,我的配置思路是:按业务隔离、按任务类型定参数、根据容量评估设置队列和拒绝策略,再用监控和动态配置持续调优。
查看原文 QA
Question 5: 你在项目中是怎么配置线程池的?
Answer: 在项目里我一般不会只用默认线程池,而是按业务场景拆分线程池,并根据任务类型来配置。 首先,我会先区分任务是 CPU 密集型还是 IO 密集型。CPU 密集型任务,比如复杂计算、规则判断,线程数通常设置为接近 CPU 核数,避免过多线程切换;IO 密集型任务,比如调用下游接口、查库、发消息,线程数会适当放大,因为线程会有较多阻塞时间。 其次,我会按业务隔离线程池,而不是所有异步任务共用一个线程池。比如下单、库存、消息推送、报表任务会拆开配置,这样某一个业务积压时不会拖垮其它业务。 参数上我通常重点看这几个: 1. corePoolSize:根据机器核数和任务类型设置基础并发; 2. maximumPoolSize:设置线程池扩容上限,防止流量突增时扛不住; 3. workQueue:根据任务是否允许堆积来选队列和容量,核心链路不会无限堆积; 4. keepAliveTime:控制非核心线程空闲回收; 5. RejectedExecutionHandler:根据业务选择拒绝策略,核心链路更倾向于快速失败、降级或限流,而不是无脑堆积。 另外我会配套监控,关注活跃线程数、队列长度、任务耗时、拒绝次数等指标。如果发现队列长期积压,我不会只靠加线程解决,而是先判断瓶颈到底在 CPU、数据库、Redis 还是下游接口。 在线上落地时,我通常还会把线程池参数接入配置中心,支持动态调整,并限制上下限,防止误调。线程池名称、线程工厂、异常日志、MDC 透传这些细节也会处理,方便排查问题。 总结来说,我的配置思路是:按业务隔离、按任务类型定参数、根据容量评估设置队列和拒绝策略,再用监控和动态配置持续调优。
相关题库题
面经原题待沉淀数据库
一个 1800 万订单库、日增 30 万、查询性能差,怎么做分库分表?
并发系统设计MySQL
我会先判断是不是真的需要分库分表,因为查询性能差不一定是数据量本身导致的,也可能是索引不合理、SQL 写法有问题、冷热数据混在一起、分页方式低效,或者数据库实例本身资源不足。1800 万订单数据量不算特别夸张,但如果是高并发订单系统、查询维度复杂、写入持续增长,确实可以评估分库分表。 我的思路一般是这样: 第一步,先做现状诊断。 1. 看核心慢 SQL,确认是单表过大、索引失效、回表过多,还是排序分页导致的性能问题; 2. 看业务访问模式,是按订单号查得多,还是按用户、商家、时间范围查得多; 3. 看写入压力和未来增长趋势,日增 30 万意味着一年大概新增上亿级,提前做拆分是有必要的。 第二步,确定拆分目标。 订单表通常既有写入压力,也有查询压力,而且查询维度比较多,所以一般要优先保证核心查询路径,比如按订单号查详情、按用户查订单列表、按时间查近几个月订单。 第三步,设计分片键。 订单系统里最常见的分片方式有两类: 1. 按 user_id 分片:适合“查某个用户的订单列表”这类高频场景,用户维度查询命中单库单表; 2. 按 order_id 分片:适合按订单号查询,但用户维度查询可能要额外设计索引表。 如果系统用户侧查询很多,我会优先考虑按 user_id 分库分表;同时保证 order_id 全局唯一,并建立 order_id 到分片路由的能力。如果订单号查询也很多,可以增加一张订单路由表,或者在订单号编码里带上分片信息。 第四步,分库分表方案。 例如可以先按 user_id 做 4 库 16 表,或者 8 库 32 表,具体要结合当前 QPS、单表大小、未来 2 到 3 年增长来定。分片数不要只看当前规模,还要预留扩容空间。表结构和索引在各个分片里保持一致。 第五步,配套改造。 1. 引入分库分表中间件或框架,比如 ShardingSphere,统一路由规则; 2. 主键生成要改成分布式 ID,如雪花算法,避免自增主键在多库下冲突; 3. 事务上尽量避免跨库事务,把业务拆成最终一致; 4. 分页查询避免深分页,优先用基于游标或覆盖索引的方式; 5. 对跨分片统计、后台报表这类场景,尽量走离线数仓、ES 或汇总表,而不是实时扫多个分片。 第六步,冷热数据治理。 订单通常强时效查询集中在近几个月,历史订单访问频率低。可以按时间做归档,把历史数据迁到历史库或冷库,线上只保留热点数据,这往往比一上来就大规模分库分表更有效。 第七步,迁移方案。 不能直接一次性切换,通常会双写或灰度迁移: 1. 先建新分片结构; 2. 开发路由层; 3. 老数据批量迁移; 4. 新老链路双写并校验; 5. 灰度放量读取新库; 6. 完成切流后下线旧库。 如果面试官问我的结论,我会说:这类订单库我会先做 SQL 和索引诊断,再根据核心查询路径选择 user_id 或 order_id 作为分片键,结合分布式 ID、路由、历史归档和灰度迁移方案推进分库分表,而不是只回答“按某个字段拆”这么简单。
查看原文 QA
Question 6: 一个 1800 万订单库、日增 30 万、查询性能差,怎么做分库分表?
Answer: 我会先判断是不是真的需要分库分表,因为查询性能差不一定是数据量本身导致的,也可能是索引不合理、SQL 写法有问题、冷热数据混在一起、分页方式低效,或者数据库实例本身资源不足。1800 万订单数据量不算特别夸张,但如果是高并发订单系统、查询维度复杂、写入持续增长,确实可以评估分库分表。 我的思路一般是这样: 第一步,先做现状诊断。 1. 看核心慢 SQL,确认是单表过大、索引失效、回表过多,还是排序分页导致的性能问题; 2. 看业务访问模式,是按订单号查得多,还是按用户、商家、时间范围查得多; 3. 看写入压力和未来增长趋势,日增 30 万意味着一年大概新增上亿级,提前做拆分是有必要的。 第二步,确定拆分目标。 订单表通常既有写入压力,也有查询压力,而且查询维度比较多,所以一般要优先保证核心查询路径,比如按订单号查详情、按用户查订单列表、按时间查近几个月订单。 第三步,设计分片键。 订单系统里最常见的分片方式有两类: 1. 按 user_id 分片:适合“查某个用户的订单列表”这类高频场景,用户维度查询命中单库单表; 2. 按 order_id 分片:适合按订单号查询,但用户维度查询可能要额外设计索引表。 如果系统用户侧查询很多,我会优先考虑按 user_id 分库分表;同时保证 order_id 全局唯一,并建立 order_id 到分片路由的能力。如果订单号查询也很多,可以增加一张订单路由表,或者在订单号编码里带上分片信息。 第四步,分库分表方案。 例如可以先按 user_id 做 4 库 16 表,或者 8 库 32 表,具体要结合当前 QPS、单表大小、未来 2 到 3 年增长来定。分片数不要只看当前规模,还要预留扩容空间。表结构和索引在各个分片里保持一致。 第五步,配套改造。 1. 引入分库分表中间件或框架,比如 ShardingSphere,统一路由规则; 2. 主键生成要改成分布式 ID,如雪花算法,避免自增主键在多库下冲突; 3. 事务上尽量避免跨库事务,把业务拆成最终一致; 4. 分页查询避免深分页,优先用基于游标或覆盖索引的方式; 5. 对跨分片统计、后台报表这类场景,尽量走离线数仓、ES 或汇总表,而不是实时扫多个分片。 第六步,冷热数据治理。 订单通常强时效查询集中在近几个月,历史订单访问频率低。可以按时间做归档,把历史数据迁到历史库或冷库,线上只保留热点数据,这往往比一上来就大规模分库分表更有效。 第七步,迁移方案。 不能直接一次性切换,通常会双写或灰度迁移: 1. 先建新分片结构; 2. 开发路由层; 3. 老数据批量迁移; 4. 新老链路双写并校验; 5. 灰度放量读取新库; 6. 完成切流后下线旧库。 如果面试官问我的结论,我会说:这类订单库我会先做 SQL 和索引诊断,再根据核心查询路径选择 user_id 或 order_id 作为分片键,结合分布式 ID、路由、历史归档和灰度迁移方案推进分库分表,而不是只回答“按某个字段拆”这么简单。
相关题库题
面经原题待沉淀系统设计
接口性能从 1 秒优化到 200 毫秒后,怎么进一步提升?
并发系统设计MySQL网络Redis
已经从 1 秒优化到 200 毫秒,说明第一轮大头问题已经解决了,下一步不能盲目继续优化,而是要先拆清楚这 200 毫秒到底花在哪,然后按收益最高的方向继续做。 我一般会这样回答: 第一,先做精细化分析,定位剩余耗时。 把接口耗时拆成几段:参数处理、业务计算、数据库查询、Redis 访问、远程调用、序列化反序列化、网络传输等,最好通过链路追踪、APM、日志埋点把各阶段耗时打出来。只有知道瓶颈在哪,才能继续优化。 第二,优先看是不是还能减少调用次数。 很多接口慢,不一定是单次调用慢,而是调用链太长、重复查数据太多。可以考虑: 1. 合并数据库查询,减少 N+1 查询; 2. 合并远程调用,避免串行调用多个下游; 3. 去掉重复计算和重复查询; 4. 前置过滤无效逻辑,减少不必要处理。 第三,能并行的地方尽量并行。 如果接口内部有多个彼此独立的查询或远程调用,可以改成异步并发执行,再汇总结果。但要注意线程池隔离、超时控制和异常兜底,避免为了追求快把系统稳定性搞差。 第四,用缓存换时间。 如果接口结果有明显热点,或者部分数据变化不频繁,可以把热点数据放到本地缓存或 Redis,减少数据库和下游服务压力。这里要注意缓存一致性、穿透、击穿和雪崩问题。 第五,继续优化数据库和索引。 如果剩余耗时主要在 DB,可以继续看: 1. 是否命中了合适索引; 2. 是否只查需要的字段,避免 select *; 3. 是否存在深分页、排序、回表问题; 4. 是否可以做覆盖索引、联合索引或数据预聚合。 第六,做数据预处理。 对于复杂聚合或实时计算型接口,可以考虑提前计算、异步化、物化视图、汇总表,或者把部分非实时逻辑迁到离线任务,避免请求线程现场做重活。 第七,关注序列化和网络开销。 当业务逻辑已经很快时,JSON 序列化、大对象传输、字段过多、压缩与解压、网关转发等也可能成为瓶颈。这时可以精简返回字段、减少报文体积、优化协议和调用路径。 第八,评估收益和成本。 200 毫秒通常已经不错,继续往下压可能进入边际收益递减阶段。所以要结合业务目标看是追求 P50、P95 还是 P99,是为了用户体验、吞吐量还是成本优化。很多时候与其追求 200 毫秒到 150 毫秒,不如优先提升稳定性、降低超时率和抖动。 如果让我总结,我会说:先通过监控把剩余 200 毫秒拆开,找到最主要瓶颈,然后从减少调用、并行化、缓存、SQL 优化、预计算和网络开销几个方向继续优化,并且结合业务收益决定是否值得继续投入。
查看原文 QA
Question 7: 接口性能从 1 秒优化到 200 毫秒后,怎么进一步提升?
Answer: 已经从 1 秒优化到 200 毫秒,说明第一轮大头问题已经解决了,下一步不能盲目继续优化,而是要先拆清楚这 200 毫秒到底花在哪,然后按收益最高的方向继续做。 我一般会这样回答: 第一,先做精细化分析,定位剩余耗时。 把接口耗时拆成几段:参数处理、业务计算、数据库查询、Redis 访问、远程调用、序列化反序列化、网络传输等,最好通过链路追踪、APM、日志埋点把各阶段耗时打出来。只有知道瓶颈在哪,才能继续优化。 第二,优先看是不是还能减少调用次数。 很多接口慢,不一定是单次调用慢,而是调用链太长、重复查数据太多。可以考虑: 1. 合并数据库查询,减少 N+1 查询; 2. 合并远程调用,避免串行调用多个下游; 3. 去掉重复计算和重复查询; 4. 前置过滤无效逻辑,减少不必要处理。 第三,能并行的地方尽量并行。 如果接口内部有多个彼此独立的查询或远程调用,可以改成异步并发执行,再汇总结果。但要注意线程池隔离、超时控制和异常兜底,避免为了追求快把系统稳定性搞差。 第四,用缓存换时间。 如果接口结果有明显热点,或者部分数据变化不频繁,可以把热点数据放到本地缓存或 Redis,减少数据库和下游服务压力。这里要注意缓存一致性、穿透、击穿和雪崩问题。 第五,继续优化数据库和索引。 如果剩余耗时主要在 DB,可以继续看: 1. 是否命中了合适索引; 2. 是否只查需要的字段,避免 select *; 3. 是否存在深分页、排序、回表问题; 4. 是否可以做覆盖索引、联合索引或数据预聚合。 第六,做数据预处理。 对于复杂聚合或实时计算型接口,可以考虑提前计算、异步化、物化视图、汇总表,或者把部分非实时逻辑迁到离线任务,避免请求线程现场做重活。 第七,关注序列化和网络开销。 当业务逻辑已经很快时,JSON 序列化、大对象传输、字段过多、压缩与解压、网关转发等也可能成为瓶颈。这时可以精简返回字段、减少报文体积、优化协议和调用路径。 第八,评估收益和成本。 200 毫秒通常已经不错,继续往下压可能进入边际收益递减阶段。所以要结合业务目标看是追求 P50、P95 还是 P99,是为了用户体验、吞吐量还是成本优化。很多时候与其追求 200 毫秒到 150 毫秒,不如优先提升稳定性、降低超时率和抖动。 如果让我总结,我会说:先通过监控把剩余 200 毫秒拆开,找到最主要瓶颈,然后从减少调用、并行化、缓存、SQL 优化、预计算和网络开销几个方向继续优化,并且结合业务收益决定是否值得继续投入。
相关题库题
面经原题待沉淀系统设计
举一个你工作中排查问题的具体案例。
系统设计MySQLRedis
我可以举一个线上接口偶发超时的排查案例。 当时现象是某个核心查询接口平时 RT 大约在 100 到 200 毫秒,但高峰期会偶发升到 2 到 3 秒,用户侧有明显感知。我的排查思路是按“先确认范围,再逐层定位瓶颈”的方式进行。 第一步,我先看监控面板,确认问题是不是全局性的。结果发现应用 CPU 并不高,Full GC 也没有异常,说明不是 JVM 或机器资源先出的问题;但接口 RT 和超时数在高峰期明显上升。 第二步,我看链路日志和 APM,拆分接口耗时。发现应用本身业务计算只占很小一部分,主要时间消耗在数据库查询上。 第三步,我把慢 SQL 拉出来做分析,发现其中一条按条件分页查询的 SQL 在数据量上来后开始明显变慢,而且执行计划显示没有很好命中预期索引,存在回表和扫描行数过多的问题。 第四步,我结合业务场景看 SQL 写法,发现查询条件里有一个选择性不高的字段放在前面,联合索引设计不合理,同时分页还用了较深的 offset,导致越往后翻页越慢。 第五步,我做了几个优化: 1. 按实际查询条件重建联合索引; 2. 将 select * 改成只查必要字段; 3. 把深分页改成基于游标或上次最大 ID 的翻页方式; 4. 对热点数据增加 Redis 缓存,减少重复查询。 第六步,优化后我做了压测和灰度验证。结果接口 RT 从高峰期的秒级下降到 200 毫秒以内,慢 SQL 数量明显下降,数据库 CPU 和连接压力也回落了。 最后我还补了两个长期措施: 1. 给这个接口加了更细的耗时埋点和慢查询告警; 2. 在需求评审阶段把分页方式和索引设计纳入检查项,避免同类问题再次出现。 这个案例里我的重点不是“猜哪里有问题”,而是通过监控、链路、慢 SQL、执行计划逐层收敛,最后把问题定位到 SQL 和索引设计上,再通过索引优化、分页优化和缓存组合解决。
查看原文 QA
Question 8: 举一个你工作中排查问题的具体案例。
Answer: 我可以举一个线上接口偶发超时的排查案例。 当时现象是某个核心查询接口平时 RT 大约在 100 到 200 毫秒,但高峰期会偶发升到 2 到 3 秒,用户侧有明显感知。我的排查思路是按“先确认范围,再逐层定位瓶颈”的方式进行。 第一步,我先看监控面板,确认问题是不是全局性的。结果发现应用 CPU 并不高,Full GC 也没有异常,说明不是 JVM 或机器资源先出的问题;但接口 RT 和超时数在高峰期明显上升。 第二步,我看链路日志和 APM,拆分接口耗时。发现应用本身业务计算只占很小一部分,主要时间消耗在数据库查询上。 第三步,我把慢 SQL 拉出来做分析,发现其中一条按条件分页查询的 SQL 在数据量上来后开始明显变慢,而且执行计划显示没有很好命中预期索引,存在回表和扫描行数过多的问题。 第四步,我结合业务场景看 SQL 写法,发现查询条件里有一个选择性不高的字段放在前面,联合索引设计不合理,同时分页还用了较深的 offset,导致越往后翻页越慢。 第五步,我做了几个优化: 1. 按实际查询条件重建联合索引; 2. 将 select * 改成只查必要字段; 3. 把深分页改成基于游标或上次最大 ID 的翻页方式; 4. 对热点数据增加 Redis 缓存,减少重复查询。 第六步,优化后我做了压测和灰度验证。结果接口 RT 从高峰期的秒级下降到 200 毫秒以内,慢 SQL 数量明显下降,数据库 CPU 和连接压力也回落了。 最后我还补了两个长期措施: 1. 给这个接口加了更细的耗时埋点和慢查询告警; 2. 在需求评审阶段把分页方式和索引设计纳入检查项,避免同类问题再次出现。 这个案例里我的重点不是“猜哪里有问题”,而是通过监控、链路、慢 SQL、执行计划逐层收敛,最后把问题定位到 SQL 和索引设计上,再通过索引优化、分页优化和缓存组合解决。
相关题库题
来源原文
## 八股
1. volatile 修饰的关键词能不能避免 aba
2. 那如何避免 aba
3. 动态线程池如何配置
4. redis 分布式锁应该考虑哪些问题
## 场景题
1. 你在项目中是怎么配置线程池的
2. 一个 1800w 的订单库,日增 30w,查询性能差,怎么做分库分表
3. 接口性能从 1s 优化到 200ms 后,怎么进一步提高速度
4. 举一个工作中排查 case 的具体例子