今天继续分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经,复盘面试过程中踩过的坑,整理面试过程中提到的知识点,希望能给正在准备面试的你一些参考和启发,希望对你有帮助,愿你能够获得心仪的 offer !
第二轮面试之后的后续,隔了几天没有通知结果。但是某天晚上接到一个电话,是另一位面试官,得知是简历被另一个部门捞起来了,应该是之前的流程没过,但是简历被其他部门捞起来了。这次约的是晚上 7 点,仍然是视频远程面试,下面是面试时语音实录复盘。
面试官:请简单介绍一下你的项目。
候选者:好的,balabala…(按照提前准备的进行答复)。
面试官:这个项目的定位是什么,和 xxx 有什么区别?
候选者:xxx 平台更偏向,而 xxx 主要面向。
面试官:单点登录(SSO)有哪些实现方案?
候选者:
1)基于 JWT(客户端存储 Token,服务端解析)。
2)基于浏览器 Cookie(共享 Cookie 进行身份验证)。
3)基于 Session(服务端共享 Session)。
4)基于 SSO 网关(所有请求经过网关统一认证)。
面试官:统一鉴权怎么做?
候选者: 通过浏览器 Cookie 或 Header 获取 Token,解析身份信息。在 Gateway 统一设置线程上下文,让后续服务能直接获取用户身份。
面试官:你提到了线程上下文,那么线程上下文可能遇到什么问题,有遇到过吗?
候选者:(1)线程污染问题:在后端 HTTP 请求通常由线程池处理,比如 Tomcat 或 Netty 都会复用线程池中的线程来处理请求。例如请求 A 进入后端,线程池分配 线程 T1 处理它,A 的请求过程中设置了 线程上下文(ThreadLocal 变量),但没有在请求结束时清理。请求 B 进入后端,线程池又分配到 线程 T1,由于 T1 之前被请求 A 污染,B 可能会获取到 A 的数据,导致数据错乱或权限问题。
候选者:(2)线程上下文参数传递问题:使用 ThreadLocal 时,线程上下文参数比如租户ID存储在当前线程中,无法传递的一个子线程中,比如有异步的逻辑会进行日志记录、消息发送,任务触发等操作,这个时候线程上下文拿不到这个参数会导致业务出错。
候选者: 针对上面连个问题的解决方案。(1)针对线程污染问题可以通过统一拦截的清理机制,或者手动清理,确保 finally 里 clear() 线程变量;(2)线程上下文父子线程之间传递的问题可以通过使用阿里一个 TTL(TransmittableThreadLocal) 库解决线程池透传问题。TTL 通过增强 ThreadLocal 机制,可以保证 父线程的 ThreadLocal 变量正确地传递给线程池中的子线程,即使子线程是复用的。
面试官:你提到 ThreadLocal ,那么 ThreadLocal 是怎么实现的?
候选者:每个 Thread 维护一个 ThreadLocalMap,Key 是 Thread.currentThread() 对象,Value 是存储的变量值。线程通过 ThreadLocal 的 get() 方法,获取当前线程的变量值,确保变量是线程私有的。
private T get(Thread t) {ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T) e.value;return result;}}return setInitialValue(t);}private void set(Thread t, T value) {ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}
面试官:实现完整的多租户需要哪些方面的改造?
候选者:租户管理大致需要租户创建、分配资源、连接池(PgBouncer)、License 限制。
(1)数据库隔离:物理隔离(独立库)、逻辑隔离(租户 ID 过滤)。
(2)访问控制:租户的访问地址、权限模型。
(3)数据层:MyBatis AbstractRoutingDataSource 自动路由数据源。
面试官:你提到了你尝试几种 SaaS 化实现,哪种 SaaS 改造方式比较好?
候选者:
(1)小规模:逻辑隔离(共享数据库,按业务表添加租户ID字段区别,或者按照租户ID分表)。
(2)大规模:物理隔离(独立数据库),提高安全性和扩展性。
(3)再大规模:多租户集群 + 读写分离 + 连接池优化(PgBouncer)。
面试官:假设未来要支持 1 万个租户,如何优化资源使用?你上面说的一个租户一个库的方案有什么问题?
候选者:(这里感觉掉坑里了,当时没有答好,此前说了几种方式,但是没有仔细思考里面的问题,下面梳理复盘整理如下)
(1)数据库实例管理成本高: 1 万个租户 = 1 万个数据库,数据库的运维、备份、监控成本极高。
(2)资源利用率低: 许多租户可能业务量小,独立数据库的大量资源处于闲置状态,造成浪费。高负载租户可能会遇到资源不足的问题,扩展性差。
(3)数据库连接数受限: 数据库的 最大连接数 是有限的,例如 MySQL 默认最大连接数 151,无法同时支持大量租户的并发访问。需要使用连接池(如 PgBouncer 或 HikariCP),但连接数仍然受物理资源限制。
(4)Schema 变更困难: 1 万个数据库要同时执行 DDL 变更(如表结构调整)是非常麻烦的,升级难度高。需要设计 数据库版本管理 机制,避免版本不一致导致问题。
面试官:介绍一下二级缓存(Redis + 内存缓存)解决了什么问题?
候选者:二级缓存(Caffeine + Redis)可优化使得本地缓存命中率高,减少 Redis 压力(Redis 连接、网络I/O消耗)。
面试官:二级缓存如何保证数据一致性?
候选者:(1)分布式锁(RLock):确保缓存更新时并发安全。(2)事件订阅(Redis Key 过期监听):数据更新后删除内存缓存。
面试官:Redis 集群有使用过吗?
候选者:使用过 Redis Cluster,采用分片存储,提高高可用性,支持 主从同步 + 哨兵模式 保证故障恢复能力。
面试官:讲一下如何使用异步线程?
候选者:(1)新建线程(new Thread)。(2)Spring @Async 注解,基于线程池执行。(3)线程池(ThreadPoolExecutor),支持任务队列、线程回收。
面试官:详细讲一下 @Async,有看过它的源码吗?
候选者:(光记得拿来就用,后悔没有看过…)整理一下:@Async 依赖 TaskExecutor 线程池,默认是 SimpleAsyncTaskExecutor,可通过 @EnableAsync 配置 ThreadPoolTaskExecutor。
面试官:自己实现一个阻塞队列,如何设计?说一下思路
候选者:(G,开始上强度了)(1)基于 CAS 设计无锁队列,避免线程竞争。(2)使用 ReentrantLock + Condition 实现阻塞队列。(3)采用 LinkedBlockingQueue 支持 FIFO 消息存储,避免数据丢失。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class MyBlockingQueue<T> {private final Object[] items; // 用数组存储队列元素private int count, head, tail; // count 记录当前队列元素个数,head 头指针,tail 尾指针private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition(); // 队列满时,生产者等待private final Condition notEmpty = lock.newCondition(); // 队列空时,消费者等待public MyBlockingQueue(int capacity) {items = new Object[capacity]; }// 生产者入队(如果满了,阻塞)public void put(T element) throws InterruptedException {lock.lock();try {while (count == items.length) { // 队列满,生产者阻塞等待notFull.await();}items[tail] = element;tail = (tail + 1) % items.length; // 循环队列,防止数组越界count++;notEmpty.signal(); // 通知消费者可以消费了} finally {lock.unlock();}}// 消费者出队(如果空了,阻塞)@SuppressWarnings("unchecked")public T take() throws InterruptedException {lock.lock();try {while (count == 0) { // 队列空,消费者阻塞等待notEmpty.await();}T element = (T) items[head];items[head] = null; // 释放对象,防止内存泄漏head = (head + 1) % items.length; // 维护循环队列count--;notFull.signal(); // 通知生产者可以继续生产了return element;} finally {lock.unlock();}}// 获取队列当前大小public int size() {lock.lock();try {return count;} finally {lock.unlock();}}
}
面试官:RBAC、ABAC、PBAC 的区别?
候选者:(1)RBAC(基于角色的访问控制):用户 → 角色 → 权限。(2)ABAC(基于属性的访问控制):通过用户、环境、资源等属性控制权限。(3)PBAC(基于策略的访问控制):基于策略定义权限。
📢 如果对你有帮助的话,还请帮忙点赞 + 收藏!!!(谢谢!!!)