【面经分享】互联网大厂Java面试:音视频场景下的技术深挖
面试官:您好,请简单介绍一下您自己。
谢飞机:我叫谢飞机,是XX大学计算机专业毕业的,有5年的Java开发经验,主要做后端服务。之前参与过直播平台的后端开发,负责过用户鉴权、房间管理、消息推送等功能模块。我对Spring生态和MySQL比较熟悉。
面试官:很好,那我们来聊聊您在直播平台项目中的核心业务。
谢飞机:直播平台的核心业务主要包括以下几个方面:
- 用户系统:包括用户注册、登录、认证、权限管理。
- 直播房间:创建房间、加入房间、房间信息管理。
- 消息推送:聊天消息、礼物打赏、系统通知等。
- 音视频处理:这部分主要是与第三方服务商对接,比如推流、拉流、转码等。
- 数据统计与分析:用户行为分析、直播数据监控等。
面试官:在用户系统中,你们是如何实现用户登录和认证的?
谢飞机:我们使用的是Spring Security + JWT的方式来实现用户登录和认证。用户输入用户名和密码后,会调用一个登录接口。这个接口会验证用户的凭证,如果验证成功,就会生成一个JWT token返回给客户端。之后,客户端在每个请求的Header中携带这个token,服务端通过解析token来确认用户的身份和权限。
面试官:JWT token有什么优缺点?在分布式环境下,如何处理用户登出?
谢飞机:JWT token的优点是它自包含,不需要服务器端存储会话信息,非常适合分布式系统。缺点是token一旦签发,就无法在有效期内被服务器端撤销(除非引入黑名单机制)。
对于用户登出,我们的做法是在Redis中存储一个黑名单列表,将需要登出的JWT token加入这个列表。每次请求时,先检查token是否在黑名单中。
面试官:在直播场景中,聊天消息的推送量非常大,你是如何设计消息推送服务的?
谢飞机:我们采用了WebSocket来实现实时双向通信。客户端连接成功后,我们会将它的session ID和用户ID关联起来。当需要向某个用户或某群用户推送消息时,我们就根据这些关联关系,将消息通过WebSocket发送给对应的客户端。为了应对高并发,我们使用Netty作为WebSocket的底层框架,因为它性能更好。
面试官:Netty相比传统的Tomcat有什么优势?在Netty中,你是如何管理线程模型的?
谢飞机:Netty是一个异步事件驱动的网络应用框架,它比Tomcat更轻量级,资源消耗更少,吞吐量更高。在Netty中,我们通常采用主从多Reactor线程模型。一个主Reactor线程负责监听和处理客户端的连接请求,然后将已建立的连接注册到一个或多个从Reactor线程上,由从Reactor线程来处理连接的读写事件。这样可以充分利用多核CPU的性能。
面试官:聊完前端和通信,我们来谈谈后端的数据存储。你们的用户数据和房间数据是如何存储的?
谢飞机:用户数据和房间数据我们都是用MySQL来存储的。用户表包含了用户的基本信息,如用户名、密码哈希值、注册时间等。房间表则包含了房间号、主播ID、房间标题、创建时间等信息。
面试官:面对海量的直播间数据,你们是如何进行分库分表的?
谢飞机:我们采用的是按用户ID进行水平分片。我们将用户表按照ID范围或者哈希值分成多个库,每个库里又按照房间ID进行分表。这样可以将读写压力分散到不同的数据库实例上。
面试官:在MySQL中,你们是如何保证数据一致性的?有没有遇到过事务相关问题?
谢飞机:对于强一致性要求高的操作,我们会使用MySQL的事务机制。比如,用户购买了一个虚拟礼物,我们需要同时更新用户的余额和礼物的数量。这两个操作必须在一个事务里完成,以保证要么都成功,要么都失败。
我们确实遇到过一些事务问题,比如死锁。这通常发生在两个或多个事务相互等待对方释放锁的时候。我们通过优化SQL语句、减少事务的粒度、设置合理的超时时间等方法来解决。
面试官:在直播间场景中,用户的点赞、评论等高频操作,你是如何优化的?
谢飞机:对于这种高频且对一致性要求相对较低的操作,我们通常会引入缓存。我们使用了Redis来缓存用户的点赞数、评论列表等数据。当用户点赞时,我们先更新Redis中的计数,然后再异步地将数据同步到MySQL数据库中。这样可以极大地减轻数据库的压力,提高系统的响应速度。
面试官:在异步处理消息时,你们是如何保证消息不丢失的?
谢飞机:为了保证消息不丢失,我们引入了消息队列。当用户发送一条消息时,我们的服务不会直接去写数据库,而是先将消息投递到RabbitMQ队列中。然后有一个消费者服务会从队列中读取消息,并持久化到数据库。我们还会在消费者端开启事务,并且使用RabbitMQ的确认机制,确保消息被成功消费后才会从队列中移除。
面试官:如果RabbitMQ宕机了怎么办?你们是如何保证消息队列的高可用的?
谢飞机:我们部署了RabbitMQ集群,并且配置了镜像队列。当一个节点宕机时,其他节点可以继续提供服务,保证消息队列的可用性。此外,我们还会定期将消息队列中的数据备份到磁盘,防止数据丢失。
面试官:除了点赞、评论,直播间还有其他需要异步处理的业务吗?
谢飞机:有的。比如,用户观看直播的时长统计、用户行为的埋点数据等。这些数据量巨大,而且对实时性要求不高,所以我们都会通过消息队列进行异步处理。
面试官:在微服务架构下,你们是如何划分服务的?
谢飞机:我们将整个系统拆分成几个核心的微服务,比如用户服务、房间服务、消息服务、统计服务等。每个服务都有自己的数据库,通过RESTful API或者RPC(如Dubbo)进行通信。这样做的好处是服务之间解耦,可以独立部署和扩展。
面试官:在微服务间调用时,你们是如何处理服务降级的?
谢飞机:我们使用了Hystrix来实现服务降级。当某个依赖服务出现故障或者响应时间过长时,Hystrix会执行我们预先定义的回退逻辑,比如返回默认值、抛出异常或者调用本地缓存的数据,从而避免整个系统的雪崩效应。
面试官:在微服务架构中,服务发现是如何实现的?
谢飞机:我们使用了Consul来做服务发现。每个微服务启动时,都会将自己的IP地址和端口号注册到Consul中。当其他服务需要调用它时,就可以通过Consul查询到可用的服务实例列表,并进行负载均衡调用。
面试官:在微服务中,你们是如何统一日志收集的?
谢飞机:我们采用了ELK Stack(Elasticsearch, Logstash, Kibana)来进行日志收集和分析。每个微服务都会将日志输出到指定的文件,Logstash会从这些文件中读取日志,并将其发送到Elasticsearch中进行存储和索引。最后,我们通过Kibana来可视化地查看和分析日志。
面试官:在微服务中,你们是如何进行链路追踪的?
谢飞机:我们使用了Jaeger来实现链路追踪。通过在代码中注入Tracer,我们可以追踪一个请求在多个微服务间的流转过程,从而快速定位性能瓶颈和错误发生的环节。
面试官:在微服务中,你们是如何管理配置的?
谢飞机:我们使用了Spring Cloud Config。它是一个集中式的外部配置管理系统,可以将配置文件存储在Git仓库中。当微服务的配置发生变更时,只需要更新Git仓库中的配置文件,服务就会自动刷新配置。
面试官:在微服务中,你们是如何实现API网关的?
谢飞机:我们使用了Spring Cloud Gateway。它作为系统的入口,负责路由转发、身份验证、限流熔断等功能。所有外部请求都必须先经过API网关,然后才能到达具体的微服务。
面试官:在微服务中,你们是如何进行容器化的?
谢飞机:我们使用Docker来打包和部署微服务。每个微服务都会被打包成一个独立的Docker镜像。然后,我们通过Kubernetes来编排和管理这些容器,实现自动化部署、扩缩容、故障恢复等功能。
面试官:在微服务中,你们是如何实现持续集成和持续部署(CI/CD)的?
谢飞机:我们使用了Jenkins来实现CI/CD。当代码提交到Git仓库后,Jenkins会自动触发构建流程,包括编译、测试、打包等步骤。如果一切顺利,Jenkins就会将新的镜像部署到Kubernetes集群中。
面试官:好的,今天的面试就到这里。感谢您的参与,我们会尽快给您答复。
面试问题详解
1. 在用户系统中,你们是如何实现用户登录和认证的?
业务场景:用户需要通过手机号和密码进行登录,系统需要验证其身份。 技术点:Spring Security, JWT (Json Web Token) 实现原理:
- Spring Security:一个强大的安全框架,提供了认证和授权的功能。在我们的项目中,我们用它来拦截所有的登录请求,对用户提交的凭据进行校验。
- JWT (Json Web Token):一种开放标准(RFC 7519),用于在各应用组件之间安全地传输信息。它是一个紧凑的、URL安全的JSON对象。JWT包含三部分:Header(头部)、Payload(负载)、Signature(签名)。Header指定了算法,Payload包含了用户的身份信息,Signature则是用Header和Payload以及一个密钥计算出来的签名,用于验证JWT的完整性和来源。
工作流程:
- 用户在前端页面输入用户名和密码,点击登录。
- 前端将用户名和密码通过HTTPS POST请求发送到后端的认证接口(例如
/api/auth/login)。 - 后端接收到请求后,Spring Security会拦截该请求。
- Spring Security会调用UserDetailsService来加载用户特定的数据。
- 我们将用户信息(如密码哈希值)从MySQL数据库中查询出来,并与前端传来的密码进行比对。
- 如果比对成功,Spring Security会生成一个JWT token。这个token包含了用户的ID、用户名、角色等信息,并且使用一个只有服务器知道的密钥进行了签名。
- 服务器将这个JWT token以JSON格式(如
{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."})返回给前端。 - 前端收到token后,会将其存储在浏览器的localStorage或sessionStorage中。
- 之后,每当用户需要访问一个受保护的资源(例如查看个人信息),前端都会在请求的HTTP Header中添加一个
Authorization: Bearer <JWT_TOKEN>字段。 - 后端的Spring Security过滤器会拦截这个请求,并从Header中提取出JWT token。
- 过滤器会验证token的签名是否正确,以及token是否过期。
- 如果验证通过,Spring Security会将当前请求与认证过的用户关联起来,允许其访问资源。如果验证失败,则会拒绝访问,并返回401 Unauthorized错误。
2. JWT token有什么优缺点?在分布式环境下,如何处理用户登出?
业务场景:用户登录后可以访问多个服务,需要安全地退出登录。 技术点:JWT, Redis 优点:
- 无状态:JWT token自身包含了所有必要的信息,服务器无需在内存或数据库中存储会话信息,这使得它在分布式系统中非常易于扩展。
- 跨域支持:由于token是自包含的,它可以轻松地在不同的域之间传递,非常适合单点登录(SSO)场景。
- 减少数据库查询:每次请求只需要验证token的有效性,避免了频繁查询数据库来验证用户身份。
缺点:
- 无法主动失效:JWT token一旦签发,在有效期内是无法被服务器端主动撤销的。这是它最大的缺点。
- 信息暴露:JWT的Payload部分虽然不能篡改,但可以被任何人解码和查看。所以,不要在Payload中存储敏感信息。
- 大小问题:JWT token会比传统的会话ID大,如果放在cookie中,可能会影响性能。
分布式环境下的登出方案: 为了解决JWT无法主动失效的问题,我们需要一个额外的机制来实现登出功能。
-
黑名单机制:
- 当用户点击“登出”按钮时,前端会向后端发送一个登出请求。
- 后端接收到请求后,不是立即删除token,而是将这个token的唯一标识(通常是JWT的jti claim)和一个过期时间(例如,当前时间 + JWT的有效期)作为一个键值对存入Redis中。
- Redis中的这个记录就是所谓的“黑名单”。
- 每次请求到达时,Spring Security的过滤器除了验证token的签名和有效期外,还会检查这个token的jti是否存在于Redis的黑名单中。
- 如果存在,说明该token已经被登出,过滤器会拒绝该请求,并返回401错误。
- 这种方式虽然解决了登出问题,但Redis中会存储大量的黑名单记录,需要设置合适的过期时间,并在用户量极大时考虑性能问题。
-
缩短Token有效期:
- 另一种更简单的策略是使用较短的JWT有效期(例如,15分钟)。
- 当用户登出时,我们只需要在前端删除存储的token即可。
- 用户下次访问时,因为token已经过期,需要重新登录。
- 这种方式虽然牺牲了一些用户体验,但实现简单,且没有额外开销。
-
Refresh Token机制:
- 这是生产环境中最常见的方案。
- 首次登录时,服务器会下发两个token:一个短期有效的Access Token (例如5-15分钟) 和一个长期有效的Refresh Token。
- Access Token用于访问受保护的资源,Refresh Token用于获取新的Access Token。
- Refresh Token会以HttpOnly Cookie的形式存储在浏览器中,或者由客户端安全地保存。
- 当Access Token过期时,前端可以使用Refresh Token向后端申请一个新的Access Token。
- 当用户登出时,我们只需要删除存储在Redis中的Refresh Token即可,Access Token自然会因为过期而失效。
- 这种方式兼顾了安全性和用户体验。
3. 在直播场景中,聊天消息的推送量非常大,你是如何设计消息推送服务的?
业务场景:数百万观众同时在线,每秒有数十万条聊天消息需要实时推送给所有在线用户。 技术点:WebSocket, Netty 设计思路:
- 选择协议:传统的HTTP协议是基于请求-响应模式的,服务器无法主动向客户端推送数据。而WebSocket协议提供了一个全双工通信通道,允许客户端和服务端在任何时候互相发送数据,非常适合实时通信场景。
- 选择框架:在Java生态中,Spring Boot内置了对WebSocket的支持,但对于高并发的直播平台来说,性能可能不够。因此,我们选择了Netty。Netty是一个异步事件驱动的网络应用框架,它比传统的Servlet容器(如Tomcat)更轻量级,资源消耗更少,能够处理更高的并发连接数和更大的数据吞吐量。
具体实现:
- 建立连接:当用户进入直播间时,前端会通过WebSocket协议与后端的Netty服务器建立连接。
- 用户绑定:Netty服务器会为每个连接分配一个ChannelHandlerContext。我们创建一个自定义的WebSocketHandler,在这个处理器中,我们会将用户的唯一ID(例如用户ID或Session ID)与该连接的上下文对象进行绑定,并存储在一个ConcurrentHashMap中。这样,我们就可以根据用户ID找到对应的消息通道。
- 接收消息:用户在聊天框输入内容并发送,消息会经过Netty框架的处理,最终到达我们自定义的WebSocketHandler。在这里,我们解析消息,提取出发送者信息和聊天内容。
- 广播消息:我们需要将这条聊天消息发送给直播间内的所有用户。我们遍历所有在线用户的连接上下文,并通过
ctx.writeAndFlush()方法将消息发送出去。 - 处理异常:Netty框架本身提供了很好的异常处理能力。如果某个客户端连接断开,Netty会触发相应的
channelInactive事件,我们可以在此事件中从ConcurrentHashMap中移除该用户的绑定关系,避免内存泄漏。
性能优化:
- 心跳机制:为了防止网络中间设备(如防火墙)因长时间无数据传输而断开连接,我们需要实现心跳机制。客户端和服务器端会周期性地互相发送一个小的数据包(ping/pong)来确认连接仍然活跃。
- 二进制传输:对于文本消息,我们使用UTF-8编码的字符串。但对于图片、音频等二进制数据,我们应该直接使用字节数组进行传输,以提高效率。
- 连接池:Netty本身并不提供连接池,但我们可以通过合理配置EventLoopGroup的线程数,以及使用Netty提供的工具类来优化连接管理。
4. Netty相比传统的Tomcat有什么优势?在Netty中,你是如何管理线程模型的?
技术点:Netty, Tomcat, Reactor模式 Netty vs Tomcat:
- Tomcat:Tomcat是一个基于Java Servlet规范的Web应用服务器。它本质上是一个多线程的阻塞I/O服务器。当客户端发起一个连接请求时,Tomcat会为其分配一个独立的线程来处理。如果这个线程正在执行一个耗时的操作(比如读取一个大文件),那么这个线程就会被阻塞,直到操作完成。这意味着Tomcat需要为每一个并发连接都分配一个昂贵的线程,当并发量达到数万甚至十万级别时,线程的创建、切换和销毁会带来巨大的性能开销,容易导致系统崩溃。
- Netty:Netty是基于Java NIO(非阻塞I/O)的异步事件驱动框架。它使用少量线程就能处理成千上万的并发连接。Netty的核心思想是Reactor模式。它通过一个或少数几个线程来处理所有连接的I/O事件(如读、写、连接、关闭),当某个连接上有事件就绪时,Reactor线程会将这个事件分发给对应的Handler去处理。Handler处理的是实际的业务逻辑,这是一个相对轻量的操作,通常不会阻塞。因此,Netty可以在一个线程池中处理成千上万的并发连接,极大地降低了资源消耗。
Netty的线程模型: Netty的线程模型非常灵活,可以根据不同的应用场景进行调整。最常见的就是主从Reactor多线程模型:
- Boss Group (主线程组):这个线程组只有一个或多个线程。它的职责非常简单,就是负责监听服务器的TCP端口,接受客户端的连接请求。当监听到新的连接时,它会为这个连接创建一个新的NioSocketChannel,并将这个Channel注册到Selector上,同时将这个Selector交给Worker Group来处理。
- Worker Group (从线程组):这个线程组包含多个线程。Boss Group会将注册好的NioSocketChannel交给Worker Group。Worker Group中的每个线程都可以负责处理多个Channel上的I/O事件(读、写、连接、关闭等)。当某个Channel上有数据可读时,Worker线程会将其读取到ByteBuffer中,然后通过Pipeline将数据分发给下一个Handler进行处理。
为什么叫Reactor模式?
- Reactor:在Netty中,
EventLoop扮演的就是Reactor的角色。它负责监听I/O事件,并根据事件类型分发给相应的Handler。 - Proactor:与之相对的是Proactor模式,它是基于异步I/O(AIO)的。在Proactor中,I/O操作本身是异步的,操作系统在完成I/O后会直接通知应用程序,应用程序只需要处理数据,而不需要关心I/O的细节。Netty目前主要基于NIO,属于Reactor模式。
5. 聊完前端和通信,我们来谈谈后端的数据存储。你们的用户数据和房间数据是如何存储的?
业务场景:需要持久化存储用户的基本信息和直播房间的配置信息。 技术点:MySQL, SQL 存储方案:
- 用户表 (user):
CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希值', mobile VARCHAR(20) UNIQUE COMMENT '手机号', email VARCHAR(100) UNIQUE COMMENT '邮箱', avatar_url VARCHAR(255) DEFAULT '' COMMENT '头像URL', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - 房间表 (room):
CREATE TABLE room ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '房间ID', room_number VARCHAR(50) NOT NULL UNIQUE COMMENT '房间号', host_id BIGINT NOT NULL COMMENT '主播ID', title VARCHAR(100) NOT NULL COMMENT '房间标题', description TEXT COMMENT '房间描述', status TINYINT DEFAULT 0 COMMENT '房间状态 (0: 未开播, 1: 直播中, 2: 已结束)', viewer_count INT DEFAULT 0 COMMENT '当前观看人数', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', FOREIGN KEY (host_id) REFERENCES user(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计要点:
- 自增主键:
id字段使用BIGINT AUTO_INCREMENT,保证了全局唯一性。 - 唯一约束:
username,mobile,email,room_number等字段都加了UNIQUE约束,防止重复数据。 - 外键约束:
room.host_id引用了user.id,保证了数据的一致性,如果某个用户被删除,其创建的房间也会被级联删除。 - 索引:除了主键外,我们还会为
username,mobile,room_number等经常用于查询的字段创建普通索引,以提高查询性能。 - 字符集:使用
utf8mb4字符集,可以完整地存储Emoji表情等特殊字符。
6. 面对海量的直播间数据,你们是如何进行分库分表的?
业务场景:随着用户量激增,单台MySQL服务器的读写能力达到瓶颈。 技术点:ShardingSphere, MySQL Proxy 分库分表策略:
- 垂直切分 (Vertical Sharding):将一个表按列拆分成多个表,或者将表拆分到不同的数据库中。例如,将用户基本信息和用户详细信息分开,或者将日志表和数据表分开。在我们的场景中,我们没有采用这种方案,因为用户表和房间表的关系非常紧密。
- 水平切分 (Horizontal Sharding):这是我们采用的主要方式。它将一个表按照某种规则(如主键的哈希值或范围)拆分成多个子表,并将这些子表分布到不同的数据库实例上。
具体实施:
- 选择分片键:分片键的选择至关重要。我们选择
user_id作为用户表的分片键,选择room_id作为房间表的分片键。这两个字段都具有很高的基数(即取值很多),且分布均匀,非常适合做分片。 - 分片规则:我们采用哈希取模的方式。假设我们将数据平均分配到8个数据库实例上。
db_index = user_id % 8- 这样,ID为100的用户会被分到第0个数据库,ID为101的用户会被分到第1个数据库,以此类推。
- 分片实现:我们使用了开源的分布式数据库中间件 ShardingSphere。ShardingSphere提供了JDBC和Proxy两种模式。我们选择了JDBC模式,因为它对现有应用代码的侵入性更小。
- 在
application.yml中配置分片规则和数据库连接。 - 在MyBatis的Mapper XML文件中,我们只需要写普通的SQL,ShardingSphere会在运行时将SQL解析并路由到正确的数据库和表中。
- 在
- 路由查询:当应用层查询一个用户信息时,ShardingSphere会根据SQL中WHERE条件里的
user_id,计算出它应该去哪个数据库查询。例如,查询user_id=100,它会知道要去db0中查询。 - 分布式事务:分库分表后,传统的本地事务无法跨库生效。我们采用了最终一致性的方案,结合消息队列来实现分布式事务。例如,在一个事务中更新了用户表和积分表,如果其中一个失败了,我们会通过消息队列通知补偿程序,回滚另一个操作。
挑战与解决方案:
- 跨分片查询:当需要查询所有用户时,ShardingSphere会将查询分发到所有分片上,然后汇总结果。这会带来性能问题。我们尽量避免这种查询,或者使用Elasticsearch等搜索引擎来专门处理这类聚合查询。
- 扩容:当需要增加分片数量时,数据迁移是一个非常复杂的过程。ShardingSphere提供了在线数据迁移工具,可以在不影响业务的情况下,逐步将数据从一个分片迁移到另一个新分片中。
7. 在MySQL中,你们是如何保证数据一致性的?有没有遇到过事务相关问题?
业务场景:用户购买虚拟礼物,需要原子性地扣减用户余额和增加礼物库存。 技术点:MySQL事务, InnoDB引擎, 死锁 保证一致性的方法:
- ACID特性:MySQL的InnoDB引擎支持事务的ACID特性。这是保证数据一致性的基石。
- 原子性 (Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚。
- 一致性 (Consistency):事务执行前后,数据库的状态必须保持一致。
- 隔离性 (Isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。
- 持久性 (Durability):一旦事务提交,其所做的修改就会永久保存在数据库中。
- 事务隔离级别:MySQL提供了多种隔离级别,从低到高分别是:读未提交、读已提交、可重复读、串行化。
- 我们通常使用**读已提交 (READ COMMITTED)或可重复读 (REPEATABLE READ)**级别。
REPEATABLE READ是InnoDB的默认级别,它通过MVCC(多版本并发控制)机制,可以避免脏读和不可重复读,但可能会出现幻读。
- 我们通常使用**读已提交 (READ COMMITTED)或可重复读 (REPEATABLE READ)**级别。
- 悲观锁与乐观锁:
- 悲观锁:在事务开始时,就对数据加锁,认为数据会发生冲突。例如,使用
SELECT ... FOR UPDATE语句。 - 乐观锁:在事务提交时,才检查数据是否被其他事务修改过。通常通过在数据表中添加一个版本号字段(version)来实现。我们在更新数据时,会比较版本号是否变化,如果变了就重试。
- 悲观锁:在事务开始时,就对数据加锁,认为数据会发生冲突。例如,使用
遇到的事务相关问题:
- 死锁 (Deadlock):
- 现象:两个或多个事务相互等待对方持有的锁,导致谁也无法继续执行。
- 原因:最常见的原因是交叉更新。例如,事务A先更新表T1,再更新表T2;事务B先更新表T2,再更新表T1。当它们并发执行时,就可能发生死锁。
- 解决方案:
- 统一加锁顺序:强制所有事务按照相同的顺序来访问数据库对象。这是解决死锁最有效的方法。
- 降低事务的隔离级别:将
REPEATABLE READ降级为READ COMMITTED,可以减少锁的竞争,但需要评估业务逻辑是否允许。 - 减少事务的持有时间:尽量使事务变小变短,只做必要的操作。
- 设置锁超时:通过
innodb_lock_wait_timeout参数,设置一个合理的等待时间。超过这个时间,事务会自动回滚,避免无限期等待。 - 使用
SHOW ENGINE INNODB STATUS命令:当死锁发生时,这个命令可以显示最近的死锁信息,帮助我们分析问题所在。
8. 在直播间场景中,用户的点赞、评论等高频操作,你是如何优化的?
业务场景:用户对直播内容的互动非常频繁,每次点赞或评论都需要写入数据库,对数据库造成巨大压力。 技术点:Redis, 缓存击穿, 缓存雪崩 优化策略:
-
引入缓存 (Caching):
- 读优化:对于直播间的人气数据(如观看人数、点赞数),我们将其缓存在Redis中。每次有新用户进入或离开直播间时,只更新Redis中的计数,而不是每次都去查MySQL。当用户刷新页面时,直接从Redis中获取数据,响应速度极快。
- 写优化:对于点赞、评论这类高频写操作,我们采用了Cache-Aside模式。
- 当用户点击“点赞”时,后端服务首先更新Redis中的点赞计数器。
- 然后,将这个“点赞”操作作为一个消息,投递到消息队列(如RabbitMQ)中。
- 一个独立的消费者服务会从队列中取出消息,并异步地将数据持久化到MySQL数据库中。
- 这样做的好处是,用户的操作几乎是无感知的(0.1ms级别),极大地提升了用户体验和系统的吞吐量。
-
缓存击穿 (Cache Breakdown):
- 现象:当一个热点Key在Redis中过期的一瞬间,大量并发请求会直接打到数据库上,造成数据库压力剧增。
- 解决方案:
- 设置热点Key永不过期:对于那些访问量极高的Key(比如某个超级热门的直播间的人气),我们不设置过期时间。
- 互斥锁 (Mutex Lock):在Key过期时,不是让所有请求都去查数据库,而是只有一个请求去数据库查询并重建缓存,其他请求等待这个请求完成后再从缓存中读取数据。
- 逻辑过期:在缓存Value中存储一个过期时间戳。当发现Key过期时,先判断过期时间戳是否真的过期,如果是,则启动一个后台线程去异步更新缓存,而不是阻塞当前请求。
-
缓存雪崩 (Cache Avalanche):
- 现象:大量缓存在同一时间点集体失效,导致请求全部涌入数据库。
- 解决方案:
- 随机化过期时间:在设置缓存时,不要全部设置为一个固定时间(如TTL=300秒),而是加上一个小的随机偏移量(如TTL=300±30秒)。这样,缓存就不会在同一时刻集体失效。
- 缓存预热:在系统启动时,或者定期任务中,提前将热点数据加载到缓存中,避免冷启动时的大量请求。
- 多级缓存:除了Redis,还可以引入本地缓存(如Caffeine),作为一级缓存,进一步分流请求。
9. 在异步处理消息时,你们是如何保证消息不丢失的?
业务场景:用户发送的评论或打赏信息必须可靠地存储到数据库中,不能丢失。 技术点:RabbitMQ, 消息确认机制, 事务 保证消息不丢失的三重保障:
- 生产者确认 (Publisher Confirms):
- 在RabbitMQ中,生产者发送消息后,可以收到Broker的确认。如果消息成功被写入到磁盘(或镜像队列),Broker会返回一个确认。如果失败,会返回一个否定确认。
- 在生产者端,我们配置了
ConfirmCallback。当我们发送一条消息后,会等待Broker的确认。只有在收到确认后,我们才认为这条消息已经安全到达了RabbitMQ。
- 消息持久化 (Message Persistence):
- Exchange持久化:在声明Exchange时,将其设置为持久化 (
durable=true)。这样,即使RabbitMQ服务重启,Exchange也不会丢失。 - Queue持久化:同样,在声明Queue时,也设置为持久化。
- 消息持久化:在发送消息时,将消息的属性
deliveryMode设置为2(非transient)。这样,消息就会被写入磁盘。
- Exchange持久化:在声明Exchange时,将其设置为持久化 (
- 消费者确认 (Consumer Acknowledgements):
- RabbitMQ默认是自动确认的,即只要消息被传递给消费者,就会从队列中删除。这可能导致消息在处理前就丢失。
- 我们将确认模式设置为手动 (
autoAck=false)。 - 消费者在处理完消息并成功写入数据库后,会显式地调用
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false)来确认消息。 - 如果在处理过程中出现异常,我们可以选择
channel.basicNack(...)来拒绝消息,RabbitMQ会将这个消息重新放回队列,供其他消费者处理。
总结:这三道防线层层递进,共同保证了消息的可靠性。即使其中一层出现问题,另外两层也能兜底,从而确保消息不会丢失。
10. 如果RabbitMQ宕机了怎么办?你们是如何保证消息队列的高可用的?
业务场景:RabbitMQ作为系统的核心枢纽,一旦宕机,整个系统的消息流转就会中断。 技术点:RabbitMQ Cluster, Mirror Queue, HAProxy 保证高可用的方案:
- 集群部署 (Clustering):
- RabbitMQ原生支持集群模式。我们可以部署多个RabbitMQ节点,组成一个集群。
- 集群中的节点分为两种:
disc节点和ram节点。disc节点会将元数据存储在磁盘上,而ram节点则存储在内存中。为了数据持久化,我们至少需要一个disc节点。 - 集群中的所有节点共享同一个虚拟主机(virtual host)和用户权限,但每个节点都有自己独立的网络和进程。
- 镜像队列 (Mirrored Queues):
- 这是RabbitMQ实现高可用的核心机制。它将一个队列的内容(包括消息)复制到集群中的多个节点上,形成一个“镜像”。
- 客户端可以连接到任何一个节点,但如果它所在的节点宕机,消息队列会自动切换到镜像队列中的另一个节点,继续提供服务。
- 当生产者发送消息时,消息会被发送到主节点,然后主节点负责将消息同步到所有镜像节点。
- 负载均衡 (Load Balancing):
- 我们使用HAProxy或Nginx作为反向代理,将客户端的请求分发到RabbitMQ集群的不同节点上,实现负载均衡。
- 当某个节点宕机时,HAProxy会检测到该节点的健康检查失败,并自动停止向其分发新的连接,从而将流量导向健康的节点。
故障转移流程:
- 假设集群中有3个节点:A(主)、B(镜像)、C(镜像)。
- 客户端连接到了节点A,并向队列
my_queue发送了一条消息。 - 节点A作为主节点,将消息写入自己的队列,并将其同步到节点B和C。
- 突然,节点A宕机了。
- RabbitMQ集群会检测到节点A的丢失。此时,镜像队列的机制会起作用,节点B或C中的一个会被选举为新的主节点。
- 新的主节点接管了原主节点的队列,继续提供服务。
- 客户端在下一次重连时,会连接到新的主节点,消息队列服务完全不受影响。
其他注意事项:
- 网络分区 (Network Partitioning):当网络故障导致集群分裂成两个或多个部分时,RabbitMQ需要做出决策,是继续服务还是停止服务。这通常通过
ha-mode和ha-params参数来控制。 - 监控与告警:我们需要实时监控RabbitMQ集群的健康状况,包括节点状态、队列长度、内存使用率等,以便及时发现并处理问题。
11. 除了点赞、评论,直播间还有其他需要异步处理的业务吗?
业务场景:需要将一些耗时或高并发的操作从主流程中剥离,避免阻塞用户交互。 技术点:消息队列, 异步处理 异步处理的业务场景:
- 用户行为数据分析:
- 场景:当用户在直播间进行点赞、评论、送礼等行为时,我们需要记录这些数据,用于后续的用户画像分析和推荐系统。
- 异步处理:我们不会在主线程中直接将这些埋点数据写入数据库或大数据平台。而是将它们封装成事件对象,发送到Kafka或RabbitMQ队列中。
- 消费者:有一个专门的“数据管道”服务(Data Pipeline Service)订阅这些事件。这个服务会从队列中消费数据,然后将其清洗、格式化,最终批量写入HDFS或Elasticsearch,供离线/实时计算引擎(如Spark/Flink)消费。
- 用户观看时长统计:
- 场景:系统需要记录每个用户观看直播的时长,用于结算主播收益、生成报表等。
- 异步处理:当用户进入直播间时,我们会启动一个计时器。当用户退出或直播间结束时,我们会向消息队列投递一个“开始计费”或“结束计费”的消息。
- 消费者:一个独立的“计费服务”会从队列中获取这些消息,并计算实际观看时长,然后更新数据库中的用户观看记录。
- 通知服务:
- 场景:当主播开播、有人送礼、系统公告时,需要向相关用户推送通知。
- 异步处理:我们不会在主流程中直接调用短信网关或Push服务。而是将通知内容、目标用户等信息发送到消息队列。
- 消费者:有多个通知消费者,分别处理不同类型的消息。例如,一个消费者处理短信通知,一个处理App内Push通知,一个处理邮件通知。它们各自调用不同的第三方服务,并将结果记录在日志中。
- 资源转码:
- 场景:主播上传的视频需要转成不同分辨率和格式的流媒体文件,以适应不同设备和网络环境。
- 异步处理:当用户上传视频后,我们会将其元数据存储在数据库中,并将视频文件上传到对象存储(如OSS)。
- 触发异步:然后,我们向消息队列投递一个“转码任务”消息。
- 消费者:一个强大的“媒体处理中心”服务会从队列中获取任务,调用FFmpeg等工具进行转码,完成后将生成的文件路径等信息更新到数据库。
通过这些异步化处理,我们的核心业务服务得以保持轻量和高响应,整个系统的可扩展性和鲁棒性也得到了极大的提升。
12. 在微服务架构下,你们是如何划分服务的?
业务场景:随着业务复杂度增加,单体应用难以维护和扩展。 技术点:微服务架构, 领域驱动设计(DDD) 服务划分原则: 微服务的边界不应该按照技术栈来划分,而应该按照业务功能来划分。我们遵循以下几个原则:
- 单一职责原则 (Single Responsibility Principle):每个微服务应该只负责一个核心的业务领域。
- 高内聚、低耦合:服务内部的功能高度相关,服务与服务之间的依赖尽可能少。
- 可独立部署:每个服务都应该能够独立地进行开发、测试、部署和扩展。
我们的服务划分: 基于直播平台的业务,我们将系统划分为以下几个核心微服务:
-
用户服务 (User Service):
- 职责:负责用户的全生命周期管理,包括注册、登录、信息修改、密码重置、权限管理等。
- 数据库:拥有独立的MySQL数据库。
-
房间服务 (Room Service):
- 职责:负责直播房间的创建、查询、更新、删除等操作。管理房间的状态、标题、封面等信息。
- 数据库:拥有独立的MySQL数据库。
-
消息服务 (Message Service):
- 职责:负责聊天消息、礼物消息的存储和实时推送。这是我们前面详细讨论的WebSocket服务。
- 数据库:为了性能,消息的存储可能部分使用了Redis,但核心数据(如消息历史)还是会持久化到MySQL。
-
统计服务 (Analytics Service):
- 职责:负责收集各种用户行为和业务数据(如观看时长、点赞数、评论数),并进行初步的聚合和存储。
- 数据库:通常使用时间序列数据库(如InfluxDB)或大数据存储(如HBase)来存储海量时序数据。
-
支付服务 (Payment Service):
- 职责:处理虚拟商品(如礼物、会员)的购买、退款、充值等交易逻辑。与第三方支付平台(如支付宝、微信)对接。
- 数据库:拥有独立的MySQL数据库,对事务性要求极高。
-
通知服务 (Notification Service):
- 职责:负责发送短信、邮件、App Push等各种通知。
- 数据库:存储通知模板、发送记录等。
服务间通信:
- 同步调用:对于需要即时返回结果的调用(如获取用户信息),我们使用RESTful API或RPC(如Dubbo)。
- 异步调用:对于不需要即时反馈的操作(如发送通知、记录日志),我们使用消息队列进行解耦。
13. 在微服务间调用时,你们是如何处理服务降级的?
业务场景:当某个依赖的服务出现故障或响应缓慢时,如何保证核心业务的正常运行。 技术点:服务降级, 熔断器模式 服务降级策略: 当一个微服务A依赖于另一个微服务B时,如果B因为某些原因(如网络超时、服务器宕机)无法正常响应,A如果一直等待下去,自身的资源(如线程)就会被耗尽,从而导致A自身也被拖垮。这就是所谓的“雪崩效应”。
为了解决这个问题,我们引入了熔断器 (Circuit Breaker) 模式和服务降级 (Fallback) 机制。
-
熔断器模式 (Circuit Breaker Pattern):
- 熔断器的思想来源于电力系统中的保险丝。当电路中的电流过大时,保险丝会自动熔断,切断电路,保护电器不被烧毁。
- 在微服务中,熔断器会监控对服务B的调用情况。当失败的调用达到一定阈值(例如,连续10次调用超时或失败),熔断器就会“跳闸”,不再调用服务B。
- 在熔断状态下,对服务B的所有请求都会立即失败,或者直接返回一个预设的降级结果,而不是白白浪费时间等待。
- 经过一段时间后,熔断器会进入“半开”状态,尝试放行一两个请求到服务B。如果这些请求成功了,说明服务B已经恢复,熔断器就会关闭;如果还是失败,熔断器又会回到“闭合”状态,继续拒绝请求。
-
服务降级 (Fallback):
- 当熔断器打开,或者我们手动触发降级时,我们需要提供一个替代方案,这就是降级逻辑。
- 降级逻辑可以是:
- 返回默认值:例如,当用户信息服务不可用时,我们可以返回一个匿名的“游客”用户信息。
- 抛出特定异常:告知调用方“服务暂时不可用,请稍后重试”。
- 调用本地缓存:如果服务B的数据可以容忍短暂的不一致,我们可以从本地的Caffeine缓存中获取数据。
- 简化业务流程:例如,在直播时,如果礼物服务不可用,我们可以暂时屏蔽送礼功能,让用户先观看直播。
实现工具:
- Hystrix:Netflix开源的一个库,专门用来实现熔断器模式。它提供了丰富的功能,如命令模式、线程池隔离、实时监控等。
- Resilience4j:一个更现代的轻量级容错库,设计理念更清晰,社区活跃度更高。我们后期可能会考虑迁移到Resilience4j。
14. 在微服务中,服务发现是如何实现的?
业务场景:微服务数量众多,IP地址经常变化,如何让服务A知道服务B在哪里? 技术点:服务发现, Consul 为什么需要服务发现: 在传统的单体应用中,所有的服务都运行在同一个进程中,或者通过固定的IP地址进行访问。但在微服务架构中,服务是独立部署的,它们的IP地址和端口可能会动态变化(例如,在Kubernetes中进行滚动更新)。如果服务A要调用服务B,它就不能再写死服务B的IP地址了,否则当服务B重启后,它的IP变了,A就会调用失败。
服务发现的解决方案: 服务发现的核心思想是:
- 注册 (Registration):服务启动时,将自己的网络地址信息注册到一个公共的注册中心。
- 发现 (Discovery):服务A需要调用服务B时,它会向注册中心查询所有可用的服务B的实例列表。
- 负载均衡 (Load Balancing):从服务B的实例列表中,选择一个最合适的实例进行调用。
我们使用的方案 - Consul: Consul是由HashiCorp公司开发的一款开源工具,它集成了服务发现、配置管理和分布式一致性等多个功能。
-
Consul Server:
- 部署一个或多个Consul Server,构成Consul集群。它们是中心化的服务注册表,存储着所有注册的服务信息。
-
服务注册:
- 客户端模式:每个微服务在启动时,会启动一个嵌入式的Consul Client。这个Client会将自己的服务名、IP地址、端口号等信息,通过HTTP API或DNS接口注册到Consul Server上。
- 健康检查:Consul会定期对注册的服务进行健康检查(默认是HTTP GET请求到
/actuator/health)。如果某个服务连续几次检查失败,Consul就会将其标记为“不健康”,并从可用服务列表中移除。
-
服务发现:
- Consul Client:当我们的微服务A需要调用微服务B时,它会向本地的Consul Client查询服务B的信息。
- DNS接口:Consul提供了一个DNS接口,服务A可以通过DNS查询来发现服务B。例如,查询
rooms.service.consul,Consul会返回所有可用的房间服务实例的IP和端口。 - HTTP API:服务A也可以通过HTTP API向Consul Server发送请求,获取服务B的详细列表。
-
负载均衡:
- 在获得了服务B的实例列表后,服务A可以使用Ribbon(Spring Cloud自带)来进行客户端负载均衡。Ribbon会从列表中随机选择一个实例,或者根据权重、最少连接数等策略进行选择。
15. 在微服务中,你们是如何统一日志收集的?
业务场景:每个微服务都会产生自己的日志,如何在海量的日志中快速定位某个请求的问题? 技术点:ELK Stack, Filebeat 统一日志收集的必要性: 在微服务架构中,一个用户请求可能会经过十几个不同的服务。如果每个服务都把自己的日志打印在不同的服务器上,排查问题时就像大海捞针一样困难。我们需要将所有服务的日志集中起来,并提供强大的搜索和分析能力。
我们采用的方案 - ELK Stack: ELK Stack是目前最流行的集中式日志解决方案,它由三个开源工具组成:
-
Filebeat:
- 角色:日志收集器。
- 工作方式:在每个微服务的服务器上部署一个Filebeat Agent。Filebeat会监控该服务器上的日志文件(如
/var/log/app.log)。当有新日志产生时,Filebeat会将这些日志行发送到Logstash或Elasticsearch。
-
Logstash:
- 角色:日志处理和转发器。
- 工作方式:Logstash接收来自Filebeat的原始日志。它会进行一系列的处理,包括:
- 过滤 (Filtering):使用Grok插件解析非结构化的日志行,提取出时间、日志级别、服务名、请求ID等结构化字段。
- 增强 (Enrichment):可以添加额外的信息,如IP地理位置等。
- 路由 (Routing):根据处理后的内容,将日志路由到不同的目的地。
- 最终,Logstash将处理后的结构化日志发送到Elasticsearch中存储。
- 优点:功能强大,可以进行复杂的日志处理逻辑。
- 缺点:资源消耗大,延迟较高。
-
Elasticsearch:
- 角色:分布式搜索引擎,也是存储引擎。
- 工作方式:Elasticsearch是一个基于Lucene的分布式搜索引擎。它将所有的日志数据建立倒排索引,使得我们可以进行极其快速的全文搜索。
- 优点:
- 海量数据存储:可以存储PB级别的日志数据。
- 极速搜索:通过倒排索引,即使搜索关键词出现在亿级数据中,也能在毫秒级返回结果。
- 强大的聚合分析能力:可以对日志数据进行各种维度的统计分析。
-
Kibana:
- 角色:日志的可视化界面。
- 工作方式:Kibana是一个基于Web的界面,它与Elasticsearch直接交互。运维人员和开发者可以通过Kibana的Discover功能来实时查看和搜索日志,也可以通过Dashboard来创建各种图表,展示系统的运行状况。
总结:Filebeat负责采集,Logstash负责加工,Elasticsearch负责存储,Kibana负责展现。这套组合拳让我们能够轻松应对微服务带来的日志难题。
16. 在微服务中,你们是如何进行链路追踪的?
业务场景:一个HTTP请求从网关进来,经过了订单服务、库存服务、支付服务,最终在数据库层面失败。如何快速定位是哪个服务出了问题? 技术点:链路追踪, Jaeger 链路追踪的必要性: 在微服务架构中,一个请求会跨越多个服务。当请求失败或响应缓慢时,传统通过日志排查的方式效率极低。因为你需要登录到每个服务器的日志文件中,手动拼接时间线,才能还原出完整的调用链。链路追踪技术就是为了解决这个问题而生的。
链路追踪的原理: 链路追踪的核心思想是分布式上下文传播 (Distributed Context Propagation)。
- Trace (追踪):一个完整的请求链路被称为一个Trace。
- Span (跨度):Trace是由多个Span组成的。Span代表了一个服务内部的某个操作,比如一次数据库查询,一次远程服务调用。
- 上下文传播:当一个请求进入第一个服务时,会生成一个唯一的Trace ID。这个Trace ID会通过请求的Header(如
x-trace-id)传递给下一个服务。下一个服务接收到请求后,会从这个Header中提取出Trace ID,并为自己生成一个新的Span,将这个Span加入到当前的Trace中。这个过程会一直持续到请求的终点。
我们使用的方案 - Jaeger: Jaeger是由Uber公司开源的端到端的分布式追踪系统,它与OpenTracing API兼容。
-
Agent:
- 在每个应用的宿主机(物理机或虚拟机)上部署一个Jaeger Agent。它的作用是监听UDP端口,接收来自应用发送的Span数据,然后将其转发给Collector。
-
Collector:
- Collector是Jaeger的核心,它负责接收、处理和存储来自Agent的Span数据。它提供了gRPC和HTTP两种接口。
-
Query:
- Query服务提供了REST API,它从Storage中读取Trace数据,并返回给前端界面。
-
Sampler:
- Sampler决定了有多少Trace数据会被采样并发送到Collector。在高流量系统中,不可能采集每一条Trace,因为这会带来巨大的性能开销。我们通常会使用恒定采样率(如1%)或自适应采样。
-
Storage:
- Span数据最终被存储在数据库中,如Cassandra或Elasticsearch。
在代码中的实现: 我们以Spring Cloud Sleuth + Zipkin为例(Jaeger的实现类似):
- Sleuth:它是一个Spring Cloud库,为Spring Boot应用自动添加了链路追踪的能力。它会自动在日志中为每条请求生成Trace ID和Span ID,并将其打印出来。
- Zipkin Reporter:我们需要在应用中配置一个Zipkin Reporter,让它将生成的Span数据发送到Zipkin Server。
如何使用: 当在Jaeger UI中看到一个失败的Trace时,你可以看到:
- 这个Trace经过了哪些服务。
- 在每个服务内部,都有哪些耗时操作(Span)。
- 每个Span的耗时、错误信息等。
- 甚至可以深入到每个SQL语句的执行时间。 这就像给整个分布式系统装上了可视化的X光眼,任何问题都无处遁形。
17. 在微服务中,你们是如何管理配置的?
业务场景:每个微服务的配置(如数据库连接、第三方API密钥)都写在各自的配置文件中,当配置变更时,需要重新打包部署,非常麻烦。 技术点:Spring Cloud Config, Git 中心化配置管理的必要性: 在微服务架构中,服务的数量众多,每个服务都有大量的配置项。如果每个服务的配置都散落在各个jar包或docker镜像中,会带来以下问题:
- 配置分散:很难统一管理,容易遗漏。
- 部署成本高:每次修改一个配置,都需要重新构建和部署该服务,周期长。
- 缺乏版本控制:配置文件没有纳入版本控制系统,无法追溯变更历史。
- 环境隔离难:不同环境(dev/test/prod)的配置混杂在一起,容易出错。
我们采用的方案 - Spring Cloud Config: Spring Cloud Config是Spring Cloud生态系统中的一个项目,它为分布式系统中的外部配置提供了服务器和客户端支持。
-
Config Server (配置服务器):
- 我们搭建了一个独立的Config Server微服务。
- 它的核心功能是提供一个RESTful API,用于读取配置文件。
- 我们通常将所有的配置文件存储在Git仓库中,按环境组织,例如:
config-repo/user-service-dev.propertiesconfig-repo/user-service-prod.ymlconfig-repo/room-service-test.yaml
- Config Server通过Git API与Git仓库通信,获取最新的配置文件。
-
Config Client (配置客户端):
- 每个需要外部配置的微服务都是一个Config Client。
- 它们在启动时,会向Config Server发起请求,获取自己的配置文件。
- 请求的URL通常遵循这样的格式:
http://config-server/{application}/{profile}[/{label}]{application}:对应Git仓库中的文件名(不含后缀)。例如,user-service。{profile}:对应激活的环境。例如,dev、prod。{label}:对应Git的分支或标签。例如,main、v1.0。
-
自动刷新 (Refresh Scope):
- 当Git仓库中的配置文件发生变更并被提交后,Config Client并不会自动获取最新配置。这是因为Spring的Environment对象在应用启动时就被初始化了,之后不会改变。
- 解决方案:
- @RefreshScope注解:在需要动态刷新配置的那个Bean上,加上
@RefreshScope注解。这样,当配置发生变化时,Spring Cloud会为该Bean创建一个新的代理对象,从而实现配置的动态刷新。 - /actuator/refresh端点:我们可以发送一个POST请求到这个端点,强制所有
@RefreshScope的Bean刷新配置。 - Spring Cloud Bus:这是最优雅的解决方案。它是一个消息总线,利用消息队列(如RabbitMQ)将
/actuator/refresh事件广播到所有相关的微服务实例,从而实现配置的分布式刷新。
- @RefreshScope注解:在需要动态刷新配置的那个Bean上,加上
18. 在微服务中,你们是如何实现API网关的?
业务场景:所有外部请求都必须先经过一个统一的入口,我们需要在这个入口处进行鉴权、限流、路由等操作。 技术点:API Gateway, Spring Cloud Gateway API网关的作用: API网关是位于所有微服务前的单一入口点。它不仅仅是路由转发那么简单,它还承担着许多横切关注点(Cross-Cutting Concerns)的职责,是微服务架构的“门面”。
我们使用的方案 - Spring Cloud Gateway: Spring Cloud Gateway是Spring官方基于Spring 5.0、Spring Boot 2.0和Project Reactor等技术,开发的网关服务。它旨在提供一种简单而有效的方式来路由到后端不同的微服务,并为它们提供横切关注点的能力。
核心功能:
-
路由 (Routing):
- 这是最基本的功能。Gateway会根据配置的规则,将请求转发到对应的微服务。
- 例如,所有以
/api/users/**开头的请求,都会被路由到user-service。 - 配置通常使用YAML或Java代码来定义。
-
断言 (Predicates):
- 路由规则不仅仅基于URI,还可以基于其他条件,比如请求头、请求参数、HTTP方法等。这些条件被称为断言。
- 例如,你可以配置一个路由,只有当请求头中包含
X-Auth-Token时才生效。
-
过滤器 (Filters):
- 过滤器是Gateway最强大的功能之一。它允许我们对请求或响应进行任意的修改和操作。
- 内置的过滤器非常丰富,例如:
AddRequestParameter:添加请求参数。SetStatus:设置HTTP状态码。RewritePath:重写请求路径。
- 我们也可以自定义过滤器来实现复杂的业务逻辑。
在直播平台中的应用:
- 身份认证与鉴权:
- 所有请求进入Gateway后,第一个过滤器就是JWT的校验。它会从请求头中取出JWT token,解析并验证其有效性。如果无效,直接返回401错误,阻止请求进入下游服务。
- 请求限流:
- 为了防止某个API被恶意刷爆,我们可以在Gateway层对每个API进行限流。例如,限制某个用户每分钟最多只能发送100条消息。Spring Cloud Gateway可以很方便地与Redis结合,实现令牌桶或漏桶算法。
- 黑白名单:
- 我们可以配置一个过滤器,检查客户端的IP地址是否在黑名单中。如果是,直接拒绝访问。
- 日志记录:
- 在Gateway中,我们可以统一记录所有请求的日志,包括请求路径、客户端IP、耗时等,方便后续的审计和分析。
19. 在微服务中,你们是如何进行容器化的?
业务场景:传统的部署方式(如war包部署到Tomcat)已经无法满足快速迭代和弹性伸缩的需求。 技术点:Docker, Dockerfile 容器化的优势:
- 环境一致性:开发、测试、生产环境完全一致,解决了“在我机器上能跑”的问题。
- 快速部署:镜像构建完成后,可以快速在任意支持Docker的平台上部署。
- 资源隔离:每个服务运行在自己的容器中,互不干扰。
- 易于扩展:配合Kubernetes,可以实现服务的自动化扩缩容。
我们的容器化实践 - Docker: Docker是容器化技术的代表。它将应用程序和其所有的依赖打包在一个轻量级的、可移植的镜像中。
-
Dockerfile:
- Dockerfile是一个文本文件,包含了一系列的指令,用于指导Docker如何构建一个镜像。
- 下面是一个典型的Java微服务的Dockerfile示例:
# 1. 基础镜像 FROM openjdk:11-jre-slim # 2. 维护者信息 MAINTAINER xie_feiji <feiji@example.com> # 3. 工作目录 WORKDIR /app # 4. 拷贝jar包到镜像中 COPY target/my-live-platform-user-service.jar app.jar # 5. 暴露端口 EXPOSE 8080 # 6. 启动命令 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom","-jar", "/app/app.jar"]
-
构建镜像:
- 在项目的根目录下,执行
docker build -t my-live-platform-user-service:v1.0 .命令。 - 这会根据Dockerfile的指令,将源代码、依赖和配置文件打包成一个名为
my-live-platform-user-service:v1.0的镜像。
- 在项目的根目录下,执行
-
运行容器:
- 构建完成后,我们可以使用
docker run命令来运行容器。 - 例如:
docker run -p 8081:8080 --name user-service-container my-live-platform-user-service:v1.0 - 这会在本地启动一个容器,并将宿主机的8081端口映射到容器的8080端口。
- 构建完成后,我们可以使用
Docker Compose:
对于本地开发和测试,我们通常使用Docker Compose来编排多个容器。一个docker-compose.yml文件可以定义一个服务及其依赖的所有其他服务,一键启动整个应用栈。
20. 在微服务中,你们是如何实现持续集成和持续部署(CI/CD)的?
业务场景:开发人员每天会产生数十次代码提交,如何将这些代码快速、安全地部署到生产环境? 技术点:CI/CD, Jenkins, GitLab CI CI/CD的目标:
- 持续集成 (Continuous Integration, CI):开发人员频繁地将代码合并到主干,每次合并都会触发自动化构建和测试,及早发现和解决集成问题。
- 持续交付/部署 (Continuous Delivery/Deployment, CD):通过自动化的流程,将通过了所有测试的软件新版本,快速地、可靠地部署到预发布或生产环境中。
我们采用的方案 - Jenkins + Kubernetes: 这是一个经典的DevOps工具链组合。
-
代码提交与触发:
- 当开发人员将代码push到Git仓库的主分支(如
main)时,会触发CI/CD流程。
- 当开发人员将代码push到Git仓库的主分支(如
-
持续集成 (CI) 阶段:
- 拉取代码:Jenkins从Git仓库拉取最新的代码。
- 编译:调用Maven/Gradle,将Java源码编译成class文件。
- 单元测试:运行所有单元测试(JUnit),确保代码逻辑的正确性。
- 代码质量检查:使用SonarQube等工具进行代码静态扫描,检查潜在的bug、漏洞和代码坏味道。
- 构建Docker镜像:如果通过了所有检查,Jenkins会调用
docker build命令,为这个服务构建一个新的Docker镜像,并打上版本标签(如v1.0.1)。
-
持续部署 (CD) 阶段:
- 推送镜像:Jenkins将构建好的Docker镜像推送到私有的Docker Registry(如Harbor)中。
- 更新Kubernetes Deployment:Jenkins连接到Kubernetes集群,找到对应的Deployment资源,将它的镜像版本更新为新构建的镜像。
- Kubernetes自动滚动更新:Kubernetes会自动执行滚动更新。它会先启动一个新版本的Pod,等新Pod健康检查通过后,再停掉旧版本的Pod。整个过程对用户是透明的,服务不会中断。
- 蓝绿部署/金丝雀发布:对于生产环境,我们还可以结合Kubernetes的Service资源和Istio等工具,实现更高级别的部署策略,如蓝绿部署(新旧版本并行,瞬间切换)或金丝雀发布(新版本只对一小部分用户开放,观察效果)。
优势:
- 提效:从代码提交到生产部署,整个过程完全自动化,大大缩短了交付周期。
- 降险:通过严格的自动化测试关卡,降低了将缺陷带入生产环境的风险。
- 可追溯:每一次部署都对应一个确定的代码版本和镜像,可以追溯到任何一次线上问题的根源。
通过这套CI/CD流水线,我们的软件交付过程变得前所未有的高效和稳定。