力鑫科技Java后端一面 简历实习逐条拷打
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 挑一个你熟悉的项目,聊聊用了哪些技术、解决了哪些问题。
- 从前端发起一个请求,经过哪些处理、怎么回显到页面?
- 鉴权是怎么做的?
- JWT 在哪里生成的?
- JWT 最基本包含几个部分?
- 鉴权在哪个服务做的?
- 鉴权通过后怎么知道该去哪个服务?
- 网关除了鉴权还做了哪些?
- 限流用什么实现?
- 服务是 MVC 结构吗?有没有 controller / service / mapper?
- controller 层做了什么?
- 业务逻辑放在哪一层?
- 业务层处理时发生异常怎么处理?
- 异常前端怎么知道?
- 业务处理报错(500)怎么办?
- 有没有做全局异常捕获?
- 全局异常捕获的原理是什么?
- service 层多个 update 如何保证事务?
- 分布式事务的场景描述一下。
- MySQL 自身的事务是怎么实现的?
- 纯业务代码里(service 调 mapper 写 SQL)怎么开启事务?
- A 方法加 @Transactional,里面调用 B 方法(B 无注解),A 异常会怎样?
- 查询慢怎么排查解决?
- 什么是最左匹配原则?
- like 模糊查询(后缀 %,即
code%)会走索引吗? - 接口性能优化从 3.2 秒降到 600ms:什么场景、为什么慢、具体怎么做?
- DB 已更新但缓存没更新,怎么避免看到旧数据?
- 线程池用的什么?
- 线程池有哪些参数?
- 定时任务用什么?
- 运行数据日增千万级导致分析慢,怎么办?
- 引入 ClickHouse 时有没有做数据迁移?
- 怎么迁移(存量 + 增量)?
- 迁移量大概多少、每次耗时多长?
- 第一次上线查 ClickHouse 查不到数据怎么办?
- ClickHouse 没查到走 MySQL 后,ClickHouse 什么时候写入?
- 分布式锁的锁粒度是什么?
- 续期时间配置多长?
- 消息堆积是什么场景出现的?
- 会不会重复消费?
- 优化后压测一秒能支撑多少 QPS / TPS?
《参考解析》
网关鉴权与路由:JWT 是 header.payload.signature 三段 Base64URL 拼起来的自包含令牌——头部声明算法,载荷放 userId、过期时间等声明,签名用服务端私钥(HS256 是共享密钥)对前两段签名。它一般由认证服务在登录成功后签发,网关只做验签与解析,不签发,这样密钥不用下发到每个服务。验签通过后把 userId/租户写进请求头或上下文往下传,再按路径前缀或服务名查路由表转发到后端服务;「怎么知道去哪个服务」正是路由表 + 注册中心(Nacos/Eureka)的职责,网关拉取实例列表并做负载均衡。网关除鉴权通常还担了路由转发、限流(令牌桶/漏桶,Sentinel 或 Redis + Lua 做集群限流)、灰度与黑白名单、跨域、日志与链路追踪埋点。要主动补一句 JWT 的短板:无状态所以服务端没法主动失效,只能靠短过期时间 + refresh token + Redis 黑名单来兜。
@Transactional 的失效与自调用:Spring 的声明式事务是 AOP 代理实现的,只有经过代理对象的调用才会开启事务。所以「A 方法上有 @Transactional,内部直接调本类的 B 方法(B 无注解)」时,这次调用走的是 this,不经过代理,B 不会得到独立的事务语义,它仍然跑在 A 开启的那个事务里——因为 A 和 B 在同一个物理事务,B 里做的写操作也会跟着 A 的异常一起回滚。真正会因为自调用失效的是「B 上才有 @Transactional」的场景:B 的注解被完全忽略。另外两个高频追问:一是异常被 catch 住没往外抛,事务管理器感知不到,照样提交;二是默认只对 RuntimeException 和 Error 回滚,受检异常必须写 rollbackFor = Exception.class。想拆分事务就把 B 挪到另一个 bean,或注入自身代理、用 AopContext.currentProxy(),需要「B 独立提交、不受 A 回滚影响」时用 REQUIRES_NEW(注意它会占用另一个连接,别在大事务里滥用)。
最左匹配与 like 'code%' 是否走索引:联合索引 (a, b, c) 在 B+ 树里按 a、b、c 的顺序排序,所以能利用的只有「从最左列开始且连续」的前缀:where a=? and b=? 能走,where b=? 走不了,a=? and c=? 只能用到 a,中间的范围条件会让后面的列失效。like 'code%' 是前缀确定的模糊查询,B+ 树可以定位到 code 开头的区间并顺序扫描,因此能走索引;like '%code' 和 '%code%' 前缀不确定,无法定位起点,只能全表扫或全索引扫。还要补几个「看起来能走其实走不了」的坑:列上有隐式类型转换(字符串列传数字、字符集不一致)、列被函数包裹(date(create_time) = ...)、以 or 连接的非索引列条件、以及优化器估算回表代价过大时主动放弃索引改全表——这时可以用覆盖索引把要查的列都放进索引里,免回表。
DB 与缓存的一致性:主流做法是 Cache Aside:先更新数据库,再删除缓存(是删不是改)。为什么删而不是更新——并发下两个写请求可能交错,后落库的旧值反而后写进缓存,留下长期脏数据;删除则让下一次读自然回填最新值。为什么先库后缓存——先删缓存再更新库时,读请求可能把旧值读出来又写回缓存。即便如此仍有极小概率不一致(读请求在更新前拿到旧值、更新提交后才写回缓存),所以工程上要叠三层兜底:给缓存 key 设过期时间,接受「最终一致」;对一致性敏感的写路径用延迟双删(更新后隔一小段时间再删一次);量级大、要求高就上 binlog 订阅(Canal/Debezium)异步失效缓存,把「谁改了库」这件事交给数据库日志而不是业务代码。再往上一层可以给缓存值带上版本号或更新时间,读的时候发现比库里的旧就丢弃重读;真正要求强一致的场景(余额、库存扣减)不要靠缓存,直接走库加锁或用原子操作。
ClickHouse 迁移与「查不到回源」的收口:存量数据用离线批量灌——从 MySQL 导出按时间分片的文件(CSV/Parquet)再 clickhouse-client 导入,或直接 INSERT INTO ... SELECT 走 JDBC 桥,按天分区、分批提交,单批几十万行比较稳;增量数据有两条路,binlog CDC(Canal/Flink CDC)保证准实时,或按 update_time 水位线定时拉取,后者要额外处理「更新不改时间戳」和物理删除。迁移必须可重跑:按分区覆盖写、用 ReplacingMergeTree 或版本字段去重,迁移完用条数与金额合计跟源端对账。
上线初期 ClickHouse 里只有增量、没有历史,查询会大面积落空,所以业务侧要写「先查 CH,Miss 再回源 MySQL」的兜底,同时后台跑历史回填任务;这个阶段最危险的是缓存击穿——同一批热点 key 全部回源把 MySQL 打挂,得用互斥锁或单飞(singleflight)把并发收敛成一次查询。收口标准要量化:把回源比例和回源 QPS 做成监控指标,随着回填推进持续下降,降到接近 0 再摘掉兜底分支,而不是「感觉差不多了」就删代码。至于「CH 什么时候写入」,取决于选的是 CDC 还是定时同步,但无论哪条路都要保证写入是幂等的,否则回源与主链路会互相打架。