Java实习模拟面试实录:微服务OJ系统架构、JWT安全机制与Redis排行榜深度解析(实在智能-Java开发-实习一面)
关键词:Java实习面试 | 微服务OJ系统 | Docker沙箱安全 | JWT令牌管理 | Redis ZSet排行榜 | 线程池原理
引言
在准备Java后端开发实习岗位的过程中,模拟面试是查漏补缺、提升表达能力的重要环节。本文基于一次真实的“实在智能”公司Java开发实习生一面的完整问答记录,以结构化对话 + 专业解析的形式,还原面试官连环追问的逻辑,并结合专业知识给出高质量回答,帮助读者深入理解微服务、安全控制、缓存设计等核心知识点。
一、自我介绍
面试官提问:
请先做个简单的自我介绍吧。
回答:
您好!我是XX大学计算机专业的大三学生,目前主攻Java后端开发方向。在校期间参与过多个项目实践,其中最核心的是一个基于Spring Cloud Alibaba的微服务在线判题系统(OJ)。该项目支持多语言代码提交、Docker沙箱隔离执行、JWT鉴权、Redis实时排行榜等功能。我主要负责后端服务模块的设计与实现,包括判题服务、用户服务和排行榜模块。希望通过这次实习机会,进一步提升工程能力和对高并发、分布式系统的理解。
二、项目深挖:微服务OJ系统
Q1:介绍一下你的项目(微服务OJ系统)
回答:
我们的OJ系统采用微服务架构,整体划分为以下几个核心服务:
- 用户服务(user-service):处理注册、登录、JWT生成;
- 题目服务(problem-service):管理题目信息、测试用例;
- 判题服务(judge-service):核心模块,接收代码、调用Docker沙箱执行、比对输出;
- 排行榜服务(ranking-service):基于Redis ZSet实现实时排名;
- 网关服务(gateway):统一入口,路由与限流。
整个系统通过Nacos做服务注册发现,Sentinel做熔断降级,使用RabbitMQ解耦判题任务,保证高可用性。
Q2:Docker沙箱是每次提交代码都会创建新的吗?还是会复用?
回答:
我们采用的是每次提交都创建新容器的策略。虽然启动容器有一定开销,但为了强隔离性和安全性,必须确保每个用户的代码运行在完全独立的环境中,避免状态污染或恶意代码影响其他任务。
不过我们也做了优化:
- 使用轻量级基础镜像(如Alpine);
- 预拉取常用语言镜像(Java/Python/C++);
- 设置容器超时自动销毁(5秒执行上限)。
这样在保证安全的前提下,尽量降低资源消耗。
Q3:判题逻辑是怎么样的?最后样例输出是直接读取Docker沙箱的输出吗?
回答:
是的。判题流程如下:
- 用户提交完整代码(ACM模式,非力扣模板);
- 判题服务将代码、输入样例、时间/内存限制打包;
- 启动对应语言的Docker容器,挂载代码文件;
- 容器内执行编译 → 运行 → 重定向标准输出到指定文件;
- 容器退出后,宿主机读取输出文件内容;
- 与预期输出逐行比对(忽略末尾空格/换行差异);
- 返回判题结果(AC/WA/TLE/MLE等)。
注意:Docker内部不包含业务逻辑,只负责“黑盒执行”,所有判题规则都在宿主机服务中处理。
Q4:用户提交的代码里如果有恶意代码怎么办?
回答:
这是安全设计的重点。我们从多个层面防御:
- 资源限制:通过Docker的
--memory、--cpus、ulimit限制内存、CPU、文件句柄等; - 网络隔离:容器默认无网络(
--network none),禁止外联; - 文件系统只读:除代码挂载目录外,其余路径只读;
- 禁用危险系统调用:使用seccomp或AppArmor过滤syscall(如
fork,execve等); - 超时强制终止:超过5秒自动kill进程;
- 日志审计:记录所有提交行为,便于事后追溯。
即使用户写while(true)或尝试读取/etc/passwd,也会被系统拦截或超时终止。
Q5:项目支持多语言编程,是如何实现的?
回答:
目前我们采用的是ACM竞赛模式,即用户提交完整可运行程序(而非函数片段)。因此,每种语言都有对应的执行脚本模板,例如:
- Java:编译为
.class,用java Main运行; - Python:直接
python3 code.py; - C++:
g++ -o a.out code.cpp && ./a.out。
这些脚本预置在Docker镜像中。当用户选择语言后,判题服务会调用对应镜像,并传入用户代码。没有前端模板填充,因为不是力扣那种“补全函数”模式。
✅ 补充说明:若未来支持力扣模式,可在前端提供函数模板,后端拼接成完整程序再执行。
三、JWT安全机制深度探讨
Q6:介绍一下JWT
回答:
JWT(JSON Web Token)是一种无状态的认证令牌,由三部分组成:
- Header:算法(如HS256)和类型(JWT);
- Payload(Claims):用户信息(如user_id、role)、过期时间(exp)等;
- Signature:用密钥对前两部分签名,防止篡改。
优点:
- 服务端无需存储session,适合分布式系统;
- 可跨域传递;
- 自包含用户信息,减少DB查询。
缺点:
- 无法主动失效(除非引入黑名单);
- payload明文(需避免敏感信息)。
Q7:客户端A和B同时登录同一用户,B修改了用户信息,两个JWT会冲突吗?如何解决?
回答:
这是一个经典的令牌同步问题。传统JWT确实存在这个问题——因为它是无状态的,服务端无法感知用户信息是否变更。
解决方案:
可以在JWT的claims中加入一个“版本号”字段,比如user_version。每次用户关键信息(如密码、权限)变更时,更新数据库中的version字段。
验证JWT时,不仅校验签名和过期时间,还要比对当前数据库中的user_version是否与token中一致。如果不一致,说明用户信息已变更,要求重新登录或刷新token。
📌 面试官提示正是这个思路:通过claims携带校验标记,实现逻辑上的“令牌失效”。
四、浏览器Cookie机制澄清
Q8:浏览器中的Cookie是在什么时候触发携带、保存的?
回答(修正版):
Cookie的保存和携带是浏览器自动完成的,无需手动写JS代码(与localStorage不同)。
- 保存:当服务器响应头包含
Set-Cookie: token=xxx; Path=/; HttpOnly时,浏览器自动存储; - 携带:后续向相同域名+Path的请求,浏览器会自动在请求头加上
Cookie: token=xxx。
❗ 我最初误答成localStorage,这是错误的。LocalStorage需要手动
setItem/getItem,而Cookie是HTTP协议层的自动行为。
Q9:同一浏览器中不同网站的Cookie会冲突吗?
回答:
不会。Cookie的隔离基于域名(Domain)和路径(Path)。
a.com的 Cookie 不会被b.com访问;- 即使是子域,如
api.a.com和www.a.com,也需显式设置Domain=.a.com才能共享; - 浏览器通过同源策略严格隔离不同站点的Cookie。
这是Web安全的基础机制之一。
五、Redis排行榜设计详解
Q10:说说Redis一些常见命令
回答:
常用命令包括:
- 字符串:
SET/GET/INCR - 哈希:
HSET/HGET/HGETALL - 列表:
LPUSH/RPOP/LRANGE - 集合:
SADD/SMEMBERS/SINTER - 有序集合(ZSet):
ZADD/ZRANK/ZREVRANGE/ZSCORE
Q11:ZSet的原理是什么?
回答:
Redis的ZSet底层由两种结构实现:
- ziplist:元素少、成员短时,用压缩列表;
- skiplist + hash table:元素多时,用跳跃表(skiplist)维护排序,哈希表维护成员到分数的映射,实现O(logN)的插入、删除、排名查询。
跳跃表是一种概率型数据结构,通过多层索引加速查找,类似“链表的二分查找”。
Q12:为什么排行榜不用List/Set/Hash,而用ZSet?
回答:
- List:无法按分数排序,只能按插入顺序;
- Set:无序,不支持排名;
- Hash:可存分数,但无法高效获取Top N(需全表扫描);
- ZSet:天然支持按分数排序 + 快速范围查询 + 实时排名,完美匹配排行榜场景。
Q13:日排行榜、周排行榜如何实现?
回答:
我们采用多Key策略:
- 日榜:
ranking:day:20260127 - 周榜:
ranking:week:2026-W04 - 总榜:
ranking:total
每天凌晨通过定时任务(原计划用XXL-JOB,但因Bug未上线)将当日得分聚合到周榜、总榜。也可用Redis的EXPIRE自动过期日榜数据。
⚠️ 当前因任务模块出Bug,暂时由手动脚本处理,后续会修复。
Q14:排行榜数据有持久化到数据库吗?
回答:
核心数据会持久化。Redis作为缓存层提供高性能读写,但为防数据丢失:
- 用户每次得分变更时,异步写入MySQL的
user_score_log表; - 每日凌晨将Redis中的最终排名快照存入
daily_ranking表; - 系统重启时可从DB重建Redis ZSet。
做到缓存+持久化双保险。
六、微服务架构与多线程
Q15:项目整体划分为了哪些服务?
回答:
如前所述,共5个微服务:
- user-service(用户)
- problem-service(题目)
- judge-service(判题)
- ranking-service(排行榜)
- gateway(网关)
通过Feign调用,Nacos注册中心,配置中心统一管理。
Q16:项目用到多线程了吗?
回答:
业务逻辑中未显式使用多线程,但在某些工具类中了解过TransmittableThreadLocal(TTL)。
TTL用于解决线程池中ThreadLocal变量传递问题,比如在异步任务中传递用户上下文(如traceId、userId)。普通ThreadLocal在线程复用时会丢失上下文,而TTL通过装饰Runnable/Callable实现父子线程间的数据透传。
Q17:了解线程池吗?说一下核心参数
回答:ThreadPoolExecutor的核心参数有:
corePoolSize:核心线程数,即使空闲也不会被回收;maximumPoolSize:最大线程数;keepAliveTime:非核心线程空闲超时时间;workQueue:阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue);threadFactory:线程工厂;handler:拒绝策略(如AbortPolicy、CallerRunsPolicy)。
Q18:核心线程和非核心线程的区别?
回答:
- 核心线程:常驻线程,即使空闲也不销毁(除非
allowCoreThreadTimeOut=true); - 非核心线程:临时线程,空闲超过
keepAliveTime会被回收。
Q19:非核心线程是在阻塞队列满之后才创建的吗?
回答:
是的。线程池的执行策略是:
- 若当前线程数 < corePoolSize → 创建核心线程;
- 否则,尝试将任务放入workQueue;
- 若队列已满,且当前线程数 < maximumPoolSize → 创建非核心线程;
- 若线程数已达上限 → 触发拒绝策略。
所以,非核心线程只在队列满之后才会创建。
结语
这次模拟面试覆盖了微服务架构、容器安全、认证授权、缓存设计、并发编程等多个维度,既有广度也有深度。尤其在JWT令牌同步、Redis ZSet选型、线程池机制等细节上,暴露出知识盲区,也明确了后续学习方向。
💡建议:实习面试不仅考编码,更看重系统设计思维与问题解决逻辑。平时多思考“为什么用这个技术?有没有更好的方案?”,才能在连环追问中游刃有余。
欢迎关注我的CSDN主页,后续将持续更新Java后端面试实战系列!
如有疑问,欢迎评论区交流~