面经总结
万能的 STAR 原则
- Situation(背景):做这个项目的背景是什么,比如这是个人项目还是团队项目,为什么需要做这个 项目,你的角色是什么,等等。
- Target(目标):该项目要达成的目标是什么?为了完成这个目标有哪些困难?
- Action(行动):针对所要完成目标,你做了哪些工作?如何克服了其中一些困难?
- Result(结果):项目最终结果如何?有哪些成就?有哪些不足之处可以改进?
如何通俗的讲述你的研究成果
WCL
无线通信系统正在逐渐发展发展到第6代(6G),其中一个关键技术叫“超大规模天线系统”(XL-MIMO),可以理解为基站上装了非常多的天线,能同时与大量的用户通信,大大提升通信速度和容量。 但天线太多也带来了新问题:用户与基站之间的通信由于阵列尺寸增大变为近场通信;由于天线阵列非常大,有些天线能收到某个用户的信号,有些则收不到,这叫“空间非平稳性”。 所以我们面临一个难题:如何在基站天线非常多、用户位置复杂、且每个用户只能被部分天线“看见”的情况下,准确估计出信道状态?
以前的工作大都基于是数字接收机,能耗与计算复杂度比较高,我们首次在混合接收架构下处理近场+非平稳信道。
我针对两种不同的场景提出了不同的信道估计算法,
- 在天线阵列分组已知时:由于近场效应导致信号模型复杂,设计了一个极域变换矩阵,采用“极域码本”将连续位置离散化,把信道估计问题转化为稀疏信号恢复问题,就像从一堆模糊的信息中找出最关键的那几项。 然后我设计了一个“两级匹配追踪算法”,先找用户大概在哪个方向和距离,再判断哪些天线组能“看见”这个用户。
- 不知道天线是怎么分组的:信道估计非常困难,采用分治的思想。先用“变分块稀疏恢复算法”初步猜测哪些天线能收到信号。 然后基于“多数表决+枚举”的方法,生成几种可能的“可见区域”候选。 对每一种候选,都用匹配追踪法估计信道,最后选效果最好的那个作为最终结果。
所提出的算法克服了计算复杂、模型误差、分区未知等挑战。通过大量仿真验证了所提方法的有效性,提出的方法性能优于现有算法,尤其在低信噪比下优势明显。为6G通信的实际部署与应用提供了重要理论技术支撑。
模型误差。 用有限的“位置库”网格去近似连续的信号位置,会有误差(网格不匹配)。 解决: 在已知分区方案下,我引入了梯度下降微调(Off-Grid Refinement)步骤,对初步定位结果进行修正,显著提高了精度。
OJCOM
未来的手机基站(比如在6G时代)会装备成百上千根天线(比现在的4G/5G多几十倍),就像一个巨大的“天线墙”。这能带来超高速率和连接更多用户的好处。 但天线太多也带来了新问题:用户与基站之间的通信由于阵列尺寸增大变为近场通信;由于天线阵列非常大,有些天线能收到某个用户的信号,有些则收不到,这叫“空间非平稳性”。
我的目标就是如何高效的指挥调度所有的天线,把信号精准的发送给每个用户,通过避免不同天线之间的相互干扰。 传统的方法(混合波束成形)相当于给基站配一个昂贵的、高精度的调度器(数字处理器),成本高、能耗大。 我们则利用“空间非平稳性”特性来设计这种用户-天线配对开关网络来取代了传统方案中数字波束成形器。 这个配对网络本质上是一个“开关系统”,它只需要进行简单的接通和断开操作。这使得我们可以使用非常廉价、低精度的数模转换器,降低成本与功耗。 并且既然每个用户本来就只能看到部分天线,那我就干脆只把这些能“看见”用户的天线分配给他,物尽其用,避免了资源的浪费。
进一步设计了一套复杂的联合优化算法,像一位超级调度员,同时做三件事:
- 功率分配:给每个用户分配多少功率(就像调节每盏灯的亮度)。
- 模拟波束成形:调整天线发射信号的相位(对准用户)。
- 用户-天线配对:决定哪一组天线为哪一个用户服务(操作开关网络)。
这三个问题(功率、角度、配对)相互关联,牵一发而动全身,构成了一个非常复杂、非凸的优化“死结”,传统方法根本无法直接求解。 我采用了 “分而治之,逐个击破” 的策略,也就是 “交替优化” 框架。 先固定其中两个变量,去优化第三个;然后再固定另外两个,优化下一个……如此循环迭代,直到系统性能达到最佳。 对于每一个子问题的求解,分别采用专门的凸优化算法
- 黎曼共轭梯度法:专门解决带相位约束的模拟波束成形问题。
- 投影梯度上升法:高效地进行功率分配。
- 数学规划与均衡约束方法:处理复杂的开关配对问题。
通过大量的计算机仿真,我们验证了所提出方案的有效性。我们提出的新架构,在性能上虽然比理想的传统方案有差距,但成功实现了用极低的硬件成本完成通信任务的目标,为实际部署提供了重要参考。 我们设计的优化算法,其性能远超“随机配对”、“平均分配功率”等简单方案,证明了我们算法的智能性和有效性。 们不仅设计了追求系统总速率最大化的算法,还设计了保证每个用户最低服务质量的“最大最小公平性”算法,确保了所有用户的基本体验。
我的这项研究,就是为未来6G的超大规模MIMO系统,设计了一套成本低、能耗低的“信号调度方案”。 通过硬件架构的创新和核心算法的攻关,解决了大规模系统部署中成本和复杂度的关键难题。 这项工作是通往实用化、低成本6G通信的一次有价值的探索。
INFOCOM
在未来巨大的智能工厂中,会有成千上万台设备(比如传感器、机器人)需要随时无线连接到基站终端,这些设备数量巨大(海量用户),但每个设备发送数据的时刻是随机的、短暂的(突发性)。
- 传统方式像“打电话”:每台设备需要先拨号、申请,等基站同意后才能通话。设备一多,申请通道就会堵塞,效率极低。
- 我们的方式:所有设备在设置的频段和时间,直接“发送信号,基站需要同时听清并分辨出:1)是谁在唱?(用户检测)2)他们各自站在什么位置、声音听起来怎么样?(信道估计)
这就是免授权随机接入,是实现海量设备高效连接的关键。 未来6G发展的一项关键技术是超大规模MIMO,可以理解为基站上装了非常多的天线。 但天线太多也带来了新问题:用户与基站之间的通信由于阵列尺寸增大变为近场通信;由于天线阵列非常大,有些天线能收到某个设备终端的信号,有些则收不到,这叫“空间非平稳性”。 因此,我研究的核心问题就是当海量设备随机发送接入请求时,在这个充满遮挡的、复杂的环境下,基站如何准确识别发送信号的每个终端及其精确位置,并正确接收它们的数据。
我将这个复杂的联合检测与估计问题,转化成一个寻找“最稀疏解”的数学问题。我们发现,最终需要求解的信号具有三层稀疏性。
传统的解决方法要么太耗电(需要为每根天线配一个昂贵的接收器),要么没有充分考虑“近场效应”和“部分遮挡”这两个新特性,导致识别精度不高。 我们同时考虑节能混合接收架构、近场球面波和空间非平稳(部分遮挡) 的复杂环境下,系统地用“三层稀疏”模型来刻画免授权随机接入问题的研究。
这个联合优化问题极其复杂,三个维度的变量(谁、在哪、被谁看见)纠缠在一起,像一个死结。采用分治思想。利用信号的三重稀疏性,将这个死结拆解成多个可以顺序或迭代解决的小问题。 基于此,通过对三层稀疏性的不同利用,我设计了两种高效的基于正交匹配追踪的信道估计算法,让基站能快速完成这场复杂的辨认任务。
- 算法一(Turbo-OMP,涡轮增压匹配追踪):像一个同步进行的多线程处理器。它同时、反复地猜测“谁在唱?”和“他在哪?”,每猜对一点,就立刻用这个新信息去优化另一个猜测,如此循环,像涡轮增压一样快速逼近最终答案。
- 算法二(TS-OMP,两阶段匹配追踪):像一个分步执行的流水线。第一阶段先集中精力把所有正在“唱歌”的人找出来;第二阶段,再针对这些被找出来的人,逐一精确估计他们的位置和信道状态。
通过大量的计算机仿真,我们验证了所提出方案的优越性,我们提出的两种算法,在用户检测成功率和信道估计精度上,均优于现有的主流算法。 为未来6G支持海量低功耗物联网设备的大规模、高可靠、低时延接入提供了重要的技术支撑。贡献了一个可行的技术方案。
TVT
在未来的智慧城市或智能工厂,里面有数不清的小型设备(比如环境传感器、设备状态监测器)。这些设备平时“沉默”,但会在需要时(比如检测到异常)突然“喊”一小段数据给基站。它们数量庞大(潜在用户K很多),但同一时间真正在“喊话”的只是少数(活跃用户Ka远小于K),且“喊话”时刻是随机的、短暂的。 为了覆盖这么多设备,未来基站会装备几百甚至上千根天线,像一面巨大的“天线墙”。这能提供超大容量。天线太多也带来了新问题:用户与基站之间的通信由于阵列尺寸增大变为近场通信;由于天线阵列非常大,有些天线能收到某个设备终端的信号,有些则收不到,这叫“空间非平稳性”。
为了简化流程和节省信令开销,我们采用“免调度”方式:设备想发数据就直接发,不用先申请“通话许可”。所有设备共享一个公共的“口令本”(码本)。设备发送数据时,其实是发送这个数据对应的“口令”(码字)。但这就带来一个问题:多个设备可能碰巧选了同一个“口令”(码字碰撞),基站需要能分辨出来。
因此,核心问题就是这个复杂的通信环境下,面对海量设备随机消息发送,基站如何快速分辨消息,识别消息的传播环境,解决码字碰撞问题, 以及最重要的如何把同一个设备在不同时间段喊的多个“口令片段”拼成完整的“一句话”。
研究发现:一个设备的位置(决定了其VR和角度)在短时间内是基本不变的!这就像一个人在不同时间喊话,他的声音方向和能被听到的区域是相对固定的。
我设计了一套完整的解决方案,核心是利用设备角度信息和信号传播的稳定性(VR和角度不变性)来简化问题。 设计了CCA-TS-OMP 算法,在每个短的通信时间段(时隙)内,识别出哪些“口令”被喊了(哪些码字活跃),并弄清喊这个口令的设备的声音特征(信道信息,包含VR和角度信息)。 通过分析不同设备”口令“的声音特征,自动将他们区分开,并分别记录下他们想传递的信息。
接着基于 K-Medoids 聚类的算法,利用设备位置(VR/角度)的天然稳定性作为“粘合剂”来拼接消息,比传统依赖复杂编解码规则的方法更直接、高效,尤其适合设备众多的场景。
通过大量的计算机仿真,我们提出的方案显著降低了错误率,有效解决了码字碰撞的问题。 这套方案有望显著降低海量设备连接的复杂度与开销,为实现“万物智联”提供了有力的技术支撑。
反问
- 如果能顺利入职咱公司,对我的期望是如何?
- 若能有幸加入团队,有什么好的建议让我可以在入职前作准备的
- 请问咱们一共几轮面试呢,大概多久可以有结果呢?
- 想了解一下贵公司的人才培养计划怎么培养刚入职的新人
- 可以再详细介绍一下这个岗位的具体职责吗?
- 公司/部门的氛围如何?工作节奏怎么样?
- 工作时间?工作地点?加班and出差情况?通勤是否方便?办公环境如何?试用期多久?转正要求是什么?试用期工资/年终奖情况?
- 如果有幸入职,我在工作上需要准备些什么呀?
- 从您的角度而言,您认为贵公司该岗位最重要的一个或多个特质是什么呀
- 你觉得对于该岗位,更希望面试者具备哪些能力呢,面试者最重要的特质是什么
- 未来有哪些发展机会
- 您认为作为新入职的员工, 前期应该注重哪些能力的培养和提升呢
- 作为新入职的应届生, 是会从基础业务切入工作, 还是会直接接触到核心业务与新的模块
- 晋升与培训机制是怎样的,试用期和转正的考核标准,什么时候出结果
HR面
你专业是通信,为什么想去后端开发的工作呢
你觉得你遇到过的最难的问题,是怎么解决的
在做课题研究时,遇到的一个问题,当时的场景要求得出一个三维变量耦合的非凸问题的最优解,这个问题的难点主要体现在两方面
- 非凸问题无法求得最优解
- 三维变量耦合带来的复杂性
对于第一个问题,通过查找凸优化领域的论文和书籍,总结出几种将非凸问题转换为凸问题的方案, 结合所研究问题的稀疏性特点,与导师交流探讨,选择能够使得转换得到的凸优化问题最简洁的方案。
其次对于多维变量耦合的凸优化问题,无法直接求得闭式解并且复杂度很高,采用分治的思想,将三个变量解耦,拆分为四个子问题分别进行优化。
查询凸优化领域的文献,根据每个子问题的特点,以保证算法的高性能和低复杂度为目标,归纳选择最适合的凸优化算法。最后通过数值仿真验证所提出算法的复杂度和收敛性, 并且基于仿真结果与现有算法对比,查找性能不足的地方,逐步的优化、修改算法参数,逐步完善和改进现有的凸优化算法,实现了最适合我们所研究问题的凸优化算法。
最后对整个问题的算法方案进行归纳总结,阐述与现有算法的优劣势,以及后续可以进一步改进的目标。
讲述一次团队协作经历
在本科的一次课题项目中,我们是六人小组,我主要承担项目经理和机械外观组装的工作。仅解决了中途的人员突发问题,还让项目在 6 个小组里拿到了第三名的成绩。 首先是项目刚开始是,开了整体的会议,按照每个人所擅长的工作进行分工,并根据工作的难易程度,进行了贡献度的初步分工。并且约定每两周开一次总结会。商定每个人的工作交付时间为截止前两周,这样还有剩余时间可以做改善和优化。 在距离截止时间还剩三周时,我在了解组员的工作进度时,有一位负责软件的组员没能按时完成工作,并且告诉我因为某些原因要退出。
这时候我面临的核心任务(任务 T),一方面是不能让项目停滞,得快速补上空缺的电路任务;另一方面还要稳定其他组员的情绪 —— 毕竟突然多出来的工作量很容易引发抱怨,影响团队凝聚力。 这时我做了三件关键的事,先和该组员沟通原因,确认到确实无法完成了,并且明确告知他的工作贡献度会缩减,他也明白。也就没有继续勉强他。
而是召集其他组员,开了一个短会,坦诚告知情况,现在离截止只剩 3 周,大家一起看看怎么解决。并根据每个人的能力和剩余工作对该组员的工作进行了分工。
由我每三天去探讨一次各位组员的工作情况,看有没有遇到什么困难,并提供对应的帮助。并约定截止日期最后一周,我安排好会议室,大家每天抽出两到三个小时一起完成所有未完成工作和做进一步的优化和改进。
整个过程没人抱怨,反而因为一起扛过压力,大家配合更默契了。
最后结果也很顺利,成功完成了所有的任务,并且拿到了总评第三的成绩。
有两个很深的体会:一是团队协作里 “快速解决问题比纠结原因更重要”,遇到组员退出时,不指责、不内耗,而是聚焦 “怎么补位”,才能稳住团队节奏;二是作为协调者,既要 “看得见每个人的压力”,主动分担,也要 “盯得住整体目标”,通过细化进度让大家有方向感。
展示开源项目,讲实现思路;有没有从 0 到 1 的项目经验
在项目开发和日常使用中考虑到传统模型的痛点:幻觉、知识截止、以及不够灵活的问题,构建一个可配置、可编排的 AI Agent 智能体平台,实现为业务提效(例如需求文档分析、文档资料编写(+消息通知)、ELK 日志检索、监控日志分析) 等功能。
- 学习了 AI Agent 开发相关的基础知识和开发框架,通过博客文章和论文。规划出 AI Agent 开发中的重要组件: RAG 知识库、 MCP、提示词、模型。
- 首先是关于知识库的搭建,使用 Apache Tika 解析多种格式文件(MD、TXT、 SQL),结合 TokenTextSplitter 按 token 分块。通过 PgVectorStore 将文本块向量化并存储,添加对应的知识库标签。 在用户提问时,根据知识库标签检索相关文档, 与用户问题为提示词生成回答。
- 实现了支持文件操作、 CSDN 发帖、微信通知等功能的 MCP 工具,并做了初步验证,考虑到 stdio(本地工具)传输模式的局限性,又实现了 SSE(远程服务) 传输。 通过 Spring 的 ToolCallbackProvider 将 MCP 工具注册为 Bean,供 Agent 调用。
- 针对底层大模型,最初使用 Ollama DeepSeek 开源模型,由于响应数据较慢,购买了 OpenAI 的 tokens,改用 chat-4.1-min 与 chat-4o
- 针对提示词的设计,主要遵循以下原则:
- 清晰的指令
- 提供上下文和例子
- 善用符号与语法
- 让模型一步一步的思考
- 激励模型反思和给出思路
- 给容错空间
- 让模型给出信息来源
- 初步生成后,再通过 AI 优化提示词设计。
- Agent 动态编排与执行引擎
- 利用 Spring 的 BeanDefinitionRegistry 动态注册客户端、模型、顾问组件,实现配置热更新。通过 DynamicContext 在节点间传递数据,避免重复查询数据库。
- 使用 Spring AI 的
Flux<ChatResponse>实现流式回答,结合本地缓存记录 - 对话历史(Session ID 绑定上下文)。通过 PromptChatMemoryAdvisor 维护对话记忆,支持多轮追问。
- 通过责任链模式构建执行链路: RootNode(数据加载) → AiClientToolMcpNode(工具注册) → AiClientAdvisorNode(顾问注册) → AiClientModelNode(模型绑定) → AiClientNode(执行)。通过策略工厂实现不同执行策略, 根据 Agent 类型(自动/流程/智能决策)选择执行策略。 采用**“思考( Think) → 行动( Act) → 观察( Observe) → 判断(完成/继续) ”**的循环逻辑。 设计任务分析、精准执行、质量监督、执行总结四步骤。
- 扩展优化
- 通过 one-api 网关统一模型接口。实现多模型的适配。 通过 “统一入口接收请求 → 内部适配模型差异 → 统一格式返回结果” 的逻辑,彻底屏蔽了不同 AI 模型的接口、参数、认证差异,让开发者 / 应用能 “用一套代码,调用所有模型”。
- 对于向量库采用 MSTG 索引算法, 结合传统树算法与图算法的优势 ,在保证精度的同时显著降低资源消耗。
- 采用父子分段模式作为数据分块策略 ,并在向量检索时增加重新排序步骤。
- 针对不用的模型与应用场景还需要优化设置合理对应的提示词。
- 增加规划分析决策链路: 类似**“人类解决问题的过程”**:遇到问题时,先思考方案(Think) → 尝试行动(Act) → 观察结果是否符合预期(Observe) → 若不符合,再思考新方案(循环),直到问题解决或放弃。对于热点知识,可以采用 Redis 缓存,减少数据库频繁检索的压力
- 增加 API 鉴权: Nginx 层 JWT Token 校验或者用户密码登录,限制 IP 白名单。
挑一个项目中你遇到的技术挑战,说一下整体的解决思路
抽奖项目中的高并发场景下的库存管理
在抽奖场景下,高并发请求可能导致库存超卖。传统数据库行锁在高并发下会成为瓶颈。使用 redis 缓存数据库数据,在缓存中实现库存扣减。最初采用了 redis 独占锁,但这样会出现排队问题,导致有库存,但吞吐量不佳。 修改设计为颗粒度更小的近似无锁化设计。采用 redis 的 decr 操作实现库存扣减,考虑到在临界状态、库存恢复、异常处理等情况下的超卖问题,对每次扣减后的库存状态进行setnx 加锁兜底。来保证不超卖。 库存扣减在缓存中完成,因此要维护 Redis 缓存与 MySQL 数据库的一致性。通过延迟队列+定时任务的方式,在库存成功扣减后,发送消息到延迟队列中,有定时任务进行消费更新数据库状态,通过唯一 ID 保证幂等。 在高并发场景下,缓存库存可能很快耗尽,而此时可能数据库只同步了几条数据。因此,针对缓存快速耗尽的情况,增加判断条件,当缓存库存为 0 时,通过消息队列发送消息,直接更新数据库库存为 0,并清除延迟队列中剩余的待处理消息。减少数据库的更新操作,提升性能。
考虑到消息可能发出后由于网络波动等原因,还没来得及消费就丢失了,因此我们增加 task 任务表,记录每条消息及其状态,只有消息成功消费了,才会更新状态为已使用,通过定时任务遍历任务表,对于未使用分消息进行重复发生,防止丢失。
AI Agent 系统设计与架构挑战
Agent 执行链路需支持固定流程、循环调用、动态决策等多种执行模式,逻辑分支复杂。 将复杂流程拆解为独立节点(如 MCP 工具、RAG 顾问),通过责任链模式组合。采用多种设计模式,对执行流程进行解耦和实现。
针对组件间的依赖关系管理复杂和 Spring 容器生命周期管理问题。 通过 AbstractArmorySupport 提供 Bean 动态注册能力 ,使用 DynamicContext 在节点间传递数据,避免重复查询。
通过 Flux 实现流式传输,提升用户体验。考虑到需要处理长连接下的上下文同步问题(如用户追问时传递历史对话)
- 前端:利用 localStorage 存储对话历史(如
localStorage.setItem('chatHistory',JSON.stringify(history)))。 - 后端:通过 CHAT_MEMORY_CONVERSATION_ID_KEY 参数绑定对话 ID,结合 Spring AI 的 MessageWindowChatMemory 实现会话记忆。
为了实现可自由编排的 AI Agent 构建,将组件参数信息配置到数据库中,通过 flowgram.ai 框架,前端实现了一个基于流程图的可视化拖拽界面,后端采用多表结构存储拖拽配置信息, 通过 IAiAgentDrawConfigDao 接口提供数据访问能力。 使用 DrawConfigParser 工具类解析配置数据,提取节点和边的信息,执行数据验证和转换。
你对未来第一份正式工作的期待是什么?
- 期待 “有挑战的成长”:在真实业务中夯实技术能力,建立后端开发的核心竞争力, 希望能参与核心系统的开发与优化
- 希望能参与核心系统的开发与优化: 希望开发的功能能被真实使用并产生反馈,
- 期待 “有温度的协作”:在开放扁平的氛围中,培养 “团队型开发者” 的意识
这就是我对第一份工作的期待,也是我选择xxx的核心原因。
未来的职业发展
- 积极了解xxx的技术栈、开发流程和团队文化,快速理解部门的核心业务逻辑。全力以赴完成所负责企业业务提效相关业务的开发,打下扎实的业务和技术基础。
- 主动去发现研发流程中的效率瓶颈或系统隐患。争取能够独立负责一个中等复杂度的模块或项目,从技术方案设计、跨团队沟通协调,到最终上线和复盘,全程主导。并且不断了解与学习前沿技术,应用到实际的业务开发中。
- 争取参与或主导部门内某个重要系统的架构演进或重构。能够站在更高维度,思考如何构建更稳定、更高效、更能支撑未来业务发展的技术平台。
从三个因素来说如何选择职业方向
- 行业的长期价值,行业的增长空间直接决定职业天花板。帆软所处的 BI 行业正处于高速增长期,帆软的产品(如 FineBI)能帮助企业挖掘数据价值,成为政策红利的直接受益者;同时,帆软积极布局 AI+BI(如 FineChatBI),紧跟大模型技术浪潮,确保在技术革新中占据先机。
- 优质平台赋能个人成长,企业的竞争力决定个人成长的加速度。帆软的产品矩阵(FineBI、FineReport 等)在细分赛道占据绝对优势,技术人员能参与前沿产品的研发,提升核心技术能力;
- 职业选择需与个人特质、能力和价值观深度契合,企业价值观(如公平、务实)与个人理念一致,能减少职业内耗,提升工作幸福感。企业的晋升通道与个人职业目标匹配,能避免陷入 “一眼望到头” 的困境。
如何学习技术
- 目标驱动:明确”为什么学“,避免 “盲目学新技术”,根据职业阶段定目标。
- 输入:选择高质量学习资料,以官方文档和经典书籍为理论基础,了解开源项目与技术博客上的内容。
- 实践:“边学边练” 是关键:了解理论知识后,尝试做测试与项目实践,主动接手项目中的技术难点,debug跟踪新技术或新特性的运行过程,了解源码,理解核心原理
- 输出:倒逼知识体系化,将学习笔记整理为技术文章发布到博客上,可以以做一些技术分享,可以通过他人的提问发现知识漏洞。参与开源项目,提升实战能力。
- 复盘:定期总结优化,对于新技术定期做总结与优化,复盘学习经历,总结成学习经验。
比较困难、有挑战性的一件事
科研经历讲述
文献综述
系统梳理了XXX(超大规模MIMO/信道估计/下行预编码)近五年的研究成果,通过对比分析识别出四个未被覆盖的研究方向,提炼出需要解决的问题及原因。 为整个课题设计提供了核心依据。 该思路被纳入最终项目方案
实验/数据分析
在通过仿真验证方案有效性时,发现性能结果与预期有较大出入,偏差在15%,通过排查仿真参数,调整算法收敛阈值,优化算法的执行流程,最终将方案的性能得到很大提升,推导得出标准的阈值计算公式。 并形成可复用的算法仿真流程。
课题组项目
作为核心成员深度参与国自然基金项目,独立负责超大规模MIMO的信道估计和下行预编码,参与编写国自然基金项目申请书。取得了不错的成果。
电科研究所面试
自我介绍
面试官您好,我叫张修宇,来自江苏徐州,本科和研究生阶段均就读于西安电子科技大学。在校期间成绩优异,本科期间班级排名第6,研究生专业排名前2%。我系统掌握了无线通信的基础知识,熟悉稀疏信号处理和通信相关的凸优化方法,熟悉超大规模MIMO和近场通信等技术。熟练使用仿真工具 MATLAB 进行进行数值仿真辅助研究,能够独立完成通信系统建模与性能评估。熟悉Java语言,能够独立完成小型项目的开发。 本硕期间多次获得奖学金,本科荣获优秀共青团员和优秀军训学员称号。 硕士期间的研究方向是6G 的关键技术超大规模MIMO的信道估计和预编码。并以第一作者身份发表论文三篇,在投论文两篇。其中SCI论文三篇,CCF A类会议一篇,EI检索会议1篇,均为通信领域高水平期刊和会议。 在科研过程中,我不断提升抗压能力与问题解决能力,逐步形成了严谨、专注的工作习惯。 本科和硕士期间,本人始终积极向上、奋发进取,全面提高自身的综合素质。曾担任过通院科协建设部部长,多次组织科技宣讲会的举办,策划负责校级大创、互联网+和星火杯等科创比赛的举办。 非常感谢这次面试机会,希望面试官接下来能多多指点。
研究方向和论文描述
XL-MIMO作为6G中的关键技术,有望实现无线通信频谱效率和空间分辨率的空前提升。( XL-MIMO 具备更大规模的天线阵列(数量达百甚至千级), 可以同时服务大量用户,通过波束赋形(Beamforming)和空间复用技术,使多个用户在相同时间频率资源下共存,大幅提高频谱利用率。 天线尺寸增大后,空间上的分辨能力增强,可以更精确地区分和定位不同用户或信道路径。). 同时由于近场和空间非平稳的特性也面临一些挑战。 由于天线孔径很大,信道在空间域变得非平稳,XLAA的不同子阵将遭受不同的传播环境,因此引入可见区(VR)的概念来描述空间非平稳。 在我的研究中,通过稀疏向量来表示可视区域,如果子阵对用户可视。那么该子阵就对应在稀疏向量中为非零值,否则为0。近 场特性依赖距离和角度那么传统的角域信道稀疏性就不适用XL-MIMO,因此研究中设计了一个极域变换矩阵。通过极域来代替角域实现信道稀疏性。
ICCT:在典型的毫米波/太赫兹通信中,通常使用子连接混合波束形成(BF)来做预编码。 这种方案可以有效地降低能量成本,因为它需要更少的射频链和移相器。然而,大多数现有的混合BF技术没有同时考虑近场信道和空间非平稳性。 本文提出了一种子阵-用户配对策略来充分利用空间非平稳特性。仿真结果证明利用子阵-配对策略的平均误码率大约会降低50%。
WCL:之前的工作仅研究了具有全数字接收机的CE,这将导致非常高的能量消耗,因为所需的射频(RF)链的数量非常大。 并且前人的所有工作都假设子阵列划分是已知的,当这种先验信息不可用时,这是不切实际的。因为在一些实际场景中,你并不能保证子阵列划分已知。 我将CE问题通过数学变换表示为具有结构稀疏性的压缩感知问题。 本文研究了考虑空间非平稳的混合组合接收机下的CE,通过正交匹配追踪算法求解,并进一步解决了子阵列划分未知情况下的CE。 通过变分块稀疏恢复算法实现。与经典方案的归一化均方误差大约有5dB的提升
INFOCOM:考虑到免授权随机接入(GF-RA)作为5G/6G 中支持短小突发数据业务的大规模机器类通信(mMTC)的一项关键技术, 研究了XL-MIMO的近场空间非平稳信道下的免授予随机接入的活跃用户检测(AUD)和信道估计(CE)。 充分考虑了空间分稳态特性。将联合活跃用户检测和信道估计的问题转换成具有三层稀疏性的压缩感知问题,通过对结构稀疏性的不同利用,提出了两种基于正交匹配追踪的算法。
TVT:随着用户数量的迅速增长,尤其是在物联网(IoT)的大规模部署中,为每个用户分配唯一导频变得困难,甚至不可能。 为了解决这一瓶颈问题,我们研究了近场空间非平稳XL-MIMO通信中的无源随机接入方案。 在利用到达角(AoA)信息的基础上,我们进一步引入了可见区域(VR)的不变特性。自适应地解决码字冲突的问题,进一步利用VR和角度特性完成消息拼接。 相比较已存在工作利用信道特性来做消息拼接和解决码字冲突,复杂度更低,性能更优一些。
OJCOM:XL-MIMO中 SnS性质引起的近场资源分配的主要挑战是如何进行用户调度和天线选择以提高SE和EE,本文的目标就是解决这个挑战。利用SnS特性提出了一种新颖的预编码方案,通过用用户-子阵列配对网络替代传统混合波束赋形中的数字波束赋形部分。该方案有效利用了空间非平稳信道特性,有助于实现高速处理并支持低分辨率数模转换器的部署。该网络通过开关矩阵实现用户与子阵列的动态连接,利用非平稳信道的可见区域(VR)特性,仅将信号路由至用户可见的子阵列。低复杂度实现,低分辨率DAC兼容性:直接发送调制符号至RF链,无需额外数字处理,可兼容1-bit DAC(如QPSK调制),显著降低能耗(对比传统HBF需高分辨率DAC)。进一步降低了预编码的能耗问题。 通过二次变换(Quadratic Transformation)将非凸的和速率问题转化为可解形式,引入辅助变量解耦目标函数中的分式项。通过交替优化框架分别优化多个变量。 研究了联合设计功率分配、用户-子阵列配对网络和模拟波束赋形,以最大化总速率和用户最小速率的优化问题。将多参数的非凸问题通过分式规划重构成凸优化问题。 由于无法得到闭式解,利用交替优化解耦多个参数,并采用黎曼共轭梯度法、投影梯度上升法以及带平衡约束的数学规划交替方向法分别优化各个变量,来求得优化问题的局部最优解。
你遇到压力最大的时候,是如何缓解压力?
我一般会通过跑步或者公园散步的方式缓解压力,同时还能清除混乱的思绪,保持头脑清醒。我印象最深的一次压力高峰,是研二时为了赶在一个会议截稿日期之前解决一个新的问题,一个月内完成现有工作查找,解决遇到的难题,实现系统仿真,论文撰写。那段时间经常在实验室待10多个小时。精神一直紧绷。这种情况下,我会先把任务拆解成多个阶段,并为每个阶段设置期限,逐步完成,避免被大目标遥不可及的焦虑淹没。 科研需要深度专注,但长时间沉浸反而会陷入思维定式。我会每天强制自己离开实验室半小时,要么去操场跑步,让身体的疲惫带走精神的紧张;要么去公园散步,冷静下大脑。很多时候的灵感都是在·散步中得到的。 同时定期和导师同步进展,交流研究思路,来激发灵感。
当你需要解决棘手的,你所没碰过的问题,你如何解决?
我会先将大问题分解为可操作的小模块。比如在研究中遇到陌生算法时,我会先厘清它的输入输出、数学基础、基本原理和适用场景。然后优先查阅领域内权威论文或行业报告,同时利用开源社区(GitHub/Kaggle)寻找类似问题的解决方案。结合自己的思考尝试使用已存在的工具去求解,并不断完善求解过程。
你领导安排不属于你的工作,让你做,你该如何处理?
当领导安排不属于我的工作时,我会首先接受任务,因为作为团队成员,我认为理解和支持团队目标是至关重要的。接着,我会尽力完成任务,尽管它不在我的日常职责范围内。同时,我会与领导进行沟通,说明我目前的工作进度和优先级,以确保我可以合理地分配时间和资源来完成新任务,同时不影响原有工作的质量和进度。最后,我会把这次经验视为一个学习机会,从中吸取经验教训,以便更好地适应未来可能出现的类似情况。
这么做的意义,为什么要做这个工作(比如为什么会选择这个研究方向),解决了什么问题,和现在有的工作相比的优势和特点,要突出工作的一个重点,不需要把侧重点关注在使用了什么方法,完成了什么内容上。注重逻辑表达和整体的叙述,了解军工相关的背景,培养相关的军工情怀
联通数科实习生
如何解决项目中的高并发难题?
docker部署项目的流程
[本地项目代码]
│
├──➤ 编写 Dockerfile
│
├──➤ 构建镜像:docker build
│
├──➤ 运行容器:docker run / docker compose
│
└──➤ 测试访问 & 持久化配置(端口、卷、网络)
常用的git命令
# 克隆并开发
git clone <repo-url>
cd repo
git checkout -b feature/login
# ...开发
git add .
git commit -m "添加登录功能"
git push origin feature/login
springboot自动装配的流程:自动装配的本质是:Spring Boot 根据类路径下的依赖和配置,自动将 Bean 注册到 Spring 容器中。 例如,当你引入 spring-boot-starter-web 依赖时,Spring Boot 会自动配置嵌入式 Tomcat 服务器、Spring MVC 等。
@SpringBootApplication是启动类上的核心注解,它组合了三个注解: @SpringBootConfiguration:等同于 @Configuration,声明当前类是配置类。 @EnableAutoConfiguration:启用自动装配机制。 @ComponentScan:扫描 @Component、@Service 等组件。
项目描述
基于什么业务场景,做了什么设计,实现了什么功能,使用了什么结构。
AI Agent智能体
面试官您好,本套 Ai Agent 综合智能体项目,旨在构建智能化的业务提效工具。构建一个可配置、可编排的AI Agent智能体平台,实现需求文档分析、文档资料编写(+消息通知)、ELK 日志检索 、 监控日志分析等功能。 整套项目在架构设计上使用了 DDD 分层架构进行设计,运用了组合模式的规则引擎构建执行链路,并结合工厂、策略、责任链等方式来实现多种组合方式的Ai Agent执行过程,解耦系统功能的实现。使用规则树+责任链模式实现动态执行链路,支持AutoAgent、FlowAgent等多种策略。抽象设计拆分了 Ai Agent 执行过程所需的各项组件(Advisor、Prompt、MCP、Model)能力到数据库表中,使其具备自由配置编排组装的特性。以此方式结合应用中实际场景诉求,通过可视化编排工具编排出满足具体需求的AI Agent。
幸运营销汇
面试官您好,幸运营销汇-积分抽奖服务是我独立负责实现的一个学习项目,参考了拼多多、稀土掘金社区的营销抽奖方案,构建一个高可用、高扩展、易维护的营销抽奖平台,实现了多场景抽奖、用户行为返利(签到)、积分账户体系以及积分兑换等核心功能。项目模块在架构设计上运用了 DDD 分层架构和模板模式、责任链模式、组合模式、工厂模式等,设计模式对业务流程进行解耦和实现,利用责任链、组合模式构建可配置的规则引擎,提升系统的灵活性。设计“无锁化”库存扣减方案,解决超卖问题,通过延迟消息+定时任务异步、批量更新数据库来保证数据最终一致性,降低了数据库压力,提升系统性能。实施分库分表与数据同步方案,支撑系统扩展。在4C8G的服务器上,经过JMeter压测,系统成功支撑了单机近600 TPS的抽奖请求,平均响应时间在300ms左右。 在极端高并发下,响应时间的长尾现象(99%线较高)仍有优化空间,未来可考虑进一步优化JVM GC参数,或对热点数据做更极致的本地缓存。 目前分库分表组件为自研,功能上虽满足需求,但在易用性和生态整合上对比ShardingSphere等成熟产品有差距,后续可考虑平滑迁移。
抽奖系统以“装配—参与—抽奖—结算”四段式流程为主线:首先,活动装配阶段会把目标活动的基础信息、SKU 库存与SKU发放策略、奖品清单及其概率散列表(若规则中包含积分决定奖品,则将积分-奖品散列表一并)统一加载进 Redis。 装配完成后,用户可通过购买、签到等多种 SKU 活动累积抽奖次数;系统会验证 SKU 活动是否处于有效期、库存是否充足,校验通过后在一个事务内同时为用户增加账户总额度,并在 raffle_activity_order 表写入流水,确保额度与订单原子一致。
当用户发起抽奖请求时,系统先校验活动可用性,再查询是否存在状态为 noused 的待抽奖订单;若存在直接返回,否则按“总额度-月额度-日额度”三级维度顺序扣减,并生成 user_raffle_order 记录。 扣减过程中,SKU 库存以 Redis 原子扣减为准,扣到 0 时异步发布消息回刷数据库;奖品库存则是在 Redis 扣减后通过 MQ 触发单件落库,避免超卖。(这里也会采用商品库存槽位锁来确保)
抽奖逻辑分三段:抽奖前、抽奖中、抽奖后。抽奖前根据活动 ID 动态装配责任链:若未定制链,则走默认链;若链中配置了黑名单、积分权重等节点,则优先判定——命中黑名单或已由积分直接命中奖品时,流程短路直接返回,不再进入后续规则树。 若责任链放行,进入抽奖后阶段的规则树处理。抽奖后通过活动-规则树 ID 建立树根、节点、边的有序结构,装配规则引擎并逐节点校验次数门槛、库存阈值等条件;若某一步校验失败则走幸运奖兜底,具体奖项依数据库树配置而定。
当最终中奖结果产生,系统会把 user_raffle_order 状态更新为 used,并把中奖内容写入 user_award_account ;随后异步发送 MQ 消息通知下游做奖品发放,并在任务表写入一条补偿任务,以便消息丢失时可定时重推,保证全链路一致性与可恢复性。
整体方案以领域分层(domain、infrastructure、trigger)、责任链 + 规则树双层过滤、Redis 原子扣减 + MQ-Task 最终一致、事务保障一致四大技术手段,确保高并发下的抽奖正确性与库存安全。
通用组件
为解决多业务系统共性需求(规则引擎设计、动态配置管理、限流熔断)重复开发问题,设计组件化平台提供可复用的技术支撑,提升整体研发效率与系统一致性。实现动态配置中心、规则引擎与责任链、动态限流熔断、任务调度等功能。
构建一个动态配置中心,它允许应用在不重启的情况下,动态的修改配置项的值。它被封装成⼀个 Spring Boot Starter, 任何 Spring Boot 项⽬都可以⽅便地引⼊并使⽤。基于Redis的Pub/Sub机制实现配置推送,从⽽减少 IO 操作。结合本地缓存(ConcurrentHashMap)保证可用性;
对代码中共用的设计模式的抽象,既减少了各个业务之间重用部分,也标准化了设计模式在业务中的使用形式。在规则树和责任链中,将流程控制部分和节点的业务执行部分解耦合,保证了模板的灵活度。
基于Guava RateLimiter 限流组件,使用 aop 切面技术,实现一款统一限流服务组件。对于频繁访问的用户动态添加黑名单拦截,再通过动态调用方法返回拦截后的结果信息。
通过动态化设计和函数式编程,提供了一个轻量级、易扩展的任务调度解决方案。基于Spring的ThreadPoolTaskScheduler扩展,动态创建线程池实例,支持多任务并发执行。通用逻辑下沉为组件,新系统接入配置中心、规则引擎等能力仅需引入依赖+注解配置,提升开发效率。
小红书
一面
短链接设计(长链接与短链接的转换,高并发,高可用) 如何设计一个短链系统
redis持久化,RDB文件和AOF文件
并发编程中的锁
- 按照锁的获取机制(看待并发同步的角度):乐观锁/悲观锁
- 按照锁的竞争策略:公平锁/非公平锁,
- 按照锁控制的资源范围:偏向锁/轻量级锁/重量级锁/分段锁
- 按照功能特性:可重入锁/读写锁/自旋锁/互斥锁
- 按照持有方式:独享锁/共享锁
- 乐观锁:假设线程的并发访问不会发生冲突,操作时不加锁,在更新数据时采用尝试更新,如有冲突则重试。乐观锁在Java中即无锁编程例如原子类,通过CAS自旋实现原子操作的更新。
- 悲观锁:假设线程并发一定会有冲突,每次操作前必须先加锁,阻止其他线程干扰。公平锁:
- 线程按照申请锁的顺序获取锁,先等待先获得。优缺点:公平,但是效率低,存在线程唤醒开销。
- 非公平锁:线程获取锁时不按顺序,允许“插队”。优缺点:效率高,吞吐量大,但是可能导致优先级反转或饥饿现象。
- 偏向锁、轻量级锁、重量级锁都是锁的状态,是针对synchronized的概念,通过对象监视器在对象头中的字段来表明的。
- 偏向锁:是JVM对synchronized的优化,如果只有一个线程访问同步资源,一旦线程获取锁,后续无需重复加锁。
- 轻量级锁:当偏向锁被多个线程竞争时升级为轻量级锁,其他线程会通过自旋尝试获取锁,不会阻塞。
- 重量级锁:轻量级锁自旋失败一定次数后升级为重量级锁,线程进入阻塞。 重量级锁依赖操作系统的互斥量实现,重量级锁会使其他申请的线程进入阻塞,性能降低。
- 分段锁:将大对象拆分为多个小段,对每个段单独加锁,细化锁的粒度,减少锁竞争。
- 分段锁是一种锁的设计,不是具体的锁。
- 以ConcurrentHashMap中put操作为例,不会对整个hashmap加锁,会先通过hashcode计算放入的分段,对分段加锁。如果不是放在同一个分段中,可以实现并行插入。
- 可重入锁(递归锁):线程可以重复获取已持有的锁,避免自己锁死自己。实现方式:synchronized(隐式)ReentrantLock(显式)// 由于可重入锁的特性,setB可以正常执行
- 读写锁:区分“读操作”、“写操作”,允许多个读线程并发访问,读和写互斥,写和写互斥。ReadWriteLock.
- 自旋锁:线程获取锁失败时不立即阻塞,而是循环尝试获取锁,循环有次数限制。优缺点:减少线程上下文切换开销,但是循环会消耗CPU。
- 互斥锁:通过互斥机制保证同一时间只允许一个线程持有锁。ReentrantLock.互斥锁/读写锁是独享锁/共享锁的具体体现。
- 独享锁:同一时间只能有一个线程持有锁
- ReentrantLock 是独享锁
- ReadWriteLock 写锁是独享锁
- 共享锁:同一时间允许多个线程同时持有锁,线程间不互斥。
- ReadWriteLock 读锁是共享锁,保证并发读高效,而读写、写读、写写的过程互斥。
| 锁 | 使用场景 |
|---|---|
| 乐观锁 | 读操作频繁,冲突概率低的场景 |
| 悲观锁 | 写操作频繁,冲突概率高 |
| 偏向锁 | 单线程反复访问同步块 |
| 轻量级锁 | 短时间、低冲突并发场景 |
| 重量级锁 | 长耗时、高冲突并发场景 |
| 分段锁 | 对大对象的并发访问 |
| 读写锁 | 读多写少 |
Java 中具体工具
| 工具 | 适用场景 |
|---|---|
| synchronized | 实现互斥锁。对共享资源的访问进行同步控制 |
| ReentrantLock | 实现可重入锁。可手动控制锁的获取和释放,支持公平锁,适合更高级别控制场景 |
| ReadWriteLock | 读写锁接口。适用于读多写少场景 |
| StampedLock | 乐观读写锁。并发性能更高,适用于读多写少场景。 |
| AtomicInteger | 基于 CAS 的原子操作类(无锁)。实现共享变量的原子更新 |
rabbitMQ消息发送和接收的流程,及可靠性保证
- 生产者准备消息并指定路由键:生产者创建消息(通常包含业务数据,如 JSON 字符串),并在发送时指定路由键(Routing Key) 和交换机名称。
- 生产者将消息发送到交换机:生产者通过 RabbitMQ 客户端(如 Java 的 amqp-client、Python 的 pika)与 RabbitMQ 服务器建立连接,将消息发送到指定的交换机。 此时交换机仅接收消息,不直接存储消息(若消息无法路由到任何队列,可能被丢弃或返回给生产者,取决于交换机类型和配置)。
- 交换机根据规则路由消息到队列:交换机根据自身类型和 “绑定键(Binding Key)” 与 “路由键(Routing Key)” 的匹配规则,将消息转发到对应的队列:
- Direct 交换机:仅当路由键与绑定键完全匹配时,消息才会被路由到队列(如路由键 order.create 匹配绑定键 order.create)。
- Topic 交换机:支持通配符匹配(* 匹配一个单词,# 匹配多个单词),如路由键 order.create 可匹配绑定键 order.* 或 #.create。
- Fanout 交换机:无视路由键,将消息广播到所有绑定的队列。
- Headers 交换机:通过消息头(而非路由键)匹配,较少使用。
- 消息在队列中存储等待消费:队列收到消息后,会按顺序存储消息(队列是 FIFO 结构),直到被消费者取用。 队列可配置持久化(消息不会因 RabbitMQ 重启丢失)、过期时间(超时未消费自动删除)等属性,确保消息可靠性。
- 消费者从队列获取消息:消费者通过客户端与 RabbitMQ 建立连接,并 “订阅” 目标队列,当队列中有消息时,RabbitMQ 会将消息推送给消费者(或消费者主动拉取,较少用)。 消费者需指定队列名称,且需确保队列已存在(否则会报错)。
- 消费者处理消息并确认:消费者接收到消息后,进行业务处理(如更新数据库、调用接口等),处理完成后向 RabbitMQ 发送消息确认(Ack):
可靠性保证
- 持久化:队列持久化确保队列元数据不丢失,消息持久化确保消息被写入磁盘(而非仅存于内存)。RabbitMQ 会将持久化消息定期刷盘,即使服务器宕机,重启后也能从磁盘恢复消息。
- 镜像队列:在 RabbitMQ 集群中,单个节点故障可能导致该节点上的队列及消息丢失。镜像队列机制可将队列复制到集群中的多个节点(镜像节点),当主节点故障时,从节点自动切换为主节点,保证消息不丢失。
- 生产者确认机制(确保消息到达交换机 / 队列)
- Publisher Confirm(发布确认):生产者开启确认模式后,RabbitMQ 会在消息成功到达交换机并路由到队列(若需持久化则等待消息写入磁盘)后,向生产者返回 “确认”(ack);若失败则返回 “否定确认”(nack)。 生产者可根据确认结果重试失败的消息。
- Publisher Return(发布返回):当消息到达交换机但无法路由到任何队列时(如路由键不匹配且未设置备份交换机),RabbitMQ 会将消息返回给生产者,避免消息无声丢失。
- 消费确认机制:默认情况下,RabbitMQ 采用 “自动确认”(autoAck=true),即消息一旦被消费者接收,就从队列中删除。若消费者在处理前崩溃,消息会丢失。 需改为手动确认(autoAck=false):消费者处理完消息后,主动发送确认信号(ack),RabbitMQ 才删除消息。
- 消费者限流:若消费者处理速度慢于消息生产速度,队列会堆积大量消息,可能导致消费者过载崩溃。可通过 basicQos 限制消费者每次预取的消息数量,确保 “处理完一批再取一批”。
- 异常处理:对于 “无法正常消费” 的消息,通过死信队列和重试机制避免消息无限循环或丢失。
- 重试机制:对于临时故障(网络波动等),可以通过通过有限次重试解决。
分布式锁
在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。可以通过锁来时下按共享资源的互斥访问。 更准确的说应该是悲观锁, 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
对于单机多线程来说,在Java中,我们通常使用ReentrantLock类、synchronized关键字这类JDK自带的本地锁来控制一个JVM进程内的多个线程对本地共享资源的访问。
分布式锁保证不同的服务/客户端运行在不同的JVM进程上时,如果多个JVM进程共享同一份资源,时下按资源的互斥访问。满足以下条件:
- 互斥:任意时刻,锁只能被一个线程持有
- 高可用:锁服务是高可用的,一个锁服务出现问题,可以自动切换到其他的锁服务。
- 可重入:一个节点获取锁后,还可以再次获取
- 高性能:获取和释放锁的操作应该快速完成,不会对系统的性能造成太大的影响
- 非阻塞:如果获取不到锁,不能无限期等待,避免对系统的正常运行造成影响。
分布式锁实现方案:
- 基于关系型数据库比如MySQL实现分布式锁。
- 关系型数据库的方式一般是通过唯一索引或者排他锁实现。性能太差,不具备锁失效机制
- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
- SETNX 命令可以帮助我们实现互斥。如果key不存在的话,设置key的值。如果key已经存在, SETNX啥也不做。
- DEL命令删除对应的key,释放锁
- 通过Lua脚本保证锁操作的原子性,防止误删其他的锁。
- 给key设置一个过期时间,保证设置指定 key 的值和过期时间是一个原子操作。
- 通过Redisson的Watch Dog机制实现锁的自动续期。
- 基于分布式协调服务ZooKeeper实现分布式锁。
- ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。
- 获取锁:
- 首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
- 假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断lock1是否是/locks下最小的子节点。
- 如果lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
- 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
- 释放锁:
- 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
- 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
- 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
- ZooKeeper相比于Redis实现分布式锁,可靠性更高一些,提供了Watch机制实现公平的分布式锁。不过性能会差一些
- 推荐使用Curator来实现ZooKeeper分布式锁。
- InterProcessMutex:分布式可重入排它锁
- InterProcessSemaphoreMutex:分布式不可重入排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
设计模式在项目中具体应用
大营销项目
- 模板模式
AbstractRaffleActivityAccountQuota:定义抽奖活动账户额度的标准流程;AbstractRaffleActivityAccountQuota 定义了 createOrder 标准流程:参数校验 → 未支付订单查询 → 基础信息查询 → 账户额度校验 → 活动动作规则校验 → 构建订单聚合对象 → 交易策略处理。AbstractRaffleStrategy:定义抽奖策略的标准流程;AbstractRaffleStrategy 定义了 performRaffle 标准流程:参数校验 → 责任链抽奖计算 → 规则树抽奖过滤AbstractRaffleActivityPartake:定义参与抽奖的抽象流程;子类如RaffleActivityAccountQuotaService和DefaultRaffleStrategy实现具体的抽象方法
- 策略模式:通过构造函数注入不同的交易策略实现类到Map中;根据订单类型动态的选择相应的交易策略进行处理。
- 工厂模式:
- DefaultChainFactory 负责创建和组装责任链的逻辑节点
- DefaultTreeFactory 负责创建决策树引擎,用于规则树的逻辑处理
- 通过工厂模式统一管理复杂对象的创建过程
- 责任链模式:在抽奖策略中,通过责任链处理各种前置规则校验;每个链节点负责特定的业务逻辑,支持链式调用和扩展。
- 建造者模式:项目中大量实体类使用了@Builder注解;简化复杂对象的构建过程;提供链式调用的优雅API;支持可选参数的灵活设置。
- 单例模式:通过 Spring 的 @Service 、 @Component 注解实现单例管理;如 DefaultChainFactory 、各种 Service 类都是单例模式。
- 原型模式(Prototype Pattern):在 DefaultChainFactory 中通过 ApplicationContext 获取原型对象。
为什么redis可以用来做缓存,分担数据库的压力,从底层原理,为什么轻量级讲,缓存如redis为什么快。
Redis 所有数据存储在内存中,读写操作直接在 RAM 完成,避免磁盘 I/O 瓶颈(磁盘访问延迟约毫秒级,内存仅纳秒级)。 Redis 原生支持多种数据结构,操作时间复杂度低,如String、Hash、Set、List、ZSet等。 Redis 使用文本协议(RESP),格式简单,解析效率高,网络传输开销小。 Redis 采用单线程处理命令,避免多线程锁竞争和上下文切换的开销,在缓存场景下(读多写少),单线程反而能够提高吞吐量,并且代码复杂度更低。
AI Agent智能体
- 建设者模式
- 工厂模式:- 作为策略工厂,负责创建和管理不同类型的策略处理器;提供 StrategyHandler 方法返回 RootNode 实例;集中管理对象创建逻辑,降低系统耦合度。
- 策略模式 (Strategy Pattern) + 责任链模式 (Chain of Responsibility):通关Node节点实现流程体系:
RootNode.java: 作为根节点,管理异步数据加载和策略路由AiClientToolMcpNode.java: 处理MCP工具配置和客户端创建AiClientNode.java: 构建完整的聊天客户端AiClientAdvisorNode.java: 管理顾问配置
- 模板模式:提供通用的 Bean 注册模板方法 registerBean();定义了标准的 Bean 生命周期管理流程;子类可以复用通用逻辑,专注于具体业务实现。
- 配置模式:使用 @ConfigurationProperties 实现配置外部化;支持不同环境的配置切换(dev、prod);提供默认值和类型安全的配置管理。
- 适配器模式:传输协议适配: 在 AiClientToolMcpNode 中根据 transportType (SSE/STDIO)创建不同的传输客户端;顾问类型适配: 在 AiClientAdvisorNode 中将不同类型的顾问(ChatMemory/RagAnswer)适配到统一的 Advisor 接口。
- 代理模式:Spring AOP: 通过 @Autowired 、 @Resource 注解实现依赖注入代理;数据库访问: MyBatis 动态代理生成 DAO 接口实现 ;异步处理: CompletableFuture 在节点中实现异步代理
二面
基本没有技术问题,都是一些看法和需求和过往经历。 有些像聊天面,问了对部门的了解,对企业的看法,还有项目中的RAG的想法,MCP的优缺点
面试中需要改进的点:对话过程中,自身表现得有些冷淡,并没有特别想来该公司的意味,需要更热切一些。
对springboot的看法。研究过程中的一些心得,学到了什么。
Spring 最初是作为重量级企业开发框架 Enterprise JavaBeans (EJB) 的一种轻量级替代方案而出现的。 它旨在简化企业级 Java 开发,其核心思想是利用依赖注入 (Dependency Injection, DI) 和面向切面编程 (Aspect-Oriented Programming, AOP) 这两大特性, 通过普通的 Java 对象 (Plain Old Java Object, POJO) 来实现过去通常由 EJB 提供的复杂功能。 但其配置比较复杂,管理版本依赖也很麻烦。
- Spring Boot 的核心目标是通过简化配置、优化依赖管理、加速项目启动和部署流程,帮助开发者专注于业务逻辑的实现,减少在环境搭建和配置上的时间消耗。提供了很多开箱即用的功能。
- Spring Boot 通过自动配置、起步依赖 (Starters) 和其他开箱即用的功能,极大地减少了项目初始化、配置编写和样板代码的工作量,使开发者能更快地构建和交付应用。
- Spring Boot 能够方便地整合 Spring 框架下的其他成熟模块(如 Spring Data、Spring Security、Spring Batch 等),充分利用 Spring 强大的生态系统,简化整合工作。
- 遵循“约定优于配置”的原则,Spring Boot 能够根据项目依赖自动配置大量的常见组件(如数据源、Web 容器、消息队列等),提供合理的默认设置。同时也允许开发者根据需要轻松覆盖或定制配置,极大减少了繁琐的手动配置工作。
- Spring Boot 自带内嵌的 HTTP 服务器(如 Tomcat、Jetty),开发者可以像运行普通 Java 程序一样运行 Spring Boot 应用程序,极大地简化了开发和测试过程。
- Spring Boot 使得每个微服务都可以独立运行和部署,简化了微服务的开发、测试和运维工作,成为构建微服务架构的理想选择。
- Spring Boot 为常用的构建工具(如 Maven 和 Gradle)提供了专门的插件,简化了项目的打包(如创建可执行 JAR)、运行、测试以及依赖管理等常见构建任务。
- 通过 Spring Boot Actuator 模块,可以轻松地为应用添加生产级的监控和管理端点,方便了解应用运行状况、收集指标、进行健康检查等
三面
主管面试官很冷漠,体验感很差,感觉根本就对我不感兴趣,纯纯KPI,走个过场,估计挂了
挑一篇论文讲一下其中的创新点
INFOCOM:
- 考虑节能混合接收架构、近场球面波和空间非平稳(部分遮挡) 的复杂环境下的免授权随机接入问题
- 利用三层稀疏性来刻画问题,将耦合在一块的三个变量拆解成多个小问题。
- 通过对三层稀疏性的不同利用,我设计了两种高效的基于正交匹配追踪的信道估计算法
讲一下你的项目
HRBP面
发展城市倾向
介绍一下论文和项目
为什么要投递企业效率部门呢
因为没实习,所以你在面临第一份正式工作时,觉得自己会有哪些担心,优缺点。
葡萄城
一面
面向对象设计的优缺点。
面向过程编程(Procedural-Oriented Programming,POP)和面向对象编程(Object-Oriented Programming,OOP)是两种常见的编程范式, 两者的主要区别在于解决问题的方式不同:
- 面向过程编程(POP):面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象编程(OOP):面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
相比较于 POP,OOP 开发的程序一般具有下面这些优点:
- 易维护:由于良好的结构和封装性,OOP 程序通常更容易维护。
- 易复用:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能。
- 易扩展:模块化设计使得系统扩展变得更加容易和灵活。
POP 的编程方式通常更为简单和直接,适合处理一些较简单的任务。
redis持久化机制
Spring是如何提供开箱即用的,为什么项目中选择使用Spring,MyBatis如何集成到Spring中的
Spring 的 “开箱即用” 核心在于通过依赖注入(DI)、控制反转(IoC) 和丰富的 starter 组件,简化开发流程,减少重复配置。 通过 IoC 减少组件间耦合,便于维护和测试;AOP 封装通用逻辑,避免代码冗余; 丰富的生态(如 Spring MVC、Spring Security)覆盖 Web 开发、安全等全场景,无需集成第三方框架。 MyBatis 的核心组件被纳入 Spring IoC 容器管理,实现与 Spring 无缝集成。
分布式理论
分布式事务的终极目标就是保证多个系统中多个相关联的数据库中的数据一致性。
CAP理论
- 一致性(Consistency) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAO理论中P是一定要满足的,A和C任选其一; 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。Redis更倾向于AP架构。MySQL更偏向于CP架构。 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。
BASE
BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。
- Basically Available(基本可用):基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
- Soft-state(软状态):允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
- Eventually Consistent(最终一致性):强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
一致性的三种级别
- 强一致性:系统写入什么,读出来就是什么;
- 弱一致性:不一定会读到最新的值,也不保证什么时候读到的值是最新的,只会尽量保证某个时刻达到数据一致的状态。
- 最终一致性:在一段时间内达到数据一致的状态。
柔性事务:柔性事务追求的是最终一致性,就是 BASE 理论 +业务实践。 柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。 像 TCC、 Saga、MQ 事务 、本地消息表 就属于柔性事务。
刚性事务:刚性事务追求的就是 强一致性。像2PC 、3PC 就属于刚性事务。
Spring bean的线程安全
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。 几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可 。prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。 singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。 如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。
不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是:
- 避免可变成员变量: 尽量设计 Bean 为无状态。
- 使用ThreadLocal: 将可变成员变量保存在 ThreadLocal 中,确保线程独立。
- 使用同步机制: 利用 synchronized 或 ReentrantLock 来进行同步控制,确保线程安全。
项目中哪里使用到了Java集合,例如HashMap,ConcurrentHashMap等数据结构。
大营销项目
DefaultChainFactory中使用 ConcurrentHashMap 存储策略责任链缓存;在多线程环境下缓存策略ID对应的责任链,避免重复构建,提升性能。- 在
StrategyRepository中构建规则树时,使用HashMap<String, Integer> 存储规则锁计数; - ArrayList在
StrategyArmoryDispatch中用于存储奖品概率分布,实现按概率抽奖的核心算法; - 在策略装配中使用 LinkedHashMap 保持插入顺序
AI Agent智能体
- ConcurrentHashMap,用于存储任务ID与任务执行器的映射,确保在多线程环境下的线程安全。该集合用于管理定时任务的生命周期,支持动态添加、删除和查询任务状态。
- HashMap:在策略工厂的动态上下文中存储各种类型的数据对象,提供灵活的键值对存储机制。以及RAG中处理请求上下文和用户参数,支持动态的参数传递和上下文管理。
- ArrayList:在AI智能体聊天服务中管理对话消息列表。管理MCP同步客户端列表和顾问列表,支持动态的客户端配置。
AI Agent项目的向量库和MCP相关,分布式在项目中的使用
大营销项目
- 项目使用Dubbo作为RPC框架,通过
application.yml配置了Dubbo服务。支持多实例部署,实现RPC负载均衡。 - 分布式缓存 - Redis
- 消息队列 - RabbitMQ
- 分布式任务调度 - XXL-Job
- 分库分表技术
- 多层负载均衡 :HTTP负载均衡 :使用Nginx进行HTTP请求的负载均衡;RPC负载均衡 :大营销服务支持RPC负载均衡; 多实例部署 :支持应用的多实例部署。
- 分布式协调 - Zookeeper:作为分布式协调服务,配合其他组件实现服务发现和配置管理。
- Canal将数据同步到Elasticsearch。
AI Agent智能体
- 分布式数据存储:MySQL :主要业务数据存储,配置了HikariCP连接池(最大连接数10,最小空闲连接5);PostgreSQL + pgvector :向量数据库,用于AI知识库的向量存储和检索
ThreadPoolConfig.java实现了可配置的线程池(核心线程数20,最大线程数50);AsyncConfiguration.java配置了异步执行器。- 微服务通信机制:sse和stdio机制
项目中有没有做多源数据库的实现
- MySQL :作为主数据源,存储业务数据(客户端配置、模型配置、系统提示词等)
- PostgreSQL (PgVector) :专门用于向量存储,支持AI向量检索和RAG功能
手撕题:二叉树,查找子路径路径和为n的个数,不能拐弯,以及正常的。
二面
直接给一个需求:打印输出一个柱状图,下面是每个柱的名字,最上面显示数量。
一步步优化,名称占的格子和数量长度占的格子。
面试中需要改进的点:面对面试官的提问(对程序还有没有什么问题),没有表现出对问题的进一步探究。
三面
做了一道算法题,代码风格有点不太好。
深挖项目,比如抽奖高并发的体现与实现,抽奖策略的审核等等
计算机科学知识,服务器IP和域名的转换,DNS的实际运行过程。为什么你的项目地址链接有端口号,有的没有。域名如何和服务器IP绑定。 服务器的安全问题,请求体,前端的数据会保存咋爱哪里,以什么格式发送给后端做解析。
得物
常用的Java集合的底层原理实现,CurrentHashMap与HashTable的区别
为什么要加setNX锁,直接使用decr操作根据返回的结果是否小于0判断是否扣减成功不就可以了
- 在 redis 集群模式下,decr 请求操作可能发生网络抖动超时返回。这个时候 decr 有可能成功,也有可能失败。 可能是请求超时,也可能是请求完的应答超时。那么 decr 的值可能就不准。 【实际使用中10万次,可能会有10万零1和不足10万】, 那么为了这样一个临界状态的可靠性,增加了setNx锁。setNx 是 “存在性判断”,结果只有成功(锁创建,可扣减) 或失败(锁已存在,不可扣减)
- 发生主从切换的时候,如果主节点的 decr 还没同步到从节点,主节点挂了,丢失了部分未同步的数据,decr 的值从 8 变成 6, 如果没有加锁就可能超卖,属于极端情况下的一种兜底策略,有 setNX 锁拦截后,会更加可靠。setNx 的锁是基于 Redis 键值对存在性的,和主从同步无关。
Java开发规范,比如Arrays.aList使用时的注意事项。
- 返回固定大小的列表:Arrays.asList() 返回的列表是基于原始数组的视图(view)。其大小在创建时就已固定(由原数组长度决定)。任何试图改变其大小的操作(如 add, remove, clear)都会导致 UnsupportedOperationException 异常。
- 解决方案:如果你需要一个可增删的列表,最简单的方式是用 ArrayList 包装一下。
- 与原始数组的数据共享:Arrays.asList() 返回的 List 并不是一个数据的独立副本,而是与原始数组共享同一块内存(引用相同的对象数组)。因此,对 List 中元素的修改(例如 set 方法)会反映到原始数组上;反之,修改原始数组的元素也会反映到 List 中。
- 解决方案:若需独立,可先复制数组:Arrays.asList(Arrays.copyOf(originalArray));
- 处理基本类型数组的陷阱:Arrays.asList 方法的参数是可变参数 T... a,这意味着它期望接收的是对象引用(Object references)。如果你直接传入一个基本数据类型(如 int[], byte[])的数组,整个基本类型数组会被当作一个单一的 Object 对象(或者说是 T 类型的一个实例),进而被当作 List 的一个元素,而不是把数组中的每个基本数据类型值作为 List 的元素。
- 使用包装类型数组(如 Integer[]),或使用 Stream API 转换(如 Arrays.stream(arr).boxed().collect(Collectors.toList()));
- 非标准 ArrayList:Arrays.asList() 返回的 List 是 java.util.Arrays 类的一个私有静态内部类 ArrayList,它虽然也实现了 List 接口,但与常用的 java.util.ArrayList 类不同。这个内部类没有重写 add, remove 等方法,而是直接使用了 AbstractList 中的默认实现,这些默认实现就是简单地抛出 UnsupportedOperationException。所以,当你尝试进行增删操作时就会遇到异常。
- java.util.ArrayList 明确其仅为“视图”,操作受限。需要标准 ArrayList 时主动包装。
- 注意 null 元素:如果原始数组中包含 null 元素,那么返回的 List 中自然也会包含相应的 null。在后续操作这个 List(例如遍历并调用元素的方法)时,如果没有进行 null 检查,就很容易抛出 NullPointerException
- 使用前进行 null 检查,或在 Stream 操作中使用 filter(Objects::nonNull) 过滤
Java并发编程中锁
介绍一下CAS和AQS,CAS的底层实现,AQS设计上有哪些对象,比如状态机,状态码一类的
CAS(Compare-And-Swap, 比较并交换)是一个无锁的原子操作,用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。实现CAS的一个关键类是sun.misc.unsafe,其提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的 CAS 操作。
Unsafe类中的 CAS 方法是native方法,直接调用底层的硬件指令实现原子操作。更准确点来说,Java中CAS是C++内联汇编的形式实现的,通过JNI(Java Native Interface)调用。因此,CAS的具体实现与操作系统以及CPU密切相关。
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是一个抽象类,为同步器提供了通用的执行框架。它定义了资源获取和释放的通用流程, 而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 基础“底座”,而同步器则是基于 AQS 实现的 具体“应用”。 AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 进一步优化实现的。
CLH 锁是一种基于 自旋锁 的优化实现。通过引入一个队列来组织并发竞争的线程,对自旋锁进行了改进:
- 每个线程会作为一个节点加入到队列中,并通过自旋监控前一个线程节点的状态,而不是直接竞争共享变量。
- 线程按顺序排队,确保公平性,从而避免了 “饥饿” 问题。
AQS在 CLH 锁的基础上进一步优化,形成了其内部的 CLH 队列变体。主要改进点有以下两方面:
- 自旋 + 阻塞: CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了 自旋 + 阻塞 的混合机制:
- 如果线程获取锁失败,会先短暂自旋尝试获取锁;
- 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。
- 单向队列改为双向队列:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为双向队列,新增了 next 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。
Node 节点 waitStatus 状态含义
AQS 中的 waitStatus 状态类似于状态机 ,通过不同状态来表明 Node 节点的不同含义,并且根据不同操作,来控制状态之间的流转,通过volatile修饰。
| Node 节点状态 | 值 | 含义 |
|---|---|---|
| CANCELLED | 1 | 表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。 |
| SIGNAL | -1 | 表示后继节点需要当前节点唤醒。在当前线程节点释放锁之后,需要对后继节点进行唤醒。 |
| CONDITION | -2 | 表示节点在等待 Condition。当其他线程调用了 Condition 的 signal() 方法后,节点会从等待队列转移到同步队列中等待获取资源。 |
| PROPAGATE | -3 | 用于共享模式。在共享模式下,可能会出现线程在队列中无法被唤醒的情况,因此引入了 PROPAGATE 状态来解决这个问题。 |
| 0 | 加入队列的新节点的初始状态。 |
在 AQS 的源码中,经常使用 > 0 、 < 0 来对 waitStatus 进行判断。
- 如果
waitStatus > 0,表明节点的状态已经取消等待获取资源。 - 如果
waitStatus < 0,表明节点的状态处于正常的状态,即没有取消等待。 其中 SIGNAL 状态是最重要的,节点状态流转以及对应操作如下:
| 状态流转 | 对应操作 |
|---|---|
| 0 | 新节点入队时,初始状态为 0。 |
| 0 -> SIGNAL | 新节点入队时,它的前继节点状态会由 0 更新为 SIGNAL 。SIGNAL 状态表明该节点的后续节点需要被唤醒。 |
| SIGNAL -> 0 | 在唤醒后继节点时,需要清除当前节点的状态。通常发生在 head 节点,比如 head 节点的状态由 SIGNAL 更新为 0 ,表示已经对 head 节点的后继节点唤醒了。 |
| 0 -> PROPAGATE | AQS 内部引入了 PROPAGATE 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到) |
条件变量(ConditionObject)
- AQS内部类ConditionObject实现了Condition接口,用于实现等待/通知机制。
- 每个ConditionObject都维护了一个单向的条件队列,用于存放调用await()方法后等待条件的线程。
- 这与synchronized搭配Object.wait()/notify()类似,但一个AQS可以关联多个条件队列,实现更精细的线程等待控制。
volatile的作用
保证变量的可见性;但不能保证原子性;
防止指令的重排序,如果将变量声明成volatile,在对这个变量进行读写操作时,会通过插入特定的内存屏障来禁止指令的重排序。
ThreadLocal的使用场景,如何把主线程中的ThreadLocal的数据同步到异步线程中
项目中的分库分表路由组件使用ThreadLocal来存储当前线程的路由信息(如数据库索引、表索引等),确保在同一个线程中的所有数据库操作都路由到正确的数据源。
- 手动传递 (Manual Propagation):在创建异步任务前,主动从主线程的ThreadLocal中取出值,并将其作为参数传递给异步任务(例如通过Runnable的构造函数或CompletableFuture.supplyAsync的参数)。
- InheritableThreadLocal:InheritableThreadLocal 是 ThreadLocal 的子类。它通过覆盖 childValue 等方法,在创建子线程时将父线程的变量副本自动复制给子线程。 但其主要缺点是:复制发生在线程创建时。如果使用线程池,线程会被复用,后续任务可能会读到之前任务设置的值或脏数据,且父线程对值的修改不会更新到已创建的子线程中。
- 阿里开源的TransmittableThreadLocal(TTL)是解决此问题的强大工具。它通过包装Runnable/Callable(TtlRunnable/TtlCallable),在任务提交时捕获父线程上下文,并在任务执行前将其设置到子线程,任务执行后自动恢复/清理,完美支持线程池。
线程池的拒绝策略,你用到了哪种拒绝策略,选择的标准是什么
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行者自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
- ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
- 自定义拒绝策略
如果不允许丢弃任务并且可以承受延迟的话,可以选择CallerRunsPolicy; 可以丢失,考虑DiscardPolicy/DiscardOldestPolicy 需明确感知失败,选择 AbortPolicy,通过异常捕获做后续处理;
线程池中核心线程数的设置
根据CPU密集型或者IO密集型设置,
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。 比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。 一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。 因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
数据库的深度分页以及如何优化深度分页,使用索引的注意事项
深度分页是指查询结果集的偏移量(OFFSET)较大时的分页操作(例如 LIMIT 10000, 20,表示跳过前 10000 条数据,取后续 20 条)。 深度分页的性能瓶颈源于OFFSET(偏移量)的 “全量扫描特性”:数据库执行LIMIT offset, size时,并不会直接“跳过前 N 条”, 而是需要从数据起始位置(或索引起始位置)扫描到 offset + size 条数据,再丢弃前 offset 条,返回剩余 size 条。
优化的核心思路是 “避免全量扫描偏移量”,通过“定位起始位置”“减少扫描范围”“异步预取”等方式降低开销,不同方案适用场景不同:
| 优化方案 | 核心原理 | 适用场景 | 优缺点对比 |
|---|---|---|---|
| 1. 主键/唯一键分页 | 利用主键(如 id)的有序性,用“大于条件”替代 OFFSET(WHERE id > 上一页最大id) | 主键自增/唯一键有序,且分页无复杂筛选条件 | ✅ 性能极高(直接命中索引,无偏移扫描);❌ 不支持“跳页”(如直接从第1页跳第100页) |
| 2. 覆盖索引+延迟关联 | 先通过“覆盖索引”查询出符合条件的主键,再用主键关联原表取完整数据 | 有复杂筛选条件(如 WHERE status=1 AND create_time>xxx) | ✅ 避免回表扫描大量数据;❌ 需维护符合筛选条件的组合索引 |
| 3. 书签分页(Seek Method) | 类似主键分页,用“上一页的最后一条数据的关键列”作为“书签”,定位下一页起始 | 关键列有序(如时间、编号),支持“连续翻页” | ✅ 性能优于 OFFSET,支持多条件筛选;❌ 不支持“跳页”,需记录上一页书签 |
| 4. 预计算中间结果 | 对高频筛选的深度分页场景,提前计算并存储“筛选后的主键列表”(如定时任务生成) | 筛选条件固定(如“近30天已支付订单”),数据更新不频繁 | ✅ 查询时直接取预存主键,性能极高;❌ 数据有延迟,需维护预计算任务 |
| 5. 限制分页深度 | 业务层限制最大偏移量(如“最多支持前100页分页”),超过则提示“请缩小筛选范围” | 非核心业务(如普通列表页),用户无“深分页”刚需 | ✅ 零开发成本,避免性能风险;❌ 牺牲部分用户体验 |
正确使用索引得建议
- 选择合适的字段创建索引:不为 NULL 的字段;被频繁查询的字段;被作为条件查询的字段;频繁需要排序的字段;被经常频繁用于连接的字段;
- 被频繁更新的字段应该慎重建立索引
- 限制每张表上的索引数量
- 尽可能的考虑建立联合索引而不是单列索引
- 注意避免冗余索引
- 字符串类型的字段使用前缀索引代替普通索引
- 避免索引失效
- 删除长期未使用的索引
- 知道如何分析 SQL 语句是否走索引查询
如何分析一条SQL语句是否有问题,执行计划是怎样的
先定位慢查询,再借助 EXPLAIN 深入分析执行计划,聚焦 type, key, rows, Extra 等关键字段,识别全表扫描、索引失效、额外排序等常见问题,最后针对性地优化索引或SQL写法。
在SQL语句前加上 EXPLAIN 关键字即可查看其执行计划(注意:这并不会真正执行该SQL语句),部分数据库(如MySQL 8.0+、PostgreSQL)还支持 EXPLAIN ANALYZE,它会实际执行语句并返回更详细的执行时间和实际扫描行数等统计信息。
MVCC机制详解
redis持久化机制
RabbitMQ的组件有哪些,当消息过多,也就是出现消息堆积时如何处理
| 组件名称 | 英文名 | 核心作用 |
|---|---|---|
| 生产者 | Producer | 创建并发送消息到交换机的客户端应用程序。 |
| 消费者 | Consumer | 从队列中订阅、获取并处理消息的客户端应用程序。 |
| 交换机 | Exchange | 消息的第一站,接收生产者消息,并根据类型和规则路由到一个或多个队列。 |
| 队列 | Queue | 消息的缓冲区,用于存储消息,等待消费者消费。消息是先进先出(FIFO)的。 |
| 绑定 | Binding | 连接交换机和队列的虚拟链路,并定义路由规则(如路由键)。 |
| 连接 | Connection | 应用程序与 Broker 之间的一个TCP连接,是信道的基础。 |
| 信道 | Channel | 建立在 TCP 连接上的虚拟连接。大部分实际操作(发消息、消费等)都在信道中进行,避免了频繁创建和销毁 TCP 连接的开销。 |
| 消息代理 | Broker | 指 RabbitMQ 服务器本身,负责接收、存储和转发消息。 |
| 虚拟主机 | Virtual Host | 提供逻辑隔离,类似于命名空间。不同 vHost 中的交换机、队列等互不可见,用于多租户和环境隔离。 |
| 生产者(Producer)通过连接(Connection)和信道(Channel)将消息发送到交换机(Exchange)。 | ||
| 交换机根据其类型(如 Direct、Fanout、Topic、Headers)和与队列(Queue)之间的绑定规则(Binding),将消息路由到特定的队列。 | ||
| 队列存储消息,等待消费者(Consumer)通过连接和信道来获取并处理。 |
消息堆积是指消息在队列中因无法被及时消费而大量积压的现象。
- 增加消费者数量:部署多个消费者示例;
- 优化消费者性能:优化消费端代码;使用多线程消费
- 使用惰性队列(Lazy Queue):惰性队列将消息直接写入磁盘,而不是先保存在内存中再刷盘,因此几乎不受内存限制,可以支持数百万甚至更多消息的存储。
- 增加队列上限
- 优化消息生命舟曲:设置过期时间;使用死信队列;监控与告警;
RabbitMQ消费者消费消息是使用poll还是push的方式,两种方式分别有什么优缺点。
推模式是 RabbitMQ 默认且更常用的模式。当消息到达队列时,Broker 会主动将消息推送给订阅了该队列的消费者。
- 工作原理:消费者使用 channel.basicConsume 方法订阅队列,并提供一个回调函数(例如继承 DefaultConsumer)。一旦有消息可用,RabbitMQ 会立即通过 handleDelivery 方法将消息推送过来。
- 优点:实时性高:消息一到就推送,延迟极低。吞吐量高:减少了消费者频繁请求的网络开销,能更有效地处理消息流,实现高吞吐量。 编程简单:消费者只需处理送达的消息,无需关心获取过程。
- 缺点:可能压垮消费者:如果生产者速度远大于消费者处理能力,推模式可能导致消费者缓冲区溢出或资源耗尽。流控依赖配置:需要通过 basicQos 设置预取计数(prefetch count)来限制未确认消息的数量,从而实现流控,避免消费者过载。
拉模式则由消费者主动向 Broker 请求消息。
- 工作原理:消费者使用 channel.basicGet 方法显式地从指定队列中检索一条消息。这是一个同步操作,通常需要在循环中进行。
- 优点:消费者自主控制:消费者可以完全控制消费的节奏和批量,根据自身处理能力拉取消息,避免被压垮。 避免无效推送:消费者可以在准备好处理消息时才发起请求。
- 缺点:延迟较高:由于需要不断轮询,消息消费可能不够及时,增加了延迟。 吞吐量较低:频繁的轮询请求会增加网络开销和 Broker 的负担,可能降低系统整体吞吐量。 编程复杂:需要自己管理轮询逻辑和消息确认。
MQ的应答机制保证消息发送和消费成功,有哪几种应答机制
- 自动应答 (Auto Ack):当 autoAck 参数设置为 true 时,RabbitMQ 一旦将消息交付给消费者,就立即认为消息已成功处理,并立刻从队列中删除该消息;
- 手动应答 (Manual Ack):当 autoAck 参数设置为 false 时,消费者必须在消息处理完成后,显式调用以下方法之一进行确认:
- basicAck(deliveryTag, multiple):肯定确认。告知 RabbitMQ 消息已成功处理,可以安全删除。
- basicNack(deliveryTag, multiple, requeue):否定确认,告知 RabbitMQ 消息处理失败。requeue 参数决定是否将消息重新放回队列(true)还是直接丢弃(false)。
- basicReject(deliveryTag, requeue):拒绝消息。功能类似 basicNack,但只能拒绝单条消息。
介绍一下应用架构,应用设计有几个模块,模块之间如何串联的
应用架构图
types、trigger、api、domain、app、infrastructure
big_market-app (启动层)
↓ 依赖
big_market-trigger (触发器层)
↓ 依赖
big_market-domain (领域层)
↓ 依赖
big_market-infrastructure (基础设施层)
↓ 依赖
big_market-api + big_market-types (接口&类型层)
- 请求入口 :HTTP请求 → Trigger层Controller
- 业务编排 :Controller调用多个Domain服务协作
- 数据操作 :Domain通过Repository接口 → Infrastructure实现
- 事件驱动 :Domain发布事件 → Infrastructure处理 → MQ异步消息
- 任务调度 :定时Job → Domain服务 → 数据更新
项目中比较复杂,有挑战性的工作
项目架构的设计和库表的设计
抽奖流程和策略的设计
二面
红黑树增删改查的复杂度,红黑树平衡如何保证
都为 O (log n)。自平衡机制约束了树的高度。
ThreadLocalMap是全局变量还是非全局变量,是静态变量还是非静态变量,为什么
是每个 Thread 实例独有的非静态成员变量,非全局变量。
静态变量(static 修饰)的核心特征是 “属于类本身,所有类实例共享同一变量”。非静态变量:属于 Thread 实例而非 Thread 类,不符合 “静态变量” 的类级共享特性。 而 ThreadLocalMap 是 Thread 类的非静态成员变量(无 static 修饰)
全局变量的核心特征是 “被所有线程共享,可在任意线程中访问同一实例”。非全局变量:线程间完全隔离,不共享,不符合 “全局变量” 的共享特性。
举一个幻读的例子
幻读是指在一个事务内,多次执行相同的查询(例如相同的 WHERE条件),但返回的结果集行数不一致,仿佛出现了“幽灵”行。 这通常是因为在事务执行过程中,其他并发事务插入或删除了符合该查询条件的记录。
举例:事务A第一次查询,返回2条记录;此时,事务B插入了一条新记录并提交,事务A第二次查询,返回3条记录
MySQL如何解决幻读
InnoDB存储引擎在 RR 级别下通过 MVCC和 Next-key Lock 来解决幻读问题:
- 执行普通 select,此时会以 MVCC 快照读的方式读取数据:在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。 所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
- 执行 select...for update/lock in share mode、insert、update、delete 等当前读: 在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用 Next-key Lock 来防止这种情况。 当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读参考。
这两种机制并非二选一,而是协同工作的关系,共同确保了在高并发环境下,数据的一致性和隔离性。
volatile如何保障并发线程的数据安全
在多线程环境中,每个线程会将主内存中的变量复制到自己的 “工作内存”(如 CPU 缓存)中操作,默认情况下,线程对变量的修改不会立即同步到主内存, 其他线程也不会主动从主内存刷新变量值,这会导致线程间数据不可见(如线程 A 修改了变量,线程 B 仍读取到旧值)。
volatile 解决这一问题的机制是:
- 写操作强制刷新主内存:当线程修改 volatile 变量时,会立即将修改后的值同步到主内存,并 ** invalidate(失效)其他线程工作内存中该变量的缓存副本 **。
- 读操作强制从主内存加载:当线程读取 volatile 变量时,会放弃工作内存中的旧值,直接从主内存加载最新值。
线程池处理任务的流程了解吗?
- 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
- 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用
RejectedExecutionHandler.rejectedExecution()方法。
线程池处理任务的流程中,先队列后扩容和先扩容后队列的优劣
线程池任务处理方案核心流程与实现
| 方案类型 | 核心流程 | 典型实现(以 Java ThreadPoolExecutor 为例) | 适合场景 | 典型业务案例 |
|---|---|---|---|---|
| 方案 1:先队列后扩容 | 1. 新任务优先分配给空闲核心线程; 2. 核心线程满 → 任务入队列等待; 3. 队列满 → 创建非核心线程(直到 maxPoolSize); 4. 线程数达 max 且队列满 → 触发拒绝策略。 | 默认策略(如用 LinkedBlockingQueue、ArrayBlockingQueue 等有界 / 无界队列) | 1. 任务执行时间短(毫秒级); 2. 任务量波动大但对延迟不敏感; 3. 需严格控制系统负载。 | 后台批处理(数据同步、日志分析)、定时任务 |
| 方案 2:先扩容后队列 | 1. 新任务优先分配给空闲核心线程; 2. 核心线程满 → 直接创建非核心线程(直到 maxPoolSize); 3. 线程数达 max → 任务入队列等待; 4. 队列满 → 触发拒绝策略。 | 用 SynchronousQueue(无容量队列),或自定义队列优先级逻辑 | 1. 任务执行时间较长(秒级); 2. 对延迟敏感(需快速响应); 3. 任务量可控(无极端突发)。 | 用户 API 接口、实时消息处理、RPC 调用 |
方案 1:先队列后扩容
- 资源利用率更高:核心逻辑“尽量复用已存在线程,线程是操作系统的 “重量级资源”(每个线程占用 1-2MB 栈空间,切换需 CPU 上下文切换), 方案 1 通过队列缓冲任务,避免了 “任务量波动时频繁创建非核心线程” 的问题。
- 响应速度较慢,延迟较高:核心线程满后,任务需先在队列中排队,直到队列满才会扩容非核心线程。 若队列容量较大(如LinkedBlockingQueue默认无界),任务可能长时间排队(甚至永远排队,若核心线程未空闲),对 “延迟敏感” 的任务不友好。
- 负载更平稳,稳定性更高:队列起到 “缓冲阀” 作用,即使任务量突发激增,也不会瞬间创建大量线程,而是通过队列缓慢释放任务给线程, 避免 CPU 因 “线程上下文切换频繁”(CPU 时间片被拆分给过多线程)导致的利用率下降,也避免内存因 “线程栈 + 队列任务” 过度占用而 OOM。
方案 2:先扩容后队列
- 资源利用率较低:逻辑核心是 “优先用新线程处理任务,减少排队”,但会导致线程数快速逼近 maxPoolSize。若任务量突发增大(如瞬时 1 万任务), 会短时间创建大量非核心线程,即使任务执行完后线程会被回收(取决于keepAliveTime),也会产生额外的创建 / 销毁开销,且线程闲置时仍占用栈内存。
- 响应速度更快,延迟更低: 核心线程满后,新任务无需排队,直接分配给新创建的非核心线程(直到 maxPoolSize),避免了 “队列等待” 的延迟。适合对延迟敏感的场景,能快速处理突发任务。
- 负载波动大,稳定性较低 任务突发时会快速创建线程到 maxPoolSize,若 maxPoolSize 设置过大(如Integer.MAX_VALUE),会导致:上下文切换频繁;大量线程栈占用内存,若任务队列堆积,容易触发OOM; 需要严格控制maxPoolSize,否则容易导致系统过载
美团
一面
数据库与缓存的一致性如何保证
三种缓存策略:旁路缓存;读写穿透;一异步缓存
项目中使用的是旁路缓存策略做很多读操作,使用延迟队列加定时任务的方式保证最终一致性。 不需要保证实施一致性,因为库存会提前预热到redis中,库存扣减操作都是在redis中完成的。只要保证数据的最终一致性即可, 比如库存为0时,直接异步更新数据库即可。
超卖问题如何解决的
在redis中通过decr原子操作实现库存扣减,初步解决超卖问题。再通过setNx做兜底操作。
MQ会不会出现重复消费,如何解决
通过设置唯一业务id保证幂等性,比如用户ID_返利类型_外部业务号:bizId。
设计模式在抽奖流程中的应用
以责任链和规则树距离,结合工厂,组合等策略具体说明。
通过模板模式定义了抽奖的标准流程,由子类实现具体逻辑,由DefaultRaffleStrategy.java 提供默认实现。 基于工厂类DefaultChainFactory创建责任链节点,比如黑名单规则节点、权重规则节点、默认规则节点。通过 openLogicChain 方法动态构建责任链实现链式连接。
基于工厂类DefaultTreeFactory和决策引擎DecisionTreeEngine实现决策节点,例如次数锁节点、库存节点、兜底奖励节点
通过策略模式讲不同的抽奖规则(黑名单、权重、默认)作为不同的策略实现,通过 ILogicChain 接口统一调用。
JVM虚拟机内存模型,垃圾回收算法,新生代老生代相关
volitate关键字,作用
- 可见性:如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
- 防止 JVM 的指令重排序。在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
锁:CAS思想,锁的分类
线程池相关
- corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
- maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
- keepAliveTime:当线程池中的线程数量大于 corePoolSize ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :拒绝策略
消息队列在项目中的应用
- 发送奖品消息;发送返利消息;积分调整成功消息;活动SKU库存清零消息;
- 返利消息消费者;奖品发送消费者;积分调整成功消费者;活动SKU库存清零消费者;
- 任务补偿机制
- 事务一致性保证:业务操作和消息记录在同一事务中完成;事务提交后异步发送MQ消息;发送成功更新任务状态为completed;发送失败更新任务状态为fail;定时任务扫描fail状态的任务进行重试。
隔离级别,MVCC
MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读,对数据一致性的保证太弱。
- READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。 幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
MySQL的数据一致性如何保证
事务的四大特性;MVCC;redo log 重做日志。
可重复读如何解决幻读?
当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。 按理论来说,只有到 可串行化 的最高隔离级别才能解决幻读问题,但是 MySql 在可重复读的隔离级别下就已经通过一些手段解决了幻读问题:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。无锁化,生成 ReadView 时版本链上已经提交的事务可见。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。每次都读最新记录,通过锁来控制并发。
这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。
为什么要选择DDD架构?
- 业务复杂度高 :多领域、多规则的营销系统
- 团队协作需求 :清晰的分层便于团队分工
- 可维护性要求 :领域逻辑与技术实现分离
- 可扩展性考虑 :支持业务快速迭代和技术演进
- 业务表达力 :代码结构直接反映业务模
项目难点?
小米
一面
Array的底层结构?
ArrayList:Object[] 数组 LinkedList:双向链表 看源码相关
HashMap,treeMap底层原理,是否安全?
都不是线程安全的。 看源码相关
concurrentHashMap深挖
JDK不同版本的特点,注意1.几版本和8、11等版本的区别
JDK 1.0(1996):首个正式版本,奠定 Java 基础语法和运行时环境。 JDK 1.5(2004):重大更新,引入泛型、注解、枚举、foreach 循环、自动装箱拆箱等核心特性。 JDK 1.7(2011):优化语法和性能,引入 try-with-resources、菱形语法等。 JDK 1.8(2014):里程碑版本,引入 Lambda 表达式、Stream API、函数式接口等,彻底改变 Java 编程范式。 JDK 9(2017):引入模块化系统(JPMS),支持接口私有方法,增强 Stream API。 JDK 11(2018):长期支持版本(LTS),移除永久代,增强字符串处理,引入 HttpClient 等。 JDK 17(2021):最新长期支持版本(LTS),强化密封类、模式匹配,移除不安全的 API,性能大幅优化。 JDK 21(2023):是 Java SE 平台的最新 LTS 版本。 JDK 24(2024):JDK 24 的二进制文件可在生产环境中免费使用和重新分发 JDK 25(2025):JDK 25 目前处于开发阶段,计划于 2025 年 9 月 16 日正式发布。
ThreadLocal深挖:内存泄漏,线程安全
ThreadLocalMap中ThreadLocal为key,是弱引用,会导致内存泄漏。
synchronized及其使用方式
synchronized 属于 重量级锁,效率低下。底层就是获取 对象监视器 monitor 的持有权。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
- 修饰实例方法 (锁当前对象实例):给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 。
- 修饰静态方法 (锁当前类):给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁。 这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
- 修饰代码块 (锁指定对象/类):synchronized(object) 表示进入同步代码块前要获得给定对象的锁;synchronized(类.class) 表示进入同步代码块前要获得给定 Class 的锁。
构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。
创建线程的方式,线程的状态深挖,项目中有没有配置线程池
一般来说,创建线程的方式有很多,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。上述这些准确来说属于是Java中使用多线程的方法。
严格来说,Java创建线程的方式只有一种,即new Thread().start()。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
- NEW: 初始状态,线程被创建出来但没有被调用 start() 。
- RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
- BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像
- WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
项目中的线程池参数配置:
- 核心线程数(corePoolSize):默认值为 20
- 最大线程数(maxPoolSize):默认值为 200
- 线程保活时间(keepAliveTime):默认值为 10L 秒
- 阻塞队列大小(blockQueueSize):默认值为 5000
- 拒绝策略(policy):默认值为 AbortPolicy
垃圾回收器?CMS,G1
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。 CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。以“标记-清除”算法实现。在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。
G1收集器 G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。
关系型数据库和非关系型数据库
关系型数据库基于关系模型(二维表格) 设计,核心是 “表、行、列” 的结构化组织,通过 SQL(结构化查询语言)操作数据。比如MySQL、PostgreSQL等。
非关系型数据库(Not Only SQL)摒弃了固定表格结构,针对不同场景设计了多样化的数据模型,核心是 “灵活存储 + 高扩展性”。如Redis、MongoDB等。
索引了解吗?常用的索引?主键索引,普通索引
mysql隔离级别
MySQL三大日志redolog,undolog,binlog
binlog日志
binlog(binary log 即二进制日志文件) 主要记录了对 MySQL 数据库执行了更改的所有操作(数据库执行的所有 DDL 和 DML 语句), 包括表结构变更(CREATE、ALTER、DROP TABLE…)、表数据修改(INSERT、UPDATE、DELETE...),但不包括 SELECT、SHOW 这类不会对数据库造成更改的操作。
不过,并不是不对数据库造成修改就不会被记录进 binlog。即使表结构变更和表数据修改操作并未对数据库造成更改,依然会被记录进 binlog。
可以通过 show binary logs 查看所有的二进制日志列表。
binlog 通过追加的方式进行写入,大小没有限制。我们可以通过max_binlog_size参数设置每个 binlog 文件的最大容量,当文件大小达到给定值之后,会生成新的 binlog 文件来保存日志, 不会出现前面写的日志被覆盖的情况。
binlog的主要应用场景就是主从复制。还要同步MySQL数据到其他数据源的工具(如canal)底层也依赖binlog。
| 类型 | 核心操作对象 | 典型语句 | 事务特性 |
|---|---|---|---|
| DDL | 数据库/表的结构 | CREATE/ALTER/DROP | 自动提交,不可回滚 |
| DML | 表中的数据 | INSERT/UPDATE/SELECT | 需手动提交,可回滚 |
| 简单来说:DDL管“表的样子”,DML管“表里的内容”。 |
redolog
redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据, 保证数据的持久性与完整性。
undo log
每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。 undo log 属于逻辑日志,记录的是 SQL 语句。
undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment(undo 日志段), undo log segment 包含在 rollback segment(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment, 这有助于管理多个并发事务的回滚需求。
通常情况下, rollback segment header(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分, 通常在回滚段的第一个页。history list 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。 这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
redis持久化
springboot是什么?哪个注解比较重要?解释这个注解?支持哪些内嵌容器?
自动装配:springBootApplication
支持的三种内嵌Web容器:Tomcat、Jetty、Undertow
spring-boot-start的实现,比如实现一个spring-boot-start的具体流程
简要流程:编写业务 Bean → 封装为自动配置类(含条件)→ 暴露可绑定属性 → 在资源文件注册自动配置 → 发布依赖 → 业务项目引入并通过配置启用/定制 → 用 ApplicationContextRunner 验证行为。
Mybatis详解
睿联
一面
后端开发工程师的主要的工作任务和岗位职责分别是什么呢
Linux文件权限是如何控制的
操作系统中每个文件都拥有特定的权限、所属用户和所属组。权限是操作系统用来限制资源访问的机制,在 Linux 中权限一般分为读(readable)、写(writable)和执行(executable),分为三组。 分别对应文件的属主(owner),属组(group)和其他用户(other),通过这样的机制来限制哪些用户、哪些组可以对特定的文件进行什么样的操作。
我们通过ls l命令查看某个文件下的目录或目录的权限。Linux 中权限分为以下几种:
- r:代表权限是可读,r 也可以用数字 4 表示
- w:代表权限是可写,w 也可以用数字 2 表示
- x:代表权限是可执行,x 也可以用数字 1 表示
修改文件/目录的权限的命令:chmod
在 Linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。
- 所有者(u) :一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者,用 ls ‐ahl 命令可以看到文件的所有者,也可以使用 chown 用户名 文件名来修改文件的所有者。
- 文件所在组(g) :当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组用 ls ‐ahl命令可以看到文件的所有组也可以使用 chgrp 组名文件名来修改文件所在的组。
- 其它组(o) :除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。
操作系统中进程和线程的区别是什么呢
- 进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。进程是操作系统资源分配的基本单位。
- 线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。而线程是任务调度和执行的基本单位
- 协程(Coroutine):用户态的轻量级 “线程”,完全由程序(代码)控制调度,不依赖操作系统内核,资源消耗极低。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
- 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
- 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
有了进程为什么还需要线程
- 进程切换是一个开销很大的操作,线程切换的成本较低。
- 线程更轻量,一个进程可以创建多个线程。
- 多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。
- 同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
为什么要使用多线程
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
- 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。
说一下你对虚拟内存的理解
虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。
- 隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
- 提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
- 简化内存管理:进程都有一个一致且私有的虚拟地址空间,程序员不用和真正的物理内存打交道,而是借助虚拟地址空间访问物理内存,从而简化了内存管理。
- 多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。这些库对于每个进程而言都是公用的,它们在内存中实际只会加载一份,这部分称为共享内存。
- 提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
- 提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。这是因为当物理内存不够用时,可以利用磁盘充当,将物理内存页(通常大小为 4 KB)保存到磁盘文件(会影响读写速度),数据或代码页会根据需要在物理内存与磁盘之间移动。
操作系统中交换分区有什么用
换分区(Swap Partition) 是一块专门划分出来的磁盘空间,核心作用是作为物理内存(RAM)的 “补充扩展”, 当物理内存不足以支撑当前运行的程序时,系统会借助交换分区临时存放部分数据,避免程序崩溃或系统卡死。
redis常见的数据结构,借助redis如何实现一个队列
- 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
- 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
- 基于 List 实现 “基础队列”
- 基于 Sorted Set 实现 “优先级队列”
- 基于 Stream 实现 “高可靠消息队列”
数据库要存储对精度有明确要求的数据,比如金额,要用什么类型来存
DECIMAL 类型
脏读、幻读、不可重复读的区别和如何体现的,举例说明
- 脏读(Dirty Read):读取 “未提交的脏数据” 核心定义:一个事务(T1)读取到了另一个事务(T2)尚未提交的数据。若 T2 后续回滚,T1 读取到的就是 “无效的脏数据”,会导致业务逻辑错误。
- 不可重复读(Non-Repeatable Read):同一事务内 “重复读结果不一致” 核心定义:一个事务(T1)在同一执行过程中,多次读取同一数据,但由于另一个事务(T2)对该数据进行了 “已提交的修改 / 删除”,导致 T1 每次读取的结果不一致。
- 幻读(Phantom Read):同一事务内 “读的行数变了” 核心定义:一个事务(T1)在同一执行过程中,多次执行同一查询语句(通常是范围查询),但由于另一个事务(T2)对该范围的数据进行了 “已提交的插入 / 删除”,导致 T1 每次查询返回的 “行数不一致”(像出现了 “幻觉”)。
当前读和快照读的区别
快照读(一致性非锁定读)就是单纯的 SELECT 语句,但不包括下面这两类 SELECT 语句:
SELECT ... FOR UPDATE
# 共享锁 可以在 MySQL 5.7 和 MySQL 8.0 中使用
SELECT ... LOCK IN SHARE MODE;
# 共享锁 可以在 MySQL 8.0 中使用
SELECT ... FOR SHARE;
快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。
快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。 只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用一致性非锁定读:
- 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
- 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。
当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。
# 对读的记录加一个X锁
SELECT...FOR UPDATE
# 对读的记录加一个S锁
SELECT...LOCK IN SHARE MODE
# 对读的记录加一个S锁
SELECT...FOR SHARE
# 对修改的记录加一个X锁
INSERT...
UPDATE...
DELETE...
索引失效的场景
- 创建了组合索引,但查询条件未遵守最左匹配原则;
- 在索引列上进行计算、函数、类型转换等操作;
- 以 % 开头的 LIKE 查询比如 LIKE '%abc';;
- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
- 发生隐式转换;
为什么采用B+数作为数据库的底层数据结构
- 二叉查找树(BST) :解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表;
- 平衡二叉树(AVL) :通过旋转解决了平衡的问题,但是旋转操作效率太低;
- 红黑树 :通过舍弃严格的平衡和引入红黑节点,解决了 AVL 旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO 次数太多;
- B 树 :通过将二叉树改为多路平衡查找树,解决了树过高的问题;
- B+树 :在 B 树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。
什么是子网掩码,为什么说TCP是流式的传输协议
子网掩码(Subnet Mask)是IPv4 网络中用于划分 “网络地址” 和 “主机地址” 的 32 位二进制数,本质是通过 “与运算” 将 IP 地址拆分为两部分, 从而实现 “子网划分” 和 “判断两台设备是否在同一网段” 的核心功能。
TCP传输的是无边界、连续的字节流,而非 “数据包” 形式 —— 这与 UDP 的 “数据报式传输” 形成鲜明对比。 发送 / 接收节奏不强制对应:TCP 发送方可以 “多次小数据合并发送”,接收方也可以 “一次接收大量数据后分多次读取”,双方的 “发送次数” 和 “接收次数” 无需匹配。字节流有序且可靠.
HTTP常见的请求方法,服务端如何知道请求的长度
GET、POST、PUT、DELETE
- GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。
- GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。
Cookie 和 Session 有什么区别?
- Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。 服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
- Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更高。 如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。
- GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式, 如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。
- 由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。
- GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。 另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中。
URL由哪几部分组成
URL(Uniform Resource Locators),即统一资源定位器。网络上的所有资源都靠 URL 来定位,每一个文件就对应着一个 URL,就像是路径地址。理论上,文件资源和 URL 一一对应。
URL组成结构:
- 协议。URL 的前缀通常表示了该网址采用了何种应用层协议,通常有两种——HTTP 和 HTTPS。
- 域名。域名便是访问网址的通用名,这里也有可能是网址的 IP 地址。
- 端口。如果指明了访问网址的端口的话,端口会紧跟在域名后面,并用一个冒号隔开。
- 资源路径。域名(端口)后紧跟的就是资源路径,从第一个/开始,表示从服务器上根目录开始进行索引到的文件路径。
- 参数。参数是浏览器在向服务器提交请求时,在 URL 中附带的参数。
- 锚点。是在要访问的页面上的一个锚。要访问的页面大部分都多于一页,如果指定了锚点,那么在客户端显示该网页是就会定位到锚点处,相当于一个小书签。
Java有没有无符号整数这种类型呢
没有原生的无符号整数类型。所有基本整数类型(byte、short、int、long)默认都是有符号的。
Java中接口和抽象类的区别,说一下对多态的理解
接口和抽象类的共同点实例化:
- 接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
- 抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
接口和抽象类的区别
- 设计目的:接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 继承和实现:一个类只能继承一个类(包括抽象类),因为 Java 不支持多继承。但一个类可以实现多个接口,一个接口也可以继承多个其他接口。
- 成员变量:接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。抽象类的成员变量可以有任何修饰符(private, protected, public),可以在子类中被重新定义或赋值。
- 方法:
- Java 8 之前,接口中的方法默认是 public abstract ,也就是只能有方法声明。自 Java 8 起,可以在接口中定义 default(默认) 方法和 static (静态)方法。 自 Java 9 起,接口可以包含 private 方法。
- 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写。
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。 多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类重写的方法,如果子类没有重写父类的方法,执行的是父类的方法。
波克城市
一面
HashMap底层结构
反射、优缺点及应用场景
Java 反射 (Reflection) 是一种在程序运行时,动态地获取类的信息并操作类或对象(方法、属性)的能力。
优点:
- 灵活性和动态性:反射允许程序在运行时动态地加载类、创建对象、调用方法和访问字段。这样可以根据实际需求(如配置文件、用户输入、注解等)动态地适应和扩展程序的行为,显著提高了系统的灵活性和适应性。
- 框架开发的基础:许多现代 Java 框架(如 Spring、Hibernate、MyBatis)都大量使用反射来实现依赖注入(DI)、面向切面编程(AOP)、对象关系映射(ORM)、注解处理等核心功能。反射是实现这些“魔法”功能不可或缺的基础工具。
- 解耦合和通用性:通过反射,可以编写更通用、可重用和高度解耦的代码,降低模块之间的依赖。例如,可以通过反射实现通用的对象拷贝、序列化、Bean 工具等。
缺点:
- 性能开销:反射操作通常比直接代码调用要慢。因为涉及到动态类型解析、方法查找以及 JIT 编译器的优化受限等因素。不过,对于大多数框架场景,这种性能损耗通常是可以接受的,或者框架本身会做一些缓存优化。
- 安全性问题:反射可以绕过 Java 语言的访问控制机制(如访问 private 字段和方法),破坏了封装性,可能导致数据泄露或程序被恶意篡改。此外,还可以绕过泛型检查,带来类型安全隐患。
- 代码可读性和维护性:过度使用反射会使代码变得复杂、难以理解和调试。错误通常在运行时才会暴露,不像编译期错误那样容易发现。
线程池核心参数和拒绝策略
JVM的内存区域
线程私有的
- 程序计数器:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理;在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
- Java 虚拟机栈:由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的.
- 本地方法栈:为虚拟机使用到的 Native 方法服务。
线程共享的:
- 堆:字符串常量池,存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
- 方法区:运行时常量池,存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 直接内存
垃圾回收机制以及垃圾回收器(CMS,G1)
新生代垃圾回收,老生代垃圾回收,整堆收集,混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。只有G1有这个模式。
标记-清除算法:标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
复制算法:将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 这样就使每次的内存回收都是对内存区间的一半进行回收。
标记-整理算法:标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法:根据对象存活周期的不同将内存分为几块。根据各个年代的特点选择合适的垃圾收集算法。比如新生代采用“复制”算法,老生代采用”标记-复制“或“标记-整理”算法。
为什么采用B+树作为数据库底层数据结构
事务的四大特性以及如何保证的
| 特性 | 英文全称 | 核心定义 | 如何保证(以 MySQL 为例) |
|---|---|---|---|
| 原子性(A) | Atomicity | 事务是“不可分割的最小单位”,要么所有操作全部执行成功,要么全部失败回滚(无中间状态)。 | 1. 日志机制:通过 Undo Log(回滚日志)记录事务执行前的状态,若事务失败,通过 Undo Log 恢复到执行前; 2. 事务隔离级别:强制事务要么完整执行,要么不执行。 |
| 一致性(C) | Consistency | 事务执行前后,数据库的“业务规则一致性”不被破坏(如转账前后总金额不变、余额不能为负)。 | 1. 业务逻辑约束:代码层校验(如转账前判断余额是否足够); 2. 数据库约束:主键、外键、唯一索引、非空约束、CHECK 约束(如 CHECK (balance >= 0));3. 依赖原子性、隔离性、持久性的共同保障。 |
| 隔离性(I) | Isolation | 多个事务并发执行时,一个事务的操作不会被其他事务“干扰”,每个事务都像独立执行一样。 | 1. 锁机制: - 行锁(InnoDB 支持):锁定单行数据,减少并发冲突; - 表锁(MyISAM 支持):锁定整张表,适合读多写少场景; 2. MVCC(多版本并发控制):InnoDB 核心技术,通过“数据版本”让不同事务读写不冲突(如读事务用旧版本,写事务生成新版本); 3. 隔离级别配置:通过设置不同隔离级别(读未提交、读已提交、可重复读、串行化)控制隔离程度。 |
| 持久性(D) | Durability | 事务一旦执行成功(提交后),其修改的数据会“永久保存”到数据库,即使断电、崩溃也不会丢失。 | 1. Redo Log(重做日志):事务执行时,先将修改记录到 Redo Log(磁盘存储,而非内存),即使数据库崩溃,重启后可通过 Redo Log 恢复已提交的事务; 2. 刷盘策略:InnoDB 可配置 innodb_flush_log_at_trx_commit,控制事务提交时 Redo Log 刷盘的时机(如设为 1 时,每次提交必刷盘,确保持久性)。 |
Bean的生命周期
- 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
- Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。
- Bean 初始化:
- 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
- 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
- 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
- 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
- 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法。
- 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。%0D%0A如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
- 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法。
- 销毁 Bean:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
- 如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
- 如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy 注解标记 Bean 销毁之前执行的方法。
synchronized 和 ReentrantLock 有什么区别?
什么是伪共享
CPU执行速度远快于内存读写,因此硬件层面引入了CPU缓存。 缓存的核心设计原则是 “空间局部性”:当 CPU 访问某个数据时,会将该数据及其相邻的一小块数据(通常是 64 字节) 一起加载到缓存中,这一小块数据被称为 “缓存行(Cache Line)”。 但当 多线程并发修改 “同一缓存行中的不同变量” 时,问题就出现了:缓存行的 “整体性” 会导致无关变量的修改互相干扰,这就是伪共享。
本质是 “缓存设计与多线程并发访问” 冲突导致的性能问题,并非真正的 “共享资源竞争”,因此被称为 “伪” 共享。
对于以下情况,伪共享可以无需处理
- 变量是 “只读” 的(不会修改,缓存行不会失效);
- 多线程修改频率低(缓存颠簸次数少,性能影响可忽略);
- 变量本身已占满一个缓存行(如大数组、大对象)。
RabbitMQ的持久性保证和顺序性保证
可靠性:
- 生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
- RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
- RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。
顺序性:
- 单个队列单个消费者模式:确保一个队列中的消息只被一个消费者消费,这样可以保证消费者按照消息到达的顺序来处理消息。这种方法简单有效,但可能会限制消息处理的并发性和吞吐量。
- 多队列多消费者模式:将消息分散到多个队列中,每个队列有一个消费者。这样可以在保证顺序的同时提高并发处理能力。但这种方法会增加管理的复杂性,因为需要维护多个队列。
- 内存队列排队:在消费者内部使用内存队列对消息进行排队,然后分发给不同的工作线程(worker)处理。这种方法可以在单个消费者内部实现并发处理,同时保持消息的顺序性。
动态代理实现的方式
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象, 就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法,private 方法也无法代理。 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀。
缓存穿透、缓存击穿、缓存雪崩及解决方案
场景题:高并发的登陆场景,在5-10分钟内有大量的登录请求,流程设计
一、Cookie + Session 登录
Cookie 是服务器端发送给客户端的一段特殊信息,这些信息以文本的方式存放在客户端,客户端每次向服务器端发送请求时都会带上这些特殊信息。 在 B/S 系统中,登录功能通常都是基于 Cookie 来实现的。当用户登录成功后,服务端会将登录状态记录到 Session 中,同时需要在客户端保存一些信息(SessionId),并要求客户端在之后的每次请求中携带它们。 在这样的场景下,使用 Cookie 无疑是最方便的,因此我们一般都会将 SessionId 保存到 Cookie 中,当服务端收到请求后,通过验证 Cookie 中的 SessionId 来判断用户的登录信息。
实现流程:
- 用户输入用户名和密码,前端将用户提交的用户名和密码发送到后端进行验证。
- 后端验证用户信息是否正确,并创建一个Session。Session是一种服务器端保存用户会话信息的机制,用于识别多次请求之间的逻辑关系。
- 后端将Session ID(通常是一个随机的字符串)返回给前端,并通过 Cookie 的方式将Session ID保存在浏览器中。这样就可以保证当用户再次发送请求时,后端可以通过该 Session ID 来识别用户身份,并完成相关的操作。
- 在后续的请求中,浏览器会自动将保存的 Cookie 信息发送到后端进行验证,如果 Session ID有效,则返回相应的数据。如果 Session ID 失效或者不存在,则需要重新登录获取新的 Session ID。
- 在用户退出时,后端需要删除对应的 Session 信息,以保证安全性。
二、Token 登录
Token 是通过服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。
实现流程:
- 用户输入用户名和密码,前端将用户提交的用户名和密码发送到后端进行验证。
- 后端验证用户信息是否正确,并生成一个 Token。Token 是一串加密的字符串,包含了用户的身份信息和权限等相关信息。
- 后端将 Token 返回给前端,并保存在客户端的LocalStorage或者SessionStorage中。
- 每次向后端发送请求时,前端都需要在请求头部携带 Token 信息。
- 后端接收到请求后会从 Token 中解析出用户身份信息,并通过权限校验等操作来判断请求是否合法。
- 如果校验通过,则返回相应的数据,否则返回错误信息。
- 在用户退出时,前端需要删除保存的 Token 信息。
由于 Token 信息存储在客户端,因此不同于 Session 机制,它可以轻松地跨域使用,而且不需要考虑 Session 共享、分布式管理等问题。同时,由于 Token 机制不依赖服务器端的资源,因此在大规模高并发访问时,它具有更好的性能表现。
三、SSO 单点登录
SSO(Single Sign-On,单点登录)是一种在多个应用程序(比如 Web 服务)中实现认证和授权的方法。它允许用户只需登录一次,就可以访问多个应用程序,大大提高了用户体验和工作效率。
实现流程:
- 用户通过浏览器访问第一个应用程序,并输入用户名和密码进行登录。
- 第一个应用程序验证用户信息后,生成一个 Token 并将该 Token 返回给浏览器端。同时,它会将 Token 与该用户的身份信息绑定并存储在一个共享的认证数据源中,如 LDAP、数据库等。
- 用户再次访问另一个应用程序时,该应用程序检查用户是否已经登录过。如果用户未登录,则引导用户到第一个应用程序进行登录;如果用户已经登录,则从共享的认证数据源中获取用户的身份信息,并生成一个新的 Token 返回给浏览器端。
- 浏览器将 Token 发送给第二个应用程序,第二个应用程序使用相同的认证数据源来验证 Token,以确认该用户是否有权限访问该应用程序。
- 如果 Token 有效,则第二个应用程序返回相应的数据;否则,它要求用户重新进行登录或者提示用户无权访问。
- 用户访问其他应用程序时,重复上述过程。
实现方式:基于 Cookie 实现;基于 Session 实现;认证中心;基于 OAuth 实现;基于 OpenID Connect 实现
通过Redis的数据存储结构,可以将用户登录信息、状态信息、token等二进制数据存储在缓存中,快速查询和验证用户信息,从而降低与数据库的交互,提高整个登录流程的效率。 设置多级缓存,通过验证码等方式保证登陆安全。
对于频繁登录的用户(用大量的手机号和密码登录,造成大量错误登录),如何处理。
后台使用 Redis 记录当前 ip 的尝试登录次数:
- key 为该 ip 请求登录的唯一标识。
- value 为当前 ip 的尝试登录次数。
我们需要给这个 key 设置一个过期时间,用来实现指定时间内无法再次登录的效果。并且,每次对 key 对应的 value 进行修改时,都需要重置过期时间。
对于ip在算时间内登录次数过多的用户,就加入黑名单,返回相应的信息限制登陆。
直接在用户表里增加两个字段:
- 输错密码次数 num
- 禁止登录的截至时间点 lock-time
我们需要记录输错密码的次数 num,当输入正确密码之后重置 num 和 lock-time 字段的值,当输错密码次数达到 3 次之后,修改 lock-time 为允许再次登录的时间。
整个逻辑也很简单(我们这里假设错误阈值为 3 ):
- 当用户提交用户名和密码登录时,先判断当前时间点是不是比 lock-time 小。
- 如果比 lock-time 小的话,说明当提前用户暂时被限制登录,返回“输入密码错误次数达到 3 次,请 xx 分钟后再尝试”。
- 如果大于等于 lock-time 的话,表明当前未被限制登录,进一步判断 num 的大小是否小于 3。
- 如果小于 3 则代表还能继续尝试登录,用户名和密码校验通过,则返回“登录成功”,并重置 num 和 lock-time 字段的值;否则,就返回“登录失败,用户名/密码错误”,并将 num 的值加 1。
- 如果 num 等于 3,则表明该 ip 已经尝试登录过 3 次,返回“输入密码错误次数达到 3 次,请 xx 分钟后再尝试”,并更新 lock-time 的值。%0D%0A9 人点赞%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A9
将登陆失败次数过多的用户也加入黑名单。
对于反馈登录慢的用户,如何定位问题处理,(分为单用户问题还是多批量用户问题)。
对于单用户,可能是该用户的网络延迟问题,
对于批量用户问题,通过监控监测问题出现的位置
- 边缘节点/运营商抖动、WAF 规则误杀、证书/OCSP 问题。
- CPU/线程池饱和:bcrypt/argon2 线程池队列变长、GC 增多。
- 检查中间件:Redis:RT 上升/连接池满/主从切换;限流、会话、凭证缓存都依赖它;数据库:主库 RT 升高、锁表/慢查询;凭证回源增多(缓存命中率下降);MFA/验证码供应商:区域性抖动。
有人恶意拦截网络请求,该如何处理,使用什么通信协议
对外统一用 HTTPS(HTTP/2 或 HTTP/3)= TLS 1.3;服务间一律 mTLS;必要时引入请求签名/重放防护。
HTTPS/TLS 1.3 + HSTS + mTLS(内网)是“拦截防线”的地基;在此之上,用 短期令牌 + PKCE/签名 + 重放防护 + BFF 把“拦截了也不能用”做实,最后靠监控与演练闭环。
高并发处理流程
- 分而治之,横向扩展:采用分布式部署的方式,部署多台服务器,把流量分流开,让每个服务器都承担一部分的并发和流量,提升整体系统的并发能力。
- 微服务拆分:这样就可以达到分摊请求流量的目的,提高了并发能力。 所谓的微服务拆分,其实就是把一个单体的应用,按功能单一性,拆分为多个服务模块。
- 分库分表:拆分为多个数据库,来抗住高并发的毒打。
- 池化技术:即数据库连接池、HTTP 连接池、Redis 连接池等等。使用数据库连接池,可以避免每次查询都新建连接,减少不必要的资源开销,通过复用连接池,提高系统处理高并发请求的能力。
- 主从分离:做主从分离,然后实时性要求不高的读请求,都去读从库,写的请求或者实时性要求高的请求,才走主库。这样就很好保护了主库,也提高了系统的吞吐。
- 使用缓存:Redis缓存,JVM本地缓存,memcached等等。
- CDN 加速静态资源访:商品图片,icon等等静态资源,可以对页面做静态化处理,减少访问服务端的请求。
- 消息队列削锋
- ElasticSearch:用ES来支持简单的查询搜索、统计类的操作。
- 降级熔断:熔断降级是保护系统的一种手段。最简单是加开关控制,当下游系统出问题时,开关打开降级,不再调用下游系统。还可以选用开源组件Hystrix来支持。
- 限流:可以使用Guava的RateLimiter单机版限流,也可以使用Redis分布式限流,还可以使用阿里开源组件sentinel限流。
- 异步:以借用消息队列实现。比如在海量秒杀请求过来时,先放到消息队列中,快速相应用户,告诉用户请求正在处理中,这样就可以释放资源来处理更多的请求。秒杀请求处理完后,通知用户秒杀抢购成功或者失败。
- 常规的优化:接口优化
- 压力测试确定系统瓶颈:在系统上线前,需要对系统进行压力测试,测清楚你的系统支撑的最大并发是多少,确定系统的瓶颈点,让自己心里有底,最好预防措施。 压测完要分析整个调用链路,性能可能出现问题是网络层(如带宽)、Nginx层、服务层、还是数据路缓存等中间件等等。loadrunner是一款不错的压力测试工具,jmeter则是接口性能测试工具,都可以来做下压测。
- 应对突发流量峰值:扩容+切流量
- 扩容:比如增加从库、提升配置的方式,提升系统/组件的流量承载能力。比如增加MySQL、Redis从库来处理查询请求。
- 切流量:服务多机房部署,如果高并发流量来了,把流量从一个机房切换到另一个机房。
招银网络
一面
@Transactional注解和Spring事务
Sring 管理事务的方式:
- 编程式事务:在代码中硬编码(分布式系统推荐使用),通过
TransactionTemplate或者TransactionManager手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。 - 声明式事务:在 XML 配置文件中配置或者直接基于注解(单体应用或者简单业务系统推荐使用),实际上是基于AOP实现(
@Transactional)
事务传播行为是为了解决业务层方法之间互相调用的事务问题。 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
TransactionDefinition.PROPAGATION_REQUIRED:如果存在事务,加入该事务;如果不存在事务。就创建一个新事物;TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事物,如果存在事务,就把当前事务挂起;TransactionDefinition.PROPAGATION_NESTED:如果存在事务,就创建一个事务当作当前事务的嵌套事务来运行,如果不存在事务,就创建一个新事物;TransactionDefinition.PROPAGATION_MANDATORY:如果存在事务,就加入该事务,如果不存在事务,就抛出异常。TransactionDefinition.PROPAGATION_SUPPORTS:如果存在事务,就加入该事务;如果不存在事务,就以非事务的形式运行TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务的形式运行,如果存在事务,就把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER:以非事务的形式运行,如果存在事务就抛出异常。
@Transactional注解是声明式事务管理的核心注解,它允许开发者通过简单的配置管理数据库事务,无需编写繁琐的事务代码。
@Transactional的实现依赖于Spring AOP(面向切面编程)。如果一个类或类中的public方法被@Transactional注解注解修饰,Spring容器就会在启动前为其创建一个代理类。 当调用@Transactional注解修饰的public方法时,实际上调用的是TransactionInterceptor的invoke()方法,这个方法的作用就是在目标方法启动前开启事务,如果方法执行过程中遇到了异常就回滚事务, 方法调用完成之后提交事务。
当一个方法被标记了@Transactional注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。这是由 Spring AOP 工作原理决定的。 因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。
@Transactional默认的回滚策略是遇到RuntimeException(运行时异常)和Error时才会回滚事务,对于CheckedException(受检查异常)不会回滚,因为Spring认为受检查异常是可预期的错误。可以通过业务逻辑来处理。 而RuntimeException和Error是不可预期的错误。
如果想要修改默认的回滚策略,可以通过rollbackFor和noRollbackFor属性来指定哪些异常需要回滚,哪些异常不需要回滚。
@Transactional的作用范围:
- 方法·:推荐使用在方法上,不过只对public方法生效
- 类:使用在类上,就对类中所有的public方法生效;
- 接口:不推荐,因为违背了 “Java 注解的继承规则” 和 “Spring 事务的代理机制”,大概率导致事务失效;同时还会造成 “技术细节侵入接口” 的逻辑混乱,增加维护成本。
常用配置参数(比较常用的5个):
| 属性名 | 说明 |
|---|---|
| propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 |
| isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 |
| timeout | 事务的超时时间,默认值为 -1(不会超时)。如果超过时间限制但事务还没有完成,则自动回滚事务。 |
| readOnly | 指定事务是否为只读事务,默认值为 false。 |
| rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
字节
一面
一、IOC 的基本原理
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 二、IOC 容器的底层实现流程 Spring IOC 容器的工作流程可分为容器初始化和Bean 实例化两大阶段,核心步骤如下:
- 容器初始化(加载配置,解析 Bean 定义)
- 步骤 1:资源定位, 容器扫描指定路径(如@ComponentScan标注的包),加载配置元数据(XML 配置、注解如@Component/@Service等)。 SpringBoot 默认扫描启动类所在包及其子包,通过@SpringBootApplication间接触发@ComponentScan。
- 步骤 2:Bean 定义解析, 将配置元数据解析为BeanDefinition对象(存储 Bean 的类名、 scope、依赖关系等元信息),注册到BeanDefinitionRegistry(注册表)中。 例如:@Service标注的类会被解析为BeanDefinition,记录其类型为UserService,scope 为单例(默认)。
- 步骤 3:BeanFactory 初始化, 初始化BeanFactory(IOC 容器的基础实现,如DefaultListableBeanFactory),并将BeanDefinition注册表传入工厂。
- Bean 实例化(创建对象,注入依赖) 当容器启动或首次请求 Bean 时,触发 Bean 的实例化,核心流程如下:
- 步骤 1:选择构造函数 根据BeanDefinition和依赖关系,确定实例化 Bean 的构造函数(默认无参构造,若有@Autowired则按参数匹配)。
- 步骤 2:实例化 Bean(创建对象) 通过反射(Class.newInstance()或Constructor.newInstance())创建 Bean 的实例,此时对象仅完成初始化,依赖尚未注入。
- 步骤 3:依赖注入(DI) 容器根据BeanDefinition中的依赖信息,自动查找并注入依赖的 Bean: 构造函数注入:实例化时通过构造函数参数传入依赖。 Setter 注入:调用 Setter 方法(如setUserDao(UserDao dao))注入依赖。 字段注入:通过反射直接给@Autowired标注的字段赋值(不推荐,破坏封装性)。
- 步骤 4:初始化 Bean 执行初始化逻辑: 调用@PostConstruct标注的方法。 执行InitializingBean接口的afterPropertiesSet()方法。 调用自定义的初始化方法(如 XML 中init-method指定的方法)。
- 步骤 5:注册到容器 实例化完成的 Bean 被放入容器的缓存(如单例 Bean 存储在singletonObjects缓存中),供后续使用。
DDD领域驱动设计来实现项目,那么涉及的领域和一些实体详细讲一下,项目中的领域和实体是如何划分的,具体举例说明
- 策略领域 (Strategy Domain)
- 核心职责 :负责抽奖策略的配置、规则处理和奖品分发逻辑
- 主要实体 :
StrategyEntity:策略实体,包含策略ID、描述和规则模型StrategyAwardEntity:策略奖品实体RaffleAwardEntity:抽奖奖品实体RuleActionEntity:规则动作实体
- 值对象 :RuleTreeVO、RuleWeightVO、StrategyAwardStockKeyVO等
- 领域服务 :
AbstractRaffleStrategy抽象抽奖策略服务
- 活动领域 (Activity Domain)
- 核心职责 :管理营销活动的生命周期、用户参与和订单处理
- 主要实体 :
ActivityEntity:活动实体,包含活动基本信息和状态- ActivityAccountEntity:活动账户实体
- ActivityOrderEntity:活动订单实体
- SkuProductEntity:SKU商品实体
- 聚合根 :CreatePartakeOrderAggregate、CreateQuotaOrderAggregate
- 值对象 :
ActivityStateVO活动状态枚举、OrderStateVO订单状态等
- 积分领域 (Credit Domain)
- 核心职责 :用户积分账户管理、积分交易和订单处理
- 主要实体 :
CreditAccountEntity:积分账户实体- CreditOrderEntity:积分订单实体
- TradeEntity:交易实体
- 聚合根 :
TradeAggregate交易聚合,封装积分交易的完整业务逻辑 - 值对象 :TradeTypeVO交易类型、TradeNameVO交易名称
- 奖品领域 (Award Domain)
- 核心职责 :奖品发放、用户奖品记录管理
- 主要实体 :
- UserAwardRecordEntity:用户奖品记录实体
- DistributeAwardEntity:分发奖品实体
- UserCreditAwardEntity:用户积分奖品实体
- 聚合根 :GiveOutPrizesAggregate、UserAwardRecordAggregate
- 值对象 :AwardStateVO奖品状态、AccountStatusVO账户状态
- 返利领域 (Rebate Domain)
- 核心职责 :用户行为返利、日常任务奖励
- 主要实体 :
- BehaviorEntity:行为实体
- BehaviorRebateOrderEntity:行为返利订单实体
- 聚合根 :BehaviorRebateAggregate
- 值对象 :BehaviorTypeVO行为类型、RebateTypeVO返利类型
- 任务领域 (Task Domain)
- 核心职责 :异步任务处理、消息补偿机制
- 主要实体 :TaskEntity任务实体
清晰的领域边界
- 每个领域都有独立的model、service、repository、event包结构
- 领域间通过领域事件和接口进行交互,避免直接依赖
丰富的领域模型
- 实体(Entity) :具有唯一标识的业务对象,如StrategyEntity、ActivityEntity
- 值对象(ValueObject) :描述性对象,如ActivityStateVO、TradeTypeVO
- 聚合根(Aggregate) :管理一致性边界,如TradeAggregate、BehaviorRebateAggregate
领域服务设计
- 抽象类定义标准流程:如AbstractRaffleStrategy定义抽奖标准流程
- 责任链模式:处理复杂的业务规则链
- 决策树模式:处理抽奖后的规则过滤
事件驱动架构
- 每个领域都包含event包,实现领域事件的发布和订阅
- 支持异步处理和最终一致性
redis如何保障它的原子性
单线程模型(最核心的保障); 原子操作命令(基础); Lua脚本(复杂操作的基石); 事务(特定场景)。
SSE协议的优缺点,它是基于TCP还是UDP的
SSE 是基于 TCP(更准确地说,是基于 HTTP)的应用层协议。
多线程之间的通信
线程通过读写共享的变量来进行通信,但需要适当的同步机制来保证数据一致性。
wait()/notify()/notifyAll() 方法,这些是 Object 类的方法,必须在同步块中使用。
Lock 和 Condition 接口,Java 5 引入的 java.util.concurrent.locks 包提供了更灵活的线程通信机制。
Java中的原子操作
原子类 (java.util.concurrent.atomic 包)提供了一系列原子操作类,如 AtomicInteger、AtomicLong、AtomicReference 等。
synchronized 关键字通过互斥锁保证代码块的原子性。显式锁(ReentrantLock)提供比 synchronized 更灵活的锁机制。
钉钉
一面
深挖项目
Ai Agent的执行流程,以CSDN自动发帖,日记检索分析为例,通过代码分析具体执行过程
AI Agent动态多轮执行策略(四个步骤)的具体实现,步骤三质量检查的具体实现
质量监督节点的核心实现:
- RagAnswerAdvisor质量检查机制 ,项目中的质量监督主要通过
RagAnswerAdvisor.java实现;执行结果质量评估:- onFinishReason()方法 :通过检查响应结果的 FinishReason 元数据来判断执行是否完成
- 向量搜索质量验证 :在 before() 方法中进行相似性搜索,获取相关文档上下文
- 响应元数据处理 :在 after() 方法中处理响应元数据,保存检索到的文档信息
- Advisor链式质量监督,项目采用多层Advisor进行质量监督:
- RagAnswerAdvisor :负责向量搜索和上下文质量检查
- PromptChatMemoryAdvisor :负责对话记忆管理和上下文连贯性
- SimpleLoggerAdvisor :负责日志记录和执行监控
- 任务执行状态管理,通过
AgentTaskJob.java进行任务调度和状态管理,执行监控:- 定时任务每分钟检查任务状态( refreshTasks() 方法)
- 每10分钟清理无效任务( cleanInvalidTasks() 方法)
- 异常捕获和日志记录
FinishReason 元数据判断执行完成的实现机制
在 RagAnswerAdvisor.java 中,通过 onFinishReason 方法实现执行完成状态的判断。
判断规则实现
- 基本判断逻辑
private Predicate<AdvisedResponse> onFinishReason() {
return (advisedResponse) -> advisedResponse.response().getResults().stream()
.filter((result) -> result != null
&& result.getMetadata() != null
&& StringUtils.hasText(result.getMetadata().getFinishReason()))
.findFirst().isPresent();
}
- 判断条件(三重验证)
- result != null : 确保响应结果对象存在
- result.getMetadata() != null : 确保元数据对象存在
- StringUtils.hasText(result.getMetadata().getFinishReason()) : 确保FinishReason字段有有效文本内容
实际上只是根据 AI是否自然结束生成,达到了合理的停止点、是否达到最大长度限制、生成的文本内容中是否包含关键字来判定是否符合质量检查。基本上就是只要包含有效的文本内容,就会认定为已完成。
二面
你能讲一下你项目中 MCP 的核心实现原理吗?它在整个 AI Agent 架构中起到什么作用?
MCP 是一种基于 stdio/sse 协议 的轻量级通信机制,用于让大模型(如 ChatGPT、通义千问等)与外部系统(如日志分析、公众号通知、监控告警)之间建立可控、可配置的交互通道。 本质上是一个“模型调用外部服务”的标准化接口规范。
- 协议定义:采用标准的 stdin/stdout 流式输入输出 + SSE(Server-Sent Events)推送响应,兼容主流 LLM 接口;
- 能力抽象:将每个外部功能封装为一个独立的 MCP 插件(如 log_analyzer, wechat_notifier),提供统一 JSON Schema 输入输出;
- 动态加载:通过数据库配置 MCP 插件列表和执行顺序,在 Agent 运行时按需注入到 Spring 容器中;
- 上下文绑定:每次调用自动携带当前任务上下文(如用户ID、会话ID、知识库片段),确保行为可追溯;
- 错误隔离:插件失败不会中断主流程,而是记录日志 + 回退策略(比如降级为人工处理提示);
在项目中提到的 Advisor 顾问角色是如何设计和工作的?它的核心逻辑和原理是什么?
Advisor(顾问角色) 来作为 AI Agent 的“大脑中枢”,它不是简单调用模型接口,而是基于 领域驱动设计(DDD)+ RAG 知识库 + 动态 Prompt 编排 实现智能决策与上下文感知。
Advisor 是一个 可配置、可插拔的推理引擎,它负责:
- 从知识库(如 PostgreSQL 向量库)中检索相关上下文;
- 结合当前任务状态动态生成 Prompt;
- 控制 MCP 插件执行顺序与条件判断;
- 最终输出结构化指令给大模型或直接执行动作。
你为什么选择 Spring AI 框架?有没有其他框架可以实现类似的逻辑?
选择 Spring AI 是因为它在 Java 生态中提供了开箱即用的统一抽象层,能快速集成主流大模型(如通义千问、Gemini、Ollama 等), 同时天然支持 RAG(检索增强生成)、MCP(模型控制协议)和 Advisor 角色编排等核心能力。
- LangChain(Java/Python):提供了更灵活的链(Chain)、代理(Agent)等概念,适合构建复杂的 AI 应用流程。
- LlamaIndex:专注于数据索引和检索增强生成(RAG)场景,在处理私有数据与 AI 结合时更有优势。
- 轻量级工具库:如 Apache OpenNLP(偏向传统 NLP)、Hugging Face Transformers(可本地部署模型)等,适合需要深度定制模型或处理逻辑的场景。
你在做 RAG(检索增强生成)文档解析时,遇到准确度不高的问题,你是如何衡量其有效性,并确保它能真正落地为可用场景的?
从 业务价值、可解释性、稳定性 三个维度来衡量是否真正可用。
命中率(Hit Rate):用户提问后,系统返回的答案是否来自知识库中的文档片段?
相关性评分(Relevance Score):人工标注 top3 结果的相关性(1~5分),平均≥4即达标;
误判率(False Positive Rate):模型虚构答案但无依据的比例 ≤5%。
灰度发布 + AB测试:先让 10% 用户走 RAG 路径,对比原始对话质量;
建立反馈闭环:用户点击“这不是我想要的答案”按钮 → 记录到日志 → 定期训练改进;
监控异常行为:比如连续 5 次都返回空结果,自动触发告警并降级回默认模型。
你刚刚提到使用了多种大模型(如 ChatGPT、通义千问等),你觉得它们在实际应用中最大的问题是什么?
- 幻觉严重(Hallucination)
- 模型经常生成看似合理但完全错误的信息,比如编造文档内容、虚构API接口或参数;
- 在 RAG 场景中,即使有知识库支撑,仍可能出现“引用不存在的段落”或“把多个文档混为一谈”的情况;
- 我在做 AI-Agent 项目时发现:仅靠 prompt 控制无法杜绝幻觉,必须结合向量检索+规则校验+人工标注反馈机制才能降低风险。
- 输出不可控(Control Lacking)
- 不同模型对同一问题的回答差异极大,缺乏一致性,难以用于自动化流程(如代码生成、日志分析);
- 缺乏细粒度控制能力(如限制输出格式、强制调用特定工具、禁止某些关键词);
- 我通过设计 MCP(Model Control Protocol)服务层,统一管理模型行为,例如:
- 强制要求返回 JSON 格式;
- 限制只能从指定知识源中提取答案;
- 若不确定则返回“暂无答案”,而非胡乱猜测。
- 推理成本高 & 响应慢
- 大多数模型在中文语境下推理延迟超过 3s,不适合高频交互场景(如客服机器人);
- 即使是本地部署模型,也常因显存不足而无法并发处理请求;
- 我采用“轻量模型 + 知识蒸馏”策略,在保证准确率的前提下将响应时间压缩至 800ms 内。
Java 的“更稳定”具体体现在哪些方面?
- 编译时类型检查(静态类型): Java 是静态语言,编译期就能发现很多潜在错误(如变量未定义、方法签名不匹配),避免运行时崩溃。Python 是动态类型,错误往往在运行中才暴露,调试成本更高。
- JVM 内存管理与垃圾回收机制: JVM 提供了成熟的 GC 算法(如 G1、ZGC),能自动回收无用对象,防止内存泄漏;而 Python 的引用计数 + 垃圾回收机制在复杂场景下容易出现循环引用问题,影响稳定性。
- 多线程与并发控制机制成熟: Java 提供了 synchronized、ReentrantLock、ThreadLocal、CAS 等底层原语,配合线程池和 AQS 框架,可构建高并发、低延迟的服务。相比之下,Python 的 GIL(全局解释器锁)限制了多线程并行能力,难以支撑大规模并发请求。
- 企业级生态完善: Spring Boot、Dubbo、RocketMQ 等框架都基于 Java 构建,提供了事务管理、服务治理、熔断限流等生产级能力,保障系统长期稳定运行。这些组件经过大量真实业务验证,比 Python 生态更可靠。
JVM 是如何避免内存泄漏的?
- 自动垃圾回收(GC)机制: JVM 的垃圾收集器会定期扫描堆内存中的对象,标记不再被引用的对象并释放其占用的空间。这从根本上减少了手动管理内存导致的泄露风险。
- 强引用、软引用、弱引用、虚引用的区分: JVM 提供多种引用类型,开发者可以根据业务需求选择合适的引用方式。例如,使用 WeakReference 可以让对象在内存紧张时被 GC 自动回收,防止长期持有无用对象。
- 类加载器隔离 + 弱引用清理: 每个类加载器都有独立的命名空间,当应用重启或热部署时,旧类加载器会被置为不可达,从而触发其加载的所有类和静态变量被回收,避免因类未卸载造成的内存泄漏。
- 线程本地变量(ThreadLocal)的清理机制: 如果使用 ThreadLocal,必须在使用后调用 remove() 方法清除数据,否则可能造成内存泄漏(尤其是在线程池场景下)。JVM 不会自动清理 ThreadLocal,但可以通过工具如 WeakHashMap 或显式清理规避问题。
平常都通过哪些渠道去学习?
你觉得在写代码过程中,什么样的代码是比较好的代码?
三面HR面
专业是通信工程,为什么学Java
通过哪些渠道学习的Java开发
对钉钉有什么了解,除了钉钉还投了哪些公司
有没有看钉钉8.0的发布会
面试通过后,什么时候能来实习呢?
项目中遇到的最大的困难,如何解决的
对工作节奏的看法
反问
需要先实习两个月左右,会看转正。工作时间九点上班,下班不定(看任务完成情况),说是双休。
帆软
一面
项目中延迟队列与定时任务的实现
如何实现一个定时任务(不调用注解,不适用定时任务类)
while循环+sleep
while+sleep方法实现延迟逻辑(如每秒检查一次)的缺陷
- 资源浪费严重:每个延迟任务都占用一个线程,无法扩展。比如你要处理1万条延迟消息,就得开1万个线程,极大消耗系统内存和CPU。
- 不精确且不可控:sleep 是阻塞式等待,无法动态调整延迟时间,也无法取消任务。
- 缺乏容错机制:如果应用重启或宕机,所有 sleep 中的任务都会丢失,无法保证最终一致性。
- 难以维护:代码逻辑混乱,无法集中管理、监控或批量操作。
- JVM 调度不确定性: Java 的 Thread.sleep() 是操作系统层面的阻塞等待,受 JVM GC、线程调度策略影响(尤其是 Full GC 时可能暂停数秒)。 即使你减掉任务时间,也可能因系统负载导致下一次唤醒延迟 2~5 秒甚至更多。
在 Java 中,如何确保一个对象在完全不再被引用时释放其持有的本地资源(如调用 C/C++ 接口的指针)?
使用 try-with-resources + AutoCloseable,配合弱引用或 PhantomReference 精准监控对象生命周期。
MYSQL隔离级别
binlog日志
MVCC
项目中提到了ELS,问了ELS相比较MYSQL的优点
在 Java 中,如何不使用临时变量来交换两个 int 类型的变量?
- 利用算术运算(加减法):通过数学公式实现无临时变量交换。
- 利用位运算(异或 XOR):利用异或的自反性(a ^ a = 0)实现交换。
给定一个由不重复数字组成的整数(不含0),如何快速找到所有排列中比当前数大的最小数?
这个问题本质是求“下一个字典序更大的排列”,是一个经典的算法题。核心思路是 从右往左找第一个递减位置,交换并调整右侧部分为最小升序。
- 从右往左找第一个 nums[i] < nums[i+1] 的位置 —— 这个位置左侧的前缀不变,右侧需要变大。
- 在右侧找比 nums[i] 大但最小的那个数(即最右边的更大值),与 nums[i] 交换。
- 将交换后的右侧部分反转为升序,使其最小。
压测时有没有遇到问题
rabbitMQ宕机
线程池配置的太少
确保API密钥以Bearer开头(Spring AI 1.0.0-M8版本需要手动添加)
二面
B+ 树与 B 树的区别,以及在 MySQL 中为何常用 B+ 树作为索引结构?
核心区别在于数据存储方式和查询效率。
- 数据存储位置不同
- B 树:每个节点(包括内部节点和叶子节点)都存储键值和对应的数据记录指针。这意味着查找某个键时,可能在任意一层节点就找到目标数据,无需遍历到叶子层。
- B+ 树:只有叶子节点存储完整的数据记录,内部节点仅保存键值用于导航。所有数据都在叶子层集中存放。
- 查询性能更优(尤其范围查询)
- B+ 树的叶子节点通过双向链表连接,支持高效的顺序扫描和范围查询(如 WHERE age BETWEEN 20 AND 30),这是 B 树无法做到的。
- B 树的非叶子节点也存数据,导致树高更高、缓存命中率低,在范围查询时需多次跳转。
- 更适合磁盘 I/O 优化
- B+ 树的所有叶子节点在同一层,高度一致,且每层节点更“满”,减少磁盘读取次数(每次 IO 能加载更多键值),提升查询效率。
- MySQL 的 InnoDB 引擎默认使用 B+ 树索引,正是为了兼顾点查询(快速定位)和范围查询(高效遍历)的需求。
MySQL 使用 B+ 树是因为它在范围查询、数据分布、磁盘 I/O 效率方面优于 B 树,是平衡查询性能与存储结构的理想选择。
在使用 B+ 树作为索引结构时,是否存在性能反而下降的场景?如果有,请说明具体原因。
- 频繁的插入/删除导致页分裂或合并(写密集型场景):B+ 树为了保持平衡,每次插入或删除都可能触发内部节点的分裂或合并,尤其在高并发写入时,会频繁加锁、移动数据,显著降低性能。
- 范围查询过宽或无有效过滤条件(低选择性字段):如果查询条件选择性差(如性别、状态等只有少量值),B+ 树虽然能快速定位叶子节点,但需要扫描大量数据块,效率接近全表扫描。
- 索引列长度过大或类型不匹配(空间浪费 + 缓存命中率低):若索引字段是长字符串(如 VARCHAR(255))或 JSON 字段,B+ 树每个节点存储的键值数量减少,树高度增加 → 更多磁盘 IO。
- 缓存命中率低(冷热数据分布不均):B+ 树依赖操作系统或数据库的缓冲池缓存叶子节点。若热点数据分散在多个叶子页中,频繁换页会导致缓存失效,I/O 增加。
在 B+ 树索引的增量删除或写入过程中,节点分裂是如何发生的?能否描述其大致过程?
B+ 树在插入或删除时触发分裂(Split)的核心目的是保持树的平衡性和高度最小化。以下是插入导致节点分裂的典型过程:
- 定位目标页:从根节点开始,根据键值比较逐层向下查找应插入的位置(叶子节点)。
- 尝试插入新键值:若叶子节点未满(未达到预设最大条目数),直接插入即可,无需分裂。
- 触发分裂条件(节点已满)
- 当要插入的键值使当前叶子节点超出容量限制(如每页最多存储 100 条记录),则需进行分裂操作:
- 将原节点中的键值平均分成两组(通常左半部分保留,右半部分新建一个节点);
- 新节点的最小键值作为父节点新增的键值;
- 父节点也可能会因新增键值而再次分裂(递归向上)。
- 当要插入的键值使当前叶子节点超出容量限制(如每页最多存储 100 条记录),则需进行分裂操作:
- 更新父节点与链表指针
- 更新父节点中对应键值指向新的子节点;
- 叶子节点间通过双向链表连接,分裂后需维护链表顺序。
频繁的 B+ 树节点分裂除了导致性能下降外,还会引发哪些问题?
- 索引结构碎片化(Fragmentation):分裂后旧节点被拆分,新节点可能分散在不同物理页上,导致数据访问不连续 → 磁盘读取效率降低,缓存命中率下降。
- 内存占用增加(空间浪费):每次分裂都会创建新的叶子节点或内部节点,即使数据量未显著增长,也会占用额外内存和磁盘空间(尤其在高并发写入时)。
- 事务回滚复杂度上升(InnoDB 机制):若在事务中发生分裂,需确保事务一致性。分裂涉及多个页修改,若事务中途失败,回滚操作更复杂,可能触发死锁或长时间阻塞。
- 影响备份与恢复效率:大量分裂会导致索引页数量激增,备份工具(如 mysqldump 或 xtrabackup)扫描更多页 → 备份时间变长、资源消耗更高。
B+ 树频繁分裂导致的数据碎片化问题,MySQL 本身是否有解决方案?一般思路是什么?
定期维护和重建索引,MySQL 自带工具与命令:OPTIMIZE TABLE(对表进行优化,同时重建索引)、REBUILD INDEX(单独重建指定索引,不影响表结构)。
当分库分表后,如何保证多个表同时插入数据时的一致性?比如一个用户在不同分片上插入记录,怎么避免数据不一致?
- 分布式事务(XA / TCC / Saga)
- 本地消息表 + 最终一致性:在主库写入“业务数据”+“待同步消息”,异步任务消费消息并更新其他分片
- 基于全局唯一 ID 的幂等设计(防重复)
- 用雪花算法或 UUID 生成全局唯一主键,确保多分片插入不会冲突;
- 插入前查重(如 Redis 缓存 key:userId),避免重复写入。
请说明在什么场景下需要打破 Java 类加载的双亲委派模型?为什么?
- JNDI、SPI 等服务发现机制(如 JDBC、日志框架)
- JDK 自带的 ClassLoader(如 AppClassLoader)无法加载第三方实现类(如 MySQL 的驱动类),需由应用类加载器主动加载;
- 解决方案:使用 线程上下文类加载器(TCCL),让子类加载器可以“反向”委托给父类加载器,从而加载到正确的实现类。
- 热更新/模块化系统(如 OSGi、Spring Boot 插件机制)
- 需要动态加载和卸载模块,若严格遵循双亲委派,模块间无法隔离;
- 打破方式:自定义类加载器(如 URLClassLoader),绕过父加载器直接加载本地 jar 包,实现模块独立性。
- Web 容器(Tomcat、Jetty)多应用隔离
- 同一服务器运行多个 Web 应用,每个应用可能依赖不同版本的类库(如 log4j);
- Tomcat 使用 WebAppClassLoader,先尝试自己加载,失败才交给父类加载器,防止冲突。
在 Java 应用中,如果发现老年代中存在大量小对象(异常情况),导致频繁 Full GC,应如何定位和解决这个问题?
- 定位根源:分析堆内存分布与对象来源
- 使用 jstat -gc 或 jmap -histo 查看各代内存占用比例,确认老年代中小对象占比异常;
- 通过 GC 日志(-Xlog:gc=debug) 分析是否因新生代晋升过快或大对象直接进入老年代(如大数组、缓存对象);
- 若是“小对象堆积”,可能是代码中存在未及时释放的缓存、静态集合膨胀、线程局部变量未清理等。
- 排查具体原因:结合堆转储(heap dump)分析
- 使用 jcmd GC.run_finalization + jmap -dump:format=b,file=heap.hprof 生成堆快照;
- 用 Eclipse MAT / VisualVM 打开分析:查找“存活但体积小”的对象,判断其是否为非预期持有引用(如监听器未注销、ThreadLocal 泄漏);
- 特别注意:是否有短生命周期对象被错误地长期持有(如静态 Map 缓存、定时任务未取消)。
- 针对性优化策略:调整 JVM 参数 + 代码层改进
- 短期缓解:调低 -XX:NewRatio(如 2 → 1)减少老年代空间,避免小对象挤占;
- 长期修复:
- 检查是否存在大对象直接进入老年代(-XX:+UseCMSInitiatingOccupancyOnly + -XX:CMSInitiatingOccupancyFraction 设置合理阈值);
- 启用 G1 或 ZGC 等低延迟收集器,自动分区管理,避免老年代碎片化;
- 代码层面:使用弱引用/软引用缓存、及时清理 ThreadLocal、避免长生命周期对象持有短生命周期数据。
请用精简的几句话说明操作系统虚拟内存的重要设计点。
虚拟内存通过“隔离 + 抽象 + 懒加载”机制,在有限物理内存下支撑更大规模应用运行,是现代操作系统的核心能力之一。
- 地址空间隔离:每个进程拥有独立的虚拟地址空间,防止内存访问冲突,提升系统安全性与稳定性;
- 内存抽象与扩展:通过页表映射将逻辑地址转换为物理地址,使程序认为自己拥有连续大内存,实际可小于物理内存(如使用磁盘作为后备);
- 按需加载与交换:仅在需要时才将页面从磁盘加载到内存(Demand Paging),不常用页可被换出到 swap 分区,提高内存利用率;
- 硬件支持(MMU):由 CPU 的内存管理单元(MMU)实现高效地址翻译,配合 TLB 缓存加速查找,减少性能损耗。
多级页表的设计是为了什么目的?它解决了哪些核心问题?
解决大地址空间下页表占用内存过大的问题,提升内存利用率和系统效率。
- 节省物理内存空间
- 单级页表在32位系统中需 4GB 地址空间 × 4字节/项 = 16MB 内存存储页表,但实际程序往往只用到其中一小部分;
- 多级页表(如两级页表)仅对当前使用的虚拟地址范围分配页表项,避免为未使用区域浪费内存。
- 支持大虚拟地址空间
- 现代操作系统(如Linux x86-64)支持高达 48 位虚拟地址(256TB),若用单级页表将导致页表体积巨大(约几十MB);
- 多级结构通过分层索引(如页目录 + 页表)实现稀疏映射,只需加载当前活跃的页表层级即可。
- 提高页表管理灵活性与缓存效率
- 可按需加载/释放页表页,减少内存碎片;
- TLB(Translation Lookaside Buffer)命中率更高,因为常用页表项集中在局部范围内。
操作系统如何保障从逻辑地址到物理地址的映射过程快速寻址?有哪些关键设计?
- TLB(Translation Lookaside Buffer)缓存机制
- TLB 是 CPU 中的高速缓存,存储最近使用的页表项(逻辑页号 → 物理帧号),避免每次访问都查页表;
- 若命中 TLB,只需一次内存访问即可完成地址翻译,极大提升性能(如 Intel x86 系统中 TLB 命中率可达 95%+)。
- 多级页表结构(如两级/三级页表)
- 解决单级页表占用过大内存的问题(如32位系统需16MB页表),仅加载当前活跃的页表层级;
- 减少无效页表项的内存占用,提高内存利用率,同时降低缺页异常频率。
- 硬件 MMU 单元自动处理地址转换
- MMU(Memory Management Unit)是 CPU 的专用硬件模块,负责将逻辑地址自动转换为物理地址;
- 支持分页机制,配合页表和 TLB 实现透明高效的地址映射,无需软件干预。
- 按需加载与稀疏页表策略
- 虚拟内存只在需要时才分配物理页(Demand Paging),减少初始页表大小;
- 页表采用稀疏结构,未使用的虚拟页不建立映射,节省资源并加快查找速度。
有8杯酒,其中1杯有毒,小白鼠喝下后10分钟内死亡。如何用最少的小白鼠在10分钟内确定哪一杯有毒?
利用二进制编码思想,将每杯酒映射为一个唯一的二进制编号,让每只小白鼠代表一位二进制位,通过观察哪些老鼠死亡来唯一确定毒酒。
- 将8杯酒编号为 0~7(共8种状态),对应3位二进制数(因为 $2^3 = 8$)
- 使用3只小白鼠分别代表二进制的第0位、第1位、第2位(从右往左)
- 对于每杯酒,若其二进制某一位是1,则让对应的小白鼠喝一口该酒
- 10分钟后观察哪些白鼠死亡,将死亡白鼠对应的位设为1,存活设为0,组成一个3位二进制数,即为毒酒编号。
三面
八股问的很深,一个问题会一直深挖直到回答不出来
Spring 中的 IOC(控制反转) 和 AOP(面向切面编程) 的使用场景以及实现原理
IOC(控制反转):将对象的创建和依赖关系交给 Spring 容器管理,而不是由程序员手动 new 对象或写死依赖。
- 使用场景:
- 解耦业务逻辑与对象创建:比如 Service 层调用 Dao 层时,无需直接 new UserDao,而是通过 @Autowired 注入。
- 配置灵活、易于测试:可通过 XML 或注解动态替换实现类(如 mock 数据库访问)。
- 单例/多例 Bean 管理:Spring 默认单例模式,适合共享资源(如数据库连接池),也可按需配置 prototype。
- 实现原理:
- Spring 启动时扫描 @Component / @Service / @Repository 等注解;
- 构建 BeanDefinition(元信息)并注册到容器;
- 使用 反射 + 工厂模式 创建实例(如 getBean());
- 自动完成属性注入(如 @Autowired),底层是基于类型匹配或名称匹配(@Qualifier)。
AOP(面向切面编程):在不修改原有代码的前提下,将横切关注点(日志、事务、权限校验)统一处理,提升模块的复用性。
- 使用场景:
- 日志记录:方法执行前后自动打印日志;
- 事务管理:@Transactional 自动开启/提交/回滚;
- 性能监控:统计接口耗时;
- 权限校验:登录拦截、参数校验等通用逻辑集中处理。
- 实现原理:
- Spring 基于 JDK 动态代理(接口)或 CGLIB(类) 实现 AOP;
- 使用 @Aspect 定义切面类,配合 @Before / @After / @Around 等通知;
- 切点表达式(Pointcut)定义要织入的目标方法(如 execution(* com.example.service..(..)));
- 运行时通过代理对象拦截目标方法调用,插入额外逻辑(如事务开启、日志打印)。
如果我有一个接口 A,里面有个方法 aa;同时有一个类 B 实现了这个接口,并重写了方法 aa。那么使用 JDK 动态代理时,它是否能代理 B 的方法 bb?
这个问题其实是在问:JDK 动态代理是否只能代理接口中的方法,而不能代理实现类中额外定义的方法(如 bb)?
不能!JDK 动态代理只能代理接口中声明的方法,无法拦截实现类中新增的、不在接口里的方法(比如 bb())。这是因为 JDK 代理的本质是基于接口的反射调用。
- 它通过 Proxy.newProxyInstance(...) 创建一个代理对象;
- 这个代理对象实现了你传入的那个接口(如 A);
- 所有对接口方法的调用都会被转发到 InvocationHandler.invoke() 方法中处理;
- 但如果你在实现类 B 中添加了一个新方法 bb(),它不属于接口 A 的一部分,所以代理对象根本不知道这个方法的存在!
他这个接口的作用是啥?就只是为了说声明哪些方法要被代理吗?
接口的核心作用就是“声明哪些方法可以被代理”,除此之外,其更深层的价值在于支撑动态代理的技术原理、保障代理逻辑的合法性与灵活性。
JDK 动态代理的核心是通过 Proxy.newProxyInstance() 动态生成一个 “代理类字节码”,而接口是这个动态类能被 JVM 识别、且能与目标类(被代理类)协同工作的基础。
- 动态代理类的 “类型继承” 约束
- 保障代理类与目标类的 “方法一致性”
接口的存在让 JDK 动态代理的核心组件 InvocationHandler(代理逻辑处理器)能够脱离具体目标类,成为 “通用的代理逻辑模板”,无需为每个目标类单独编写代理逻辑。
- InvocationHandler 与目标类的解耦
- 支持 “多接口代理”,扩展代理能力
在 Java 的强类型体系中,接口是 “类型校验” 的关键,确保动态生成的代理对象能被安全地使用
- 编译期类型检查
- 限制 “代理范围”,避免越权调用
JDK 动态代理:只能代理接口中的方法,通过 Proxy.newProxyInstance() 创建一个实现了目标接口的代理类;
CGLIB 代理:可以代理任意类(无论有没有接口),通过字节码增强技术生成目标类的子类,并重写其方法。
CGLIB 是不是所有场景都能实现代理?
并非所有场景。CGLIB 的核心逻辑是通过 ASM 字节码框架生成目标类的子类,并重写目标方法以植入代理逻辑。因此,凡违反 “子类继承规则” 或 “字节码修改限制” 的场景,CGLIB 均无法生效。
- 目标类/方法为 final 类/方法
- 目标方法为 private 私有方法:Java 中 private 方法的访问权限仅限于 “目标类内部”,子类既无法访问也无法重写该方法
- 目标类没有默认构造方法(无参构造器):CGLIB 生成代理时,默认调用目标类的无参构造方法。若目标类没有无参构造方法且未显式指定构造方法,会抛出 InstantiationException。
- 目标类是枚举(Enum)或注解(Annotation):枚举和注解在 Java 中本质上是 final类,且其继承结构被严格限制(枚举隐式继承 Enum,注解隐式继承 Annotation)。
- 目标类由特殊类加载器加载(如受安全管理器限制):若目标类由自定义类加载器加载,且安全管理器禁止动态生成类(如 checkCreateClassLoader权限被拒绝),CGLIB 无法生成代理。
如果一个方法被final修饰了,我该如何操作才能重写这个方法
绕过JVM的final校验
- 使用组合(Composition)替代继承 思想:不继承父类,而是将父类对象作为子类的成员变量,通过委托调用实现自定义逻辑。
- 利用模板方法模式 思想:父类提供一个 final的模板方法,内部调用一个可被子类重写的保护方法。
- 使用反射(高风险操作) 注意:此方法违反 Java 设计规范,仅在特殊场景下谨慎使用(如某些框架内部实现)。 原理:通过反射修改方法的访问权限并强制调用。
常见的垃圾回收器有哪些?它们各自的特点是什么?
- Serial GC(串行收集器)
- 单线程工作,适合 小内存应用(如嵌入式或桌面级);
- 停顿时间短但吞吐量低,适合单核 CPU。
- Parallel GC(并行收集器)
- 多线程并行处理年轻代和老年代回收;
- 目标是 最大化吞吐量,适合后台计算密集型任务;
- 默认使用 -XX:+UseParallelGC。
- CMS(Concurrent Mark Sweep,老年代并发回收)
- 并发标记清除,减少 STW(Stop-The-World)时间;
- 适合 低延迟场景(如 Web 服务);
- 缺点:碎片化严重、不支持大堆、已被弃用(JDK 9+)。
- G1(Garbage First)
- 将堆划分为多个区域(Region),优先回收垃圾最多的区域;
- 平衡吞吐量与停顿时间,适合 大堆(>6GB)且对延迟敏感的应用;
- 默认推荐用于现代 Java 应用(JDK 9+);
- ZGC(Zero GC)
- 专为超大堆设计(TB级),STW 时间控制在 10ms 内;
- 适合 高吞吐 + 超低延迟场景(如金融交易系统);
- JDK 11 引入,现已成熟稳定。
- Shenandoah GC
- 类似 ZGC,基于并发标记-重定位算法;
- 优点:可扩展性强,适合大规模堆内存;
- JDK 12 引入,性能接近 ZGC,但社区支持略少。
为什么 CMS 垃圾回收器会产生碎片化?
CMS(Concurrent Mark Sweep)采用“标记-清除”算法,清除阶段不进行内存压缩,导致堆空间中出现大量不连续的空闲区域,从而产生内存碎片。
- 活跃对象分布不均,删除后留下多个小块空闲空间;
- 这些碎片无法合并成一个大块,即使总和足够也无法分配大对象;
- 当遇到大对象时(如 new byte[50MB]),会触发 Full GC(因为找不到连续空间)→ 导致长时间 STW。
G1 垃圾回收器是如何解决 CMS 产生的碎片化问题的?它为什么能支持大堆且不产生碎片?
解决 CMS 碎片化问题的核心设计
- 分区(Region)模型
- 物理分区:将堆内存划分为多个大小相等的 Region(默认 1MB~32MB),每个 Region 可动态切换角色(Eden、Survivor、Old、Humongous) 。
- 逻辑分代:年轻代(Eden/Survivor)和老年代(Old)在物理上不连续,仅通过 Region 角色动态划分,避免传统分代收集器的连续内存碎片问题 。
- 复制算法与局部整理
- 新生代回收:采用复制算法,将存活对象从 Eden/Survivor 复制到新 Region,天然避免碎片 。
- 老年代回收:通过混合收集(Mixed GC)选择垃圾比例高的 Region,将存活对象复制到空闲 Region,实现局部整理。整个过程通过标记-复制算法完成,而非传统标记-清除,避免碎片积累 。
- SATB(Snapshot-At-The-Beginning)算法
- 并发标记优化:在标记阶段记录初始快照,通过写屏障(Write Barrier)跟踪引用变化,仅处理并发修改部分,减少全堆扫描需求,降低碎片产生概率 。
- 记忆集(RSet)与卡表(Card Table)
- 跨 Region 引用跟踪:每个 Region 维护 RSet,记录其他 Region 对本 Region 的引用,避免全堆扫描,减少碎片整理的开销。
- 卡表优化:将 Region 划分为小块(Card),仅标记被修改的 Card,提升并发标记效率。
支持大堆且避免碎片的关键原因
- 分区动态管理
- 灵活分配:堆内存按需划分为 Region,老年代和年轻代无需固定比例,可根据应用负载动态调整 。
- 增量回收:每次 Mixed GC 仅回收部分高垃圾 Region,避免一次性处理整个堆,减少停顿时间和碎片积累 。
- 复制算法的天然优势
- 内存紧凑性:复制算法将存活对象移动到连续内存区域,释放的 Region 直接标记为空闲,物理上形成连续空间,避免碎片 。
- 局部整理:老年代回收时仅整理选中的 Region,而非全堆,保持其他区域内存的完整性 。
- 停顿预测与可控性
- 目标停顿时间:通过 -XX:MaxGCPauseMillis参数限制单次 GC 停顿时间,优先回收高收益 Region,确保大堆场景下的低延迟 。
- 分阶段回收:全局并发标记与混合回收结合,分阶段处理老年代,避免单次 Full GC 导致的长时间停顿 。
- 大对象处理优化
- Humongous Region:超大对象(超过 Region 50%)直接分配到专用 Humongous Region,避免碎片化。若多个 Humongous Region 无法连续分配,触发 Full GC(Serial Old)
Java 中有 CopyOnWriteArrayList,你了解它吗?它的原理和适用场景是什么?
CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)提供的一种线程安全的 List 实现, 其核心设计思想是 “写时复制”(Copy-On-Write),通过牺牲一定的写性能,换取读操作的高效与线程安全。
核心原理
- 写时复制(Copy-On-Write)
- 写操作(Add/Remove/Set): 当对列表进行修改时,底层逻辑会:
- 复制原数组:创建一个新的数组副本。
- 修改副本:在新数组上完成添加、删除或更新操作。
- 原子替换引用:通过 volatile修饰的数组引用,将原数组指向新数组。
- 关键点:写操作会触发全量复制,因此开销较大。
- 写操作(Add/Remove/Set): 当对列表进行修改时,底层逻辑会:
- 读操作(Get/Iterator): 直接读取当前数组的快照,无需加锁,也不依赖同步机制。 关键点:读操作完全无锁,性能极高。
- 内存可见性保证: 数组引用通过 volatile修饰,确保多线程环境下修改后的数组引用对所有线程立即可见。
适用场景:读多写少的高并发场景
- 读密集型业务(如配置缓存)
- 迭代操作远多于修改操作
CopyOnWriteArrayList 和读写锁(ReadWriteLock)的区别是什么?
读写锁(ReadWriteLock)
- 分离锁机制
- 读锁(共享锁):允许多个线程同时读取数据(通过 readLock())。
- 写锁(独占锁):仅允许一个线程写入数据(通过 writeLock()),读写、写写互斥。
- 数据一致性:写锁会阻塞所有读写操作,确保写操作的原子性和可见性。
- 适用场景:读多写少,但需要强一致性:如数据库连接池、缓存更新;需要细粒度控制:允许部分读写操作并发,但需保证写操作的独占性。
普通锁是“所有操作互斥”,而读写锁是“读共享、写独占”——本质是让并发读不阻塞,提升性能!
| 特性 | CopyOnWriteArrayList | 读写锁(ReadWriteLock) |
|---|---|---|
| 读性能 | 高(无锁,直接读快照) | 高(多线程并发读) |
| 写性能 | 低(需复制数组,时间复杂度 O(n)) | 中等(仅需获取写锁,时间复杂度 O(1)) |
| 内存占用 | 高(写时复制导致新旧数组共存) | 低(无需复制数据) |
| 实时性 | 低(读操作可能读取到旧数据) | 高(写完成后立即可见) |
在转账场景中,如果事务执行到一半数据库崩溃(比如 A 减了 100,但 B 还没加),重启后 MySQL 的 redo log 和 undo log 是如何保证数据一致性的?
MySQL 通过 redo log 恢复已提交事务、undo log 回滚未完成事务,确保 ACID 中的持久性和原子性 —— 数据不会丢失也不会出现部分更新!
如果数据库崩溃后通过 redo log 恢复了 A 的余额(从 900 恢复成 1000),而 B 的加钱操作未完成导致事务未提交,那么在可重复读(Repeatable Read)隔离级别下:当前事务中查询 A 的余额是多少?其他事务中查询 A 的余额又是多少?
当前事务和其他事务查询 A 的余额均为 1000。
- Redo Log 确保已提交的数据(A=1000)在崩溃后恢复。
- Undo Log 回滚了 B 的未提交操作,其修改未持久化。
- 可重复读隔离级别下,事务的读视图仅包含已提交数据,未提交事务对其他事务不可见。
数据库中已提交的事务和未完成的事务中的数据更新分别是存储在哪里的
- 未完成事务(未提交 / 回滚中)的数据更新:存储于 “内存 + 重做日志”,属于临时数据
- 内存结构:Buffer Pool(缓冲池)与 Undo Log 缓存
- 磁盘日志:Redo Log(重做日志),保证事务持久性。
- 已提交事务的数据更新:存储于 “主数据文件 + 内存脏页”
一个二维数组,满足每一行的元素递增,每一列的元素递减,找出矩阵中target的个数,二分查找实现
说了一下思路。找到数组中target列索引最大,其次行索引最大的位置,和target优先行索引最大,列索引最小的位置,根据索引计算出现的次数。
途虎养车
一面
项目中在库存扣减完成后,会将订单信息吸入数据库,并更新数据库中的用户参与订单为已使用,那么假如有大量的并发请求同时访问抽奖,岂不是会有大量的请求同时访问数据库,不会造成数据库压力过大宕机么?
设置合理的数据库链接参数。
通过延迟队列+定时任务异步更新数据库,避免同一时刻大量数据打入数据库。当库存为0时,直接异步更新数据库的库存为0,并清空延迟队列中的消息。
- 用户消费库存,通过Redis原子操作减少缓存中的库存计数
- 当库存减至0时,系统发布一条MQ消息
- ActivitySkuStockZeroCustomer 监听器接收到消息
- 监听器调用 clearActivitySkuStock 方法,直接将数据库中的库存设置为0
- 监听器调用 clearQueueValue 方法,清空延迟队列中的所有消息
使用Spring的 TransactionTemplate 进行编程式事务管理,在一个事务内进行写入中奖记录,写入数据库,更新抽奖单。将数据分散到多个库表,减轻单表压力。
项目中缓存与数据库的一致性是如何实现的
"先更新缓存,延迟更新数据库“,库存扣减成功后,将更新任务放入Redis延迟队列,而不是立即更新数据库。通过定时任务从队列中获取更新任务,批量更新数据库。
使用唯一索引防止重复提交
对于库存归零等特殊场景,项目使用消息队列进行处理。
负值保护 :当库存小于0时,恢复为0并返回失败。
库存锁定 :对每个扣减的库存单位创建锁标记,防止重复消费。
在订单处理等场景中,使用分布式锁保证操作的原子性。
操作系统中的内存管理,分段机制,分页机制,优缺点
内存管理
内存的分配与回收:对进程所需的内存进行分配和释放,malloc函数:申请内存,free函数:释放内存
地址转换:将程序中的的虚拟地址转换成内存中的物理地址
内存扩充:当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。
内存映射:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
内存优化:通过调整内存分配策略和回收算法来优化内存使用效率。
内存安全:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。
内部内存碎片:已经分配给进程使用但未被使用的内存
外部内存碎片:由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段切不连续的内存空间就被称为外部碎片。
分段机制
分段机制(Segmentation) 以段的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
分页机制(Paging) 把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。现代操作系统广泛采用分页机制。
分段机制和分页机制有哪些共同点和区别
共同点:
- 都是非连续内存管理的方式。
- 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
区别:
- 分页机制以页面为单位进行内存管理,而分段机制以段为单位进行内存管理。页的大小是固定的,由操作系统决定,通常为 2 的幂次方。而段的大小不固定,取决于我们当前运行的程序。
- 页是物理单位,即操作系统将物理内存划分成固定大小的页面,每个页面的大小通常是 2 的幂次方,例如 4KB、8KB 等等。而段则是逻辑单位,是为了满足程序对内存空间的逻辑需求而设计的,通常根据程序中数据和代码的逻辑结构来划分。
- 分段机制容易出现外部内存碎片,即在段与段之间留下碎片空间(不足以映射给虚拟地址空间中的段)。分页机制解决了外部内存碎片的问题,但仍然可能会出现内部内存碎片。
- 分页机制采用了页表来完成虚拟地址到物理地址的映射,页表通过一级页表和二级页表来实现多级映射;而分段机制则采用了段表来完成虚拟地址到物理地址的映射,每个段表项中记录了该段的起始地址和长度信息。
- 分页机制对程序没有任何要求,程序只需要按照虚拟地址进行访问即可;而分段机制需要程序员将程序分为多个段,并且显式地使用段寄存器来访问不同的段。
项目中用到了哪些索引
- 所有表都使用了自增ID作为主键索引(PRIMARY KEY)
- 唯一索引:
- 活动相关表:uq_activity_count_id (活动次数ID),uq_user_id_activity_id (用户ID和活动ID),raffle_activity_sku : uq_sku (商品SKU).
- 订单相关表:uq_order_id (订单ID);uq_out_business_no (外部业务编号)
- 用户中奖记录表:uq_order_id (订单ID)
- 用户行为返利表:uq_order_id (订单ID);uq_biz_id (业务ID)
- 普通索引:idx_activity_id_activity_count_id (活动ID和活动次数ID);idx_award_id (奖品ID/策略ID); idx_user_id (用户ID)等
AOP什么场景下会失效
- 同一类内部方法调用,切面不生效
- 非 Spring 容器管理的对象,切面不生效
- final/private/static 方法,切面不生效
- 切面表达式匹配错误,导致切面未命中
- 自调用时,被调用方法的权限修饰符导致代理失效
- 使用了不兼容的 AOP 实现方式
项目中的组件有哪些
核心业务组件
- AI代理服务组件
- AiAgentChatService
- 实现 IAiAgentChatService 接口
- 提供AI智能体对话功能,处理用户与AI的交互
- 集成向量存储和模型管理功能
- AiAgentRagService
- 实现知识库检索增强生成(RAG)功能
- 支持文件上传和知识库构建
- 为AI代理提供上下文增强能力
- AiAgentTaskService
- 实现 IAiAgentTaskService 接口
- 负责AI代理任务调度和管理
- 支持定时任务和任务状态管理
- AiAgentPreheatService
- 负责AI模型预热和资源预加载
- 提高系统响应速度和用户体验
- 客户端管理组件
- AiClientModel
- 管理AI模型配置信息(如OpenAI模型)
- 存储模型名称、Base URL、API Key等配置
- 支持模型动态配置和切换
- AiClientAdvisor
- 客户端顾问管理,提供AI对话的引导和优化
- 管理顾问配置和策略
- AiClientToolConfig
- 客户端工具配置管理
- 定义和管理AI可以使用的工具集
- AiClientToolMcp
- MCP(Model Context Protocol)工具管理
- 负责外部工具的集成和调用配置
- AiClientSystemPrompt
- 系统提示词管理
- 定义和管理AI代理的系统提示词模板
数据访问组件
- AgentRepository
- 实现 IAgentRepository 接口
- 提供统一的数据访问入口
- 封装对多个DAO的调用,实现数据聚合查询
- DAO接口及实现
- IAiClientModelDao:客户端模型数据访问
- IAiClientModelConfigDao:客户端模型配置数据访问
- IAiClientToolConfigDao:工具配置数据访问
- IAiClientToolMcpDao:MCP工具数据访问
- IAiClientAdvisorDao:顾问配置数据访问
- IAiClientSystemPromptDao:系统提示词数据访问
- IAiAgentTaskScheduleDao:任务调度数据访问
- IAiRagOrderDao:RAG订单数据访问
控制器组件
- AiAgentController
- 主要业务接口控制器
- 提供AI代理对话、RAG、预热等核心API
- 管理控制器
- AiAdminClientModelController:客户端模型管理
- AiAdminClientModelConfigController:客户端模型配置管理
- AiAdminClientToolConfigController:工具配置管理
- AiAdminClientAdvisorController:顾问管理
- AiAdminAgentTaskScheduleController:任务调度管理
- AiAdminAgentClientController:客户端关联管理
- AiAdminClientToolMcpController:MCP工具管理
配置与基础设施组件
- 应用配置
- application.yml:主配置文件
- application-dev.yml:开发环境配置
- application-prod.yml:生产环境配置
- 数据库配置
- MySQL:存储业务数据
- PostgreSQL with PGVector:向量存储,用于RAG功能
- 第三方集成组件
- OpenAiApi:OpenAI API客户端
- OpenAiChatModel:OpenAI对话模型
- McpSyncClient:MCP协议同步客户端
- SyncMcpToolCallbackProvider:MCP工具回调提供者
- PgVectorStore:向量存储实现
ChatClient 对话客户端是如何实例化的
ChatClient的实例化是通过策略树模式实现的,整个过程从RootNode开始,经过多个处理节点(AiClientToolMcpNode、AiClientAdvisorNode、 AiClientModelNode、AiClientNode)最终完成ChatClient的创建和注册。
- 在RootNode的multiThread方法中,异步并行加载所有必要的配置数据, 包括:AiClientModelVO、AiClientToolMcpVO、AiClientAdvisorVO、AiClientSystemPromptVO和AiClientVO等配置信息。 这些数据被存储在DynamicContext对象中,供后续节点使用
- ChatClient的实际创建在AiClientNode类的doApply方法中完成。
- Bean注册机制
- 使用AbstractArmorySupport类中的registerBean方法将ChatClient注册到Spring容器
- 该方法使用DefaultListableBeanFactory实现Bean的动态注册
- 注册前会检查Bean是否已存在,如果存在则先移除再注册
- 所有ChatClient实例以"ChatClient_"+clientId的格式命名
- ChatClient获取方式
- DefaultArmoryStrategyFactory提供了chatClient(Long clientId)方法获取已注册的ChatClient实例
- AiAgentChatService通过调用该方法获取ChatClient进行对话
二面
领域驱动设计(DDD)和传统的面向对象设计(OOD)相比有哪些差异?
传统面向对象设计(OOD): 强调“类”作为基本单元,以数据结构和行为封装为核心,常基于代码层面的职责划分(如DAO、Service、Controller分层)。
- 优点:结构清晰、易于实现;
- 缺点:容易陷入“技术导向”,忽视业务语义,导致模型难以应对复杂业务逻辑变更。
领域驱动设计(DDD):以业务领域为核心,通过“领域模型”抽象出统一语言(Ubiquitous Language),将业务规则显式表达为聚合根、实体、值对象等概念。
- 降低业务与技术之间的认知鸿沟(比如抽奖策略、积分阶梯等用“规则树”建模);
- 支持高内聚低耦合,便于模块化扩展(如责任链编排校验节点);
- 可复用性强(如通用抽奖模板+规则组合模式支撑多种活动类型)。
你在 AI-Agent 平台中如何处理多个大模型的差异化能力?如何根据任务类型选择最适合的模型来发挥其优势?
用户自由编排组装AI Agent流程如何实现的
除了Flux,还有其他实现流式响应的组件么
对话上下文记忆存储如何实现的,如何确保不同用户的对话上下文不会互相影响?
接口响应时间的耗时主要发生在哪些方面
- 数据库操作耗时(约120-150ms),主要耗时点:
- 分库分表路由计算:DBRouter组件需要计算分片位置
- 多表关联查询:活动信息、策略配置、用户账户等多表查询
- 订单插入操作:raffle_activity_order分表插入,涉及唯一性校验
- 账户余额更新:raffle_activity_account表的并发更新
- Redis操作耗时(约50-80ms)
- 库存原子操作:DECR命令 + setNX锁操作
- 缓存多层读取:活动配置、规则树、奖品信息等多key读取
- 集群网络开销:Redis集群模式下的节点通信
# Redis配置 pool-size: 10 # 连接池偏小 connect-timeout: 10000 # 连接建立耗时 - 规则引擎处理耗时(约60-90ms)
- 规则树动态装配:从Redis/DB加载规则配置并构建执行树
- 多节点条件判断:黑名单、权重计算、次数锁等节点依次执行
- 概率计算逻辑:奖品概率的随机算法和权重分配
- 其他系统开销(约30-50ms)
- 网络IO:服务间调用、中间件通信
- 序列化/反序列化:JSON数据转换
- 线程上下文切换:高并发下的线程调度
user_award_record 插入有唯一键冲突(日志里的 DuplicateKeyException ),虽已过滤错误日志,但 实际插入失败会导致业务回滚 / 重试,间接拉高 RT 均值(若有重试逻辑,需看 draw 接口的 99% 分位数 是否因重试放大 )。
TPS为600的性能依据
- 连接池使用率:数据库连接池150,Redis连接池10,均接近饱和
- CPU使用率:61.8%,仍有提升空间但受限于单机性能
- 网络带宽:仅使用3%,不是瓶颈
- 内存使用:JVM堆内存配置合理,GC频率正常
三面
简要介绍一下科研论文中的创新点
为什么选择Java后端开发这个职业方向
介绍一下复杂度和挑战性比较高的项目
项目中印象毕竟较深的难点是哪方面
学习方法
压力比较大的情况,如何缓解的
有没有经历过挫折,展开说说
最有成就感的事
职业选择的倾向和期待
职业目标规划
如果入职后发现岗位的某些方面与自己的预期不同,或者在工作内容、流程、团队氛围等方面感到不太适应,你会如何应对?
目前的offer和意向,base地
途游
一面
TPS和QPS的区别
QPS(Query Per Second) :服务器每秒可以执行的查询次数;
TPS(Transaction Per Second) :服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程);
QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个 TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器 2 次,一次访问,产生一个“T”,产生 2 个“Q”。
吞吐量优化
虚拟机内存分配、线程池、数据库慢查询和索引优化、死锁和阻塞
引入分布式架构 + 负载均衡
性能监控与调优:全链路追踪 + JVM优化
- 建立Prometheus + Grafana监控体系;
- 启用SkyWalking做链路追踪,定位慢SQL、GC频繁等问题;
- JVM参数调优(如新生代比例、GC策略);
项目中线程池的生命周期是怎么管理的?
项目中线程池的生命周期管理主要基于Spring框架的Bean生命周期机制。
- 线程池通过Spring的依赖注入机制进行初始化,主要通过 ThreadPoolConfig 类实现
- 线程池创建后,在应用运行期间持续提供服务,主要用于处理异步任务
- 对于实现了 ExecutorService 接口的 ThreadPoolExecutor ,Spring会自动调用其 shutdown 方法
如果你消费的是 MQ 数据,并投递到线程池中处理,但当线程池队列满后,你想暂停上游 MQ 的消费(不再拉取消息),怎么实现?
通过监控线程池队列长度 + 手动控制 MQ 消费者生命周期(basicCancel)或引入 Redis 信号量,可实现上下游流量自适应调节,避免积压崩溃。
- 使用线程池容量监控+MQ手动ack控制
- 在 Spring Boot 中使用 @RabbitListener 监听队列;
- 使用 Channel.basicQos(1) 或自定义 QoS 控制每次最多拉取的消息数;
- 结合 Redis + 背压信号量
- 使用 Redis 记录“是否允许消费”的状态(Boolean 类型);
- 消费前先检查 Redis 是否为 true,否则跳过;
- 线程池消费完成后,若队列变空,更新 Redis 为 true,允许重新消费;
- 可配合心跳机制定期刷新状态,防止宕机导致死锁。
- Spring Cloud Stream + 自定义 Backpressure
- 利用 Spring Cloud Stream 提供的 @StreamListener 和 BindingServiceProperties;
- 设置 spring.cloud.stream.bindings.input.consumer.maxAttempts=1;
- 结合 MessageHandler 实现手动确认 + 动态关闭消费者能力;
- 支持在 Kafka/RabbitMQ 上统一管理消费速率。
异步方法可以有返回值吗
异步方法当然可以有返回值,关键是用 CompletableFuture 或 Future 来封装异步结果,实现非阻塞+回调的异步编程模型。
如果你在一个线程池中投递任务,希望在每个任务开始执行前和完成之后都打印日志,有什么实现方案?
借助 Spring AOP + 自定义注解 + 线程池包装器实现任务级日志追踪。使用 ThreadPoolTaskExecutor 包装 + AOP 切面
- 自定义注解标记需要监控的任务
- AOP 切面捕获方法调用(执行前后)
- 线程池封装:注入日志逻辑
说一下 Spring Task 和 XXL-JOB,它们的区别在哪里?为什么使用 XXL-JOB?
Spring Task 是轻量级定时任务工具(适合单机场景),而 XXL-JOB 是分布式调度平台(适合多节点、高可用、可视化管理的生产环境)。
在真实项目中,我们从 Spring Task 迁移到 XXL-JOB 是因为要解决分布式环境下任务重复执行的问题,并提升可观测性和可维护性。XXL-JOB 不仅是一个定时任务框架,更是整个系统的‘调度中枢’。
XXL-JOB 是否能保证一个任务只执行一次?
XL-JOB 默认支持“幂等性”和“分布式锁机制”,可以有效防止同一任务在多个节点重复执行,从而保障“只执行一次”。
XXL-JOB 的架构是 调度中心(Admin)+ 多个执行器(Executor)。 每个任务由调度中心统一分配给某个执行器去执行,不会出现多个实例同时执行同一个任务的情况。
ES 和 MySQL 的区别?为什么在项目中选用 ES 而不是 MySQL?
MySQL 是关系型数据库,适合事务强一致性和结构化查询;Elasticsearch 是分布式搜索引擎,擅长全文检索、实时聚合和高并发非结构化数据查询。 项目中选用 ES 是为了支撑秒级数据分析和复杂条件聚合查询,弥补 MySQL 在查询性能上的短板。MySQL 保底数据一致性,ES 提供高性能查询能力,两者互补形成完整的数据服务体系。
- MySQL 不擅长复杂聚合查询:比如按时间范围 + 用户ID + 抽奖结果组合筛选,SQL 写起来慢且难优化;
- ES 支持秒级响应:即使百万条数据也能快速返回结果,满足运营侧“看数据”的需求;
- 与 Canal 集成方便:自动监听 MySQL 变更并同步到 ES,实现异步更新,不影响主业务流程;
- 灵活的字段映射和多维度分析:如按地区、设备类型、奖品等级做交叉统计,用 SQL 很难做到。
如果你的项目中 Redis 或 MQ(如 RabbitMQ)挂掉了,系统还能稳定运行吗?如何快速恢复?
通过“降级策略 + 异步补偿 + 健康检测”三重保障,即使 Redis 或 MQ 挂掉,系统也能维持基本可用,并在恢复后自动补救数据,保障最终一致性。
什么是SPI
SPI 是 Java 提供的“服务加载机制”,基于 META-INF/services/ 目录下的配置文件来动态加载接口实现类。 让框架或主程序不硬编码具体实现类,而是由外部提供者决定用哪个实现 —— 实现“开闭原则”(对扩展开放,对修改关闭)。从而实现解耦和插件化扩展
在项目中的应用:
- 定义一个接口(比如 Component),所有组件都必须实现它;
- 在每个组件的 META-INF/services/com.yourpackage.Component 文件中写入具体实现类名;
- 启动时用 ServiceLoader.load(Component.class) 自动加载所有已注册的组件;
- 业务系统只需依赖核心框架,不需要知道具体用了哪些组件 —— 真正做到了“零代码侵入式集成”。
SPI 和 Spring Boot Starter 有什么区别?
SPI 是 Java 层面的服务发现机制,用于插件化扩展;Spring Boot Starter 是 Spring 生态的自动化配置封装工具,用于简化依赖引入和自动装配。
- 通用技术组件工程 → 用 SPI 实现模块可插拔(解耦业务逻辑);
- 抽奖服务 / AI Agent 平台 → 用 Spring Boot Starter 快速集成 Redis、MQ、ES 等能力(提升开发效率);
通用组件项目中如何实现热更新的,流程是什么
项目的热更新机制主要通过 动态配置中心(DynamicConfigCenter) 模块实现,基于 Redis 发布订阅机制和 Spring 框架的 BeanPostProcessor 功能,实现不重启应用的情况下动态更新配置值。
运行时,配置动态更新(热更新)
当线上修改某个配置值时
- 发布消息: 外部系统(或其他服务)向 Redis 的⼀个特定 Topic (主题) 发布⼀条消息。这条消息包含了要修改的配置名和新的值 (例如 new AttributeVO("downgradeSwitch", "100"))。
- 监听消息: DynamicConfigCenterAdjustListener ⼀直在监听这个 Topic,它接收到消息后,会⽴即响应。
- 调⽤服务: 监听器会调⽤ DynamicConfigCenterService 的 adjustAttributeValue ⽅法更新配置
- 该方法首先会更新Redis中存储的值
- 然后,它会从
proxyObject存入的dccBeanGroup中读取出来需要的注入的Bean实例。 - 最后,再次通过反射直接修改那个在线的、正在运⾏的 Bean 实例的字段值,从⽽实现动态更新。
请解释一下你的 Agent 项目和普通的 ChinaFlow(或普通工作流引擎)的区别?
Agent 是一个“会思考”的智能体,而普通工作流只是“按指令走流程”的工具。
- 自动化决策能力:
- Agent:基于 RAG(检索增强生成)+ MCP(Model Context Protocol)实现智能分析与任务执行,能根据上下文自动选择下一步动作(如调用 API、查询知识库、触发外部服务)。
- 普通工作流:依赖预定义流程节点(如审批、通知),无法自主判断逻辑分支,需人工配置所有路径。
- 可配置性 & 灵活性:
- Agent:将 Advisor、Prompt、Model、API 等组件抽象为数据库驱动的能力模块,支持通过 UI 动态组合成不同 Agent,适用于多种业务场景(如文档生成、日志分析、发帖通知等)。
- 普通工作流:流程固化在代码中或 XML 配置文件里,修改成本高,难以快速响应业务变化。
- 执行机制:
- Agent:采用“问题分析 → 自主规划 → 执行 → 结果判定”的循环处理机制,具备闭环反馈能力。
- 普通工作流:单向线性执行,缺乏对执行结果的再评估与调整能力。
- 技术栈差异:
- Agent:融合 Spring AI、PostgreSQL 向量库、Redis 缓存、MCP 协议等现代 AI 工程化组件,适合复杂语义理解和多模态交互。
- 普通工作流:通常基于轻量级规则引擎(如 Drools)或简单状态机,不涉及大模型集成。
如果你的模型上下文长度限制是 128K,如何优化才能让模型尽可能记住更久远的历史对话内容(即提升长上下文记忆能力)
不是靠堆 token,而是靠“智能检索 + 动态摘要 + 外部协作”来突破模型的记忆瓶颈。 这也是我设计 Ai-Agent 平台的核心思想之一——让 AI 不只是“记事本”,而是能主动组织、查找、复用知识的“思考体”。
- 分层存储 + RAG 检索增强:在不增加 token 数的前提下,实现“虚拟无限上下文。
- 将长期上下文(如用户身份信息、历史偏好、规则配置)存入 PostgreSQL 向量库(PGVector),用 Embedding 做语义检索。
- 每次请求前先通过相似度匹配召回最近的上下文片段,拼接进 Prompt 中,避免把全部历史塞进 prompt。
- 动态压缩与摘要机制:可将原 128K 内容压缩为 5K~10K tokens,仍保留核心语义。
- 使用轻量级 LLM(如 Qwen-Turbo)对历史对话做自动摘要(保留关键事件、决策点),形成结构化摘要缓存(Redis)。
- 在新会话开始时,只传入最新摘要 + 最近 N 轮对话,大幅减少冗余信息。
- MCP 协议解耦 + 外部状态管理:模型专注分析,业务逻辑由外部服务执行,真正实现“智能+可控”。
- 把复杂逻辑(如抽奖规则树、权限校验)封装成 MCP 接口,由 Agent 自动调用而非硬编码在 Prompt 中。
- 这样即使上下文变长,也不会影响模型推理效率,且支持跨轮次状态持久化(如 Redis 记录当前进度)。
- 合理设置上下文窗口 & 分片处理:既满足 token 限制,又能灵活访问任意时间段的内容。
- 若必须保留完整上下文,采用滑动窗口机制(如保留最近 80K tokens,丢弃最老部分);
- 或者使用分片技术(chunking):将超长文本拆成多个小段落,每段独立 embedding 存储,按需召回;
你设计的 AI-Agent 平台相比像 Define 这样的开源架构,有哪些优势?你们实现了哪些它们没有的功能?
我的 AI-Agent 平台相比开源框架(如 Define)的核心优势在于 “可配置化 + 可编排 + 生产级落地能力”,尤其在企业级场景中更易扩展与维护。 我们不是简单复用开源框架,而是围绕“企业级智能体平台”的需求重构了一套可运维、可扩展、可配置的 AI 执行引擎,真正做到了从“能跑通”到“能上线”的跨越。
- 数据库驱动的 Agent 编排(Define 不支持)
- 我们将 Prompt、Model、MCP(多上下文协议)、Advisor 等组件抽象为结构化数据,存在 PostgreSQL 中;
- 用户可通过后台动态组合不同模块构建专属 Agent,无需重启服务即可上线新功能;
- Define 是硬编码式调用,不支持运行时灵活变更逻辑链路。
- MCP 协议深度集成(Define 仅支持简单 API 调用)
- 我们基于 stdio/sse 协议封装了 MCP 服务平台,支持:
- CSDN 自动发帖、微信公众号通知、ELK 日志分析、Prometheus 监控等真实业务场景;
- 多个外部工具协同执行任务,形成闭环工作流;
- Define 仅能调用静态 RESTful 接口,无法实现跨系统状态同步或异步回调。
- RAG + 向量库增强知识检索(Define 默认无本地知识库)
- 使用 PGVector 构建向量知识库,结合文档/代码/Git/Web 自动解析模块;
- 支持语义级问答、自动摘要生成、错误日志定位等功能;
- Define 依赖模型自身记忆能力,对长文本理解弱,且无法接入私有知识源。
- 生产可用性设计(Define 偏研究导向)
- 提供一键预热机制:Agent 启动前自动注入 Spring 容器,降低部署门槛;
- 支持运营级配置管理(如开关、限流、超时控制),适合灰度发布和熔断降级;
- Define 更偏向原型验证,缺少可观测性、权限隔离、多租户支持等工业级特性。
分布式消息队列技术选型
参考《Java 工程师面试突击第 1 季-中华石杉老师》
- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka、RocketMQ 和 Pulsar,但是由于它基于 Erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。 但是也因为 RabbitMQ 基于 Erlang 开发,所以国内很少有公司有实力做 Erlang 源码级别的研究和定制。 如果业务场景对并发量要求不是太高(十万级、百万级),那这几种消息队列中,RabbitMQ 或许是你的首选。
- RocketMQ 和 Pulsar 支持强一致性,对消息一致性要求比较高的场景可以使用。RocketMQ 阿里出品,Java 系开源项目, 源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。
- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。 同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响, 在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
二面
Docker云服务器部署相关
AIAgent智能体,RAG知识库、MCP相关
数据库分库分表,如何实现的
数据库连接池相关参数
- maximumPoolSize = 20:
- minimumIdle = 5:最小空闲连接
- idleTimeout : 10000毫秒(10秒,空闲连接超时时间)
- connectTimeout : 10000毫秒(10秒,连接超时时间)
- retryAttempts : 3(重试次数)
- retryInterval : 1000毫秒(1秒,重试间隔)
- pingInterval : 0(不进行定期检查)
- keepAlive : true(保持长连接
CAP和BASE理论
三面
自研的大数据框架和数据分析平台
如何优化和缓解由于海外地区DNS解析时间过长或失败的问题,假设这个问题由你负责解决?
核心问题可能包括:
本地DNS服务性能差:某些地区的DNS服务器可能存在延迟高或不稳定的情况。
跨区域访问的网络延迟:用户访问的服务与DNS服务器之间的物理距离较远,导致解析时间增加。
DNS缓存机制不足:缺乏有效的DNS缓存策略,导致每次请求都需要重新解析。
引入HTTPDNS替代传统DNS解析;
增强DNS缓存机制
- 本地缓存:在客户端和服务端分别引入DNS缓存机制,减少重复解析的次数。
- 全局缓存:使用CDN服务商提供的全局DNS缓存服务(如Cloudflare、Akamai),确保用户的DNS请求能够快速响应。
提升系统容错能力
- 备用DNS服务器:配置多个备用DNS服务器,当主DNS解析失败时,自动切换到备用服务器。
- 重试机制:在客户端实现DNS解析失败后的重试逻辑,尝试不同的DNS服务器或解析方式。
面试官的看法:
- DNS 解析是 “递归查询 + 迭代查询” 的过程:用户要访问 “a.b.c.com”,需先查顶级域名服务器→二级域名服务器→三级域名服务器,多一级域名,就多一次查询链路,多一个潜在故障点。
- 参数 / 路径替代三级域名:不新增三级域名,而是通过 “同一二级域名 + 不同参数 / 路径” 区分业务,减少需要解析的域名数量。
- “就近服务” 是 CDN / 云服务的常规优化:在全国 / 全球按 “地理大区”(如华北、华东、华南)部署 “服务节点”,用户访问时,DNS 会优先返回 “离用户最近的节点 IP”(比如北京用户解析到北京节点),核心解决 “访问延迟高” 的问题(而非解析成功率)。
- 当公共 DNS 服务(如电信 / 联通的 DNS、阿里云公共 DNS)不稳定时,可通过 “绕开公共 DNS” 或 “增强解析稳定性” 的方案提升成功率。 在企业内网或用户侧(如 APP 客户端、路由器)部署 “本地 DNS 服务器”,提前缓存常用域名的解析结果(比如小红书的所有业务域名);用户访问时,优先查询本地 DNS,不依赖外部公共 DNS。
- 用户不直接进行 DNS 解析,而是通过一个 “专用代理服务器” 访问互联网;代理服务器维护稳定的 DNS 连接(比如用高可用的 DNS 集群),统一完成域名解析后,再将流量转发给目标服务节点。
如何高效计算两亿用户中每个用户的排名,基于其积分(0到100万),并且避免全局排序带来的高时间复杂度?
为了避免对两亿用户进行全局排序,可以利用分桶统计的方法,将用户的积分分布划分为多个区间(桶),通过预计算每个区间的用户数量来加速排名查询。
- 积分范围有限:积分范围在0到100万之间,固定且较小。
- 分桶统计:将积分范围划分为若干个桶,每个桶记录该区间内的用户数量。
- 动态更新:当用户积分变化时,只需调整对应桶的计数,而无需重新排序。
利用哈希表存储、查询,底层就是基于数据。
利用积分范围固定的特性:由于积分是整数且范围固定(0到100万),可以直接使用数组存储每个积分值对应的用户数量。
构建前缀和数组prefixSum,其中prefixSum[i]表示积分为i及以上的用户总数。 查询排名时,只需通过一次数组访问即可得到结果,时间复杂度为O(1)。
如何优化查询连续7天登录的用户集合,并支持灵活组合查询(如任意天数的连续登录),以避免纯SQL查询带来的高时间复杂度和灵活性不足的问题?
- 利用Bitmap存储用户登录状态:将每天的活跃用户用Bitmap表示,每个用户的唯一ID对应Bitmap中的一个位,位值为1表示该用户当天登录,位值为0表示未登录。
- Bitmap按天滚动存储:通过滑动窗口的方式维护最近N天的Bitmap数据,确保可以快速查询任意时间段内的连续登录用户。
- 结合Redis实现高效计算:利用Redis的Bitmap操作(如BITOP)对多天的Bitmap进行逻辑运算,快速筛选出满足条件的用户集合。
挑一个项目中你遇到的技术挑战,说一下整体的解决思路
抽奖项目中的高并发场景下的库存管理。
在抽奖场景下,高并发请求可能导致库存超卖。传统数据库行锁在高 并发下会成为瓶颈。使用 redis 缓存数据库数据,在缓存中实现库存扣减。 最初采用了 redis 独占锁,但这样会出现排队问题,导致有库存,但吞吐量不佳。 修改设计为颗粒度更小的近似无锁化设计。采用 redis 的 decr 操作实现库存扣减, 考虑到在临界状态、库存恢复、异常处理等情况下的超卖问题,对每次扣减后的库存状态进行 setnx 加锁兜底。来保证不超卖。
库存扣减在缓存中完成, 因此要维护 Redis 缓存与 MySQL 数据库的一致性。通过延迟队列+定时任务的方式,在库存成功扣减后,发送消息到延迟队列中,有定时任务进行消费更新数据库状态, 通过唯一 ID 保证幂等。
在高并发场景下,缓存库存可能很快耗尽,而此时可能数据库只同步了几条数据。因此,针对缓存快速耗尽的情况,增加判断条件,当缓存库存为 0 时,通过消息队列发送消息,直接更新数据库库存为 0,并清除延迟队列中剩余的待处理消息。 减少数据库的更新操作,提升性能。考虑到消息可能发出后由于网络波动等原因,还没来得及消费就丢失了,因此我们增加 task 任务表,记录每条消息及其状态,只有消息成功消费了, 才会更新状态为已使用,通过定时任务遍历任务表,对于未使用分消息进行重复发生,防止丢失。
HRBP面
简单聊了聊项目和研究方向
有意向或offer么,期望薪资
你认为后端开发岗位应该具备哪些能力和素质
你对于第一份工作,希望在哪些方面有所成长和提高
职业规划
反问
vivo
一面
private修饰的父类的变量,子类能访问呢,如果不能,子类该如何才能访问
子类不能直接访问父类用 private修饰的变量(或方法)。这是由 private访问修饰符的特性决定的,它严格限制了成员只能在声明它的类内部被访问.
- 使用公共 (Public) 或受保护 (Protected) 的 Getter/Setter 方法. 在父类中为私有变量提供公共(public)或受保护(protected)的 getter(获取值)和 setter(设置值)方法,子类通过这些方法来间接访问.
- 反射访问
反射相关
反射赋予了我们在运行时分析类以及执行类中方法的能力。 通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性
反射允许程序在运行时检查或修改其自身的行为。其核心在于 Class对象和 JVM 的元数据。 JVM 在加载一个类时,会在方法区(Java 8+ 的元空间)创建该类的元数据(包含类名、方法、字段、构造器、父类等信息),并生成一个唯一的 java.lang.Class对象作为访问这些元数据的入口
单例模式下如何保证多线程访问的安全性
- 饿汉式:在类加载时就完成了实例的初始化,基于 ClassLoader 机制避免了多线程的同步问题。
public class Singleton {
// 类加载时就初始化实例
private static final Singleton INSTANCE = new Singleton();
// 私有构造函数
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
- 优点:实现简单,线程安全,调用效率高。
- 缺点:不是延迟加载,如果实例很大且一直未使用,会造成资源浪费。
- 懒汉式,同步方法(线程安全但效率低),通过 synchronized关键字修饰获取实例的方法,保证线程安全。
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 使用synchronized修饰方法,保证线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 优点:实现了延迟加载,线程安全。
- 缺点:每次调用 getInstance都需要同步,即使实例已经创建,效率较低。
- 双重检查锁(Double-Checked Locking) 双重检查锁在同步块内外各做一次检查,并使用 volatile关键字禁止指令重排序,以减少同步开销。
public class Singleton {
// 使用volatile关键字禁止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
// 同步代码块
synchronized (Singleton.class) {
// 第二次检查,确保线程安全
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 优点:线程安全,延迟加载,且只有在实例未创建时才进行同步,效率较高。
- 缺点:实现稍复杂。
双重校验锁的核心价值在于,它通过两次检查和配合volatile,精巧地平衡了线程安全和性能:
- 第一次检查是为了性能,避免每次调用都进入同步块。
- 第二次检查是为了安全,防止多次实例化。
- volatile 是为了安全,防止指令重排序导致其他线程获取到未初始化完全的对象。
- 静态内部类(Static Inner Class) 这种方式利用了 Java 类加载机制的特性:静态内部类只有在被主动引用时(如调用 getInstance方法)才会加载,从而初始化实例,且由 JVM 保证线程安全。
public class Singleton {
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
// 调用此方法时才会加载SingletonHolder并初始化INSTANCE
return SingletonHolder.INSTANCE;
}
}
- 优点:线程安全,延迟加载,效率高,实现相对简单。
- 缺点:无法防止通过反射或反序列化攻击创建新实例。
- 枚举(Enum) 使用枚举实现单例是《Effective Java》强烈推荐的方式。它不仅能避免多线程同步问题,还能防止反射和反序列化重新创建新的对象。
public enum Singleton {
INSTANCE; // 唯一的实例
// 可以添加其他方法
public void doSomething() {
// ...
}
}
优点:实现极其简单,线程安全,且能防止反射和反序列化破坏单例。
缺点:不是延迟加载。
对延迟加载有要求:可以考虑静态内部类或双重检查锁方式。
追求简单高效,且不介意非延迟加载:饿汉式或枚举都是不错的选择。
需要防御反射和反序列化攻击:枚举方式是最佳选择。
简单且线程安全的延迟加载:静态内部类方式通常更优。
内存的基本划分
一个进程的内存空间(也称为进程地址空间)通常会被操作系统划分为几个主要区域,每个区域有特定用途:
| 内存区域 | 存储内容 | 特性 |
|---|---|---|
| 代码区 | 程序的可执行代码(机器指令) | 只读、共享 |
| 数据区 | 已初始化的全局变量和静态变量(包括全局静态变量和局部静态变量) | 在程序开始时分配,程序结束时释放 |
| BSS 段 | 未初始化或初始化为0的全局变量和静态变量 | 在程序开始时被初始化为0或空指针;程序结束时释放 |
| 堆区 | 动态分配的内存(如 malloc, new 等) | 手动申请和释放(或由垃圾回收机制管理);空间大但分配速度相对慢;地址向高地址增长 |
| 栈区 | 局部变量、函数参数、返回地址、函数调用的上下文等 | 编译器自动管理;分配和释放速度快;空间有限;地址向低地址增长 |
| 常量区 | 字符串常量和其他类型的常量(如 const 全局变量) | 只读 |
项目中的多ChatClient是如何实现的
- 初始化阶段 :
- 系统启动时,通过RootNode异步加载所有配置数据
- 数据加载完成后存储到DynamicContext中
- 创建阶段 :
- AiClientNode从DynamicContext获取客户端配置列表
- 遍历配置列表,为每个客户端创建ChatClient实例
- 为每个ChatClient配置预设话术、ChatModel、工具和顾问
- 将ChatClient实例注册到Spring容器中
- 调用阶段 :
- 用户请求智能体对话
- 根据智能体ID查询关联的所有客户端ID
- 按顺序获取并调用每个ChatClient
- 将前一个客户端的输出作为后一个客户端输入的一部分
- 最终返回完整的对话结果
ChatClient及其与提示词、MCP、顾问的关系详解
ChatClient 是基于Spring AI框架实现的智能对话客户端,它封装了与AI模型交互的核心能力,是整个智能体系统中负责实际对话执行的组件。
ChatClient通过组合模式与提示词、MCP和顾问形成完整的对话能力体系.ChatClient的配置信息通过 AiClientVO 类进行封装.
- 提示词
- 提示词(DefaultSystem)是ChatClient的基础配置,用于设置AI模型的系统指令
- 通过 defaultSystem 方法将提示词绑定到ChatClient实例
- 提示词决定了AI模型的行为模式、专业领域和响应风格
- 系统通过 AiClientSystemPromptVO 对象存储和管理提示词内容
- MCP
- MCP是ChatClient的工具扩展机制,允许AI模型调用外部工具和服务
- 通过 SyncMcpToolCallbackProvider 将多个McpSyncClient实例集成到ChatClient
- MCP使ChatClient具备了超越纯文本对话的能力,可以执行外部操作、访问外部数据
- 系统支持为单个ChatClient配置多个MCP工具
- 顾问(Advisor)
- 顾问(Advisor)是ChatClient的功能增强组件,提供额外的能力扩展
- 通过 defaultAdvisors 方法将多个顾问绑定到ChatClient
- 常见的顾问包括聊天记忆顾问(负责维护对话历史)、RAG增强顾问(提供检索增强生成能力)等
- 顾问可以拦截和处理ChatClient的请求和响应,实现自定义逻辑
ChatClient的创建与注册流程
- 配置加载 :从数据库加载客户端配置信息,包括提示词、模型、MCP工具和顾问的ID
- 组件获取 :根据配置的ID,从Spring容器中获取对应的组件实例
- 构建实例 :使用Builder模式创建ChatClient实例,并配置所有组件
- 动态注册 :将创建好的ChatClient实例注册到Spring容器中
ChatClient是容器,提示词定义了AI的基础行为,模型是AI的核心能力,MCP扩展了AI的外部交互能力,顾问增强了AI的特定功能。
向量相似性搜索与过滤的实现步骤
- 获取RAG标签 :通过 repository.queryRagKnowledgeTag(ragId) 方法根据 ragId 查询对应的知识标签。
- 构建搜索请求 :使用 SearchRequest.builder() 创建搜索请求,并设置以下参数:
- query(message) :设置查询文本,即用户的提问
- topK(5) :设置返回最相似的5个文档
- filterExpression("knowledge == '" + tag + "'") :设置过滤表达式,只返回 knowledge 标签等于指定值的文档
- 执行相似性搜索 :调用 vectorStore.similaritySearch(searchRequest) 方法执行向量相似性搜索。
similaritySearch 方法的工作原理
- 首先,使用配置的嵌入模型(OpenAiEmbeddingModel)将查询文本转换为向量表示
- 然后,构造 SQL 查询语句,使用 PostgreSQL 的向量操作符计算查询向量与数据库中存储的向量之间的相似度,采用余弦相似度查询,计算查询向量与库中向量的距离,得到相似度分数。
- 应用过滤表达式,筛选出符合条件的文档
- 按相似度排序,返回 topK 个最相似的文档
蚂蚁
一面
假设你今天要发红包(预算10,000元),从你的视角出发,分别从业务、技术、产品三个维度来看,你需要重点关注哪些问题?
发红包看似简单,实则涉及“预算控制”、“用户体验”、“系统稳定性”和“风险防控”四大关键点。 需从业务目标(如拉新/促活)、技术实现(如并发安全)和产品设计(如用户感知)三个层面协同考虑,才能避免“超发”或“体验差”的问题。
业务视角(目标导向)
- 预算精准管控:
- 红包总金额1万元必须严格控制,不能超支。需设定“红包池”上限,并在发放时实时扣减(如Redis原子操作)。
- 设计分层策略:高价值红包(如50元)限量发放,低价值红包(如1元)可批量触发,平衡覆盖率与成本。
- 活动有效性验证:
- 通过AB测试验证红包对转化率的影响(如是否带来更多下单?)。
- 设置“冷启动保护”:前1小时只发小额红包,观察用户行为再调整策略。
技术视角(稳定可靠)
- 并发安全与库存扣减:
- 使用Redis DECR + SETNX 锁机制,防止超发(如1万红包被抢光后,后续请求直接失败)。
- 异步落库:通过RabbitMQ异步写入DB,避免高并发下数据库压力过大(参考你项目中的“最终一致性”设计)。
- 防刷与风控:
- 黑名单机制:同一IP/设备ID短时间多次领取,自动加入24小时黑名单(类似你项目中“动态黑名单拦截”)。
- 验证码/滑块:对高频用户增加人机验证,防止机器人薅羊毛。
产品视角(用户体验)
- 发放规则透明化:
- 明确告知用户“红包随机金额,最高50元”,避免用户因未抽到大额红包而投诉。
- 提供“历史记录”入口,让用户可查看已领红包明细(类似你项目中“积分明细聚合查询”)。
- 即时反馈与情绪管理:
- 成功发放后立即弹窗提示(如“恭喜获得3.8元红包!”),增强正向激励。
- 若红包发完,展示“已抢光”状态并引导用户参与其他活动(如“下次抽奖更精彩!”)。
假设现在有一个功能需求,要防止用户在同一个活动周期内重复领取红包,限制条件包括:同一用户、同一身份证、同一手机号、同一设备ID只能领取一次。请问你会如何设计实现这个功能?
本质是 防刷机制设计,核心目标是在高并发场景下确保幂等性与数据一致性。
- 唯一标识组合判定:将用户ID、身份证、手机号、设备ID组合成一个全局唯一Key(如 user_id:device_id)。
- 缓存层兜底拦截:使用Redis做快速去重校验,避免数据库频繁访问。
- 分布式锁保护临界资源:对关键操作加锁,防止并发写冲突。
- 异步补偿机制:即使失败也能通过日志或任务调度恢复状态。
如何设计幂等性机制来防止重复领取
- 数据库唯一索引兜底
- 悲观锁/乐观锁
- 去重表
- 分布式锁,分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上,需要使用分布式锁。
- Token机制
如果系统吞吐量从600 TPS提升到60,000 TPS(增加100倍),且预算仍为10,000元,你认为在技术实现上会有哪些关键差异?
需要通过增加服务器数量、分布式架构和高效资源利用来横向扩展系统能力,而非依赖单机垂直升级(Scale Up,成本高且受硬件限制)。
- 将目标系统拆分为多个微服务,例如抽奖核心服务,用户服务、奖品库存服务等等,微服务化后,每个服务可以独立扩展,避免单体架构的瓶颈。
- 从但数据库到分库分表,降低单库压力,采用冷热数据分离,混村存储高频访问的数据,消息队列异步解耦奖品发送等操作
AOF的同步策略有哪几种?
- appendfsync always
- 每次写操作都立即同步到磁盘;
- 数据最安全,最多丢失1秒内数据;
- 性能最低,适用于对数据一致性要求极高的场景(如支付系统)。
- appendfsync everysec(默认推荐)
- 每秒同步一次,由后台线程处理;
- 在性能和安全性之间取得最佳平衡;
- 适合大多数生产环境,如蚂蚁金服的高并发支付业务。
- appendfsync no
- 由操作系统决定何时同步,不主动触发;
- 性能最高但风险最大,可能因宕机丢失数秒数据;
- 不建议用于生产环境,仅用于测试或极端性能场景
三大设计模式,什么是迭代器模式
创建型、结构型、行为型。
迭代器模式就是提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。
详细讲一下Dubbo和gRPC的区别、优缺点和应用场景以及其架构
Dubbo 架构(阿里开源,Java 生态为主)
Dubbo 是基于 Java 的 RPC 框架,核心目标是解决分布式服务的注册发现、负载均衡、熔断降级等问题,架构遵循“微内核 + 插件化”设计,包含 5 个核心组件:
- Provider:服务提供者,暴露服务接口。
- Consumer:服务消费者,调用远程服务。
- Registry:注册中心(如 Zookeeper、Nacos),管理服务地址列表。
- Monitor:监控中心,统计服务调用次数、耗时等。
- Container:服务容器,负责 Provider 的启动和生命周期管理。
核心流程:
- Provider 启动时向 Registry 注册服务;
- Consumer 从 Registry 订阅服务地址,并缓存到本地;
- Consumer 通过负载均衡算法选择 Provider 发起 RPC 调用;
- 调用数据异步上报给 Monitor,用于监控和统计。
通信协议:默认支持 Dubbo 协议(基于 TCP),可扩展为 HTTP、Hessian、JSON-RPC 等。
序列化方式:默认 Hessian2,支持 JSON、Java 原生序列化等。
gRPC 架构(Google 开源,跨语言)
gRPC 是跨语言的高性能 RPC 框架,基于 HTTP/2 和 Protocol Buffers(Protobuf),核心聚焦于高效的跨语言通信,架构相对简洁:
- 服务定义:通过 Protobuf 的
.proto文件定义服务接口和数据结构。 - 客户端(Stub):根据
.proto生成的客户端代码,封装调用逻辑。 - 服务端(Service):根据
.proto实现接口的服务端代码。 - 传输层:基于 HTTP/2 协议,支持双向流、多路复用。
核心流程:
- 用 Protobuf 定义服务接口(如
service UserService { rpc GetUser(UserId) returns (User); }); - 通过编译器生成多语言客户端/服务端代码(Java、Go、Python 等);
- 客户端通过生成的 Stub 发起调用,数据经 Protobuf 序列化后通过 HTTP/2 传输;
- 服务端接收请求,反序列化后处理并返回结果。
通信协议:强制使用 HTTP/2。
序列化方式:强制使用 Protobuf(二进制协议,高效紧凑)。
| 维度 | Dubbo | gRPC |
|---|---|---|
| 开发语言 | 主要支持 Java,其他语言(如 Go、Python)支持较弱 | 跨语言设计,原生支持 Java、Go、Python、C++ 等 10+ 语言 |
| 通信协议 | 可扩展多协议(默认 Dubbo 协议,基于 TCP) | 固定 HTTP/2 协议 |
| 序列化方式 | 可扩展(默认 Hessian2,支持 JSON 等) | 固定 Protobuf(二进制) |
| 服务治理 | 内置完善(注册发现、负载均衡、熔断、限流等) | 无内置服务治理,需依赖第三方工具(如 Consul、Istio) |
| 流支持 | 有限支持(Dubbo 3.x 开始支持流式通信) | 原生支持双向流、服务端流、客户端流(基于 HTTP/2 流特性) |
| 生态依赖 | 依赖注册中心(如 Zookeeper)、Spring 等 | 轻量级,无强依赖,可独立部署 |
| 适用场景 | Java 为主的微服务架构,需复杂服务治理 | 跨语言通信、高性能实时通信(如物联网、音视频) |
Dubbo 优缺点
优点:
- 服务治理能力强:内置注册发现、负载均衡(轮询、一致性哈希等)、熔断降级(Sentinel 集成)、限流等,无需额外集成工具。
- Java 生态友好:与 Spring 无缝集成,注解驱动开发(如
@DubboService、@DubboReference),Java 开发者学习成本低。 - 协议灵活:可根据场景选择协议(如 TCP 协议适合高性能内部通信,HTTP 协议适合跨系统交互)。
缺点:
- 跨语言支持弱:虽然支持多语言,但核心功能优化集中在 Java,其他语言版本功能不全或维护滞后。
- 序列化效率一般:默认 Hessian2 序列化性能低于 Protobuf,二进制体积较大。
- 流通信支持较晚:流式通信(如长连接实时数据传输)在 3.x 版本才支持,成熟度不如 gRPC。
gRPC 优缺点
优点:
- 跨语言能力强:基于 Protobuf 定义接口,生成多语言代码,解决不同语言间通信壁垒(如 Java 服务调用 Go 服务)。
- 性能优异:HTTP/2 支持多路复用(单连接并发多个请求)、二进制帧传输,Protobuf 序列化效率比 JSON 高 3-5 倍,适合高频通信场景。
- 原生支持流式通信:支持双向流(如聊天系统)、服务端流(如日志推送)、客户端流(如文件上传),适合实时数据传输。
缺点:
- 服务治理缺失:无注册发现、负载均衡等功能,需额外集成 Consul、Kubernetes Service 等工具。
- 调试成本高:HTTP/2 + Protobuf 是二进制协议,无法直接通过浏览器调试,需专用工具(如 gRPC UI)。
- 兼容性较差:Protobuf 版本升级可能导致兼容性问题(如字段删除需谨慎),且与现有 HTTP/1.1 服务集成复杂。
Dubbo 适用场景
- Java 为主的微服务架构:如电商系统(订单、商品、支付服务均为 Java 开发),需依赖完善的服务治理能力(如熔断防止级联失败)。
- 内部系统通信:企业内部服务(非跨语言),追求开发效率和治理便捷性(如金融系统的账户服务、交易服务)。
- 需要灵活协议的场景:根据业务选择协议(如高频内部调用用 Dubbo 协议,对外提供接口用 HTTP 协议)。
gRPC 适用场景
- 跨语言服务通信:如中台系统(Java 开发)调用算法服务(Python 开发)、物联网设备(C++ 开发)上报数据到云平台(Go 开发)。
- 高性能实时通信:如监控系统(大量设备实时上报 metrics)、音视频传输(低延迟要求)、游戏服务器(高频交互)。
- 流式数据传输:如日志收集系统(服务端流推送日志)、聊天应用(双向流实时消息)、大数据同步(客户端流批量上传)。
二面
组合模式和工厂模式在项目中的应用
组合模式适用于哪种场合啊
组合模式(Composite Pattern)是一种结构型设计模式,核心是将 “单个对象” 和 “对象集合” 统一视为同一类型(通过共同接口), 从而实现 “整体与部分的递归嵌套” 操作。它适用于存在 “整体 - 部分” 层级关系,且需要统一处理单个对象和组合对象的场景, 典型特征是 “部分可以是单个元素,也可以是由多个元素组成的子整体”。
- 树形结构数据的操作:文件系统;组织结构
- UI 组件系统:界面控件;
- 权限系统:权限结构
如果让你来设计这样一个支持多源解析的模块(用于文档、Git代码、网页内容等结构化解析的核心组件),你会根据哪些条件或场景来设置它的参数(如单次最大输入量)?应该考虑哪些因素?
- 单次最大输入量:限制单次解析的数据源大小(如文件大小、API 返回内容长度)。
- 并发解析数:限制同时解析的任务数量(如同时解析 3 个文档 + 2 个 Git 仓库)。
- 解析超时时间:限制单个解析任务的最大执行时间(如网页解析超时 30 秒)。
- 结构化输出精度:控制解析结果的结构化程度(如 “基础” 仅提取文本,“高级” 提取段落层级、表格、代码块等)。
- 重试次数:解析失败(如网络波动、文件损坏)时的重试次数。
Function Calling与MCP在处理知识问答时的区别
前者聚焦 “外部工具调用以补充知识”,后者聚焦 “对话流程的整体管控”
| 维度 | Function Calling | MCP |
|---|---|---|
| 功能层级 | 模型内置的单次工具调用能力 | 跨模型/跨工具的标准化协议 |
| 设计目标 | 解决模型无法直接访问外部数据的问题 | 标准化工具接入与多工具协同 |
| 交互模式 | 模型生成结构化指令 → 外部程序执行 | 模型通过协议与工具服务动态交互 |
Function Calling(函数调用):是大语言模型(LLM)与外部工具(如数据库、API、搜索引擎等)交互的接口能力,本质是 “扩展模型知识边界” 的工具。
当模型自身知识不足(如实时数据、私有数据、复杂计算)时,通过调用外部函数获取信息,辅助生成准确回答。
解决 LLM “知识滞后”“无法访问私有数据”“计算能力弱” 等问题,让问答结果更精准、更实时。
优先 Function Calling: 任务简单、工具单一、响应要求高(如实时数据查询)。
优先 MCP: 任务复杂、需多工具协作、需维护上下文(如企业流程自动化)。
在Redis混合持久化模式下,极端情况下是否会丢失命令?如果是,如何解决?
Redis 混合持久化(默认在 Redis 4.0 + 支持)的核心是:AOF 重写时,将当前内存数据以 RDB 格式写入 AOF 文件开头,后续命令以 AOF 增量日志追加。 这种模式结合了 RDB 的快速恢复和 AOF 的实时性,但仍存在以下极端场景导致命令丢失:
- AOF 增量日志未刷盘导致的丢失
- 原理:混合持久化中,RDB 部分是全量快照(重写时生成),但重写后的增量命令仍以 AOF 日志形式追加,且默认采用everysec刷盘策略(每秒异步刷盘)。
- 极端场景:若 Redis 在 “最后 1 秒内接收了新命令” 但尚未刷盘时突然崩溃(如机器断电),这部分增量命令会丢失。
- RDB 重写期间的命令丢失
- 原理:AOF 重写(生成新的混合持久化文件)是后台异步进行的,重写过程中接收的新命令会同时写入旧 AOF 文件和重写缓冲区。
- 极端场景:若重写过程中 Redis 崩溃,且旧 AOF 文件已被删除(或重写未完成),则 “重写开始后、崩溃前” 的命令可能同时丢失(既未写入新文件,旧文件也不完整)。
- 持久化文件损坏导致的丢失
- 极端场景:若混合持久化文件(包含 RDB 头和 AOF 尾)在写入过程中损坏(如磁盘故障),Redis 重启时可能无法完整加载,导致部分数据丢失。
解决方案:降低丢失风险的核心措施
- 优化 AOF 刷盘策略(平衡性能与安全性)
- 调整刷盘策略:若业务对数据安全性要求极高(如金融场景),可将appendfsync设置为always(每次命令都同步刷盘),但会牺牲性能(IO 开销大)。
- 配置:appendfsync always (注:默认everysec是 “性能与安全的平衡”,最多丢失 1 秒数据;no由操作系统决定刷盘,丢失数据可能更多)
- 启用 AOF 重写期间的保护机制
- 保留旧 AOF 文件:确保 AOF 重写完成前,旧 AOF 文件不被删除。Redis 默认机制是:重写成功后才用新文件替换旧文件,崩溃时旧文件仍完整,可通过旧文件恢复(最多丢失 “重写期间的增量命令”)。
- 监控重写状态:通过INFO persistence命令监控aof_rewrite_in_progress状态,若重写长时间未完成(如超过阈值),触发告警排查(避免磁盘 IO 阻塞导致的潜在丢失)。
- 定期备份持久化文件
- 定时备份:通过脚本定期(如每小时)复制混合持久化文件(appendonly.aof)到异地存储,即使当前文件损坏,可通过历史备份恢复(配合 AOF 增量日志进一步减少丢失)。
- 备份校验:备份后通过redis-check-aof工具验证文件完整性(如redis-check-aof --fix appendonly.aof),避免备份本身损坏。
- 启用 Redis 集群与哨兵机制
- 主从复制:通过主从架构,让从节点实时同步主节点数据,主节点崩溃时从节点可快速切换为主节点,减少因单节点故障导致的命令丢失(从节点也会持久化,相当于多副本备份)。
- 哨兵(Sentinel):自动监控主从节点状态,主节点故障时自动切换,避免人工介入延迟导致的业务中断和数据丢失。
- 业务层补偿机制
- 关键命令日志:业务端记录核心操作日志(如支付、订单创建),Redis 恢复后通过日志回放补全丢失的关键命令。
- 幂等设计:确保命令可重复执行(如使用唯一 ID 标识请求),避免回放时产生数据不一致。
在 Redis 中,TTL(过期时间)是如何高效实现的?它在极端情况下如何保障数据一致性?
- 惰性删除
- 触发时机:仅在访问key时检查是否过期(如 GET、EXPIRE命令)。
- 实现逻辑:
- 检查 expires字典中是否存在该 key 的过期时间戳。
- 若当前时间 > 过期时间,则删除 key 并返回 nil,否则返回 value。
- 优点:减少 CPU 开销,避免全量扫描。
- 缺点:冷数据可能长期滞留内存。
- 定期删除
- 触发频率:默认每秒执行 10 次(由 hz参数控制),每次扫描随机选取 20 个 key。
- 算法细节:
- 若扫描到过期 key 占比超过 25%,则重复扫描直至时间耗尽(单次执行上限 25ms)。
- 通过 active_expire_cycle函数实现,优先清理短生命周期 key。
- 优点:主动控制内存增长,避免惰性删除的滞后性。
- 缺点:无法保证 100% 清理。
- 底层数据结构
- 主字典(dict):存储所有 key-value 对。
- 过期字典(expires):独立存储 key 的过期时间戳(毫秒级),与主字典分离以提高扫描效率
数据一致性保障
- 主从同步机制
- 主节点(Master):负责删除过期 key,并通过 异步复制 将删除操作同步到从节点(Replica)。
- 从节点:不主动删除过期 key,仅通过接收主节点的 DEL命令更新数据。
- 问题:主从延迟可能导致从节点短暂保留已过期 key。
- 解决方案:
- 延迟双删:更新数据库后,延迟 500ms 再次删除缓存,覆盖主从同步窗口期。
- 订阅 Binlog:通过 Canal 监听主库 binlog,在从库重放时同步删除操作。
- 持久化与故障恢复
- RDB 快照:不保存已过期 key,重启后从 expires字典重建过期信息。
- AOF 重写:过滤过期 key,仅记录有效操作。
- 故障恢复:主节点宕机后,新主节点继承 expires字典,通过一致性协议(如 Raft)确保过期时间同步。
- 并发控制
- 单线程模型:所有过期检查与删除操作串行执行,避免竞态条件。
- 原子性操作:EXPIRE命令通过 PEXPIREAT原子设置过期时间,防止中间状态被其他操作干扰。
- 分布式锁兜底
- 场景:高并发下大量 key 同时过期,引发缓存雪崩。
- 实现:为 key 设置随机 TTL 偏移量(如 30m ± 5m),分散过期压力。
- 锁机制:使用 SETNX或 Redisson 分布式锁,确保关键操作串行化。
你提到在高并发大促场景中使用了限流措施,那么你对流量控制(流控)有了解吗?比如常见的限流策略有哪些?
- 令牌桶算法(Token Bucket)
- 核心思想:以固定速率向桶中放入令牌,请求需要消耗一个令牌才能被处理。若桶中无令牌,则请求被拒绝或等待。
- 优点:允许突发流量(如短时间内大量请求),适合业务波动较大的场景。
- 实现示例:我用 Guava 的 RateLimiter 实现了方法级 QPS 控制,支持动态调整限流阈值。
- 漏桶算法(Leaky Bucket)
- 核心思想:请求进入漏桶后按固定速率流出,超出容量的请求直接丢弃。
- 优点:平滑输出,避免突发流量冲击下游服务。
- 适用场景:对响应时间敏感、要求稳定吞吐的系统(如支付接口)。
- 计数器限流(固定窗口/滑动窗口)
- 固定窗口:统计单位时间内请求数量,超过阈值则拒绝。
- 滑动窗口:将时间划分为多个小段,更精确地控制流量(避免“临界突刺”)。
- 注意点:固定窗口可能在窗口边界出现瞬间超限,滑动窗口可缓解此问题。
- 分布式限流(基于 Redis + Lua 脚本)
- 在微服务架构中,通过 Redis 存储计数器并用 Lua 原子操作保证一致性,适用于跨节点的全局限流。
- 自适应限流(结合熔断降级)
- 如 Hystrix 或 Sentinel 的滑动窗口 + 熔断机制,当错误率升高时自动限流或降级非核心功能。
漏桶算法更适合哪些场景
漏桶算法(Leaky Bucket)的核心特性是 **“强制请求按固定速率处理,平滑输出流量”。 无论输入流量是平稳还是突发,最终都会被 “削峰填谷” 为匀速请求,避免下游系统或资源被瞬时流量冲击。
- 对第三方 API / 服务的调用限流:多数第三方 API 会限制调用方的 “每秒请求数”(如支付接口每秒最多 100 次调用、短信接口每秒最多 50 次调用),若超出限制会触发接口封禁。
- 网络带宽控制(如下载工具限速):比如迅雷、百度网盘等软件支持设置最大下载速度(例如 5MB/s)。
- 日志采集系统(如 Filebeat → Logstash),日志数据可能在短时间内激增(如服务报错),但下游处理能力有限。漏桶机制可缓冲这些突发日志,让它们按固定节奏发送给下游,防止日志系统压垮。
如果只用数据库(DB)+乐观锁来实现库存扣减,不使用Redis,这种方案能扛多少并发量?你有实际测试过吗?
有时间可以通过压测观察一下
两阶段提交和三阶段提交
三面
未来的职业发展
为什么没有去实习呢
介绍一下课题项目和开发项目,优势和待优化的点
校园经历
团队协作相关
压力比较大的一次,如何缓解压力的
自己的优缺点
职业选择的倾向性
去哪儿
一面
为什么没实习(项目忙,导师不放)
项目相关,压测的结果分析,吞吐量的瓶颈在哪
如果两个人分别有100次和200次抽奖机会,同时疯狂抽奖,这会对系统产生什么影响?如何设计机制来避免冲突或资源竞争?
redis的decr+setnx锁兜底
在抽奖系统中,为什么要通过异步务定时更新数据库,而不是直接在每次抽奖时都写入MySQL?这样设计是否过于复杂?
为了平衡性能、一致性与可扩展性。
- 高频写入压力大:如果每个抽奖请求都直接操作MySQL,会迅速压垮数据库(尤其TPS>500时),导致响应延迟甚至宕机。
- 原子性 vs 一致性:Redis支持原子扣减(如DECR),但无法保证持久化;必须靠异步机制将状态同步到DB,实现“先快后准”的效果。
- 容错兜底能力:网络抖动或服务重启可能导致部分记录丢失,定时任务能自动补漏,避免超卖或用户投诉。
请你谈谈 Spring 框架中常用的注解有哪些?它们各自的作用是什么?
- 控制器层(@Controller / @RestController) 作用:标记类为控制器组件,处理 HTTP 请求。
- 服务层(@Service) 作用:标识业务逻辑类为 Spring Bean,通常配合 @Autowired 注入到控制器中。
- 数据访问层(@Repository) 作用:标记 DAO 层接口或实现类为数据访问组件,自动捕获数据库异常并转换为 Spring 的 DataAccessException。
- 自动装配(@Autowired) 作用:自动将 Spring 容器中的 Bean 注入到字段、构造器或方法中。
- 配置类(@Configuration + @Bean) 作用:替代 XML 配置文件,定义 Spring 容器中的 Bean。
- AOP 相关注解(@Aspect / @Before / @After / @Around) 作用:实现横切关注点(如日志、权限、事务),无需修改原代码即可增强功能。
请你描述如何用 Spring Boot 快速实现一个基础的 HTTP 服务,接收请求并返回 "Hello World"。
- 使用 Spring Boot 创建一个 Web 应用;
- 定义一个控制器类(Controller),标注 @RestController;
- 添加一个接口方法处理 /hello 请求,返回字符串 "Hello World";
- 启动应用后访问该路径即可看到结果。
具体说明如何设计并编写一个 Spring Boot 的 Controller,包括 API 接口的参数定义和实现逻辑。
- 使用 @RestController 标记类为控制器;
- 用 @GetMapping 或 @PostMapping 映射 HTTP 请求路径;
- 参数可通过 URL 路径(Path Variable)、查询参数(Query Parameter)或请求体(Request Body)传入;
- 返回值直接是字符串、对象或 ResponseEntity,Spring 自动序列化为 JSON 或文本。
详细解释一下索引的最左匹配原则、覆盖索引以及索引下推(Index Condition Pushdown)这些概念吗?
- 索引最左匹配原则(Leftmost Prefix Principle), 定义: 如果你在表上创建了联合索引(如 (a, b, c)),MySQL 查询时必须从最左边的字段开始使用索引。
- 覆盖索引(Covering Index), 定义: 当一个查询的所有字段都能通过索引直接获取,无需回表访问主键索引(即 InnoDB 的聚簇索引),这种索引称为“覆盖索引”。
- 索引下推(Index Condition Pushdown, ICP), 作用: MySQL 5.6 引入的新特性,在存储引擎层提前过滤数据,减少回表次数。 传统方式: 先根据索引找到主键,再回表查完整行 → 多次磁盘读取。
二面
深度拷打项目
DDD驱动设计解决了你项目中的什么问题?为什么DDD更适配你这个场景?
面试官看法:其实项目中只是利用了DDD设计的思想以及结合充血模型。并不是真正的DDD架构,只是做了一些解耦的业务实现
你在抽奖服务中用到了责任链模式,它主要解决什么问题?有什么好处?
抽奖流程中需要动态插入校验规则(如:先检查积分,再查黑名单,最后扣库存),且规则可能随时新增/删除。 可插拔性;已测试;灵活性;
组合模式实现规则树动态组合,它主要想解决什么问题?
为了解决多变业务规则的动态组合难题。
在操作redis指令decr时候如何由于网络抖动出现超时,可能是扣减成功,没有结果返回,也可能是未扣减成功,你该如何定位问题,如何解决
- 引入“幂等+最终一致性”机制:即使超时也能保证不重复扣减;
- 通过数据库记录状态 + 异步补偿任务:实现对 Redis 不可靠性的兜底处理;
- 结合 Redis 的 TTL 和过期键删除策略:防止缓存失效后数据错乱。
需要在redis中引入一种 状态可追溯 + 幂等校验 + 最终一致性保障 的设计模式。
引入“临时标记 + 异步补偿”机制
- 在 Redis 扣减前,先写入一个带唯一 ID 的临时标记(如 temp:deduct:);
- 若 Redis 超时,则记录本次请求为“待确认”,放入 MQ 或本地队列;
- 后续由定时任务扫描这些“待确认订单”,查询 DB 中该订单是否已被扣减(幂等校验);
- 如果没有扣减,则尝试再次扣减 Redis(此时可以安全重试)
数据库插入时加唯一约束(幂等核心)
- 插入订单表时用唯一索引(如 order_id),防止同一订单多次触发;
- Redis 中设置一个“已扣减标志”(如 deducted::),避免重复执行;
- 定时任务(XXL-Job)每日凌晨核对 Redis 与 DB 库存差异并自动修复;
假设我要抽一个高价值的奖品,库存只有1个,刚好遇到超时或者网络抖动问题,导致那个奖品没有被抽出现,就会出现再多的抽奖请求都无法抽到奖品,就会被认为没有放入该奖品,这种问题你该怎么解决呢
可能需要实现一个像MYSQL的回滚日志的机制,可以做回滚来校验数据。对于一些兜底手段,可能需要确定抽奖处于一个什么状态才能够把该状态修改成期望的状态结果。
核心目标是让 “库存扣减、中奖判定、记录生成” 成为不可拆分的原子操作,无论出现超时还是网络抖动,要么 “全成功”(用户中奖、库存扣减、记录留存),要么 “全回滚”(库存恢复、无中奖记录),避免中间态。
- 用 “分布式事务 + 悲观锁” 保障库存操作原子性
- 超时场景的 “主动解锁 + 定时巡检” 机制
- 网络抖动的 “结果幂等 + 重试机制”
三面
自我介绍
简要介绍一下项目
如何学习新的知识
遇到的困难
找工作最看重的三个点
CVTE
一面
你是如何监听 OpenAI 的响应结果并获取其输出内容的?
除了Flux,还有其他的流式输出组件么
你在 AI Agent 项目中,是怎么存储上下文信息的?用什么数据结构来保存问题和历史对话内容,以便重新输入给 OpenAI?
你了解过 Redis 的数据存储机制吗?它到底是怎么把数据存在内存里的?和我们本地的 Java Map 有什么本质区别?
可重入锁是如何定义的?它的核心特点是什么?
可重入锁允许同一个线程多次获取同一把锁而不会发生死锁。
核心特点:
- 线程独占性:锁由一个线程持有,其他线程无法获取。
- 可重入性:同一线程可以多次获取锁,不会阻塞自己。
- 公平性/非公平性:部分实现(如 ReentrantLock)支持公平锁(按请求顺序获取)和非公平锁(抢占式获取)。
- 手动释放:需要显式调用 unlock() 方法释放锁,通常配合 try-finally 使用以确保锁的释放。
可重入锁是如何标记锁的持有状态的?它是如何区分锁是可重入还是不可重入的?
可重入锁内部会维护两个状态变量,
- exclusiveOwnerThread(独占所有者线程):记录当前持有锁的线程对象(Thread实例)。若锁未被持有,此变量为null;若已被持有,指向持有锁的线程。
- state(重入次数计数器):记录当前持有线程获取锁的 “重入次数”,默认值为 0。
- 当线程第一次获取锁时:state从 0 变为 1,同时exclusiveOwnerThread设为当前线程。
- 当线程再次获取同一把锁(重入)时:state直接加 1(如从 1→2、2→3),无需重新竞争锁。
- 当线程释放锁时:state减 1,直到state变为 0 时,才将exclusiveOwnerThread设为null(表示锁完全释放)。
核心差异在于 “获取锁时的判断逻辑” —— 不可重入锁仅判断 “锁是否被持有”,可重入锁额外判断 “持有锁的是否是当前线程”。
可重入锁中的计数器除了记录锁的持有次数外,还有什么作用?
- 标记锁的 “空闲 / 持有” 状态:计数器的数值是判断锁是否被持有的核心依据
- 控制锁的 “完全释放” 时机: 可重入锁要求:只有当持有线程释放锁的次数等于获取次数时,锁才会真正释放(供其他线程获取)。计数器通过递减操作,严格控制这一过程
- 防止 “非持有线程” 释放锁:可重入锁通过计数器结合 “持有线程” 标记,确保只有当前持有锁的线程才能释放锁,避免其他线程非法释放
- 避免 “锁泄露” 或 “过度释放”:计数器的严格递减规则(每次释放只能减 1,且不能减到负数),可防止 “过度释放”(释放次数超过获取次数)导致的锁状态混乱
共享锁和互斥锁
在DDD领域取得设计中,六层架构如果我们想调用一些外部的接口的话,你主要是放在哪一层呢?如果我们想暴露一些接口给其他业务系统使用,你放在哪一层?
- 调用外部接口:放在基础设施层,外部接口(如第三方支付接口、物流接口、其他系统 API 等)属于 “技术实现细节”,不涉及核心业务逻辑,符合基础设施层 “提供技术支撑” 的定位。
- 暴露接口给其他业务系统:触发器层,暴露给其他系统的接口(如 REST API、RPC 接口、消息接口等)属于 “外部交互入口”,负责接收外部请求、转换格式、校验权限,并转发给应用层处理。
东方财富
一面
在使用Redis加锁保证安全时,如果业务处理过程中因异常导致无法释放锁,最造成死锁,该如何解决呢
- 设置锁的过期时间,即使业务逻辑崩溃或线程异常退出,锁也会自动过期释放,避免永久占用。
- 使用Lua脚本保证原子性,将“获取锁 → 执行业务 → 释放锁”封装为一个 Lua 脚本,确保整个流程原子执行,防止中间环节被中断导致锁状态不一致。
- 引入Watchdog心跳机制,对于可能超过锁过期时间的任务,可在业务执行期间定期刷新锁的过期时间
- 使用 Redlock 算法增强可靠性,若对高可用要求更高,可采用 Redis 官方推荐的 Redlock 算法,在多个独立 Redis 实例上尝试加锁,提升容错能力,降低单点故障风险。
如果 MySQL 中有一张表,有三个字段 a、b、c,并设置了联合索引 (a, b, c),那么当查询条件是 a = ? AND c = ? 时,是否会命中这个索引?
使用索引的前缀部分(a)来缩小扫描范围,MySQL 会放弃使用该联合索引的后半部分(即 b 和 c);
在 MySQL 中,假设有一张表,其中字段 id 是自增主键(步长为 1),当前表中有 6 条数据,id 分别为 1、2、3、4、5、6。如果执行 DELETE FROM table WHERE id = 6; 删除 id=6 的记录后,再插入一条新数据(未指定 id 值),那么新插入的数据的 id 值会是多少?为什么
新插入数据的 id 值是 7,核心原因是 MySQL 自增主键(AUTO_INCREMENT)的计数器 独立于表中现有数据,且只增不减,删除数据不会让计数器回退。
MySQL 自增主键的底层机制 —— 自增依赖于表的 AUTO_INCREMENT 计数器,而非 “表中现有数据的最大 id”.
通过 ALTER TABLE table AUTO_INCREMENT = N 可强制设置计数器值
你在 Spring Boot 中定义了一个接口 ITest,有两个实现类(比如 TestImplA 和 TestImplB),现在想通过属性注入的方式,用 @Autowired 注入指定的实现类(比如 TestImplA),而不是注入所有实现类到 List 或 Map 中。你该怎么实现?
使用 @Qualifier 指定 Bean 名称
@Service("testImplA")
public class TestImplA implements ITest {
// ...
}
@Service("testImplB")
public class TestImplB implements ITest {
// ...
}
@RestController
public class MyController {
@Autowired
@Qualifier("testImplA") // 明确指定要注入哪个实现类
private ITest test;
}
使用 @Resource(name = "xxx")(J2EE 标准注解)
@Resource(name = "testImplA")
private ITest test;
try catch finally
try执行 → 异常匹配 catch→ finally清理 → 后续代码。
finally中的 return会覆盖 try/catch的返回值。
3*0.1==0.3为什么输出为false
浮点数(小数)在计算机中无法被精确表示,导致计算存在微小误差。 一般采用判断两个数的差值是否小于一个极小的阈值来比较浮点数。
淘天
一面
ELS日志检索实现流程
- 用户输入自然语言查询请求 →
- AI模型处理请求并识别需要调用Elasticsearch工具 →
- 通过MCP协议将查询转换为Elasticsearch DSL →
- 执行Elasticsearch查询 →
- 将查询结果返回给AI模型 →
- AI模型分析结果并生成自然语言回复 →
- 将分析结果呈现给用户
动态分析+智能决策流程
智能分析工作流
- 初始化分析上下文,设置最大执行步数防止死循环
- 构建智能决策系统提示词,定义AI助手的能力和执行规则
- 进入循环执行过程,每步执行智能分析并决定下一步动作
- 根据执行结果动态调整策略,支持错误恢复
- 直到分析完成或达到最大步数上限
智能决策机制
- 实现了NextStepDecision类管理执行决策
- 通过analyzeStepResult方法分析AI回复,提取下一步行动指令
- 支持三种状态:分析完成、继续执行、错误恢复
- 严格按照
[ANALYSIS]和[NEXT_STEP]格式解析AI输出
项目采用了 BeanDefinitionRegistry 机制通过 DefaultListableBeanFactory 实现动态注入, 基础实现类:AbstractArmorySupport 提供通用的 registerBean 方法实现Bean动态注册, 通过 applicationContext.getAutowireCapableBeanFactory() 获取 DefaultListableBeanFactory.
AI Agent 中如何动态注入 Spring 容器
- 资源定位:确定 Spring 配置资源的位置(如 XML 文件、注解类、包扫描路径等),并封装为 Resource 对象。
- BeanDefinition 加载与解析:将定位到的资源解析为 BeanDefinition 对象(Bean 的 “元信息”,包含类名、属性、依赖、作用域等)。
- BeanDefinition 注册:将解析好的 BeanDefinition 存储到 BeanDefinitionRegistry 中,供后续创建 Bean 使用。
- BeanFactory 初始化与扩展点执行:初始化 BeanFactory 的基础配置,并执行 BeanFactoryPostProcessor(Bean 工厂后置处理器),对 BeanDefinition 进行修改或增强(核心扩展点)。
- 注册 BeanPostProcessor(Bean 后置处理器):用于在 Bean 实例化前后 对 Bean 进行增强
- 初始化事件机制(Event Infrastructure):初始化事件发布器和事件监听器,支撑 Spring 的事件驱动模型。
- 实例化非懒加载单例 Bean:对 “非懒加载的单例 Bean” 进行实例化、依赖注入和初始化(懒加载 Bean 会在首次获取时才实例化)。
- 容器刷新完成:发布 ContextRefreshedEvent 事件,通知所有监听器 “容器初始化完成”。
Spring 容器在初始化过程中是通过什么机制避免各个 Bean 之间出现循环依赖的?
- 提前暴露早期引用:在 Bean 实例化后、属性填充前,将未完全初始化的 Bean 放入 earlySingletonObjects。
- 三级缓存协作:通过 singletonObjects、earlySingletonObjects、singletonFactories协调不同阶段的 Bean 访问。
- 依赖注入策略:仅支持 Setter/字段注入的单例 Bean,构造器注入必须避免循环依赖。
| 缓存名称 | 作用 |
|---|---|
| singletonObjects | 存放完全初始化的 Bean(已填充属性、执行过初始化方法) |
| earlySingletonObjects | 存放提前暴露的 Bean(尚未填充属性,但已实例化) |
| singletonFactories | 存放 Bean 工厂对象(用于创建代理对象,解决 AOP 代理的循环依赖) |
如何实现一个读写锁
实现 “读-读共享、读-写互斥、写-写互斥”
状态变量设计
用一个32位整数 state 拆分读写状态(类似ReentrantReadWriteLock):
- 低16位:表示写锁的“重入次数”(
writeCount),0表示写锁未被持有。 - 高16位:表示读锁的“总持有次数”(
readCount),0表示读锁未被持有。
同时,需要额外变量记录:
writeOwner:持有写锁的线程(Thread类型),用于支持写锁重入。readThreads:HashMap<Thread, Integer>,记录每个线程持有的读锁次数(支持读锁重入)。
核心逻辑:读锁与写锁的获取/释放
读写锁通常拆分为两个内部类(ReadLock 和 WriteLock),分别实现Lock接口,共享同一个状态变量。
1. 写锁(WriteLock)的核心逻辑
写锁是“独占锁”,需保证“当前无读锁且无其他写锁”才能获取。
- 获取锁(
lock()):public void lock() { Thread current = Thread.currentThread(); while (true) { int currentState = state; int writeCount = currentState & 0xFFFF; // 低16位:写锁次数 int readCount = currentState >>> 16; // 高16位:读锁次数 // 条件1:有读锁持有,或写锁被其他线程持有 → 无法获取,进入等待队列 if (readCount > 0 || (writeCount > 0 && writeOwner != current)) { // 加入等待队列,阻塞当前线程(省略队列操作细节) park(); } else { // 条件2:无冲突(可重入或首次获取)→ CAS更新写锁次数 if (casState(currentState, currentState + 1)) { writeOwner = current; // 记录持有线程 break; } } } } - 释放锁(
unlock()):public void unlock() { Thread current = Thread.currentThread(); // 校验:只有持有写锁的线程才能释放 if (writeOwner != current) { throw new IllegalMonitorStateException("未持有写锁"); } int currentState = state; int newWriteCount = (currentState & 0xFFFF) - 1; int newState = (currentState & 0xFFFF0000) | newWriteCount; // 写锁次数-1 state = newState; // 若写锁完全释放(次数为0),清空持有线程,唤醒等待队列 if (newWriteCount == 0) { writeOwner = null; unparkWaiters(); // 唤醒等待的读/写线程 } }
2. 读锁(ReadLock)的核心逻辑
读锁是“共享锁”,需保证“当前无写锁(或写锁被当前线程持有,支持锁降级)”才能获取。
- 获取锁(
lock()):public void lock() { Thread current = Thread.currentThread(); while (true) { int currentState = state; int writeCount = currentState & 0xFFFF; int readCount = currentState >>> 16; // 条件1:有写锁且非当前线程持有 → 无法获取,进入等待队列 if (writeCount > 0 && writeOwner != current) { park(); // 阻塞 } else { // 条件2:无冲突(可重入或首次获取)→ 更新读锁次数 // 先更新线程的读锁计数(重入支持) int threadReadCount = readThreads.getOrDefault(current, 0); readThreads.put(current, threadReadCount + 1); // CAS更新总读锁次数(高16位+1) if (casState(currentState, currentState + (1 << 16))) { break; } else { // CAS失败,回滚线程计数 readThreads.put(current, threadReadCount); } } } } - 释放锁(
unlock()):public void unlock() { Thread current = Thread.currentThread(); Integer threadReadCount = readThreads.get(current); if (threadReadCount == null || threadReadCount == 0) { throw new IllegalMonitorStateException("未持有读锁"); } // 先更新线程的读锁计数 int newThreadReadCount = threadReadCount - 1; if (newThreadReadCount == 0) { readThreads.remove(current); } else { readThreads.put(current, newThreadReadCount); } // CAS更新总读锁次数(高16位-1) int currentState; do { currentState = state; int newReadCount = (currentState >>> 16) - 1; int newState = (newReadCount << 16) | (currentState & 0xFFFF); } while (!casState(currentState, newState)); // 若总读锁次数为0,唤醒等待队列(可能有写线程等待) if ((state >>> 16) == 0) { unparkWaiters(); } }
- 可重入性:
- 写锁:通过
writeOwner记录持有线程,同一线程可多次获取(writeCount累加)。 - 读锁:通过
readThreads记录每个线程的读次数,同一线程可多次获取(线程内计数累加)。
- 写锁:通过
- 锁降级:
支持“写锁→读锁”的降级(先获取写锁,再获取读锁,最后释放写锁),此时读锁可持有,其他读线程仍被阻塞(保证数据一致性)。 - 禁止锁升级:
不支持“读锁→写锁”的升级(否则可能导致死锁:两个读线程同时尝试升级为写锁,互相等待对方释放读锁)。 - 公平性:
可通过等待队列的“FIFO”顺序实现公平锁(严格按请求顺序获取),或允许“读锁插队”实现非公平锁(提高读并发效率)。
