面试系列|蚂蚁金服技术面【3】

今天继续分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经,复盘面试过程中踩过的坑,整理面试过程中提到的知识点,希望能给正在准备面试的你一些参考和启发,希望对你有帮助,愿你能够获得心仪的 offer !

第二轮面试之后的后续,隔了几天没有通知结果。但是某天晚上接到一个电话,是另一位面试官,得知是简历被另一个部门捞起来了,应该是之前的流程没过,但是简历被其他部门捞起来了。这次约的是晚上 7 点,仍然是视频远程面试,下面是面试时语音实录复盘。


面试官:请简单介绍一下你的项目。
候选者:好的,balabala...(按照提前准备的进行答复)。


面试官:这个项目的定位是什么,和 xxx 有什么区别?
候选者:xxx 平台更偏向,而 xxx 主要面向。


面试官:单点登录(SSO)有哪些实现方案?
候选者:
1)基于 JWT(客户端存储 Token,服务端解析)。
2)基于浏览器 Cookie(共享 Cookie 进行身份验证)。
3)基于 Session(服务端共享 Session)。
4)基于 SSO 网关(所有请求经过网关统一认证)。


面试官:统一鉴权怎么做?

候选者: 通过浏览器 CookieHeader 获取 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() 方法,获取当前线程的变量值,确保变量是线程私有的。

java 复制代码
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 消息存储,避免数据丢失。

java 复制代码
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(基于策略的访问控制):基于策略定义权限。


📢 如果对你有帮助的话,还请帮忙点赞 + 收藏!!!(谢谢!!!)

相关推荐
Lee川5 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i7 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有7 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有7 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫8 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫8 小时前
Handler基本概念
面试
Wect9 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼9 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼9 小时前
Next.js 企业级落地
前端·javascript·面试
掘金安东尼9 小时前
React 性能优化完全指南 2026
前端·javascript·面试