Java相关回答不足点整理
以下专门筛选出两轮面试中Java/后端/数据库/中间件/架构方向回答不到位的地方,按问题类别整理,方便你集中复习。
一、系统架构与微服务
1. 网关鉴权(API权限 + 数据权限)------两场均暴露
你的回答: "鉴权这块可能不太清楚,平时业务开发比较多,不太关注。"
问题所在: 在银行核心做Java开发近5年,即使没亲自写过鉴权模块,也应该知道基本链路。这个回答直接关上了话题,面试官会认为你只盯着自己一亩三分地。
需要掌握的基本链路:
网关层用Spring Security + JWT做统一认证,流程是:
- 用户登录 → 认证服务颁发JWT(含用户ID、角色、权限列表)
- 后续请求携带JWT → 网关过滤器验证签名/有效期
- 验证通过后,在请求头中注入
X-User-Id、X-Role等信息 - 下游微服务通过拦截器获取这些信息做接口权限 (角色/权限码校验)和数据权限 (只查自己部门/机构的数据,通过SQL拦截器动态追加
WHERE条件)
如果没实际做过,至少要把这个链路背下来。
问题:Spring Cloud Gateway 如何实现鉴权(GlobalFilter),并结合银行场景说明。
面试回答核心话术(可直接用于面试)
"Gateway 的鉴权我通过自定义 GlobalFilter 实现,核心思路是:在请求转发到下游微服务之前,统一校验身份令牌和权限,不合法直接拦截返回 401/403,不进入内网服务。
具体做法是:客户端登录后,认证中心(比如 OAuth2 或自研的统一认证平台)颁发一个 JWT Token,前端每次请求都把 Token 放在 Header 里带过来。Gateway 的 GlobalFilter 拦截所有请求,从 Header 中取出 Token,调用认证中心校验 Token 有效性,或直接解析 JWT 验签。校验通过后,把用户信息(比如 userId、角色)写入请求头,传给下游微服务使用;校验失败直接返回 401,根本不给下游服务增加负担。
在银行项目中,我们还做了接口级权限控制 :在 Gateway 里维护一个路由-角色映射表(或从配置中心动态加载),校验完 Token 后,判断当前用户角色是否有权访问该接口。比如 /loan/approve 只有信贷审批员能访问,普通柜员只能访问 /deposit/query。这样做的好处是把安全防线前置到网关层,避免敏感接口暴露给未授权用户。
与 Sentinel 限流的区别:限流是'你请求合法但量太大,我限制你';鉴权是'你身份不明或没权限,我不让你进'。两者都在 Gateway 层完成,一个靠 GlobalFilter 实现,一个靠 Sentinel Gateway 适配器实现,共同构成银行系统的第一道安全防线。"
详细解析
一、Gateway 鉴权的整体架构
客户端(App/Web)
│ Header: Authorization: Bearer <JWT Token>
↓
Spring Cloud Gateway
│
├─ ① GlobalFilter: 鉴权拦截器(自定义)
│ ├─ 解析 Token
│ ├─ 校验有效性
│ ├─ 校验角色权限
│ ├─ 成功:放行,写入用户信息 Header
│ └─ 失败:返回 401/403
│
├─ ② Sentinel Gateway 适配器:限流/熔断
│
└─ ③ 路由转发到下游微服务
│ Header: X-User-Id, X-User-Role
↓
deposit-service / loan-service
二、GlobalFilter 鉴权代码实现
1. 自定义鉴权过滤器
java
@Component
@Order(-1) // 优先级最高,在其他过滤器之前执行
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private AuthService authService; // 调用认证中心或本地 JWT 校验
// 白名单路径(登录接口、健康检查等不需要鉴权)
private static final List<String> WHITE_LIST = Arrays.asList(
"/auth/login", "/actuator/health", "/public/");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 1. 白名单直接放行
if (isWhitePath(path)) {
return chain.filter(exchange);
}
// 2. 从 Header 中获取 Token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) {
return unauthorized(exchange, "缺少认证令牌");
}
token = token.substring(7); // 去掉 "Bearer " 前缀
// 3. 校验 Token 有效性
UserInfo userInfo;
try {
userInfo = authService.validateToken(token);
if (userInfo == null) {
return unauthorized(exchange, "令牌无效或已过期");
}
} catch (Exception e) {
return unauthorized(exchange, "令牌校验异常");
}
// 4. 校验角色权限(接口级)
if (!hasPermission(userInfo.getRole(), path)) {
return forbidden(exchange, "无权限访问该接口");
}
// 5. 将用户信息写入请求头,传递给下游微服务
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userInfo.getUserId())
.header("X-User-Name", userInfo.getUserName())
.header("X-User-Role", userInfo.getRole())
.build();
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest)
.build();
return chain.filter(modifiedExchange);
}
// 返回 401 未认证
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] body = ("{\"code\":401,\"message\":\"" + message + "\"}").getBytes();
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
}
// 返回 403 无权限
private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] body = ("{\"code\":403,\"message\":\"" + message + "\"}").getBytes();
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
}
private boolean isWhitePath(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
private boolean hasPermission(String role, String path) {
// 简化版:从配置或数据库中查询权限映射
// 例如:/loan/approve → ROLE_APPROVER
// /deposit/* → ROLE_TELLER, ROLE_ADMIN
return permissionService.check(role, path);
}
@Override
public int getOrder() {
return -1;
}
}
三、银行场景下的权限分级
| 接口路径 | 功能 | 允许角色 |
|---|---|---|
/deposit/query |
查询余额 | 柜员、客户经理、管理员 |
/loan/approve |
贷款审批 | 信贷审批员、管理员 |
/core/transfer |
资金转账 | 柜员、管理员 |
/admin/config |
系统配置 | 仅管理员 |
/auth/login |
登录接口 | 白名单,无需鉴权 |
四、与 Sentinel 限流的配合
yaml
# Gateway 配置 Sentinel 限流
spring:
cloud:
sentinel:
scg:
fallback:
response-status: 429
response-body: "{\"code\":429,\"message\":\"请求过多,请稍后重试\"}"
两者区别:
| 机制 | 解决的问题 | 实现方式 | 返回码 |
|---|---|---|---|
| 鉴权(AuthFilter) | 你是谁?你有没有权限? | 自定义 GlobalFilter | 401 / 403 |
| 限流(Sentinel) | 你合法但请求太快了 | Sentinel Gateway 适配器 | 429 |
执行顺序:鉴权先于限流------连身份都没有的请求,根本不值得消耗限流计数器的资源。
五、Token 校验的两种方案
方案一:JWT 本地验签(推荐)
java
public UserInfo validateToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return new UserInfo(
claims.get("userId", String.class),
claims.get("userName", String.class),
claims.get("role", String.class)
);
}
优点 :无需远程调用,性能高。
缺点:无法主动失效 Token(除非加入黑名单)。
方案二:调用认证中心远程校验
java
public UserInfo validateToken(String token) {
// 通过 Feign 或 HTTP 调用统一认证中心
return authCenterClient.validate(token);
}
优点 :Token 可主动失效。
缺点:多一次远程调用,增加延迟和故障点。
银行实践 :我们采用JWT + Redis 黑名单方案。JWT 短期有效(15 分钟),配合 refresh_token 续期;当用户修改密码或强制下线时,把旧 JWT 的 ID 加入 Redis 黑名单,验签后再查黑名单,实现准实时失效。
六、下游微服务如何获取用户信息
Gateway 已经把用户信息写入 X-User-Id、X-User-Role 等 Header,下游服务直接读取:
java
@GetMapping("/balance")
public BalanceDTO getBalance(@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-User-Role") String role) {
log.info("用户 {} 角色 {} 查询余额", userId, role);
// 业务逻辑
}
或通过拦截器统一提取,放入 ThreadLocal:
java
public class UserContext {
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
public static void setUserId(String userId) { USER_ID.set(userId); }
public static String getUserId() { return USER_ID.get(); }
public static void clear() { USER_ID.remove(); }
}
七、面试加分点
- OAuth2 集成:如果面试官追问,可以说"我们用的是 Spring Security + OAuth2 + Gateway 统一鉴权,认证中心颁发 JWT,Gateway 做资源服务器校验"。
- 跨服务认证传递 :Feign 的
RequestInterceptor自动从当前请求中获取 Token 并写入下游请求头,实现全链路认证透传。 - 性能优化:JWT 解析非常快(毫秒级),不会成为网关瓶颈。对于极高并发(10 万+ QPS),可考虑把用户角色信息缓存到本地(Caffeine),避免每次都查 Redis 黑名单。
2. 流量管控(限流、削峰)------第一场
你的回答: 没答上来。
问题所在: 你讲了贷款生命周期,流程很长(合同→计提→回收→账务),面试官自然会问"流量这么大,你们怎么做限流和削峰?"你没接住。
正确方向:
银行核心系统的流量治理通常分几层:
| 层级 | 手段 | 说明 |
|---|---|---|
| 网关层 | Sentinel限流 | 按API路径配置QPS阈值,超出快速失败返回 |
| 业务层 | 线程池隔离 + 熔断降级 | 不同服务用不同线程池,下游故障时熔断 |
| 消息层 | MQ削峰填谷 | 高峰期请求先入MQ,消费者按自身能力消费,避免压垮数据库 |
| 批量处理 | 日切批处理错峰 | 核心账务类操作放到夜间批处理窗口执行 |
3. 分库分表的"架构视角"说不清楚------两场均被追问
你的回答: 集中在技术实现上------internal key取模分4个库,加DB number字段。
面试官原话: "你平时和你们架构聊,肯定也不是这么聊的,咱们可以再上升一下。"
问题所在: 面试官想听的是分库分表的前置决策,而不只是取模逻辑。
正确方向(先说业务再说技术):
"我们当时做新核心重构,分库的背景有两个:一是业务上要把贷款、存款、账务拆成独立的领域服务,对应的库天然就分开了;二是数据量太大,单库扛不住。最终定了4个库,分库键选了internal_key,因为它全局唯一且均匀分布,取模后数据分布比较均衡。分库后对上层业务透明------我们在DAO层封装了路由逻辑,业务代码只需要传internal_key,不用关心具体去哪个库。"
先交代业务拆分原因 ,再说技术实现,这就是他们要的"上升"。
二、分布式事务与一致性
4. TCC vs SAGA的区别(同步/异步、适用场景)------第一场
你的回答: "链路长短不一样,TCC链路短、SAGA链路长。"
问题所在: 太浅了。面试官需要你讲清楚同步vs异步、业务侵入性、补偿机制的差异。
正确的对比:
| 维度 | TCC | SAGA |
|---|---|---|
| 同步/异步 | 同步,全程等待 | 异步,事件驱动 |
| 资源锁定 | Try阶段锁定资源,直到Confirm/Cancel释放 | 不锁定资源,先执行正向操作 |
| 适用场景 | 短链路、一致性要求极高(如扣库存) | 长链路、跨多个服务(如贷款+记账+存款) |
| 补偿方式 | Cancel回滚已锁定资源 | 执行反向补偿操作 |
| 业务侵入性 | 高(需实现try/confirm/cancel三个方法) | 中(需实现正向+补偿两套逻辑) |
| 优点 | 强一致性,可回滚 | 不长时间占用资源,吞吐量高 |
| 缺点 | 资源占用时间长,并发能力受限 | 最终一致,需处理补偿失败的情况 |
为什么银行核心用SAGA更多:
因为贷款发放链路涉及合同、计提、记账、存款等多个服务,如果用TCC,Try阶段要锁住所有资源,用户等待时间长且并发能力差。SAGA先做正向操作(默认成功),失败了再补偿,用户体验好,代价是最终一致性------但银行可以通过日切对账来兜底。
5. 幂等设计------第一场
你的回答: 举了Redis宕机导致重复扣款的例子,用数据库唯一索引做兜底。
问题所在: 例子本身没问题,但回答过于局限,没有涵盖幂等的完整方案。
需要掌握的系统性视角:
幂等应该做多层次拦截:
| 层级 | 方案 | 说明 |
|---|---|---|
| 业务入口 | 全局流水号 + Redis去重(SETNX) | 请求进来先查Redis是否存在该流水号,存在则直接返回上次结果 |
| 数据库层 | 唯一索引兜底 | Redis万一挂了,DB唯一索引拦截重复插入 |
| 消息消费端 | 业务主键去重表 | MQ消费时先查去重表,已消费则跳过 |
注意: 你举的例子里Redis挂了导致重复扣款,说明你们没有做数据库唯一索引兜底(或者索引字段没覆盖到这笔交易)。面试官可能会追问"那你们后来怎么解决Redis单点问题的?"------可以考虑用Redis Cluster或引入本地缓存做二级缓存。
三、数据库与索引
6. 索引设计太"八股",缺少场景化案例------第一场被反复追问
你的回答: B+树结构、聚簇索引/二级索引、覆盖索引、回表、左前缀原则、少用函数、小表驱动大表......
面试官原话: "你说的都没问题,但我听不太出来你是真的在业务里做过。能不能给我讲一个你真实的场景?"
问题所在: 所有知识点都对,但缺少一个你自己的设计案例,听起来像在背书。
你需要准备一个自己的案例,比如这样:
"我当时在数据迁移时遇到一个慢查询:账户交易流水表(trans_hist)4000万数据,高频查询条件是
WHERE account_no = ? AND trans_date BETWEEN ? AND ? ORDER BY trans_time DESC。原来三个字段各有一个单列索引,但EXPLAIN发现每次只用到account_no索引,然后回表查trans_date和trans_time,平均耗时1.8秒。我改成联合索引
(account_no, trans_date DESC, trans_time DESC),查询耗时降到40ms。原因是:①覆盖了查询所需字段,不用回表;②trans_date和trans_time按降序排列,省去了额外排序。缺点是写入慢了约15%,但这张表读多写少,业务可接受。"
有表名、数据量、优化前后数字、选择的权衡,才是他们要的场景化回答。
7. 笛卡尔积的触发条件------第二场
你的回答: "记不清了。"
这是送分题,不应该丢。
正确答案:
sql
-- 1. 没有连接条件
SELECT * FROM table_a, table_b;
-- 2. 连接条件恒为真(1=1 或 无效条件)
SELECT * FROM table_a a JOIN table_b b ON 1 = 1;
-- 3. 显式使用 CROSS JOIN
SELECT * FROM table_a CROSS JOIN table_b;
-- 4. 左连接/右连接时连接条件写错导致匹配到所有行
面试官问这个,不是在考你记不记得语法,而是想确认你对SQL执行计划的基本认知------连表时如果没有有效的ON条件,优化器只能做笛卡尔积,性能灾难。
笛卡尔积触发条件 ------ 问题复盘与清晰答案
你当时的回答
面试官: "连表查询的时候,什么时候会出现笛卡尔积?"
你的回答: "连表查询产生笛卡尔积,一般也是,因为它是SQL的话,它是有三范式的嘛,就是说,有点记不清了。"
为什么这个回答很可惜
这是一个极基础的送分题,答案本身不超过3句话。你如果在这里卡住,面试官的判断会是:
- SQL基本功不扎实 ------ 连表查询是后端开发日常操作,触发笛卡尔积属于最基础的认知
- 面对简单问题缺乏推导能力 ------ 即使忘了具体场景,也可以根据原理推导出来
- 直接放弃("记不清了")而不是用已知知识推理 ------ 面试官会认为你解决问题的意愿不足
实际上,哪怕你完全没背过答案,也可以现场推理出来:
"笛卡尔积就是两表所有行两两组合。那什么情况下SQL会把所有行两两组合?当查询没有告诉数据库怎么关联的时候。所以要么是没写连接条件,要么是连接条件恒成立。"
能说出这句话,就不用背任何代码,面试官也会认可你的逻辑能力。
完整清晰的正确答案
一句话核心定义
笛卡尔积(Cartesian Product) 是指对两张表进行连接查询时,没有有效的关联条件,导致左表的每一行与右表的每一行逐一组合,结果集行数 = 左表行数 × 右表行数。
举例: A表有1000行,B表有500行,产生笛卡尔积 → 返回 500,000 行,即使这50万行里99.99%都是无意义数据。
触发笛卡尔积的4种典型场景
场景1:完全没有连接条件
sql
-- 没有任何WHERE或ON来限定关联关系
SELECT * FROM table_a, table_b;
数据库不知道两表怎么关联,只能把所有行交叉组合。
场景2:连接条件无效/恒为真
sql
-- ON条件是常量1=1,永远成立
SELECT * FROM table_a a
JOIN table_b b ON 1 = 1;
-- 或者使用了无效表达式
SELECT * FROM table_a a
JOIN table_b b ON a.无效字段 = b.任何值; -- 字段不存在→被忽略→全连接
场景3:显式使用 CROSS JOIN
sql
-- CROSS JOIN 的语义就是笛卡尔积
SELECT * FROM table_a CROSS JOIN table_b;
这是有意为之的笛卡尔积,少数场景(如生成测试数据、穷举组合)会主动使用。
场景4:连接条件写错导致一对多膨胀(近似笛卡尔积)
sql
-- 本该用唯一键关联,结果用了非唯一字段
-- 例如 table_a.customer_type 只有2种类型(个人/企业)
-- table_b.customer_type 也有2种类型
-- 每个customer_type下各有大量行,关联后数据量膨胀巨大
SELECT * FROM table_a a
JOIN table_b b ON a.customer_type = b.customer_type;
这种情况下不是严格的笛卡尔积(因为有限制条件),但效果类似------结果行数远超预期,性能同样灾难。它本质上是对连接条件选择性过低缺乏警惕,可以用"近似笛卡尔积"来向面试官说明。
如何发现笛卡尔积
sql
-- 在执行SQL之前,先用 EXPLAIN 查看执行计划
EXPLAIN SELECT * FROM table_a, table_b;
EXPLAIN输出中如果看到:
Using join buffer (Block Nested Loop)------ 没有索引可用,在做全表扫描连接filtered列值极低- rows列显示
table_a行数 × table_b行数级别的扫描行数
基本就是笛卡尔积了。
避免笛卡尔积的方法
| 方法 | 说明 |
|---|---|
| 写连接条件 | 多表查询必须带有效的ON或WHERE关联条件 |
| 用小表驱动大表 | JOIN顺序上先连数据量小的表,再连大表,减少中间结果集 |
| 关联字段加索引 | 被关联的字段(如外键)建索引,让优化器走Index Nested-Loop Join,避免全表扫描 |
| 审查执行计划 | 提交SQL前用EXPLAIN看一眼扫描行数是否合理 |
| 避免隐式连接混用 | 不要把逗号连接和JOIN混在一起写,容易漏条件 |
如果面试时真的忘了怎么办?(现场推导法)
你可以说:
"我具体场景可能记得不太清了,但我可以推一下。笛卡尔积的本质是两表行两两配对,那产生的原因一定是查询没有给数据库一个有效的连接依据。所以大概率是:①没写WHERE或ON条件;②写了条件但条件恒为真,比如 ON 1=1;③用了CROSS JOIN这种显式全连接。我一般在开发和代码审查时会特别注意这一点,避免生产环境出现慢查询。"
这样即使细节记不全,面试官也会认为你理解原理而不是死记硬背。
与银行核心业务场景的结合(加分)
如果你能把笛卡尔积和你的银行数据迁移经历联系起来,会更有说服力:
"我在做数据迁移时,遇到过一个具体案例:抽数SQL中有一张10亿的交易流水表(trans_hist)和一张账户表(account)做JOIN,连接条件本应是 trans_hist.account_no = account.account_no,但有人把条件写成了 trans_hist.trans_date = account.open_date,导致一条账户记录匹配到几千条流水,跑了40分钟没出结果。我们后来用EXPLAIN发现rows估算显示笛卡尔积量级,修正条件后降到2秒。从那以后,我们团队所有连表SQL都会先过EXPLAIN审查。"
这个案例同时展示了:业务场景 + 问题发现手段(EXPLAIN)+ 优化效果(40分钟→2秒)+ 团队规范改进,比单纯背语法要强十倍。
8. 缓存穿透/击穿/雪崩的概念混淆------第二场
你的回答:
- 穿透:传一个不存在的值 → 用布隆过滤器 ✅
- 击穿:查了一堆过期的 → 把过期时间设置大一点 ❌
- 雪崩:集体过期 → 过期时间加随机数 ✅
问题所在: "击穿"和"穿透"的概念你没区分清楚,把击穿说成了"查了一堆过期的",定义有误。
正确区分:
| 概念 | 定义 | 解决方案 |
|---|---|---|
| 穿透 | 查一个缓存和DB都不存在的key,每次都打到DB | 布隆过滤器 / 缓存空值(如存""并设短过期时间) |
| 击穿 | 一个热点key恰好过期,大量请求同时打到DB | 互斥锁(SETNX)/ 逻辑过期(提前异步刷新) |
| 雪崩 | 大量key在同一时间过期,大量请求打到DB | 过期时间加随机偏移 / 多级缓存(本地+分布式) |
"查了一堆过期的"更像是雪崩的极端情况,而不是击穿的定义。
四、中间件
9. MQ消息积压的处理------第一场直接卡住
你的回答: "答不上来。"
基础应对方案(至少要说出来):
- 扩容消费者:增加消费者实例,但要确保Topic的分区数≥消费者数,否则多余的消费者拿不到消息
- 提高消费速率 :临时调大
batchSize(批量拉取)、调高prefetchCount(预取数量) - 排查根因:是下游DB写入慢?还是消费者逻辑中有远程调用超时?针对性优化
- 临时降级:先把积压消息转存到新Topic,用更多消费者并行处理,原Topic恢复后回补
即使没实际遇到过,至少要知道这4条思路。完全说"不知道"会让面试官觉得你只关心业务CRUD,不关心系统稳定性。
五、总结:Java方向急需加强的TOP 5
按两场面试暴露的严重程度排序:
| 优先级 | 知识点 | 为什么急 | 建议行动 |
|---|---|---|---|
| 1 | 索引场景化案例 | 两场都被追问,但都没讲出具体案例 | 准备一个真实的慢查询优化故事(表名、字段、数据量、优化前后耗时) |
| 2 | 网关鉴权链路 | 直接说"不知道",扣分严重 | 背熟JWT→Gateway→下游拦截器的流程,至少讲到能让人听懂 |
| 3 | TCC vs SAGA 完整对比 | 第一场被追问同步/异步,回答太浅 | 把上表中的对比维度记熟,尤其要能结合你做的贷款业务讲为什么选SAGA |
| 4 | 缓存三兄弟(穿透/击穿/雪崩) | 第二场概念混淆 | 把定义和各自解决方案背准,这是高频面试题 |
| 5 | MQ积压处理思路 | 第一场卡住 | 把扩容消费者、调参、根因排查、临时降级4条记住即可应急 |
另外提醒一点:如果下次被问到不会的Java问题,不要直接说"不知道"就结束。试着说"这个具体配置不是我在负责,但我了解整体思路是......"------至少证明你有架构意识,而不是完全空白。