首先,关于简历中的问题主要集中在项目相关和技能相关,会涉及到简历中提到过的相关技术,相关技术实现了什么样的功能,怎样实现的
自我介绍
面试管你好!我叫张凯,毕业于青岛农业大学计算机科学与技术专业,目前在哈尔滨理工大学攻读软件工程硕士。过去几年我主要在Java开发领域积累了丰富的项目经验,例如在“健身交流网”项目中负责设计Redis集群、实现缓存优化与秒杀模块,在“即时聊天室”项目中搭建了基于WebSocket的聊天系统,同时熟练掌握了Spring Boot、SSM、Vue等主流技术。
健身交流网相关问题
首先,请你介绍一下“健身交流网”项目的整体架构和你在其中承担的具体职责。
项目介绍:健身交流网是一款面向健身爱好者的平台,主要功能包括:登录与认证、健身场馆、器材和教练的浏览,健身卡优惠秒杀、健身打卡和健身心得分享、点赞与关注并通过地理位置检索附近健身馆等功能。
主要工作:
- 设计并实现基于Redis集群的会话存储,解决多节点Session共享问题。
- 利用拦截器和JWT令牌统一鉴权,保证用户登录状态一致性。
- 采用热点数据缓存和随机过期时间策略,有效降低缓存击穿风险。
- 利用Lua脚本、Redis原子操作和互斥锁实现健身卡的秒杀功能,防止超卖现象,保证了数据一致性
- 部署Stream消息队列处理异步订单和活动通知,提升秒杀及健身打卡数据处理吞吐量。
- 使用HyperLogLog、GeoHash、Redis Set技术,实现UV统计、点赞排序和附近健身馆检索功能。
- 基于Kafka异步处理用户打卡、评论和关注等核心交互功能的推送和通知。
主要介绍一下健身交流网项目中个功能都用的哪些技术
1. 登录功能
-
账户密码登录:
-
用户输入账户、密码和验证码。
-
验证码通过通用验证码库生成后,存入 Redis 并设置合理的过期时间;用户提交时对比 Redis 中的验证码验证其正确性。
-
-
手机号登录:
-
用户输入手机号和短信验证码。
-
系统先生成短信验证码,并存入 Redis 设置过期时间;同时调用短信服务平台将验证码发送到用户手机上。
-
用户提交后,后端从 Redis 中取出验证码进行验证,确认无误后完成登录。
-
2. 点赞功能
-
点赞记录更新:
-
初步更新: 用户点击点赞后,利用 Redis 提供的原子命令立即更新点赞数,确保并发环境下操作原子性。
-
异步同步数据库: 为避免每次点赞直接更新数据库导致压力过大,采用消息队列将点赞操作记录异步收集,进行批量写入更新。
-
补偿机制: 设置补偿策略以应对消息任务执行失败的情况,确保数据最终一致性。
-
-
点赞排行榜重建:
-
当排行榜缓存失效时,当前请求检测到缓存缺失后,通过分布式锁(采用双重校验机制)保证仅有一个请求触发缓存重建。
-
将重建缓存的任务异步发送至消息队列,由后台任务完成数据库查询和缓存更新;若短时间内缓存未更新,可返回默认页面,避免对数据库造成过大压力。
-
3. 点赞排序功能
-
实现方式:
-
使用 Redis Sorted Set 存储点赞记录,将每个点赞用户的唯一标识作为 member,使用时间戳(或自定义的分数值)作为 score,保证点赞顺序(时间越早分数越低,越靠前显示)。
-
当需要展示点赞排行榜时,从 Sorted Set 中获取排序后的用户 ID 列表,再根据 ID 查询数据库获得用户详细信息。
-
4. 健身卡秒杀功能
-
全局订单 ID 生成:
-
利用 Redis 的自增数值(32 位)与当前时间戳(31 位)及符号位(1 位)组合,生成唯一订单 ID,保证全局唯一性。
-
-
一人一单限制:
-
使用 Redis 的 setnx 命令为每个用户在特定秒杀活动中设置互斥锁,确保同一用户只能下单一次;同时设置合适的过期时间以防锁死。
-
-
库存预检与扣减:
-
通过 Lua 脚本在 Redis 中实现原子性库存检查和扣减操作,确保在高并发情况下不会出现库存超卖。
-
-
异步下单:
-
将下单请求封装为消息,通过 Redis Stream 消息队列异步写入;后台消费者读取消息后完成订单持久化、库存最终确认和其他业务逻辑处理,支持批量操作和幂等性设计。
-
5. 关注功能
-
实现方式:
-
利用 Redis 的 Set 数据结构管理用户关注关系,实现关注与取消关注的快速响应操作。
-
注意:由于 Redis 持久化机制可能不如数据库稳定,建议定期同步 Redis 数据到数据库,或设置相应的容错措施,防止数据丢失。
-
6. 健身打卡功能
-
签到记录存储:
-
使用 Redis 的 Bitmap(基于 String 数据结构实现)记录用户的每日打卡情况。
-
每个用户打卡记录可用一组位表示,支持海量数据存储(最大 512MB,可表示 2^32 位)并快速查询。
-
7. 健身场馆、器材及教练浏览功能
-
后端数据存储:
-
利用 MySQL 存储健身场馆、器材、教练及用户评分等信息,通过 MyBatis 实现数据的查询和更新操作。
-
-
前端展示与交互:
-
前端使用 Vue 和 Element-UI 构建用户友好的页面,通过 RESTful API 获取数据,并支持 Ajax 异步提交评分和评论,确保交互流畅且响应迅速。
-
为什么需要基于Redis来做Session共享?你考虑过其他方案吗?
session共享是指多台tomcat服务器之间不共享session存储空间,导致不同tomcat服务时会有数据丢失的风险,所以要找到一种解决方案来解决数据共享、内存存储、以及存储数据的结构问题。
而redis就满足以上三个特点,集中存储,同时也满足高并发、高性能的业务要求,同时要注意几点————选择redis数据结构,存储token用string,存储用户用hash
Mysql或者nosql数据库都无法同时满足上述要求,
你实现的会话共享机制中,Redis是如何存储Session数据的?存储格式和数据结构是怎样的?
通常会采用 Key-Value 形式来存储 Session 数据:
-
Key: 一般为 Session ID,可以包含前缀(例如 "sess:")以便区分。也可以是业务的订单key,workout:user:1来区分
-
Value: 存储经过序列化后的用户信息或会话数据,常用 JSON 格式,也可以采用 Java 的序列化方式。
此外,对于需要存储多个字段的情况,也可以利用 Redis 的 Hash(无序) 结构,但多数场景下直接用 String 类型存储序列化后的对象更加简单高效。同时,针对会话有效期,会设置 TTL(过期时间),保证会话自动失效。
介绍一下分布式锁,锁的粒度怎么优化
提到使用热点数据缓存加随机过期时间来降低缓存击穿率,请详细讲讲这个策略的实现原理和优缺点。
-
当热点数据设置固定的过期时间时,一旦到期,大量请求会同时击穿缓存直接访问数据库,造成数据库瞬间压力增大,甚至雪崩。
-
通过在设置缓存时增加一个随机偏移值(例如,在原有 TTL 上加减一个随机时间),使得缓存的实际过期时间各不相同,分散缓存失效的瞬间压力,降低数据库的访问峰值。
你在秒杀模块中提到了使用Lua脚本结合Redis的原子操作以及乐观锁来实现秒杀预检。请详细说明这个方案的流程,以及如何保证在高并发下不会出现超卖问题?
-
秒杀请求进入: 当用户发起秒杀请求时,系统首先通过 Redis 接收到请求。
-
库存预检与扣减(Lua 脚本):
-
利用 Lua 脚本在 Redis 中执行原子操作:检查库存是否充足,如果充足,则立即扣减库存。
-
此步骤保证了在高并发下,库存检查和扣减操作不会被多个请求交叉执行,从而避免超卖。
-
-
一人一单控制:
-
同时使用 Redis 的 setnx 命令为每个用户设置分布式锁,确保同一用户在同一活动中只能下单一次。
-
-
异步下单处理:
-
扣减库存成功后,将下单请求封装为消息,写入消息队列(如 Redis Stream 或 Kafka),由后台消费者异步处理订单持久化、库存最终确认及其他业务逻辑。
-
-
数据库层面的乐观锁:
-
在订单写入数据库时,采用乐观锁机制(例如版本号机制)确保即使在并发写入时,数据也能保持一致性,防止数据冲突。
-
乐观、悲观锁
1、悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,在获取数据的时候先加锁,确保数据的安全性。
在Java中,常见的悲观锁实现是使用synchronized
关键字或ReentrantLock
类。这些锁能够确保同一时刻只有一个线程可以访问被锁定的代码块或资源,其他线程必须等待锁释放后才能继续执行。
锁实现:关键字synchronized、Lock接口的实现
使用场景:写操作比较多,先加锁可以保证写操作时数据正确
2、乐观锁
乐观锁认为自己在使用数据的时候不会被别的线程修改,所以不会添加锁,只是在更新的时候去判断之前有没有别的线程更改过这个数据
乐观锁的原理主要基于版本号或时间戳来实现。在每次更新数据时,先获取当前数据的版本号或时间戳,然后在更新时比对版本号或时间戳是否一致,若一致则更新成功,否则表示数据已被其他线程修改,更新失败。
锁实现:CAS算法,例如AtomicInteger类的原子自增底层是通过CAS实现的
使用场景:读多,不加锁的特点能够使读的性能大幅度提升
CAS算法
CAS(Compare-And-Swap,即“比较与交换”)是一种常见的无锁原子操作,用于在多线程环境下实现并发控制,确保数据安全更新。其核心思想是利用硬件级别的原子指令,在更新数据前先比较内存中的值是否符合预期,再决定是否进行替换操作。
CAS操作主要有三个参数:要更新的内存位置、期望的值和新值。CAS操作的执行过程如下:
首先,获取要更新的内存位置的值,记为var。
然后,将期望值expected与var进行比较,如果两者相等,则将内存位置的值var更新为新值new。
如果两者不相等,则说明有其他线程修改了内存位置的值var,此时CAS操作失败,需要重新尝试。
如何使用拦截器实现统一鉴权?
我在 Spring Boot 中使用 HandlerInterceptor
拦截所有请求,在 preHandle()
方法中:
- 获取请求头中的 Authorization 字段。
- 校验 JWT 的合法性(签名、过期时间等)。
- 解析出用户信息并存入
ThreadLocal
,供后续业务逻辑使用。 - 若校验失败,返回 401 未授权响应。
Lua 脚本实现库存检查与扣减原子操作的具体逻辑
在秒杀场景中,为保证高并发下库存检查和扣减操作以及一人一单资格的检查的原子性,常见做法是将整个逻辑放入 Redis 的 Lua 脚本中执行。Lua 脚本内部的所有命令会在 Redis 单线程环境下一次性执行,保证了整个检查和更新过程的原子性。也就是说,其他并发请求无法在中间插入执行,从而避免了多个线程同时读取库存并造成超卖的情况。
乐观锁与“看门狗机制”的实现及区别
-
乐观锁(利用 WATCH)
在传统的 Redis 乐观锁使用中,业务方先对关键数据进行 WATCH,随后启动事务(MULTI),在事务中执行库存扣减等操作,然后使用 EXEC 提交。如果在 WATCH 到 EXEC 期间,数据被其他客户端修改,则事务会失败,从而检测到并发冲突。此时可以捕获失败信号,进行重试或回滚。 -
看门狗机制,在订单后续处理等长流程操作中,为防止因业务处理时间过长导致锁提前释放,可以引入看门狗机制,自动发送延长锁有效期(例如通过 PEXPIRE 命令)的指令。
看门狗机制通常用于分布式锁场景(比如使用 Redisson 实现的锁),其核心思想是在获得锁后启动一个后台线程或定时任务,定期延长锁的过期时间,防止因长时间操作而导致锁意外过期,从而保证持锁业务能顺利完成。
消息队列中订单消息的完整生命周期及保障措施
订单消息生命周期
消息生产阶段:
- 当 Lua 脚本检测到库存充足且用户未抢购过后,系统会组装订单参数(如用户 ID、商品信息、扣减库存信息等)。
- 订单信息被封装成消息,并立即入队到消息队列(例如使用 Redis Stream 或 Kafka)。此阶段通常是同步的,将消息可靠地写入持久化存储中。
消息传递与消费阶段:
- 消息队列将订单消息存储并排队,等待消费者处理。
- 消费者订阅队列后,拉取消息进行订单处理(如生成订单、扣款、库存更新确认等)。
消息确认阶段:
- 消费者处理完订单后,会向消息队列发送确认(ack),告诉消息队列该消息处理成功。
- 消息队列收到 ack 后,会删除该消息,防止重复消费。
重试与补偿机制:
- 如果消费者在预定时间内未发送 ack,消息队列会将该消息重新投递给其他消费者(或同一消费者重试),确保消息最终被处理。
- 如果消息处理过程中出现异常或多次重试仍失败,可以将消息记录到补偿日志中,等待人工干预或自动补偿处理。
保障高并发下的消息一致性
- 幂等性:在订单消费过程中,业务逻辑应设计为幂等操作,即使同一消息被多次处理,也不会产生重复扣减或重复创建订单的情况。
- 持久化存储:消息队列(如 Kafka 或 Redis Stream)具备消息持久化特性,确保消息不会因服务器重启或网络故障而丢失。
- 事务性消费:一些消息队列支持事务机制,可以确保消息的消费和后续数据库更新在一个事务中完成,进一步保证数据一致性。
- 重试机制:通过设置消息确认超时,确保未被成功处理的消息能自动重试,降低因消费异常导致的消息丢失风险。
- 补偿机制:对于长时间未能处理成功的消息,系统可以设立补偿任务,重新入队或记录日志等待人工干预,确保业务数据完整性。
针对采用热点数据缓存和随机过期时间策略,缓存击穿率降低 98%提问,如何测试的缓存击穿率?
部署测试环境
- 准备一套与生产环境类似的测试系统,包含缓存(如 Redis)和后端数据库。
- 部署测试接口,这个接口在内部会先查询 Redis 缓存,再决定是否回源数据库。
配置场景
- 固定过期场景:热点数据缓存设置固定的过期时间,使得在同一时刻所有缓存项同时失效。
- 随机过期场景:对热点数据设置随机过期时间(例如在固定时间上增加 ±20% 的随机扰动),避免同时失效。
固定过期场景
- 将系统配置为固定缓存过期策略。
- 启动 JMeter 测试计划,模拟大量并发请求在缓存过期瞬间的压力。
- 记录各项指标:响应时间、错误率,同时通过后端监控(例如数据库查询日志、Redis 命中率统计)记录数据库回源查询次数。
优化测试:随机过期场景
步骤:
- 修改系统配置,将热点数据缓存设置为随机过期(例如:固定时间 ±20% 随机扰动)。
- 重新启动 JMeter 测试计划,模拟同样的并发请求压力。
- 再次记录所有指标,特别是数据库查询次数和响应性能。
预期结果:
-
由于缓存失效不再集中,数据库的回源查询次数应大幅下降(比如降低 98%),响应时间和错误率也会相对改善。
缓存击穿降低比例 = (固定过期时数据库查询次数 - 随机过期时数据库查询次数) / 固定过期时数据库查询次数 × 100%
-
在固定过期策略下,缓存同时失效引发大量回源查询,对数据库造成冲击;
-
在随机过期策略下,缓存失效时间错开,避免了瞬时回源高峰,从而显著降低了数据库压力和缓存击穿率。
即时聊天室相关问题
即时聊天室的相关实现
项目中如何使用 WebSocket 实现群聊和私聊功能?请详细描述消息的分发流程。
在项目中,我们通过 Spring Boot 内置的 WebSocket 支持实现实时聊天。具体流程如下:
- 客户端通过 WebSocket 连接到服务器,并进行身份认证(例如使用 JWT 令牌)。
- 服务端通过配置相应的 WebSocket 端点和消息处理器,根据消息中的标识(如群聊标识或私聊标识)判断消息类型。
- 对于群聊消息,服务器将消息广播给聊天室内所有在线的用户;对于私聊消息,则查找目标用户对应的 WebSocket 连接并推送消息。
- 消息经过分发后,服务端同时将消息持久化到数据库,供历史记录查询及未读消息提醒使用。
- 通过这种方式,能确保实时性和数据持久化之间的平衡。
WebSocket与HTTP协议有什么不同吗?
websoket是全双工通信协议,允许客户端和服务器端互相独立的发送消息,实施的传输数据,建立连接后,可长久开启。适应于在线游戏,即时通讯场景
Http是半双工通信协议,请求-响应模式,每次请求都要建立连接,处理后关闭连接,是英语传统网页,文件下载等场景
请解释一下 JWT 的基本结构及作用?
JWT(JSON Web Token)由三部分组成:Header
.Payload
.Signature
- Header:声明类型(typ: JWT)和签名算法(如 HS256)。
- Payload:携带用户信息(如 userId、role),还有如
exp
(过期时间)等标准字段。 - Signature:将前两部分用秘钥和算法进行签名,防止被篡改。
作用是用于在客户端和服务端之间传递经过加密签名的身份信息,实现无状态认证。
JWT的使用场景
- 身份验证(Authentication):JWT 可以被用作用户登录的身份验证凭证。当用户成功登录后,服务端可以生成一个包含用户信息的 JWT,并将其返回给客户端。以后,客户端在每次请求时都会携带这个 JWT,服务端通过验证 JWT 的签名来确认用户的身份。
- 授权(Authorization):在用户登录后,服务端可以生成包含用户角色、权限等信息的 JWT,并在用户每次请求时进行验证。通过解析 JWT 中的声明信息,服务端可以判断用户是否有权限执行特定的操作或访问特定的资源。
- 信息交换(Information Exchange):由于 JWT 的声明信息可以被加密,因此可以安全地在用户和服务器之间传递信息。这在分布式系统中非常有用,因为可以确保信息在各个环节中的安全传递。
- 单点登录(Single Sign-On):JWT 可以被用于支持单点登录,使得用户在多个应用之间只需要登录一次即可使用多个应用,从而提高用户体验。
在安全性方面,你是如何利用 JWT 和 Filter 实现用户鉴权的?
-
用户登录与 JWT 生成:
当用户提交用户名和密码后,后端服务验证凭证。如果验证成功,服务会生成一个 JWT。这个 JWT 内部包含了用户信息、权限、过期时间等声明(Claims),并使用服务器的密钥进行签名。生成的 JWT 是一个自包含的令牌,客户端无需保存服务器端状态即可进行后续请求的认证。 -
客户端存储与传输 JWT:
生成的 JWT 通常会返回给客户端,客户端一般将其存储在 LocalStorage、SessionStorage 或 Cookie 中。每次后续请求时,客户端会在 HTTP 请求头中附上这个令牌(例如在Authorization
头中使用Bearer <token>
格式)。 -
使用 Filter 拦截请求:
在服务端,可以配置一个过滤器(Filter),例如在 Spring Security 中常用的OncePerRequestFilter
。这个过滤器会拦截每一个进入的 HTTP 请求,并从请求头中提取 JWT。 -
JWT 验证与用户信息注入:
过滤器提取出 JWT 后,会进行以下验证:-
完整性验证: 检查 JWT 的签名是否正确,防止被篡改。
-
过期时间校验: 确认令牌未过期。
-
内容解析: 如果令牌有效,则解析出其中的用户信息和权限。
验证通过后,过滤器会将解析出来的用户信息放入安全上下文(例如 Spring Security 的SecurityContextHolder
中),这样后续的业务逻辑就能通过该上下文获取当前用户的认证信息。
-
-
继续处理请求或返回错误:
如果 JWT 验证成功,过滤器会让请求继续传递到后续的处理链(如 Controller),并允许访问受保护的资源。若验证失败,则过滤器会终止请求,并返回相应的未授权(如 HTTP 401)的错误信息。
项目中是如何整合阿里云 OSS 的?它在文件和表情包上传下载中起到什么作用?
OSS 为 Object Storage Service,即对象存储服务。
OSS 具有与平台无关的 RESTful API 接口,可以在任意应用、任意时间、任意地点 存储与访问 任何类型的数据。简单地理解:OSS 基于网络提供数据存储服务,通过网络可以随时存储、获取 文本、图片、音频、视频等 非结构化数据。比如网站的 图片、视频等文件就可以存放在 OSS 中(海量数据,自己维护起来麻烦,交给其他人去维护),每次从 OSS 中获取即可。
实现过程:1.获取自己的OSSbucket,记录bucket所在区域的Endpoint、AccessKeyId 以及 AccessKeySecret。
2.编写配置类,属性类实现文件上传和下载的接口即可
-
上传:
利用 OSS SDK 的putObject
(或分片上传接口)将本地文件(包括表情包)上传到指定的 Bucket 中。 -
下载:
可以通过 OSS SDK 的getObject
获取文件流进行下载,也可以生成预签名 URL 供客户端直接访问。
如何保证即时聊天室中历史消息的高效存储与检索?请谈谈 MyBatis 在这方面的应用。
1.数据库设计与分区策略
-
分库分表/分区:
聊天记录往往数据量庞大,通过按聊天室 ID、时间戳等维度进行分表或分区可以显著减少单表数据量,提升查询效率。 -
索引设计:
针对常用的查询条件(如聊天室 ID、时间戳)建立合适的索引,以便快速定位数据。
2.在.xml文件中加入分页查询、条件查询,避免一次性加载大量的数据
如何用HYperloglog实现的uv统计?又怎么进行测试实现的误差<0.8%和内存占用减少85%?
HyperLogLog(HLL)是一种用于估计大规模数据集中不同元素数量(如UV统计)的概率算法,它能够在极低的内存占用下提供相对准确的估计。HLL是基于String结构实现的,单个HLL的内存永远小于16kb,内存占用及其小,但由于它的测量结果是由概率性的,小于0.81%的误差,但完全可以忽略不计,同时HLL里面的元素是不可重复的,即使相同的用户连续访问,也只记录一次该用户
如何处理用户在多端登录的情况?(synchronized)
悲观锁,控制层登录模块的代码封装在synchronized隐式锁,同步声明了只能实现在单客户端的登录,同时,保证了自动获取或者释放锁,当一个客户端登录成功后,就会自动获取锁,而其他的登录用户就只能等待释放锁,这样就锁定了用户的登录状态
使用synchronized悲观锁的优缺点:
优:在单机环境下可以保证线程安全
缺:
- synchronized 仅适用于单个 JVM 内,不适用于跨节点的并发控制,因此不能保证全局唯一性。
- 当大量并发请求涌入时,锁的争用可能导致响应时间变长,严重时会成为系统瓶颈。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
分布式环境中如何保证全局范围内同一用户只能登录一个客户端
在分布式系统中,各个服务节点之间没有共享内存,因此单机的 synchronized 锁无法实现全局同步。常见的解决方案包括:
-
分布式锁
利用 Redis 分布式锁是常见方案之一。使用 Redis 的 SET 命令,并结合 NX(仅当键不存在时设置)和 EX/PX(设置过期时间)参数。 -
单点登录(SSO)和会话管理
除了分布式锁外,还可以通过集中式认证服务器来管理用户会话。当新客户端登录时,可以向认证中心发送通知,强制将旧会话下线,实现全局唯一登录。
WebSocket 连接管理与消息路由的设计细节
连接管理
a. 数据结构与存储:
-
在服务器端,我们通常使用线程安全的数据结构(例如 Java 的 ConcurrentHashMap)来存储连接映射。
-
结构示例:
-
Key:用户的唯一标识(如 userId)
-
Value:对应的 WebSocket 连接对象(例如 WebSocketSession 或 Netty Channel 对象)
-
b. 连接建立与注册:
-
当客户端与服务器建立 WebSocket 连接时(例如在 onOpen 或连接建立的回调方法中),服务端会解析请求中的用户信息,并将用户 ID 和对应的连接对象存入映射。
-
这样可以保证后续在消息路由时能够快速查找到指定用户的连接。
c. 心跳检测与连接维护:
-
心跳包机制:为了确保连接处于活动状态,服务器定期向所有连接发送 ping 消息,要求客户端返回 pong 消息。
-
断线处理:如果在预定时间内没有收到客户端的 pong 响应,服务器就会认为该连接已失效,随后执行断线处理逻辑(如调用 onClose 方法),从映射中移除对应的用户连接,释放资源。
-
这种机制不仅能及时清理断开的连接,也能防止消息推送到已失效的客户端。
d. 分布式部署注意事项:
-
在多节点部署的情况下,每个节点维护自己的连接映射。
-
如果需要跨节点消息路由,可以借助共享缓存(如 Redis)或消息队列来实现节点间的互通,从而实现全局的消息分发。
2. 消息路由
a. 私聊消息路由:
-
直连推送:当用户 A 发送私聊消息给用户 B 时,服务器直接从连接映射中查找用户 B 的连接对象。
-
若查找到有效连接,则直接通过该连接将消息推送给用户 B。
b. 群聊消息路由:
-
群组管理:服务端为每个群组维护一个群聊成员列表(例如存储群组 ID 与一组用户 ID 的映射)。
-
当用户在群聊中发送消息时,服务器会根据群组信息获取所有在线用户的连接。
-
采用遍历或并发方式,将消息逐个推送到所有在线成员的 WebSocket 连接上。
-
若某些用户不在线,可记录未读消息,待用户上线后通过离线消息机制补偿推送。
c. 消息发送的可靠性与异常处理:
-
消息确认:部分系统会设计 ACK(确认)机制,客户端在收到消息后发送确认,服务端据此记录消息是否成功送达。
-
重试机制:如果推送失败或未收到 ACK,系统可设计重试机制或将消息存储在离线队列中,确保消息不丢失。
-
错误处理:对于无效或错误的消息格式,服务器需要进行日志记录,并返回错误提示给发送端。
d. 性能与扩展:
-
高并发环境下:可以将消息分发与业务逻辑分离,利用异步处理或引入消息队列来降低实时消息推送对系统性能的压力。
-
负载均衡:通过前端负载均衡器(如 Nginx)将 WebSocket 连接均匀分配到各个服务器节点,避免单节点过载。
-
集群方案:在集群环境中,不同节点间通过共享消息队列或订阅发布系统(如 Redis Pub/Sub)同步消息,从而实现跨节点消息的精准路由。
高并发环境下的实时传输与系统稳定性
- 负载均衡:利用负载均衡器(如 Nginx 或 LVS)将 WebSocket 连接均衡分布到多个服务器节点,降低单点压力。
- 消息队列:在消息入口处引入消息队列,缓解瞬时高并发的压力,异步处理消息分发。
- 分库分表:对历史消息存储采取分库分表策略,例如根据消息发送时间或用户 ID 分散存储,减少单个数据库压力。
消息持久化策略与未读消息提醒机制
消息持久化:
- 实时消息在经过 WebSocket 传输的同时,所有消息都持久化到数据库中。
- 部分设计中,为了提高容灾能力,将消息文件化存储到阿里云 OSS。此举可防止系统异常或重启时消息数据丢失。
- 消息队列的使用也为消息提供了额外的持久化层,确保消息最终被处理。
未读消息提醒:
- 服务器在用户离线后仍然记录未读消息,通常通过数据库或 Redis 维护未读消息计数。
- 客户端上线时,向服务端请求未读消息列表或计数,并通过 WebSocket 推送实时更新。
- 对于多端登录场景,需要在服务端统一管理未读状态,可采用共享缓存和事件通知机制实现各端状态同步。