满帮集团 27 届全栈开发工程师一面面经
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 数据权限原来为什么要硬编码,后来怎么重构的?
- 自定义注解 + AOP + MyBatis 动态 SQL 怎么实现?
- Service 内部调用怎么解决 AOP 自调用问题?
- 多表、多角色权限怎么处理?
${}拼 SQL 有没有注入风险?- 项目中为什么用 CompletableFuture?
- 线程池使用要注意什么?线程池隔离、拒绝策略?
- ReentrantLock、公平锁 / 非公平锁以及 AQS
- MySQL 四种事务隔离级别
- 项目性能优化具体怎么做?(多次 SQL 合并成一次条件聚合、把 Java 层聚合下推到数据库)
- 微服务里的限流和降级区别
- Redis 数据结构;项目有没有用锁?Redis 分布式锁怎么实现?
- AI Coding 平时怎么用?
- 怎么看全栈?如果入职做全栈有没有挑战?
- Vue 生命周期
- 行为题:实习中最挫败的事情?Mentor 不认可方案时怎么处理?
- 有没有了解 Agent / AI 框架?
《参考解析》
- AOP 自调用为什么失效、怎么解决:Spring 的 AOP 靠代理对象生效,
this.b()走的是原始对象而不是代理,所以 b 上的切面(典型是@Transactional、自定义权限注解)全部不生效。解法有:把b()拆到另一个 bean 里通过注入调用;用AopContext.currentProxy()(需要@EnableAspectJAutoProxy(exposeProxy = true));注入自身@Lazy代理;或者用BeanFactory拿到代理对象再调用。代码评审里这类问题很难靠读代码发现,通常要写测试或者加启动期的自检。 ${}与#{}的注入风险:#{}是预编译占位符,参数走PreparedStatement,天然防注入;${}是字符串直接拼接,任何用户可控的值经过它都可能被注入。动态 SQL 里${}唯一合理的用途是拼接不能参数化的部分,比如表名、列名、order by字段——而且必须用白名单枚举校验,绝不能直接用前端传来的字符串。数据权限场景经常需要动态拼where,那里尤其要守住这条线。- 多表多角色权限怎么做:把「谁能看哪些数据」抽象成数据范围表达式,由注解声明、切面解析、动态 SQL 生效。落地时通常需要三块:角色到数据范围的映射(全部 / 本部门及下属 / 仅本人 / 自定义)、把当前用户的组织路径查出来(避免每次递归)、以及在 SQL 层把范围条件拼进去。多表的话要明确每个表的权限字段归属,join 之后以主表的范围为准还是取交集,这点没定清楚很容易越权。
- CompletableFuture 与线程池隔离:用 CompletableFuture 主要是把多个互相独立的远程调用并行化,把串行的总耗时压到最长的那一个。要注意的是:不传线程池时它默认用
ForkJoinPool.commonPool(),而 commonPool 的并行度是 CPU 核数减一,拿它跑阻塞 IO 会拖垮整个 JVM。所以必须显式传业务线程池,并且不同下游用不同池做隔离——否则一个下游变慢会把池占满,连带其他正常下游一起超时。拒绝策略按业务选:核心链路用CallerRunsPolicy做背压,非核心可以直接丢弃并打点。 - 限流和降级的区别:限流是入口侧的控制,主动拒绝超出容量的一部分请求,保护系统不被压垮;降级是链路侧的取舍,在依赖不可用时返回兜底结果(缓存、默认值、简化功能),保证主流程还能走通。两者通常配合使用:先限流挡住超额流量,再对非核心依赖做降级。熔断是降级的触发机制之一——失败率超阈值就打开断路器,快速失败一段时间后再半开重试。
- Redis 分布式锁的正确写法:加锁用
SET key value NX PX <ttl>,value 必须是每个客户端唯一的标识(UUID 或请求 ID);解锁必须用 Lua 脚本先比对 value 再删,否则可能删掉别人续期后的锁。业务执行时间可能超过 TTL 时要有看门狗续期。更严格的场景要考虑 Redis 主从切换导致锁丢失的问题(主库写入后还没同步就宕机,锁就没了),这时可以用 Redlock 或多副本确认,但也要清楚 Redlock 本身的争议——如果一致性要求极高,用带 fencing token 的方案或直接落数据库。 - AQS 与公平 / 非公平锁:AQS 用了一个 volatile 的
state加一个 CLH 变体的双向等待队列,ReentrantLock用 state 表示重入次数。非公平锁在lock()时先直接 CAS 抢一次,抢不到才入队,所以新来的线程可能插队,吞吐更高但存在饥饿风险;公平锁走hasQueuedPredecessors()检查队列里有没有等更久的线程,严格 FIFO,代价是更多的线程切换。默认是非公平。 - 性能优化:把 Java 层聚合下推到数据库:原来可能是查回一批明细到 Java 里循环统计,数据量大时既费网络又费内存。下推之后用一次条件聚合(
sum(case when ... then 1 else 0 end))或者group by直接拿结果,网络传输从 N 行变成几行。前提是聚合能用上索引、不会产生大临时表,改完要对比EXPLAIN和实际耗时,别把 Java 的锅换成数据库的锅。