场景题
当线上出现问题时,怎么做
✅线上出现问题时优先止血,防止对公司造成亏损,使用GIT版本控制回退到上一个稳定的版本,确保线上环境能为用户持续服务。
✅记录下出现问题的时间,之后结合监控指标$+$日志(traceID)$+$链路追踪,沿着网关$\longrightarrow$服务$\longrightarrow$DB/Redis/MQ/第三方逐层排查,确认是代码问题还是配置的问题
✅修复后持续观察一段时间,查看错误率是否下降并稳定,P99是否下降并稳定。最后记录好这起事故,将监控告警、降级开关、压测补齐,避免再次发生
P99
**P99(99th percentile,99 分位数)**是一个性能指标,表示:
在所有请求里,99% 的请求耗时都 ≤ P99 这个值
只有最慢的 1% 会比它更慢。用一个很生活的例子 🌰
假设你统计 100 次接口耗时,按从快到慢排序:
排序前:【100ms,100ms,…,300ms,200ms,200ms】
排序后:【100ms,100ms,…,200ms,200ms,300ms】
- 第 99 个请求的耗时 = 200ms
那 P99 = 200ms
意思是:100 次里有 99 次都不超过 200ms,只有最慢那 1 次可能更慢。
监控指标+日志(traceID)+链路追踪
✅监控指标
用来快速判断:是服务挂了?变慢了?流量暴涨了?
网关层:QPS、4xx/5xx 错误率、P99 延迟
服务层:CPU/内存、GC 次数与耗时、线程池活跃数/队列长度
依赖层:
- MySQL:连接数、慢 SQL 数、锁等待
- Redis:命令耗时、超时数、命中率
- RocketMQ:Topic 堆积量、消费延迟、消费失败次数
✅日志(traceID)
日志是文本细节,能定位到具体接口、具体参数、具体异常堆栈。
一个请求从网关进来,会带一个唯一 ID:
traceId=abc123
这个请求经过 gateway → order-service → pay-service → device-service → MQ consumer……每一段日志都会打印同一个 traceId(或传递新的 spanId),这样你能把一条请求的全链路日志串起来。
✅链路追踪
链路追踪是把一次请求拆成多个 span(网关、服务、DB、Redis、MQ…),每段都有耗时。
货柜里的例子 🌰
“下单接口变慢”,链路追踪一看:
- gateway → order-service:5ms
- order-service → Redis:2ms
- order-service → MySQL:800ms(慢点出现)
- 或者 order-service → account-service:timeout 3s
你立刻就知道该去查 MySQL 慢 SQL / 锁等待,还是查下游服务超时。
✅三个放一起怎么用?
指标先定位是哪一层出问题(网关/服务/依赖) →
用 traceId 日志抓住具体失败请求看异常 →
用 链路追踪确定慢在哪一段(DB/Redis/Feign/MQ)并验证修复效果。
4xx/5xx错误
✅4xx基本是:请求发出去了,但“请求本身不对”或“权限/频率不允许”。
- 401 Unauthorized(没登录/ token 失效)
前端:看看 Authorization 是否为空、是否过期、是否刷新 token 失败
- 403 Forbidden(有 token 但没权限)
多是后端鉴权/角色权限问题、或者资源归属不对(A 用户访问 B 的资源)
- 404 Not Found(路径不对/网关路由没配/环境不一致)
- 400 / 422(参数校验失败、格式不对)
字段名、类型、必填缺失、时间格式、枚举值不对
- 429 Too Many Requests(限流)
说明触发了网关/接口限流
✅5xx 基本是:请求没问题,但“服务端处理炸了”或“网关/上游依赖炸了”。
- 500(后端代码异常)
前端能做的:
- 把请求参数、响应体、时间点、用户信息、traceId(如果返回头有)收集好
- 让后端用 traceId 查日志栈:NullPointer、SQL 异常、序列化异常等
- 502 Bad Gateway(网关拿不到上游正常响应)
常见原因:
- 上游服务挂了/没实例
- 网关到上游网络不通
- 503 Service Unavailable(服务不可用/被摘流量/熔断)
服务重启、发布中、熔断、容器不可用
- 504 Gateway Timeout(上游太慢超时)
下游慢:DB 慢 SQL、锁等待、Redis 超时、MQ 堆积导致处理慢
处理方式:设置合理的timeout
大量数据导入 Mysql 失效排查
✅首先看是插入失败还是插入超时?
- 插入失败:比如唯一键冲突、字段类型不匹配、外键约束等
- 插入超时:长时间无响应、连接超时、锁等待
✅超时解决办法
超时往往和单次写入量过大、单事务过长有关
采用分批插入降低单次压力,例如每批从5000条降到1000条,并在每批结束后提交
✅报错的核心原因
大量导入常见是大事务导致:
- undo log 膨胀(回滚段变大)
- 事务持锁时间变长,引发等待/超时
- 资源占用上升,甚至影响整体性能
MySQL调优
框架
以下内容是挡在MySQL前面的,也就是说读取数据的时候先走它们,再走MySQL
- 读写分离
- 主从复制
- Redis缓存
- Java中的数据结构:在项目启动的时候,先从数据库里查询常用的数据存储到,如List,Map中,使用的时候直接获取
- 数据量大——>TIDB,数据量更大——>分库分表(数据量大的时候分表,并发高的时候分库)
硬件
- 内存大于32g
MySQL存在 buffer pool ,对于数据修改,先修改内存中的数据,再将内存中的数据刷入磁盘中
buffer pool 占MySQL内存的 75% ~ 80% 之间
- CPU
MySQL的连接数(Druid连接池),最优情况下:CPU核心数 * 2(如果是固态硬盘还可以 + 1)
SQL优化
- 禁止使用 Select *
会读取多余字段
失去覆盖索引的优化机会,查询缓慢
- 共享数据
比如在一个业务中两个接口都同时查询一条数据,并且A接口要调用B接口,此时可以只在B接口查询,将结果通过参数传递给A
- 深分页的问题如何解决?
什么是深分页?
使用如下SQL语句查询满足update_time条件的数据,跳过前100000条,取出10条数据
1 select * from account where update_time >= '2025-09-23 12:30:59' order by update_time limit 100000, 10;由于这条SQL语句执行顺序是 from -> where -> select -> order by -> limit,在分页之前,比如满足 update_time >= ‘2025-09-23 12:30:59’ 的数据有80万条,此时就会触发80万次回表查询,再将查询到的结果进行limit分页,这样做很耗时间。
【解决方法】
- 选择上 / 下一页
【注意】id需要为整形,并且自增的
前端需要将最后一条数据的id发过来,以及查询的条数就可以加快查询了
1 | select * from account where id >= 100000 limit 10; |
- 手动选择第 N 页
子查询优化
【注意】需要一个查询条件,且有索引(update_time)
1 | select * from (select id from account o where o.update_time >= '2025-09-23 12:30:59' order by o.update_time limit 100000, 10) as temp inner join account on temp.id = account.id; |
在子查询中通过二级索引查找满足条件的10条数据,不用进行回表查询
在连表查询中,通过匹配 id 获取满足条件的10条数据
此时只需要经历10次回表查询
- 更新什么就改什么,而不是将全部字段update回去
update的时候,将一条数据所有信息查询出来放在一个对象中,只更新其中一个数据,再将整个对象update到数据库中,在高并发场景下,会造成数据不同步问题
比如,我第一个方法查出一条数据所有信息为a = 1,b = 1,我只修改b的值,a = 1, b = 2;这个时候另一个方法也查出这条数据的所有信息为a = 1,b = 1,此时它只修改a的值为a = 2,b = 1;第一个方法先update,将b的值改变;
第二个方法再update,a的值改变,同时将b的值恢复,这就会造成数据不同步
- 日期类型选择
- datetime:8字节(占用大)
- timestamp:4字节(可以显示1970-1-1到2038的时间戳,推荐)
- data:3字节(显示时间不完全)
- 字符类型选择
- 数字范围很小用tinyint占1个字节,相比较于int占4个字节
- 如果确定字符数量长度,用char(1)性别、char(11)电话号码
系统配置
- Redo log刷盘策略,
innodb_flush_log_at_trx_commit取值(0/1/2)
- 0:每隔一秒把log buffer刷到文件系统中
- 1:每次事务提交的时候,都把log buffer刷到文件系统中,立即刷新到磁盘上去
- 2:每次事务提交的时候,都把log buffer刷到系统文件中,但不会立即写入磁盘
- bin log刷盘策略,
sync_binlog,取值(0/1/N)
- 0:依赖操作系统定期同步,性能最佳,但崩溃时可能丢失多个事务
- 1:每次事务提交后同步,确保崩溃时最多丢失一个事务,安全性最高,但性能耗损最大
- N:每提交N个事务后同步一次,平衡性能和数据安全
慢SQL查询日志
- slow_query_log
- explain字段
1 | explain select * from orders where status = '1'; |
看查询语句的效率,有如下几个参数
- key:实际使用的索引名,如果是NULL就没有索引
- key_len:使用索引的长度
- rows:预计扫描的行数
- type:访问类型
访问层级:
System > Const > Eq_ref > Ref > Range > Index > All
- System:表中只有一行数据,直接返回
- const:查询命中主键或唯一索引的全部列,性能非常好,只会读取一次索引
比如
select name from user where id = 3;
- eq_ref:只会出现在多表连接(JOIN)时,而且是被驱动表(也叫第二张表、右表)用主键或唯一索引进行等值匹配,并且这些索引列都不能为 NULL。
比如
Select * from user join orders on user.id = orders.user_id;o.user_id是唯一索引
ref:查询通过非唯一索引(普通 KEY/INDEX)、唯一索引的非全部列,或联合索引的最左前缀列做等值匹配(
=条件)range:索引范围查找,条件是 <, >, between, like ‘abc%’ 等,使用索引列的范围条件(遵守最左前缀原则)
比如:
SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';
- index:扫描整个索引树,而不是表的所有行,查询只用到索引列(覆盖索引),不必回表
比如:
select id from orders;,如果id是主键,这就是index扫描
- all:没有任何可用索引,直接从磁盘扫描
- Extra:额外信息
- Using where:查询走了索引,但是仍要在取出的数据上做一次where查询
使用了索引跳跃,在索引层做了条件判断
1
2
3
4
5
6
7
8
9
10 user (
id primary key,
name,
age,
index idx_age(age)
)
select * from user where age > 20 and name = 'tom';
# age走了索引,name = 'tom'取出数据后过滤 —> Using where
- Using index:使用了覆盖索引
1
2
3
4
5
6
7
8 user (
id primary key,
name,
age,
index idx_name(name)
)
select name from user where name = 'jasonqian';
- Using where; Using index;
1
2
3
4 select age, name from user where age > 20
# 索引idx_age_name覆盖了age和name —> Using index
# 条件age > 20需要范围扫描,进行过滤 —> Using where
- Using filesort:排序没法完全靠索引顺序完成,必须额外做排序操作
1
2
3
4
5
6
7
8 # 触发场景
# order by 列没有用上索引
index idx_age(age)
select * from user order by name;
# order by 列不符合最左前缀原则
index idx_name_age(name, age)
select * from user order by age
索引
- 最左前缀原则
- 索引覆盖
- 索引下推
- 主键索引尽量使用自增主键,减少页分裂
因为InnoDB索引底层数据结构为B+树,它需要保证内部有序,通过主键自增的方式,让b+树增加新的数据时,永远加在最后一个叶子结点尾部
- 不使用like
- 版本8.0之前,在使用order by的时候,不要既使用asc,也用desc
在8.0之前,索引排序方向必须一致,一旦order by混用了asc和desc,没法用B+树索引顺序,只能走filesort
- 索引列不做函数运算、不计算
- or两边其中一个没有索引就会导致全表扫描
在第一个事务的update中,如果where 后面的字段没有索引,会导致第二个事务update锁表吗?
第一个事务中:update table set name = 'tmo' where num = 1;
第二个事务中:update table set name = 'tmo' where num = 1;
这要分情况:
- 如果是读已提交RC隔离级别下,不会锁表;
- 如果是可重复读RR隔离级别下,会锁表
JVM调优
✅做过。
-
最初设备就100台
-
后来设备越来越多,到了600台,通过排查(使用Leader搭建的可视化工具检查dump文件)发现持续发心跳 发重量 会导致通讯服务中的 Minor GC 非常频繁,而且晋升到老年代的速率很高(Survivor 装不下导致提前晋升),老年代增长很快,进而触发 Full GC。
-
CPU处理速度变慢或导致频繁的full GC
✅怎么解决?
-
首先把每个通信服务我们最多让它连接300台~500台设备,新增的设备连到新的通信服务上。
-
通信服务做了集群,一共5台,缓存记录了每台设备连接的是哪些通信服务
-
正常情况下,老年代 : 新生代 = 2:1 将其调成 老年代:新生代 = 1:1。这是因为我们的对象大多是短命的,把 新生代 做大能降低 Minor GC 频率,让短命对象在 Eden 直接死掉,减少 GC 压力。
-XX:NewRatio=2代表 老年代:新生代 = 2:1(默认常见值之一)
-XX:NewRatio=1代表 老年代:新生代 = 1:1
- 再将新生代的 Eden : Survivor : Survivor = 8 : 1 : 1 改成 Eden : Survivor : Survivor = 3 : 1 : 1。这是因为有一部分对象会跨过 1~2 次 GC,如果 Survivor 太小就会被迫提前晋升到 Old。把 Survivor 做大可以降低晋升率,让这类对象在 新生代 里自然消亡。
-XX:SurvivorRatio=8代表 Eden : Survivor = 8 : 1(两个 Survivor 各 1)
-XX:SurvivorRatio=3代表 Eden : Survivor = 3 : 1
修改以后,查看 GC 日志里的 promotion rate(晋升量)、Old 使用曲线和 Full GC 次数,确认 Survivor 调整确实降低了提前晋升。
✅GC 日志:时间序列的“过程记录”(频率、停顿、内存变化趋势)
✅Heap Dump:某一时刻的“静态快照”(对象分布、引用链、泄漏定位)
用一个小例子帮你建立直觉 🌰
你发现服务卡顿:
- 你先看 GC 日志:发现 Full GC 很频繁,每次停顿 1s+
→ 说明“GC 行为有问题”,但还不知道“是谁占内存”。- 然后打 Heap Dump:发现某个
Map里堆了几百万条设备心跳对象,被某个缓存引用着没释放
→ 你就知道“是谁导致老年代涨”,可以去改代码或限流。
熔断、降级
✅熔断(Circuit Breaker)是什么意思?
熔断 = 发现下游不行了,就先别再一直打它了,先“断开”一段时间。
目的:保护自己 + 防止雪崩。
为什么需要熔断?
如果 order-service → account-service 调用开始变慢/超时:
- 你还一直疯狂重试、一直打过去
- 线程池/连接池被占满
- 最后把
order-service自己也拖死,扩大故障范围(雪崩)
✅降级(Degrade / Fallback)是什么意思?
降级 = 下游不行时,我仍然给用户一个“可接受的替代结果”,保证核心流程能走下去。
目的:保核心功能可用。
货柜场景的降级例子 🌰
- 结算服务(称重/计费)抖动:降级为“订单先进入 待结算 状态”,先告诉用户“取货完成”,后续异步补结算
如何做限流?
✅前端做一个验证,如滑动窗口、输入数字减少请求次数
✅如果前端通过接口访问,后端可以做Redis滑动窗口、全局限流注解
Redis滑动窗口
使用ZSET(有序集合)对每个限流维度(如userId+api)维护一个ZSET
- score:请求时间戳
- member:请求唯一id
每次请求进来的时候使用Lua脚本做:删除窗口外的旧请求、统计窗口内请求数、如果小于limit允许通过并将请求id加入、设置过期时间
✅ 优点
- 精确:严格控制“最近 N 秒/毫秒”
- 平滑:不会出现固定窗口的边界突刺
- 易落地:Redis + Lua 即可
⚠️ 缺点
- 内存开销:每个请求都要写一条记录(QPS 很高会很大)
- 性能开销:ZSET 操作是
O(logN),N 是窗口内请求数
(窗口大、QPS 高时压力上来)
全局限流注解
一开始我们在秒杀接口上加了全局限流注解,用 AOP 统一拦截,按 couponId + ip(或 userId) 生成 key,通过 Redis SET NX PX 做冷却型限流,保证多实例一致。压测时发现秒杀流量会让 Redis QPS 明显升高,热点 key 造成延迟抖动,而 Redis 又是其他业务强依赖组件,容易被连带影响。
后来我们做了优化:把限流做成两级——本地 Caffeine 先挡洪峰,用 AtomicLong nextAllowedAt + CAS 保证并发正确,
- 如果
now < nextAllowedAt就拦截 - 否则放行,更新
nextAllowedAt = now + 800ms
并设置过期时间和最大容量防止 key 膨胀;必要时再配合 Redis 做全局一致限流。这样 Redis 压力显著下降,接口 P99 更稳定,整体抗压能力更好。
Caffeine
- 并发性能好(比自己写清理线程的 Map 更稳)
- 支持 expireAfterWrite(自动过期,防止 key 堆积内存泄漏)
- 支持 maximumSize(防止被刷出海量 key)
在工作中做过的挑战是什么?
在设置秒杀优惠券的时候,如何抗住高并发?
秒杀优惠券场景最大挑战是高并发和恶意请求。前端我们先做了按钮防抖/禁用重复点击,减少误触,但核心还是后端要兜住绕过。
一开始我们在秒杀接口上加了全局限流注解,用 AOP 统一拦截,按 couponId + ip(或 userId) 生成 key,通过 Redis SET NX PX 做冷却型限流,保证多实例一致。压测时发现秒杀流量会让 Redis QPS 明显升高,热点 key 造成延迟抖动,而 Redis 又是其他业务强依赖组件,容易被连带影响。
后来我们做了优化:把限流做成两级——本地 Caffeine 先挡洪峰,用 AtomicLong nextAllowedAt + CAS 保证并发正确,
- 如果
now < nextAllowedAt就拦截 - 否则放行,更新
nextAllowedAt = now + 800ms
并设置过期时间和最大容量防止 key 膨胀;必要时再配合 Redis 做全局一致限流。这样 Redis 压力显著下降,接口 P99 更稳定,整体抗压能力更好。





