面试总结

orbisz2025/7/28面试

电科研究所面试

自我介绍

面试官您好,我叫张修宇,来自江苏徐州,本科和研究生阶段均就读于西安电子科技大学。在校期间成绩优异,本科期间班级排名第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 综合智能体项目,主要为业务应用系统提效而构建,包括;需求文档分析、文档资料编写(+消息通知)、ELK 日志检索 + 普罗米修斯监控的智能 Ai Agent 分析等功能。 整套项目,抽象设计拆分了 Ai Agent 执行过程所需的各项组件(Advisor、Prompt、MCP)能力到数据库表中,使其具备自由配置编排组装的特性。 以此方式结合应用中实际场景诉求,编排出满足具体需求的AI Agent。 该项目在架构设计上使用了 DDD 分层架构进行设计,运用了组合模式的规则引擎构建执行链路, 并结合工厂、策略、责任链等方式来实现多种组合方式的Ai Agent执行过程,以此解耦系统功能的实现。这样就可以更加灵活方便的迭代各类扩展性诉求。

幸运营销汇

幸运营销汇-积分抽奖服务是我独立负责实现的一个学习项目,此项目模块在架构设计上运用了 DDD 分层架构和模板模式、责任链模式、组合模式、工厂模式等,设计模式对业务流程进行解耦和实现。 抽奖系统以“装配—参与—抽奖—结算”四段式流程为主线: 首先,活动装配阶段会把目标活动的基础信息、SKU 库存与发放策略、奖品清单及其概率散列表统一加载进 Redis。 装配完成后,用户可通过购买、签到等多种 SKU 活动累积抽奖次数;用户就可以进行抽奖,按照抽奖前中后实现抽奖流程。 基于本项目,对分布式技术栈的运用更加熟练,也把设计模式在实际场景的使用了起来,积累了丰富的设计实现经验。这些技术学习的内容,也可以更好的应对以后的开发工作。

为解决多业务系统共性需求(如规则树的设计模式、DCC 动态配置中心、接口限流配置等)重复开发问题,设计组件化平台提供可复用的技术支撑,提升整体研发效率与系统一致性。 构建一个动态配置中心,它允许应用在不重启的情况下,动态的修改配置项的值。它被封装成⼀个 Spring Boot Starter, 任何 Spring Boot 项⽬都可以⽅便地引⼊并使⽤。 可以通过分布式发布/订阅⽅式,动态的调整配置指定注解的属性的值, 不需要每次都查询 Redis来获取,从⽽减少 IO 操作。 对代码中共用的设计模式的抽象,既减少了各个业务之间重用部分,也标准化了设计模式在业务中的使用形式。 在规则树和责任链中,将流程控制部分和节点的业务执行部分解耦合,保证了模板的灵活度。 基于Guava RateLimiter 限流组件,使用 aop 切面技术,实现一款统一限流服务组件。对于频繁访问的用户动态添加黑名单拦截,再通过动态调用方法返回拦截后的结果信息。

小红书

一面

短链接设计(长链接与短链接的转换,高并发,高可用) 如何设计一个短链系统open in new window

redis持久化,RDB文件和AOF文件

并发编程中的锁

  • 按照锁的获取机制(看待并发同步的角度):乐观锁/悲观锁
  • 按照锁的竞争策略:公平锁/非公平锁,
  • 按照锁控制的资源范围:偏向锁/轻量级锁/重量级锁/分段锁
  • 按照功能特性:可重入锁/读写锁/自旋锁/互斥锁
  • 按照持有方式:独享锁/共享锁
  1. 乐观锁:假设线程的并发访问不会发生冲突,操作时不加锁,在更新数据时采用尝试更新,如有冲突则重试。乐观锁在Java中即无锁编程例如原子类,通过CAS自旋实现原子操作的更新。
  2. 悲观锁:假设线程并发一定会有冲突,每次操作前必须先加锁,阻止其他线程干扰。公平锁:
  3. 线程按照申请锁的顺序获取锁,先等待先获得。优缺点:公平,但是效率低,存在线程唤醒开销。
  4. 非公平锁:线程获取锁时不按顺序,允许“插队”。优缺点:效率高,吞吐量大,但是可能导致优先级反转或饥饿现象。
  5. 偏向锁、轻量级锁、重量级锁都是锁的状态,是针对synchronized的概念,通过对象监视器在对象头中的字段来表明的。
  6. 偏向锁:是JVM对synchronized的优化,如果只有一个线程访问同步资源,一旦线程获取锁,后续无需重复加锁。
  7. 轻量级锁:当偏向锁被多个线程竞争时升级为轻量级锁,其他线程会通过自旋尝试获取锁,不会阻塞。
  8. 重量级锁:轻量级锁自旋失败一定次数后升级为重量级锁,线程进入阻塞。 重量级锁依赖操作系统的互斥量实现,重量级锁会使其他申请的线程进入阻塞,性能降低。
  9. 分段锁:将大对象拆分为多个小段,对每个段单独加锁,细化锁的粒度,减少锁竞争。
    • 分段锁是一种锁的设计,不是具体的锁。
    • 以ConcurrentHashMap中put操作为例,不会对整个hashmap加锁,会先通过hashcode计算放入的分段,对分段加锁。如果不是放在同一个分段中,可以实现并行插入。
  10. 可重入锁(递归锁):线程可以重复获取已持有的锁,避免自己锁死自己。实现方式:synchronized(隐式)ReentrantLock(显式)// 由于可重入锁的特性,setB可以正常执行
  11. 读写锁:区分“读操作”、“写操作”,允许多个读线程并发访问,读和写互斥,写和写互斥。ReadWriteLock.
  12. 自旋锁:线程获取锁失败时不立即阻塞,而是循环尝试获取锁,循环有次数限制。优缺点:减少线程上下文切换开销,但是循环会消耗CPU。
  13. 互斥锁:通过互斥机制保证同一时间只允许一个线程持有锁。ReentrantLock.互斥锁/读写锁是独享锁/共享锁的具体体现。
  14. 独享锁:同一时间只能有一个线程持有锁
    • ReentrantLock 是独享锁
    • ReadWriteLock 写锁是独享锁
  15. 共享锁:同一时间允许多个线程同时持有锁,线程间不互斥。
    • ReadWriteLock 读锁是共享锁,保证并发读高效,而读写、写读、写写的过程互斥。
使用场景
乐观锁读操作频繁,冲突概率低的场景
悲观锁写操作频繁,冲突概率高
偏向锁单线程反复访问同步块
轻量级锁短时间、低冲突并发场景
重量级锁长耗时、高冲突并发场景
分段锁对大对象的并发访问
读写锁读多写少

Java 中具体工具

工具适用场景
synchronized实现互斥锁。对共享资源的访问进行同步控制
ReentrantLock实现可重入锁。可手动控制锁的获取和释放,支持公平锁,适合更高级别控制场景
ReadWriteLock读写锁接口。适用于读多写少场景
StampedLock乐观读写锁。并发性能更高,适用于读多写少场景。
AtomicInteger基于 CAS 的原子操作类(无锁)。实现共享变量的原子更新

rabbitMQ消息发送和接收的流程,及可靠性保证

  1. 生产者准备消息并指定路由键:生产者创建消息(通常包含业务数据,如 JSON 字符串),并在发送时指定路由键(Routing Key) 和交换机名称。
  2. 生产者将消息发送到交换机:生产者通过 RabbitMQ 客户端(如 Java 的 amqp-client、Python 的 pika)与 RabbitMQ 服务器建立连接,将消息发送到指定的交换机。 此时交换机仅接收消息,不直接存储消息(若消息无法路由到任何队列,可能被丢弃或返回给生产者,取决于交换机类型和配置)。
  3. 交换机根据规则路由消息到队列:交换机根据自身类型和 “绑定键(Binding Key)” 与 “路由键(Routing Key)” 的匹配规则,将消息转发到对应的队列:
    • Direct 交换机:仅当路由键与绑定键完全匹配时,消息才会被路由到队列(如路由键 order.create 匹配绑定键 order.create)。
    • Topic 交换机:支持通配符匹配(* 匹配一个单词,# 匹配多个单词),如路由键 order.create 可匹配绑定键 order.* 或 #.create。
    • Fanout 交换机:无视路由键,将消息广播到所有绑定的队列。
    • Headers 交换机:通过消息头(而非路由键)匹配,较少使用。
  4. 消息在队列中存储等待消费:队列收到消息后,会按顺序存储消息(队列是 FIFO 结构),直到被消费者取用。 队列可配置持久化(消息不会因 RabbitMQ 重启丢失)、过期时间(超时未消费自动删除)等属性,确保消息可靠性。
  5. 消费者从队列获取消息:消费者通过客户端与 RabbitMQ 建立连接,并 “订阅” 目标队列,当队列中有消息时,RabbitMQ 会将消息推送给消费者(或消费者主动拉取,较少用)。 消费者需指定队列名称,且需确保队列已存在(否则会报错)。
  6. 消费者处理消息并确认:消费者接收到消息后,进行业务处理(如更新数据库、调用接口等),处理完成后向 RabbitMQ 发送消息确认(Ack):

可靠性保证

  1. 持久化:队列持久化确保队列元数据不丢失,消息持久化确保消息被写入磁盘(而非仅存于内存)。RabbitMQ 会将持久化消息定期刷盘,即使服务器宕机,重启后也能从磁盘恢复消息。
  2. 镜像队列:在 RabbitMQ 集群中,单个节点故障可能导致该节点上的队列及消息丢失。镜像队列机制可将队列复制到集群中的多个节点(镜像节点),当主节点故障时,从节点自动切换为主节点,保证消息不丢失。
  3. 生产者确认机制(确保消息到达交换机 / 队列)
    • Publisher Confirm(发布确认):生产者开启确认模式后,RabbitMQ 会在消息成功到达交换机并路由到队列(若需持久化则等待消息写入磁盘)后,向生产者返回 “确认”(ack);若失败则返回 “否定确认”(nack)。 生产者可根据确认结果重试失败的消息。
    • Publisher Return(发布返回):当消息到达交换机但无法路由到任何队列时(如路由键不匹配且未设置备份交换机),RabbitMQ 会将消息返回给生产者,避免消息无声丢失。
  4. 消费确认机制:默认情况下,RabbitMQ 采用 “自动确认”(autoAck=true),即消息一旦被消费者接收,就从队列中删除。若消费者在处理前崩溃,消息会丢失。 需改为手动确认(autoAck=false):消费者处理完消息后,主动发送确认信号(ack),RabbitMQ 才删除消息。
  5. 消费者限流:若消费者处理速度慢于消息生产速度,队列会堆积大量消息,可能导致消费者过载崩溃。可通过 basicQos 限制消费者每次预取的消息数量,确保 “处理完一批再取一批”。
  6. 异常处理:对于 “无法正常消费” 的消息,通过死信队列和重试机制避免消息无限循环或丢失。
  7. 重试机制:对于临时故障(网络波动等),可以通过通过有限次重试解决。

分布式锁

在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。可以通过锁来时下按共享资源的互斥访问。 更准确的说应该是悲观锁, 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

对于单机多线程来说,在Java中,我们通常使用ReentrantLock类、synchronized关键字这类JDK自带的本地锁来控制一个JVM进程内的多个线程对本地共享资源的访问。

分布式锁保证不同的服务/客户端运行在不同的JVM进程上时,如果多个JVM进程共享同一份资源,时下按资源的互斥访问。满足以下条件:

  • 互斥:任意时刻,锁只能被一个线程持有
  • 高可用:锁服务是高可用的,一个锁服务出现问题,可以自动切换到其他的锁服务。
  • 可重入:一个节点获取锁后,还可以再次获取
  • 高性能:获取和释放锁的操作应该快速完成,不会对系统的性能造成太大的影响
  • 非阻塞:如果获取不到锁,不能无限期等待,避免对系统的正常运行造成影响。

分布式锁实现方案:

  1. 基于关系型数据库比如MySQL实现分布式锁。
    • 关系型数据库的方式一般是通过唯一索引或者排他锁实现。性能太差,不具备锁失效机制
  2. 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
    • SETNX 命令可以帮助我们实现互斥。如果key不存在的话,设置key的值。如果key已经存在, SETNX啥也不做。
    • DEL命令删除对应的key,释放锁
    • 通过Lua脚本保证锁操作的原子性,防止误删其他的锁。
    • 给key设置一个过期时间,保证设置指定 key 的值和过期时间是一个原子操作。
    • 通过Redisson的Watch Dog机制实现锁的自动续期。
  3. 基于分布式协调服务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:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。

设计模式在项目中具体应用

大营销项目
  1. 模板模式
    • AbstractRaffleActivityAccountQuota :定义抽奖活动账户额度的标准流程;AbstractRaffleActivityAccountQuota 定义了 createOrder 标准流程:参数校验 → 未支付订单查询 → 基础信息查询 → 账户额度校验 → 活动动作规则校验 → 构建订单聚合对象 → 交易策略处理。
    • AbstractRaffleStrategy :定义抽奖策略的标准流程;AbstractRaffleStrategy 定义了 performRaffle 标准流程:参数校验 → 责任链抽奖计算 → 规则树抽奖过滤
    • AbstractRaffleActivityPartake :定义参与抽奖的抽象流程;子类如 RaffleActivityAccountQuotaServiceDefaultRaffleStrategy 实现具体的抽象方法
  2. 策略模式:通过构造函数注入不同的交易策略实现类到Map中;根据订单类型动态的选择相应的交易策略进行处理。
  3. 工厂模式:
    • DefaultChainFactory 负责创建和组装责任链的逻辑节点
    • DefaultTreeFactory 负责创建决策树引擎,用于规则树的逻辑处理
    • 通过工厂模式统一管理复杂对象的创建过程
  4. 责任链模式:在抽奖策略中,通过责任链处理各种前置规则校验;每个链节点负责特定的业务逻辑,支持链式调用和扩展。
  5. 建造者模式:项目中大量实体类使用了@Builder注解;简化复杂对象的构建过程;提供链式调用的优雅API;支持可选参数的灵活设置。
  6. 单例模式:通过 Spring 的 @Service 、 @Component 注解实现单例管理;如 DefaultChainFactory 、各种 Service 类都是单例模式。
  7. 原型模式(Prototype Pattern):在 DefaultChainFactory 中通过 ApplicationContext 获取原型对象。

为什么redis可以用来做缓存,分担数据库的压力,从底层原理,为什么轻量级讲,缓存如redis为什么快。

Redis 所有数据存储在内存中,读写操作直接在 RAM 完成,避免磁盘 I/O 瓶颈(磁盘访问延迟约毫秒级,内存仅纳秒级)。 Redis 原生支持多种数据结构,操作时间复杂度低,如String、Hash、Set、List、ZSet等。 Redis 使用文本协议(RESP),格式简单,解析效率高,网络传输开销小。 Redis 采用单线程处理命令,避免多线程锁竞争和上下文切换的开销,在缓存场景下(读多写少),单线程反而能够提高吞吐量,并且代码复杂度更低。

AI Agent智能体
  1. 建设者模式
  2. 工厂模式:- 作为策略工厂,负责创建和管理不同类型的策略处理器;提供 StrategyHandler 方法返回 RootNode 实例;集中管理对象创建逻辑,降低系统耦合度。
  3. 策略模式 (Strategy Pattern) + 责任链模式 (Chain of Responsibility):通关Node节点实现流程体系:
    • RootNode.java : 作为根节点,管理异步数据加载和策略路由
    • AiClientToolMcpNode.java : 处理MCP工具配置和客户端创建
    • AiClientNode.java : 构建完整的聊天客户端
    • AiClientAdvisorNode.java : 管理顾问配置
  4. 模板模式:提供通用的 Bean 注册模板方法 registerBean();定义了标准的 Bean 生命周期管理流程;子类可以复用通用逻辑,专注于具体业务实现。
  5. 配置模式:使用 @ConfigurationProperties 实现配置外部化;支持不同环境的配置切换(dev、prod);提供默认值和类型安全的配置管理。
  6. 适配器模式:传输协议适配: 在 AiClientToolMcpNode 中根据 transportType (SSE/STDIO)创建不同的传输客户端;顾问类型适配: 在 AiClientAdvisorNode 中将不同类型的顾问(ChatMemory/RagAnswer)适配到统一的 Advisor 接口。
  7. 代理模式: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 模块,可以轻松地为应用添加生产级的监控和管理端点,方便了解应用运行状况、收集指标、进行健康检查等

葡萄城

一面

面向对象设计的优缺点。

面向过程编程(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的区别

Java开发规范,比如Arrays.aList使用时的注意事项。

  1. 返回固定大小的列表:Arrays.asList() 返回的列表是基于原始数组的视图(view)。其大小在创建时就已固定(由原数组长度决定)。任何试图改变其大小的操作(如 add, remove, clear)都会导致 UnsupportedOperationException 异常。
    • 解决方案:如果你需要一个可增删的列表,最简单的方式是用 ArrayList 包装一下。
  2. 与原始数组的数据共享:Arrays.asList() 返回的 List 并不是一个数据的独立副本,而是与原始数组共享同一块内存(引用相同的对象数组)。因此,对 List 中元素的修改(例如 set 方法)会反映到原始数组上;反之,修改原始数组的元素也会反映到 List 中。
    • 解决方案:若需独立,可先复制数组:Arrays.asList(Arrays.copyOf(originalArray));
  3. 处理基本类型数组的陷阱:Arrays.asList 方法的参数是可变参数 T... a,这意味着它期望接收的是对象引用(Object references)。如果你直接传入一个基本数据类型(如 int[], byte[])的数组,整个基本类型数组会被当作一个单一的 Object 对象(或者说是 T 类型的一个实例),进而被当作 List 的一个元素,而不是把数组中的每个基本数据类型值作为 List 的元素。
    • 使用包装类型数组(如 Integer[]),或使用 Stream API 转换(如 Arrays.stream(arr).boxed().collect(Collectors.toList()));
  4. 非标准 ArrayList:Arrays.asList() 返回的 List 是 java.util.Arrays 类的一个私有静态内部类 ArrayList,它虽然也实现了 List 接口,但与常用的 java.util.ArrayList 类不同。这个内部类没有重写 add, remove 等方法,而是直接使用了 AbstractList 中的默认实现,这些默认实现就是简单地抛出 UnsupportedOperationException。所以,当你尝试进行增删操作时就会遇到异常。
    • java.util.ArrayList 明确其仅为“视图”,操作受限。需要标准 ArrayList 时主动包装。
  5. 注意 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,其提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对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 节点状态含义
CANCELLED1表示线程已经取消获取锁。线程在等待获取资源时被中断、等待资源超时会更新为该状态。
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 -> PROPAGATEAQS 内部引入了 PROPAGATE 状态,为了解决并发场景下,可能造成的线程节点无法唤醒的情况。(在 AQS 共享模式获取资源的源码分析会讲到)

条件变量(ConditionObject)

  • AQS内部类ConditionObject实现了Condition接口,用于实现等待/通知机制。
  • 每个ConditionObject都维护了一个单向的条件队列,用于存放调用await()方法后等待条件的线程。
  • 这与synchronized搭配Object.wait()/notify()类似,但一个AQS可以关联多个条件队列,实现更精细的线程等待控制。

volatile的作用

保证变量的可见性;但不能保证原子性;

防止指令的重排序,如果将变量声明成volatile,在对这个变量进行读写操作时,会通过插入特定的内存屏障来禁止指令的重排序。

ThreadLocal的使用场景,如何把主线程中的ThreadLocal的数据同步到异步线程中

项目中的分库分表路由组件使用ThreadLocal来存储当前线程的路由信息(如数据库索引、表索引等),确保在同一个线程中的所有数据库操作都路由到正确的数据源。

  1. 手动传递 (Manual Propagation):在创建异步任务前,主动从主线程的ThreadLocal中取出值,并将其作为参数传递给异步任务(例如通过Runnable的构造函数或CompletableFuture.supplyAsync的参数)。
  2. InheritableThreadLocal:InheritableThreadLocal 是 ThreadLocal 的子类。它通过覆盖 childValue 等方法,在创建子线程时将父线程的变量副本自动复制给子线程。 但其主要缺点是:复制发生在线程创建时。如果使用线程池,线程会被复用,后续任务可能会读到之前任务设置的值或脏数据,且父线程对值的修改不会更新到已创建的子线程中。
  3. 阿里开源的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)的有序性,用“大于条件”替代 OFFSETWHERE id > 上一页最大id主键自增/唯一键有序,且分页无复杂筛选条件✅ 性能极高(直接命中索引,无偏移扫描);❌ 不支持“跳页”(如直接从第1页跳第100页)
2. 覆盖索引+延迟关联先通过“覆盖索引”查询出符合条件的主键,再用主键关联原表取完整数据有复杂筛选条件(如 WHERE status=1 AND create_time>xxx✅ 避免回表扫描大量数据;❌ 需维护符合筛选条件的组合索引
3. 书签分页(Seek Method)类似主键分页,用“上一页的最后一条数据的关键列”作为“书签”,定位下一页起始关键列有序(如时间、编号),支持“连续翻页”✅ 性能优于 OFFSET,支持多条件筛选;❌ 不支持“跳页”,需记录上一页书签
4. 预计算中间结果对高频筛选的深度分页场景,提前计算并存储“筛选后的主键列表”(如定时任务生成)筛选条件固定(如“近30天已支付订单”),数据更新不频繁✅ 查询时直接取预存主键,性能极高;❌ 数据有延迟,需维护预计算任务
5. 限制分页深度业务层限制最大偏移量(如“最多支持前100页分页”),超过则提示“请缩小筛选范围”非核心业务(如普通列表页),用户无“深分页”刚需✅ 零开发成本,避免性能风险;❌ 牺牲部分用户体验

正确使用索引得建议

  1. 选择合适的字段创建索引:不为 NULL 的字段;被频繁查询的字段;被作为条件查询的字段;频繁需要排序的字段;被经常频繁用于连接的字段;
  2. 被频繁更新的字段应该慎重建立索引
  3. 限制每张表上的索引数量
  4. 尽可能的考虑建立联合索引而不是单列索引
  5. 注意避免冗余索引
  6. 字符串类型的字段使用前缀索引代替普通索引
  7. 避免索引失效
  8. 删除长期未使用的索引
  9. 知道如何分析 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 (接口&类型层)
  1. 请求入口 :HTTP请求 → Trigger层Controller
  2. 业务编排 :Controller调用多个Domain服务协作
  3. 数据操作 :Domain通过Repository接口 → Infrastructure实现
  4. 事件驱动 :Domain发布事件 → Infrastructure处理 → MQ异步消息
  5. 任务调度 :定时Job → Domain服务 → 数据更新

项目中比较复杂,有挑战性的工作

项目架构的设计和库表的设计

抽奖流程和策略的设计

美团

一面

数据库与缓存的一致性如何保证

三种缓存策略:旁路缓存;读写穿透;一异步缓存

项目中使用的是旁路缓存策略做很多读操作,使用延迟队列加定时任务的方式保证最终一致性。 不需要保证实施一致性,因为库存会提前预热到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:终止状态,表示该线程已经运行完毕。

垃圾回收器?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 (地理位置)。
  1. 基于 List 实现 “基础队列”
  2. 基于 Sorted Set 实现 “优先级队列”
  3. 基于 Stream 实现 “高可靠消息队列”

数据库要存储对精度有明确要求的数据,比如金额,要用什么类型来存

DECIMAL 类型

脏读、幻读、不可重复读的区别和如何体现的,举例说明

  1. 脏读(Dirty Read):读取 “未提交的脏数据” 核心定义:一个事务(T1)读取到了另一个事务(T2)尚未提交的数据。若 T2 后续回滚,T1 读取到的就是 “无效的脏数据”,会导致业务逻辑错误。
  2. 不可重复读(Non-Repeatable Read):同一事务内 “重复读结果不一致” 核心定义:一个事务(T1)在同一执行过程中,多次读取同一数据,但由于另一个事务(T2)对该数据进行了 “已提交的修改 / 删除”,导致 T1 每次读取的结果不一致。
  3. 幻读(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 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。
  • 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的生命周期

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。
  3. 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() 方法。
  4. 销毁 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 来判断用户的登录信息。

实现流程:

  1. 用户输入用户名和密码,前端将用户提交的用户名和密码发送到后端进行验证。
  2. 后端验证用户信息是否正确,并创建一个Session。Session是一种服务器端保存用户会话信息的机制,用于识别多次请求之间的逻辑关系。
  3. 后端将Session ID(通常是一个随机的字符串)返回给前端,并通过 Cookie 的方式将Session ID保存在浏览器中。这样就可以保证当用户再次发送请求时,后端可以通过该 Session ID 来识别用户身份,并完成相关的操作。
  4. 在后续的请求中,浏览器会自动将保存的 Cookie 信息发送到后端进行验证,如果 Session ID有效,则返回相应的数据。如果 Session ID 失效或者不存在,则需要重新登录获取新的 Session ID。
  5. 在用户退出时,后端需要删除对应的 Session 信息,以保证安全性。
二、Token 登录

Token 是通过服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

实现流程:

  1. 用户输入用户名和密码,前端将用户提交的用户名和密码发送到后端进行验证。
  2. 后端验证用户信息是否正确,并生成一个 Token。Token 是一串加密的字符串,包含了用户的身份信息和权限等相关信息。
  3. 后端将 Token 返回给前端,并保存在客户端的LocalStorage或者SessionStorage中。
  4. 每次向后端发送请求时,前端都需要在请求头部携带 Token 信息。
  5. 后端接收到请求后会从 Token 中解析出用户身份信息,并通过权限校验等操作来判断请求是否合法。
  6. 如果校验通过,则返回相应的数据,否则返回错误信息。
  7. 在用户退出时,前端需要删除保存的 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在算时间内登录次数过多的用户,就加入黑名单,返回相应的信息限制登陆。

直接在用户表里增加两个字段:

  1. 输错密码次数 num
  2. 禁止登录的截至时间点 lock-time

我们需要记录输错密码的次数 num,当输入正确密码之后重置 num 和 lock-time 字段的值,当输错密码次数达到 3 次之后,修改 lock-time 为允许再次登录的时间。

整个逻辑也很简单(我们这里假设错误阈值为 3 ):

  1. 当用户提交用户名和密码登录时,先判断当前时间点是不是比 lock-time 小。
  2. 如果比 lock-time 小的话,说明当提前用户暂时被限制登录,返回“输入密码错误次数达到 3 次,请 xx 分钟后再尝试”。
  3. 如果大于等于 lock-time 的话,表明当前未被限制登录,进一步判断 num 的大小是否小于 3。
  4. 如果小于 3 则代表还能继续尝试登录,用户名和密码校验通过,则返回“登录成功”,并重置 num 和 lock-time 字段的值;否则,就返回“登录失败,用户名/密码错误”,并将 num 的值加 1。
  5. 如果 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 把“拦截了也不能用”做实,最后靠监控与演练闭环。

高并发处理流程

  1. 分而治之,横向扩展:采用分布式部署的方式,部署多台服务器,把流量分流开,让每个服务器都承担一部分的并发和流量,提升整体系统的并发能力。
  2. 微服务拆分:这样就可以达到分摊请求流量的目的,提高了并发能力。 所谓的微服务拆分,其实就是把一个单体的应用,按功能单一性,拆分为多个服务模块。
  3. 分库分表:拆分为多个数据库,来抗住高并发的毒打。
  4. 池化技术:即数据库连接池、HTTP 连接池、Redis 连接池等等。使用数据库连接池,可以避免每次查询都新建连接,减少不必要的资源开销,通过复用连接池,提高系统处理高并发请求的能力。
  5. 主从分离:做主从分离,然后实时性要求不高的读请求,都去读从库,写的请求或者实时性要求高的请求,才走主库。这样就很好保护了主库,也提高了系统的吞吐。
  6. 使用缓存:Redis缓存,JVM本地缓存,memcached等等。
  7. CDN 加速静态资源访:商品图片,icon等等静态资源,可以对页面做静态化处理,减少访问服务端的请求。
  8. 消息队列削锋
  9. ElasticSearch:用ES来支持简单的查询搜索、统计类的操作。
  10. 降级熔断:熔断降级是保护系统的一种手段。最简单是加开关控制,当下游系统出问题时,开关打开降级,不再调用下游系统。还可以选用开源组件Hystrix来支持。
  11. 限流:可以使用Guava的RateLimiter单机版限流,也可以使用Redis分布式限流,还可以使用阿里开源组件sentinel限流。
  12. 异步:以借用消息队列实现。比如在海量秒杀请求过来时,先放到消息队列中,快速相应用户,告诉用户请求正在处理中,这样就可以释放资源来处理更多的请求。秒杀请求处理完后,通知用户秒杀抢购成功或者失败。
  13. 常规的优化:接口优化
  14. 压力测试确定系统瓶颈:在系统上线前,需要对系统进行压力测试,测清楚你的系统支撑的最大并发是多少,确定系统的瓶颈点,让自己心里有底,最好预防措施。 压测完要分析整个调用链路,性能可能出现问题是网络层(如带宽)、Nginx层、服务层、还是数据路缓存等中间件等等。loadrunner是一款不错的压力测试工具,jmeter则是接口性能测试工具,都可以来做下压测。
  15. 应对突发流量峰值:扩容+切流量
    • 扩容:比如增加从库、提升配置的方式,提升系统/组件的流量承载能力。比如增加MySQL、Redis从库来处理查询请求。
    • 切流量:服务多机房部署,如果高并发流量来了,把流量从一个机房切换到另一个机房。

招银网络

一面

@Transactional注解相关,Java中的事务属性

字节

一面

一、IOC 的基本原理

IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 二、IOC 容器的底层实现流程 Spring IOC 容器的工作流程可分为容器初始化和Bean 实例化两大阶段,核心步骤如下:

  1. 容器初始化(加载配置,解析 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注册表传入工厂。
  2. 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领域驱动设计来实现项目,那么涉及的领域和一些实体详细讲一下,项目中的领域和实体是如何划分的,具体举例说明

  1. 策略领域 (Strategy Domain)
  • 核心职责 :负责抽奖策略的配置、规则处理和奖品分发逻辑
  • 主要实体 :
    • StrategyEntity :策略实体,包含策略ID、描述和规则模型
    • StrategyAwardEntity :策略奖品实体
    • RaffleAwardEntity :抽奖奖品实体
    • RuleActionEntity :规则动作实体
  • 值对象 :RuleTreeVO、RuleWeightVO、StrategyAwardStockKeyVO等
  • 领域服务 : AbstractRaffleStrategy 抽象抽奖策略服务
  1. 活动领域 (Activity Domain)
  • 核心职责 :管理营销活动的生命周期、用户参与和订单处理
  • 主要实体 :
    • ActivityEntity :活动实体,包含活动基本信息和状态
    • ActivityAccountEntity:活动账户实体
    • ActivityOrderEntity:活动订单实体
    • SkuProductEntity:SKU商品实体
  • 聚合根 :CreatePartakeOrderAggregate、CreateQuotaOrderAggregate
  • 值对象 : ActivityStateVO 活动状态枚举、OrderStateVO订单状态等
  1. 积分领域 (Credit Domain)
  • 核心职责 :用户积分账户管理、积分交易和订单处理
  • 主要实体 :
    • CreditAccountEntity :积分账户实体
    • CreditOrderEntity:积分订单实体
    • TradeEntity:交易实体
  • 聚合根 : TradeAggregate 交易聚合,封装积分交易的完整业务逻辑
  • 值对象 :TradeTypeVO交易类型、TradeNameVO交易名称
  1. 奖品领域 (Award Domain)
  • 核心职责 :奖品发放、用户奖品记录管理
  • 主要实体 :
    • UserAwardRecordEntity:用户奖品记录实体
    • DistributeAwardEntity:分发奖品实体
    • UserCreditAwardEntity:用户积分奖品实体
  • 聚合根 :GiveOutPrizesAggregate、UserAwardRecordAggregate
  • 值对象 :AwardStateVO奖品状态、AccountStatusVO账户状态
  1. 返利领域 (Rebate Domain)
  • 核心职责 :用户行为返利、日常任务奖励
  • 主要实体 :
    • BehaviorEntity:行为实体
    • BehaviorRebateOrderEntity:行为返利订单实体
  • 聚合根 :BehaviorRebateAggregate
  • 值对象 :BehaviorTypeVO行为类型、RebateTypeVO返利类型
  1. 任务领域 (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 更灵活的锁机制。

最近更新 2025/8/12 22:35:32