今天要和大家分享的是我们整理的得物的一面面经。我已经把所有的问题和答案都整理好了,希望对大家有帮助:
1.假设有个场景,需要使用go实现从网络接收大量对象反序列化—序列化,转发。tps很高,怎么实现使内存占用最小。
2.如何实现一个服务注册与服务发现。
3.etcd的实现算法是什么
4.cap理论,etcd是哪种
5.节点不健康时服务中心是怎么处理,直接剔除吗
6.go zero中使用etcd是怎么处理不健康节点
7.redis限流如何实现(使用redis的数据结构)
8.普罗米修斯和grafana是做什么的
9.mysql的默认事务隔离级别,能解决幻读吗
10.Gmp模型说一下,g什么时候会阻塞
11.mysql提交和回滚那个快
12.go gc很消耗cpu,什么方法来减少gc(面试官最后提醒使用和1一样对象池)
面经详解
1.假设有个场景,需要使用go实现从网络接收大量对象反序列化—序列化,转发。tps很高,怎么实现使内存占用最小。
-
正确答案:为了在Go语言中实现从网络接收大量对象、反序列化、再序列化并转发,同时保持最小内存占用,应采用流式处理方式,避免一次性将整个数据加载到内存中。关键在于使用缓冲池(sync.Pool)、复用结构体、按需读取和写入数据,并利用Goroutine并发处理。
-
解答思路:
- 使用
io.Reader逐块读取网络数据,而不是一次性读取全部内容。 - 使用流式解析库如
json.Decoder进行反序列化,避免一次性加载整个JSON。 - 处理完的数据可以立即写回网络,不需要缓存整个响应。
- 对象的结构体实例可以通过
sync.Pool复用,减少GC压力。 - 使用Goroutine并发处理多个连接,但要控制最大并发数以防止资源耗尽。
- 如果需要中间处理逻辑,可采用流水线模式分阶段处理数据。
- 合理设置缓冲区大小,平衡性能与内存使用。
- 使用
-
深度知识讲解:
1. 流式处理 vs 全量处理
- 全量处理会把整个数据载入内存(例如
ioutil.ReadAll()),对于大文件或高并发场景非常不友好。 - 流式处理通过逐块读取和处理数据,大大减少内存峰值占用。
2. sync.Pool对象复用
- Go语言中频繁创建临时对象会增加GC负担,影响性能。
sync.Pool允许在goroutine之间安全地复用对象,减少分配次数。- 示例:每个请求复用一个结构体来保存解码后的对象。
3. JSON流式解析
encoding/json包中的Decoder支持边读边解析,适用于大JSON流。- 避免了整个JSON文档被加载进内存。
4. 缓冲区管理
- 使用固定大小的字节缓冲区进行读写操作,可以降低内存波动。
- 可结合
bytes.Buffer或bufio包优化IO效率。
5. 并发控制
- 使用GOMAXPROCS或多核调度提高吞吐量。
- 控制最大并发请求数,防止内存爆炸(OOM)。
- 可使用带缓冲的channel作为worker pool控制并发。
6. 内存对齐与结构体优化
- 结构体内存布局会影响实际内存占用,可通过字段顺序调整优化。
- 尽量避免嵌套结构体和不必要的指针。
7. GC调优
- 设置较小的GOGC值可以更频繁回收垃圾,适合内存敏感场景。
- 监控GC行为,分析是否出现频繁GC导致性能下降。
- 全量处理会把整个数据载入内存(例如
2.如何实现一个服务注册与服务发现。
-
正确答案:实现一个服务注册与服务发现机制,通常包括以下几个核心组件:服务提供者(Provider)、服务消费者(Consumer)和服务注册中心(Registry)。其基本流程是:服务启动时向注册中心注册自身信息(如IP、端口、服务名等),消费者从注册中心获取服务实例列表,并通过负载均衡选择一个实例进行调用。常见实现方式有基于ZooKeeper、Eureka、Consul或Etcd的方案。
-
解答思路:
- 理解需求:服务注册与发现的核心目标是让服务之间能够动态地感知彼此的存在和状态。
- 划分角色:明确服务提供者、消费者和注册中心三者的职责。
- 设计通信协议:服务注册信息的格式、健康检查机制、服务发现接口。
- 实现注册逻辑:服务启动时发送注册请求到注册中心。
- 实现发现逻辑:消费者定时或事件驱动地从注册中心获取服务列表。
- 考虑容错与一致性:如心跳机制、自动剔除下线服务、服务缓存等。
-
深度知识讲解:
核心概念
1. 服务注册
服务注册是指服务在启动后将自身的元数据(如服务名称、IP地址、端口号、权重、健康状态等)提交给注册中心。这些信息通常以键值对形式存储,例如:
/service/user-service/192.168.1.10:8080 = {"status": "UP", "lastHeartbeat": 1717029200}
2. 心跳机制(Health Check)
服务需要定期向注册中心发送心跳包,表明自己仍然存活。如果注册中心在一段时间内未收到某个服务的心跳,则将其标记为下线并从服务列表中移除。
3. 服务发现
服务消费者通过查询注册中心获取可用的服务实例列表。这个过程可能包括服务过滤、负载均衡(如轮询、随机、最小连接数等)。
4. 注册中心选型
- ZooKeeper:使用ZNode树结构存储服务信息,适合强一致性场景。
- Eureka:Netflix开源,AP系统,优先保证可用性。
- Consul:支持多数据中心,内置健康检查。
- Etcd:CoreOS开发,高可用分布式键值存储,常用于Kubernetes中。
数据结构与底层原理
使用 Etcd 实现服务注册与发现的数据结构示例:
每个服务实例注册为一个租约(Lease),并通过Put命令绑定到特定Key上,例如:
PUT /registry/user-service/192.168.1.10:8080 with lease=ttl=60s
消费者通过Watch机制监听该目录下的变化,实现动态更新服务列表。
负载均衡策略
- 随机(Random)
- 轮询(Round Robin)
- 最少连接数(Least Connections)
- 权重轮询(Weighted Round Robin)
扩展知识点
- CAP理论:注册中心的选择要考虑一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)之间的权衡。
- 服务治理:服务注册与发现是微服务架构的基础,后续还可扩展出熔断、限流、链路追踪等功能。
- 服务网格(Service Mesh) :如Istio+Envoy架构中,服务发现由Sidecar代理完成,应用无需关心细节。
3.etcd的实现算法是什么
-
正确答案:etcd 使用的是 Raft 共识算法来实现分布式一致性。Raft 是一种用于管理复制日志的共识算法,旨在解决多个节点之间如何就某个值达成一致的问题。etcd 通过 Raft 算法实现了高可用、强一致的分布式键值存储。
-
解答思路: 要回答这个问题,首先需要明确 etcd 是一个分布式键值存储系统,其核心目标是保证在多个节点上数据的一致性和高可用性。因此它必须使用某种共识算法来确保即使在网络分区或节点故障的情况下,集群依然能对外提供一致的数据视图。
Raft 是 etcd 的底层一致性协议实现,与 Paxos 相比,Raft 更易于理解和实现,因此被 etcd 采用。回答时应涵盖 Raft 的基本机制,包括选举机制、日志复制、安全性等关键部分,并结合 etcd 的实际应用场景说明它是如何利用 Raft 实现高可用服务的。
-
深度知识讲解:
Raft 算法的核心概念
Raft 将一致性问题分解为三个子问题:
-
Leader Election(领导者选举) :
- 每个节点初始处于 Follower 状态。
- 如果 Follower 在一定时间内未收到 Leader 的心跳,则转变为 Candidate 并发起选举。
- Candidate 向其他节点发送 RequestVote RPC 请求投票。
- 多数节点投票成功后成为新的 Leader。
-
Log Replication(日志复制) :
- Leader 接收客户端请求,将命令追加到本地日志中。
- 然后通过 AppendEntries RPC 将日志条目复制到其他节点。
- 当大多数节点确认该日志条目后,Leader 提交该日志并应用到状态机。
-
Safety(安全性) :
- Raft 保证了即使在节点崩溃、网络分区等情况下,也只可能有一个合法的 Leader 存在。
- 日志完整性规则和投票限制机制确保不会选出一个不包含全部已提交日志的 Leader。
etcd 中 Raft 的具体实现
etcd 的 Raft 实现封装在
etcd/raft包中,它并不是直接处理网络通信,而是提供了一个状态机接口供上层调用。etcd 的 Raft 层负责维护节点的状态、处理消息、推进日志复制和选举流程。- 每个 etcd 节点运行一个 Raft 实例。
- 所有写操作都必须经过 Raft 协议达成一致后才能提交。
- etcd 使用 WAL(Write Ahead Log)持久化 Raft 日志,使用 BoltDB 或 LSM Tree 存储最新的状态快照。
etcd 的架构分层
etcd 的整体架构可以分为以下几个层次:
- API 层:提供 gRPC 和 HTTP 接口供客户端访问。
- MVCC 层:多版本并发控制,支持 Watcher 机制。
- Store 层:处理具体的读写操作。
- Raft 层:实现 Raft 协议逻辑。
- WAL + Snapshot 层:持久化 Raft 日志和快照。
性能优化与扩展机制
etcd 在 Raft 基础上做了很多性能优化,例如:
- Batching:批量处理多个日志条目,减少网络开销。
- Pipeline & Streaming:提高日志复制效率。
- Joint Consensus:用于安全地变更集群成员配置。
- Lease 机制:支持租约自动过期功能,常用于分布式锁等场景。
Raft 的优缺点对比 Paxos
-
优点:
- 易于理解与实现。
- 明确的角色划分(Follower, Candidate, Leader)。
- 支持成员变更、快照等实用特性。
-
缺点:
- 写性能受限于单一 Leader。
- 对网络延迟敏感。
-
4.cap理论,etcd是哪种
-
正确答案:CAP理论指出,在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)三者不可兼得,最多只能同时满足其中两个。etcd 是一个 CP 系统,它优先保证一致性和分区容忍性,牺牲一定的可用性。
-
解答思路: 首先明确 CAP 三个字母的含义: C(Consistency):所有节点在同一时刻看到的数据是一致的。 A(Availability):每个请求都能收到响应,无论成功还是失败,但不会无限期等待。 P(Partition Tolerance):系统在网络分区发生时仍能继续运行。
etcd 是基于 Raft 共识算法实现的分布式键值存储系统,主要用于服务发现、配置共享等场景。Raft 是一种强一致性协议,因此 etcd 在网络分区的情况下会优先保障数据的一致性,而不是立即返回结果给客户端,因此属于 CP 系统。
-
深度知识讲解:
CAP理论详解
CAP 定理由 Eric Brewer 提出,适用于分布式系统的设计权衡。三种属性之间的关系如下:
- 如果系统选择了 AP,则在网络分区时,为了保证可用性,可能返回旧数据或错误数据,即牺牲一致性。
- 如果系统选择了 CP,则在网络分区时,部分节点可能无法处理请求,甚至拒绝服务以保证数据的一致性。
- 如果系统选择了 CA,则不能容忍任何网络分区,这在实际大规模分布式系统中几乎不可能实现。
因此,现代分布式系统设计通常是在 CP 和 AP 之间做取舍。
etcd 的一致性机制 —— Raft 算法
etcd 使用 Raft 协议来保证集群中的数据一致性。Raft 是一种比 Paxos 更易理解的共识算法,其核心思想是:
- 集群中只有一个 Leader 负责接收写请求。
- 写操作必须经过大多数节点(quorum)确认后才提交。
- 所有节点的数据最终保持一致。
这种机制使得 etcd 在面对网络分区时,如果 Leader 所在分区无法达到 quorum,就会触发重新选举,而原 Leader 不再提供写服务,从而保证整个系统的数据一致性。
etcd 的应用场景
- Kubernetes 中使用 etcd 存储集群状态信息,如 Pod、Service 等资源对象。
- 分布式锁服务。
- 配置中心。
- 服务注册与发现。
对比其他系统
- Zookeeper:也是 CP 系统,使用 ZAB 协议,强调一致性。
- Redis Cluster:AP 系统,强调高可用,允许在网络分区下继续提供服务,但可能导致数据不一致。
- Cassandra:AP 系统,采用最终一致性模型。
- MongoDB:默认情况下是 AP,但在副本集模式下可以配置为 CP。
因此,选择系统时应根据业务需求决定是否需要强一致性(CP)还是高可用性(AP)。
5.节点不健康时服务中心是怎么处理,直接剔除吗
-
正确答案:当节点不健康时,服务中心(如服务注册与发现组件,例如Eureka、Consul、Nacos等)通常不会立即直接剔除该节点。而是会根据心跳机制和健康检查策略进行判断,若多次未收到心跳或健康检查失败,则将其标记为下线或隔离,确保请求不再转发到该节点。
-
解答思路:
- 首先理解服务中心在微服务架构中的作用:服务注册、服务发现、健康检查。
- 然后分析节点不健康的定义:通常是服务宕机、网络中断、接口异常等情况导致无法正常提供服务。
- 接着考虑服务中心如何处理这类问题:通过心跳检测、健康检查、重试机制等方式来判断服务状态。
- 最后明确是否直接剔除:大多数情况下不会立即剔除,而是采用“软删除”或“延迟剔除”策略,以防止误判或短暂故障带来的影响。
-
深度知识讲解:
1. 心跳机制:
- 微服务启动后向服务中心注册自身信息(IP、端口、元数据等)。
- 每隔一段时间(如Eureka默认30秒)发送一次心跳包,表示自己仍存活。
- 若服务中心在设定时间内(如90秒)未收到心跳,则将该节点标记为不可用或进入“待剔除”状态。
2. 健康检查机制:
- 除了心跳外,服务中心或网关还可以主动调用服务的
/health接口来判断其是否健康。 - 如果连续多次健康检查失败,则认为该节点不健康,将其从可用服务列表中移除。
3. 容错与一致性权衡:
- 根据CAP定理,在分布式系统中很难同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)。
- Eureka属于AP系统,优先保证可用性,即使部分节点状态不一致,也尽量返回可用的服务实例。
- Consul则偏向CP系统,更强调一致性,可能在发生网络分区时暂停服务发现功能。
4. 节点剔除策略:
- Eureka:不会立即剔除,而是等待一定时间(如90秒),期间其他服务仍然可以查询到该节点,但调用可能会失败。
- Nacos:支持临时节点和持久化节点。临时节点依赖心跳,超时后会被自动剔除;持久化节点需要手动删除。
- Consul:节点离线后会在Catalog中标记为down,但仍保留元数据,可通过API查询。
5. 服务降级与熔断机制配合使用:
- 即使服务中心未及时剔除节点,客户端也可以通过Hystrix、Sentinel、Resilience4j等组件实现熔断与降级,避免雪崩效应。
6.go zero中使用etcd是怎么处理不健康节点
-
正确答案:在 Go Zero 框架中,使用 etcd 作为服务注册与发现组件时,框架会通过 etcd 的健康检查机制和 Watch 机制自动检测不健康的节点,并从可用服务列表中剔除这些节点,确保请求不会被发送到不可用的服务实例上。
-
解答思路:
- Go Zero 使用了 etcd 的服务注册功能,服务启动时将自己的地址注册到 etcd。
- 服务端定期向 etcd 发送心跳(Lease Grant + Put + Lease Renewal),以表明自己是健康的。
- 如果某个服务节点停止发送心跳或超时未续租,则 etcd 认为该节点不健康,并将其删除。
- 客户端通过 Watch 监听 etcd 中的服务节点变化,一旦有节点被删除,客户端的负载均衡器会更新可用节点列表。
- 最终,Go Zero 的 RPC 客户端只会将请求发送到当前健康的节点上。
-
深度知识讲解:
etcd 的健康检查机制
etcd 提供了租约(Lease)机制用于实现服务的心跳保活。服务端通过以下步骤维护自己的“存活”状态:
- 调用
LeaseGrant创建一个租约(如 TTL=10秒) - 调用
Put将服务信息写入 etcd 并绑定该租约 - 定期调用
LeaseRenew续租,维持租约有效
如果服务宕机或网络中断,etcd 无法收到续租请求,租约会过期,对应键值会被自动删除。
Watch 机制
etcd 支持 Watch API,允许客户端监听某个 key 或前缀的变化。Go Zero 利用这个特性来监听服务节点的注册和注销事件。当某个服务节点失效并被 etcd 删除后,Watch 会通知客户端更新服务实例列表。
负载均衡策略
Go Zero 内部集成了负载均衡器(默认使用 round-robin),它基于最新的服务节点列表进行调度。当节点列表更新时,负载均衡器也会相应地更新其调度逻辑。
- 调用
7.redis限流如何实现(使用redis的数据结构)
-
正确答案:Redis 实现限流通常使用滑动窗口算法或令牌桶算法,常用的数据结构包括 Redis 的 String、List 和 Sorted Set。其中,使用 Sorted Set 可以高效实现滑动窗口限流。
-
解答思路:
- 确定限流策略(如每秒最多请求次数)。
- 使用合适的数据结构记录请求时间戳。
- 每次请求到来时,检查是否超过限制。
- 如果未超过,则允许请求并记录当前时间戳;否则拒绝请求。
具体实现中:
- 对于滑动窗口限流,可以利用 Sorted Set 存储请求的时间戳,并定期清理过期的请求记录。
- 对于令牌桶限流,可以用 String 类型保存当前令牌数和上次更新时间,按需补充令牌。
-
深度知识讲解:
1. 滑动窗口限流(使用 Sorted Set)
原理:
滑动窗口限流基于一个时间窗口(例如1秒),每个请求的时间戳被记录在 Sorted Set 中。每次请求到来时,先删除所有超出窗口时间的旧记录,再判断当前窗口内请求数是否超过限制。
数据结构选择原因:
Sorted Set 支持范围查询和自动排序,非常适合处理带有时间戳的请求记录。
时间复杂度分析:
- 添加新请求:O(log n)
- 删除过期请求:O(log n)
- 查询请求数:O(1)
应用场景:
适用于需要精确控制单位时间内请求数量的场景,如 API 接口访问频率控制。
2. 令牌桶限流(使用 String + Lua 脚本)
原理:
令牌桶维护一个固定容量的“桶”,系统按照固定速率向桶中添加令牌。请求只有在桶中有足够令牌时才被允许执行。
数据结构选择原因:
使用 String 来存储当前剩余令牌数和上一次填充令牌的时间点。结合 Lua 脚本保证操作的原子性。
时间复杂度分析:
- 获取令牌:O(1)
- 更新令牌:O(1)
应用场景:
适用于流量削峰填谷的场景,比如防止突发流量冲击后端服务。
8.普罗米修斯和grafana是做什么的
正确答案:普罗米修斯(Prometheus)是一个开源的系统监控和警报工具,主要用于收集、存储和查询时间序列数据;Grafana 是一个开源的数据可视化平台,支持多种数据源,常用于创建交互式仪表盘来展示监控数据。两者通常结合使用,构建完整的监控与可视化解决方案。
-
解答思路: 首先明确 Prometheus 的核心功能是采集指标数据并进行存储和查询; 然后理解 Grafana 的作用是将这些数据以图表等形式进行展示; 最后说明它们在现代运维中的典型应用场景,比如微服务监控、服务器资源监控等。
-
深度知识讲解:
1. Prometheus 的工作原理与架构
Prometheus 使用拉取(pull)模式从目标节点获取指标数据,其核心组件包括:
- Retrieval:负责定期从配置的目标中抓取指标数据。
- Time Series Database (TSDB):本地存储时间序列数据,默认保留15天(可配置),按块(block)方式组织数据。
- PromQL 引擎:提供强大的查询语言 PromQL(Prometheus Query Language),用于对时间序列数据进行聚合、过滤、计算等操作。
- HTTP Server:提供 API 接口和图形化界面,用户可通过浏览器访问。
Prometheus 支持的服务发现机制包括静态配置、DNS、Kubernetes、Consul 等,适用于动态变化的环境。
2. Grafana 的作用与扩展能力
Grafana 主要提供以下功能:
- 支持多种数据源(如 Prometheus、MySQL、PostgreSQL、Elasticsearch、InfluxDB 等)
- 提供丰富的可视化组件(折线图、柱状图、饼图、热力图、状态面板等)
- 可设置变量实现动态查询
- 支持告警规则的可视化配置
- 插件系统允许开发者自定义面板、数据源和变换器
Grafana 通过插件机制实现了高度可扩展性,社区提供了大量官方和第三方插件。
3. 数据模型与采集方式对比
Prometheus 的数据模型基于时间序列,每个时间序列由一个指标名称(metric name)和一组标签(key-value pairs)唯一标识,例如:
http_requests_total{job="api-server", instance="localhost:9090", method="POST"}这种设计使得数据具有良好的结构化特性,便于聚合分析。
相比之下,传统的日志或事件型监控系统(如 ELK Stack)更适合处理非结构化文本数据。
4. 应用场景
- 微服务健康监控(响应时间、请求成功率、错误率等)
- 基础设施资源监控(CPU、内存、磁盘、网络等)
- 自定义业务指标(如订单量、活跃用户数等)
- 容器编排系统(Kubernetes)监控
- 告警通知集成(配合 Alertmanager 实现)
5. 性能与扩展性考量
Prometheus 单实例适合中小规模的监控场景,大规模部署时可采用联邦(Federation)模式或多副本+远程存储方案(如 Thanos、Cortex)来实现水平扩展。
9.mysql的默认事务隔离级别,能解决幻读吗
-
正确答案:MySQL的默认事务隔离级别是可重复读(REPEATABLE READ)。在该隔离级别下,MySQL通过Next-Key锁机制解决了幻读问题。
-
解答思路: 首先明确什么是事务隔离级别及其作用。事务隔离级别决定了事务之间可见性和并发控制的程度。MySQL有四种标准的事务隔离级别:
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)——MySQL默认
- 串行化(SERIALIZABLE)
幻读指的是在一个事务中两次执行相同的查询,结果集的数量发生了变化(通常是由于另一个事务插入或删除了数据)。解决幻读通常需要更严格的锁定机制。
在MySQL中,InnoDB存储引擎使用Next-Key锁(一种临键锁,即记录锁 + 间隙锁)来防止幻读的发生。因此,在“可重复读”级别下,MySQL实际上已经避免了幻读现象。
-
深度知识讲解:
1. 事务隔离级别的定义与影响
- 读未提交:最低的隔离级别,允许脏读。
- 读已提交:只能读取已提交的数据,但会出现不可重复读。
- 可重复读:确保在同一事务中多次读取同一数据时结果一致,但理论上可能出现幻读。
- 串行化:所有事务串行执行,彻底避免并发问题,但性能最差。
2. 幻读、不可重复读和脏读的区别
- 脏读:读到其他事务未提交的数据。
- 不可重复读:同一事务内多次读取同一行数据,结果不同(因为其他事务更新或删除了这行)。
- 幻读:同一事务内多次执行相同查询,结果集数量变化(因为其他事务插入或删除了符合条件的记录)。
3. InnoDB如何解决幻读?
MySQL的InnoDB引擎在“可重复读”级别引入了Next-Key锁机制,它结合了记录锁(Record Lock) 和 间隙锁(Gap Lock) :
- 记录锁:锁定索引中的某一条记录。
- 间隙锁:锁定索引记录之间的间隙,防止其他事务插入新记录。
- Next-Key锁 = 记录锁 + 间隙锁,锁定一个范围,并包含记录本身。
例如,假设某个索引上有值为10、20、30的三条记录,那么当事务A执行SELECT ... FOR UPDATE时,InnoDB会对(负无穷,10)、(10,20)、(20,30)、(30,正无穷)这些区间加锁,防止其他事务插入新的值,从而避免幻读。
4. ANSI SQL标准 vs MySQL实现
根据ANSI SQL标准,“可重复读”不能完全解决幻读,只有“串行化”才能保证。但在MySQL的InnoDB引擎中,通过Next-Key锁机制实现了在“可重复读”级别下避免幻读。
5. 如何验证是否解决幻读?
可以通过两个事务交替操作来进行测试:
- 事务A执行SELECT ... WHERE id > 10 FOR UPDATE
- 事务B尝试INSERT INTO ... (id=15)
- 在“可重复读”下,事务B会被阻塞,直到事务A提交或回滚
这说明InnoDB确实阻止了幻读的发生。
-
代码示例(SQL演示):
-- 设置事务隔离级别为可重复读(默认就是这个级别) SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 开启事务A START TRANSACTION; SELECT * FROM users WHERE age > 25 FOR UPDATE; -- 假设当前表中有age=30的记录 -- 开启事务B START TRANSACTION; INSERT INTO users (name, age) VALUES ('Tom', 28); -- 会被阻塞,因为InnoDB对(25,30)之间加了间隙锁 -- 提交事务A COMMIT; -- 此时事务B的INSERT语句才会成功执行上述例子中,即使在“可重复读”级别下,事务B也不能插入可能导致幻读的新记录,说明MySQL在此级别下确实可以解决幻读。
10.Gmp模型说一下,g什么时候会阻塞
-
正确答案:GMP模型是Go语言运行时系统中用于调度goroutine的机制,其中G代表goroutine,M代表工作线程(machine),P代表处理器(processor)。当一个goroutine执行阻塞操作(如系统调用、channel等待、锁竞争等)时,它会被标记为阻塞状态,调度器会将该G从当前M上移除,并尝试将其他可运行的G分配给该M继续执行。
-
解答思路:
- 首先明确GMP模型的基本结构:G是goroutine,M是操作系统线程,P是逻辑处理器,负责管理一组G的调度。
- 理解goroutine在什么情况下会进入阻塞状态。常见的阻塞操作包括系统调用(如文件读写)、channel通信、sync.Mutex等待、time.Sleep等。
- 当G执行到这些阻塞操作时,运行时系统会检测到该G无法继续执行,于是将其挂起并释放当前占用的M资源,让其他G有机会被调度执行。
- 如果当前M因为G的阻塞而空闲下来,调度器可能会将其与另一个P绑定,继续运行其他G;如果该系统调用是非阻塞的(如网络I/O使用了netpoll机制),则可能不会真正阻塞M。
-
深度知识讲解:
GMP模型详解
- G(Goroutine) :每个G对应一个用户态的协程,包含执行栈、状态、上下文等信息。
- M(Machine) :对应操作系统线程,负责实际执行G中的代码。
- P(Processor) :逻辑处理器,维护本地运行队列(LRQ),负责调度本P上的G,确保每个M都有任务可执行。
调度流程简述:
- 每个M必须绑定一个P才能进行调度。
- P维护着自己的运行队列(runq),里面存放着待运行的G。
- M会不断从绑定的P的runq中取出G来执行。
- 当G发生阻塞时,运行时会将其从当前M上分离,尝试将其他G调度到该M上运行。
常见导致G阻塞的操作:
-
系统调用(syscall) :
- 同步IO操作(如文件读写、socket accept/read/write)
- sleep、time.Sleep
- mutex.Lock、channel操作等
-
Channel通信:
- 当前G尝试接收数据但channel为空,或发送数据但channel满时,G会进入等待状态。
-
锁操作:
- 如sync.Mutex.Lock(),若锁已被占用,则G进入等待。
-
定时器:
- time.Sleep、time.After等会触发内核级休眠或事件通知。
阻塞行为对调度的影响:
- 若G执行的是阻塞式系统调用,则当前M也会被阻塞,此时Go运行时会尝试创建一个新的M来维持P的调度能力。
- 若是非阻塞式系统调用(如基于epoll/kqueue的网络IO),则G会进入等待状态,但M不会被阻塞,可以继续调度其他G。
Go调度器的优化策略:
- 抢占式调度:Go 1.14以后支持基于信号的异步抢占,防止长时间不交出CPU。
- Sysmon监控线程:定期检查所有G的状态,处理超时、死锁等问题。
- Netpoll机制:实现非阻塞IO,避免因网络请求阻塞整个M。
11.mysql提交和回滚那个快
-
正确答案:通常情况下,MySQL的回滚(ROLLBACK)操作比提交(COMMIT)操作要慢。这是因为回滚需要撤销事务中已经执行的操作,而提交只是确认这些操作并将其持久化。
-
解答思路: 提交和回滚都是事务处理中的关键操作。在分析它们的速度差异时,需要从底层机制入手:
- COMMIT 的主要工作是将事务日志(如Redo Log、Undo Log)刷入磁盘,并标记该事务为已提交。这个过程相对轻量。
- ROLLBACK 则需要利用Undo Log来回退所有未提交的更改,包括恢复数据到原始状态,这涉及到更多的I/O操作和资源清理,因此更耗时。
-
深度知识讲解:
1. 事务的基本概念
MySQL支持ACID事务(原子性、一致性、隔离性、持久性),InnoDB引擎通过Redo Log和Undo Log来实现事务的持久性和原子性。
2. Redo Log 和 Undo Log 的作用
- Redo Log:记录物理页修改的内容,用于崩溃恢复时重放事务,确保持久性。
- Undo Log:记录逻辑操作(如插入、删除、更新前的状态),用于回滚事务或MVCC(多版本并发控制)。
3. COMMIT 的流程
当执行COMMIT时,InnoDB会:
- 将事务的Redo Log写入日志缓冲区;
- 根据配置(innodb_flush_log_at_trx_commit)决定是否立即刷盘;
- 更新事务状态为“已提交”;
- 释放锁资源;
- 清理Undo Log(延迟清理,由后台线程完成)。
COMMIT是一个轻量级操作,因为它不需要改变实际的数据页内容,只需要确认事务可以持久化。
4. ROLLBACK 的流程
当执行ROLLBACK时,InnoDB会:
- 使用Undo Log回退事务的所有更改;
- 恢复行的原始版本(MVCC相关);
- 释放锁资源;
- 清理事务上下文。
回滚操作涉及大量I/O读取Undo Log并应用逆向操作,尤其是当事务修改了大量数据时,性能开销显著增加。
12.go gc很消耗cpu,什么方法来减少gc(面试官最后提醒使用和1一样对象池)
-
正确答案:Go语言的垃圾回收(GC)机制是自动进行的,主要用于回收不再使用的堆内存。由于GC会扫描堆中的对象并标记存活对象,因此在大规模内存使用或频繁分配的情况下可能会消耗较多CPU资源。为了减少GC对CPU的消耗,可以采取以下方法:
- 减少堆内存分配:尽量复用对象,例如使用sync.Pool缓存临时对象。
- 对象池技术(sync.Pool) :将短期对象放入对象池中,避免频繁创建和销毁。
- 预分配内存:例如在slice或map初始化时指定容量,避免多次扩容。
- 使用大块内存管理:例如手动管理内存池,减少小对象分配。
- 控制GOGC参数:通过调整GOGC环境变量来控制GC频率与内存占用之间的平衡。
-
解答思路: 首先明确问题背景:Go GC是一个并发标记清除算法,在运行时自动触发。当堆内存增长到一定阈值时就会启动GC,这会导致额外的CPU开销。优化目标是减少GC的触发次数和每次GC的工作量。
解题步骤如下:
- 分析GC的触发条件:堆内存增长、系统空闲时等。
- 思考如何降低堆内存的增长速度,即减少不必要的对象分配。
- 引入对象池、复用机制等手段来减少GC压力。
- 理解GOGC参数的作用及其调优策略。
- 考虑是否可以通过自定义内存管理机制进一步控制内存分配。
-
深度知识讲解:
Go GC的核心机制是基于三色标记法的并发标记清除算法(Concurrent Mark and Sweep),它分为以下几个阶段:
- 标记准备阶段(Mark Setup) :暂停所有goroutine(STW,Stop-The-World),准备开始标记。
- 并发标记阶段(Concurrent Marking) :多个后台线程并发地追踪和标记存活对象。
- 标记终止阶段(Mark Termination) :再次STW,完成最后的标记工作。
- 清理阶段(Sweeping) :并发清理未被标记的对象所占内存。
GC性能瓶颈主要来自两个方面:
- 标记阶段的开销:需要遍历整个对象图,访问每个可达对象。
- 分配频率高导致GC频繁触发:频繁的小对象分配使得堆快速增长,从而频繁触发GC。
因此,优化GC的关键在于:
- 减少对象分配数量
- 提高对象生命周期可控性
- 复用对象,减少无用对象生成
Go运行时提供了一些工具帮助分析GC行为,如pprof、runtime.ReadMemStats等。
sync.Pool 是Go标准库提供的一个本地缓存机制,适用于临时对象的复用。其底层实现为每个P(逻辑处理器)维护一个本地缓存列表,并支持溢出到全局共享池。这样可以减少锁竞争和GC压力。
GOGC参数控制GC的触发阈值,默认值为100,表示当堆内存增长超过上次GC后堆大小的100%时触发下一次GC。增大GOGC可延迟GC触发,但会增加内存使用;减小GOGC则会更早触发GC,节省内存但增加CPU开销。
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:掘金面试群。