场景题
上一份工作为什么离职?
我想去一线城市寻求更大的发展平台和更多业务场景,接触更成熟的团队和项目,提升自己的能力。
当线上出现问题时,怎么做
✅线上出现问题时优先止血,防止对公司造成亏损,使用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 index使用了覆盖索引
- 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 先挡洪峰,用key:ip,value: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 更稳定,整体抗压能力更好。
在工作中遇见的困难有哪些?怎么解决的?
接口响应慢、CPU占用高怎么排查?
一般按照系统层——>JVM——>应用层进行排查
✅系统层定位到“具体线程”
上CPU占用过高的服务器,使用top -H -p PID命令找到最耗CPU的线程TID
再通过jstack PID找到对应的nid,拿到这个线程的代码调用栈,此时我会连续抓取2~3次jstack,如果
- 栈顶元素长期不变,基本就是死循环问题导致的
- 如果大量线程BLOCKED,基本是锁竞争
✅JVM/应用层进一步确认
怀疑是GC引起的原因,我会通过jstat或者查看GC日志看Young GC或者Full GC是否频繁
✅常见的解决方法
- 代码死循环:修改代码,避免无sleep的
while(true) - 锁竞争:缩小锁的粒度
- GC压力:减少临时对象,通过调整参数,优化年轻代、老年代的比例关系
- 下游慢导致堆积:加超时、限流、熔断机制
- 慢SQL:开启慢SQL日志,通过Explain关键字查看,优化SQL
有遇见过死锁吗?如何解决的?
有遇见过,主要有两类:分布式锁“锁没有释放、MySQL行锁死锁”
✅分布式锁
我们要保证同一时间一台设备只能被一个用户下单/开门,所以在下单阶段会加 Redis 分布式锁:SET key value NX PX ttl,key 用设备维度 lock:device:{Sn},value 用随机 token。
容易出现的“死锁现象”:拿到锁后,业务链路中途异常(超时、服务重启、网络抖动),导致没走到释放锁,后续用户扫同一台设备就一直拿不到锁,看起来像“死锁”。
我们的解决方式(核心三件套):
- 锁一定要带过期时间(PX ttl),防止锁永久占用。
- token 落库:加锁成功后把 token 存到订单表
lock_token字段,释放锁时能精确校验“是不是我加的锁”。 - Lua 原子解锁:只有当
get(key)==token才del(key),避免误删别人的锁。
✅MySQL行锁
1 | 事务T1:先锁订单行(order_id=1) -> 再锁库存行(sku=100) |
解决思路:
- 统一加锁的顺序:不管哪个入口进来,都按照同样的顺序更新(比如永远先锁库存,再锁订单)
- 缩短事务:事务里面只放必要的SQL,减少锁的持有时间
- 优化SQL语句:where走索引、减少扫描范围,因为扫描越大,锁范围越大,越容易死锁
CAP原理
CAP 放到「分布式事务」里理解,其实就是一句话:一旦发生网络分区(P),你只能在“一致性(C)”和“可用性(A)”里更偏向一个 🤹♂️
C——Consistency(一致性)
口语化:要么都成功、要么都失败,别出现“你说扣款成功了,但库存还没减”的读写混乱。
A — Availability(可用性)
A 不是“必须成功”,而是“不能一直卡住/超时不回”。
P — Partition tolerance(分区容错)
一旦出现网络分区(P),比如「订单服务 ↔ 库存服务」网络断了必须立即面临选择:
- 选CP(要一致性)
宁可失败/拒绝/等待,也不要出现不一致结果
✅ 更像的方案:
- Seata AT(偏强一致语义,依赖协调器/锁,异常时影响可用性)
- 选AP(要可用性)
请求尽量先返回,允许短时间不一致,靠后续补偿/对账修正
✅ 更像的方案:
TCC(Try/Confirm/Cancel):强调业务层兜底、补偿、重试(通常被认为更偏 AP/BASE 思想)
可靠消息最终一致性(Outbox/本地消息表 + MQ)
压力测试是用的什么工具?
压测主要是测试同事负责执行,但我作为开发会配合看结果和排查瓶颈。
我关注的重点一般是 QPS、响应时间、错误率,以及应用侧 CPU、GC、线程池、Redis、MySQL、MQ 的指标。
如果某个接口抖得厉害,我会先看是不是慢 SQL、Redis 热点、线程池打满或者 Full GC。
单元测试怎么测试的?
单元测试:JUnit
接口测试:Postman
文件上传考虑什么?
我会从以下几个点进行考虑
- 文件大小的限制和类型校验,可以使用阿里云的检验产品,防止非法的文件上传
- 会考虑分片上传和断点续传,特别是对于大文件的场景
- 存储在服务器上?还是NAS还是对象存储?
- 上传成功后,记录文件名,哪名用户上传的,ip是多少?
- 注意安全性,如重名覆盖,这一点可以通过在文件名后加时间解决;恶意脚本
- 失败重传
设计一张表需要考虑什么?
首先确认这张表用来干什么的?是做日志、流水、还是回调记录?
再确认核心字段、状态字段、时间字段、唯一约束?
然后再看查询场景,决定索引怎么建?
比如说:
- 对于订单表,我会考虑订单号唯一、用户ID、设备ID、订单状态、支付状态、创建时间、更新时间这些核心字段。
- 对于支付回调表,会特别关注商户订单号唯一约束,保证幂等。
设计一个给上游调用的接口考虑什么?
设计接口我会从以下几个点进行考虑
- 入参和返回值
- 错误码和异常语义是否统一
- 幂等性,尤其是上游可能重试的场景
- 超时和重试策略
- 权限校验,可以和上游定义一个密钥
- 日志、链路追踪、监控告警
- 接口版本兼容性,避免以后升级将上游搞挂
- 写接口说明文档给用户
常用的加密算法
- MD5、SHA-256用来验证完整性
- 对称加密,AES,用于数据加密
- 非对称加密,RSA,用于密钥交换,签名验证
- 验证和鉴权,JWT,Token,2FA





