简历面试
1. 介绍一下这个 SwiftChat 项目,它的业务目标是什么,以及你在其中承担的主要职责?
业务目标主要是打造一个跨平台、功能全面且稳定的即时通讯工具,满足用户在单聊、群聊、音视频通话等方面的沟通需求,同时确保系统有良好的可扩展性,能适应后续可能增加的更多业务场景。我在这个项目里是独立开发,承担了从架构设计、技术选型,到各个功能模块的开发、测试以及优化等全流程工作,像后端服务构建、前端客户端开发,还有各种功能集成和系统稳定性保障都是我负责的。
2. 你可以讲一讲你这个 Netty 跟 WebSocket 长连接管理
- 连接与用户的绑定:当客户端通过 WebSocket 建立连接后,使用
ChannelContextUtils中的addContext方法,将用户 ID(userId)与 Netty 的Channel(连接通道)绑定。通过AttributeKey在Channel中存储 userId,同时用ConcurrentHashMap(USER_CHANNEL_MAP)维护 userId 与 Channel 的映射关系,方便快速通过用户 ID 定位其对应的连接。 - 群聊连接的分组管理:对于群聊场景,使用
GROUP_CHANNEL_MAP(同样是ConcurrentHashMap)存储群 ID(groupId)与ChannelGroup的映射。当用户加入连接时,会根据其所在的群聊列表(从 Redis 获取contactIds),将用户的 Channel 添加到对应群的ChannelGroup中,实现对群内所有连接的统一管理(如群发消息)。
面试话术:服务器后端维护了两个 ConcurrentHashMap 类型的数据 ,第一个是 user_channel_map 将用户id与 Netty 的 channel 连接通道绑定,第二个是 group_channel_map将群聊id与channelGroup绑定,channelGroup里面可以存入多个channel。
用户连接netty服务器就会把用户id和对应的channel连接通道绑定然后存入user_channel_group,然后再去查询用户的群聊列表,将用户的channel添加到对应的channelGroup里面,这样就能实现群内所有连接统一管理。
3. 讲一讲心跳机制,具体是怎么实现的吗?
服务端通过 Netty 设置 6 秒空闲检测,超时未收到客户端发送心跳,服务端这边会断开与客户端的连接,客户端断开后会尝试自动重连,若三次重连失败就不会重连了;
4. 如果用户连接断开了,你怎么来保证这个数据的一致性?
连接断开后,服务端会将所有待发送给该用户的消息,实时写入 MySQL 的 chat_message 表,同时记录消息发送时间和接收人 ID,确保数据不丢失;
用户重上线时,会查询用户的最后离线时间,然后通过‘接收人 ID + 消息发送时间 > 离线时间’的条件,从 MySQL 中筛选出断连期间的所有未接收消息;
最后将这些消息批量推送给用户,完成数据同步,保证用户上线后能完整获取断连期间的所有信息,确保数据一致性。”
5. 这个 ConcurrentHashMap 是怎么保证在多节点之间的同步的?
- 当需要跨节点推送消息时(比如用户连接在节点 A,消息却到达了节点 B),节点 B 会先查本地 ConcurrentHashMap,发现目标用户不在本地,就通过 Redisson 的发布订阅机制,将消息发布到 Redis Topic;
- 集群中所有节点都会监听这个 Topic,每个节点收到消息后,会查自己本地的 ConcurrentHashMap,判断目标用户是否连接在当前节点;
- 只有目标用户所在的节点,会通过本地 ConcurrentHashMap 找到对应的 Channel,完成消息推送。
6. 那你这个离线消息是怎么存储和同步的?
- 存储层面:用户连接断开后,所有待发送给该用户的消息(单聊、群聊)都会实时写入 MySQL 的 chat_message 表,同时记录消息发送时间、接收人 ID、消息唯一 ID 等关键信息,保证数据持久化不丢失;
- 同步层面:用户重新上线并完成连接绑定后,系统会获取该用户的最后离线时间,通过「接收人 ID = 当前用户 ID + 消息发送时间 > 离线时间」的条件,从 MySQL 中筛选出断连期间的所有离线消息,批量推送给客户端;
7. 如果说 Redis 突然宕机了,你这个消息怎么保证不丢失?
8. 你这个消息的幂等性是怎么保证的?
每个消息都有一个uuid,客户端收到消息之后并不是马上使用,会去客户端数据库查询是否存在同uuid的消息如果存在就直接不处理,不存才直接显示然后保存到本地数据库里面。
9. 你在适趣 AI 中文里面负责的功能模块,还有它的技术栈是什么?
一、负责的核心功能模块
- 文章播放优化模块:实现文章分句展示、拼音自动生成、多音字手动修改功能,解决用户阅读时的发音准确性和排版体验问题,提升内容可读性;
- 服务质量监控模块:开发 “社群老师与用户平均触达时间计算” 功能,实时统计老师对用户咨询的响应效率,为运营团队优化服务质量提供数据支撑;
- 辅助工具开发:通过
二、核心技术栈
- 后端:Java、Spring Boot、MyBatis-Plus(核心开发框架),MySQL(数据存储);
- 前端:Vue3、Element Plus(参与部分页面交互优化,适配功能需求);
- 其他:Redis(缓存高频查询数据,提升响应速度)、日常业务开发相关的工具类与 API 封装。
10. 那你具体说一下你是怎么实现的?
社群老师与用户平均触达时间计算
- 消息匹配与时间收集
- 用户和老师的消息都是存储在数据库里面,通过筛选拿到用户和老师消息列表的集合,并且这个列表是通过发送时间升序排序。
- 然后维护一个用户的消息列表,再通过遍历整个消息集合,遍历到用户的消息就直接添加到用户的消息列表里面,如果遍历到老师消息就直接判断用户消息列表是否存在数据,存在就遍历,计算用户消息与老师消息的间隔时间,存入的列表集合里面。
- 有效时间计算逻辑
- 消息间隔时间计算是封装了一个方法,通过传入两个消息的发送时间和运营老师的休息时间计算间隔时间的。
- 通过一个左指针指向用户消息的发送时间,右指针指向老师的发送时间,在
while(左指针小于右指针)里面计算时间间隔,如果左指针进入老师休息时间就直接累加然后移动指针到老师的上班时间,最后计算这个时间间隔。
11. 了解了,那你这个数据是实时计算的还是离线计算的?
只要数据库数据跟新就会计算跟新,应该属于实时计算。
12. 好的,那你这个算法的时间复杂度是多少?
从代码实现来看,这个算法的时间复杂度是 O(n),其中 n 是消息列表 weworkMsgs 的长度,具体分析如下:
- 外层循环:遍历所有消息(
weworkMsgs),每个消息只会被处理一次,这部分是 O(n)。 - 内层逻辑:
- 对于用户消息,仅执行简单的添加到列表操作(
pendingUserMsgs.add),是 O(1); - 对于老师消息,会遍历
pendingUserMsgs中的未处理用户消息,但每次遍历后会调用pendingUserMsgs.clear()清空列表,意味着每个用户消息只会被匹配一次(不会重复遍历),整体内层遍历的总次数依然是 O(n)(所有用户消息的总和不超过n)。
- 对于用户消息,仅执行简单的添加到列表操作(
- 时间计算方法(calculateReplyTimeSeconds):虽然用了
while循环处理跨天 / 跨时段的情况,但每次循环都会将current时间向前推进(至少到下一个时段临界点,如午休开始、下班时间等),不会出现无限循环。实际执行中,即使消息跨多天,循环次数也远小于n(与天数、时段数相关,属于常数级),因此这部分可视为 O(1)。
综上,整个算法的时间复杂度由消息总数决定,是线性级别的 O(n),在消息量较大的场景下也能保持高效运行,符合业务对实时性的要求。
13. 你在优化这个文章播放功能的时候,你遇到了哪些技术挑战,然后你是怎么解决的?
多音字的一个匹配
上传一段句子,前端通过pinyin第三方库给到一个json数据,里面包含这个句子的所有文字,文字下面右有一个多音数组,前端支持手动选择多音字的拼音然后保存到数据库里面
14. 你说一下 JVM 内存模型
- 主内存 共享线程
- 堆:new 出来的对象存入堆里
- 方法区:类的一些基本信息、常量、静态变量
- 工作内存 非共享线程
- 线程栈:执行一个方法时会开辟一个栈帧,该栈帧会存放方法调用所需要的局部变量、操作数栈、动态链接和方法出口。
- 本地方法栈:java 调用nativ方法时就直接通过切换本地方法栈去执行,不会用线程栈去执行。
- 程序计数器:记录程序执行的位置,行号。
15. 说下垃圾回收的基本流程吧。
垃圾回收的基本流程可简化为三个核心步骤:
- 标记垃圾:通过 “可达性分析”,从 GC Roots(如局部变量、静态变量等)出发,标记出能够被直接应用或间接引用的对象,剩下未被标记的就是 “垃圾”(不再被使用的对象)。
- 回收内存:常见方式有:直接删除垃圾(标记 - 清除)、复制非垃圾对象到新区域后清空原区域(复制算法)、移动活对象到内存一端再清除边界外垃圾(标记 - 整理)。
- 整理内存:回收后整理内存空间,减少碎片(比如让活对象连续排列),方便后续新对象高效分配内存。
16. OK,你说一下 MySQL 索引失效的常见原因有哪些?
- 函数计算索引字段
- 左模糊查询
- 联合索引不满足最左前缀原则
- 隐式类型装换
- in、is not 、!= 、> 等。。。(优化器会判断全表扫描和索引查询哪个跟优,可能导致索引失效)
- or 左右没有同时使用索引字段
17. 嗯,你再说一下 Redis 缓存击穿和雪崩的防护措施有哪些?
缓存击穿(热点key在同一时间失效了,导致请求打在MySQL上),可以通过不设置失效时间,记录过期时间字段,每次调用判断是否过期,如果过期就直接删除。
缓存雪崩(同一时间大量key失效,redis宕机,导致MySQL负载),可以对过期时间添加随机值,redis的集群部署,防止redis单节点宕机导致MySQL负载。
18. 好,我这里没有什么问题了,你对我们团队或者这份岗位还有什么想了解的吗?
19. 微服务治理
- Nacos:服务注册发现(让服务互相找到对方)+ 配置中心(统一管理配置,动态刷新)。
- OpenFeign:声明式远程调用(服务间 HTTP 调用像调本地方法),集成负载均衡。
- Gateway:微服务网关(统一入口,路由转发、过滤拦截、限流前置)。
- Sentinel:熔断降级 + 限流(保护服务,防雪崩、防过载)。
- Seata:分布式事务(保障跨服务数据一致性,解决微服务事务问题)。
