面试系列|蚂蚁金服技术面【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(基于策略的访问控制):基于策略定义权限。


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

相关推荐
JQShan4 分钟前
iOS符号表:崩溃日志中的“翻译官”,开发中的隐藏高手
面试·debug·swift
Java中文社群1 小时前
拿下美团实习~
java·后端·面试
lovebugs1 小时前
CAS是什么?AtomicInteger如何利用它?ABA问题如何解决?
后端·面试
小巫编程室1 小时前
快速入门-Java Lambda
java·后端·面试
盖世英雄酱581362 小时前
三五年前面试java都问的是什么问题
java·后端·面试
雷渊4 小时前
mybatis底层为什么设计二层缓存?
java·后端·面试
渔樵江渚上5 小时前
深入理解 ArrayBuffer、TypedArray 和 DataView
前端·javascript·面试
myyyl5 小时前
如何使用for of 遍历普通对象?
前端·javascript·面试
uhakadotcom6 小时前
uvloop让你的异步代码速度提升400%,实战讲解与代码示例
后端·面试·github