还没分享过小米的面经呢,今天它来了

wangzhongyang007 · · 115 次点击 · · 开始浏览    

今天分享的面经是**小米的一面**,之前还没有分享过相关的,来看看难度如何。 内容我都整理好了: ![](https://files.mdnice.com/user/76962/f35b35cf-77c1-4733-9d5c-5e5b57ad251b.png) 1. 自我介绍+项目问题 ## 2. 讲讲GOLANG的CSP和三色标记法 **CSP 是一种并发编程模型,而三色标记法是一种垃圾回收算法。** ### Golang 的 CSP 模型 Go 语言的创建者们受到 CSP 模型的影响,在设计 Go 时引入了 goroutines 和 channels 来简化并发编程。CSP 模型强调通过消息传递来实现进程间的通信,而不是共享内存。在 Go 中: - **Goroutines**:是轻量级的线程,由 Go 运行时调度。程序员可以轻松地启动成千上万个 goroutines。 - **Channels**:是用于 goroutine 之间通信的管道。它们提供了一种类型安全的方式来进行同步和通信,避免了传统多线程编程中常见的竞态条件等问题。 使用 CSP 模型,开发者可以通过简单的语法和结构化的方式来编写高效的并发程序,这**大大降低了并发编程的复杂性**。 ### Golang 的三色标记法 三色标记法是一种用于垃圾回收(Garbage Collection, GC)的技术,它将堆中的对象分为三种颜色:白色、灰色和黑色,以追踪哪些对象是可达的,哪些是不可达的(即垃圾),从而进行回收。 - **白色对象**:表示这些对象尚未被垃圾回收器访问到,被认为是潜在的垃圾对象。 - **灰色对象**:表示这些对象已被垃圾回收器访问到,但是还需要进一步处理其引用的对象。 - **黑色对象**:表示这些对象已经被完全扫描过,确定为存活对象。 三色标记法的过程大致分为以下几个阶段: 1. **Mark Setup (标记准备)** :开启写屏障等准备工作。 2. **Marking (标记)** :从根集合开始,递归地将可达对象标记为灰色或黑色。 3. **Mark Termination (标记结束)** :关闭写屏障,并计算下一次清理的目标。 4. **Sweeping (清理)** :回收所有白色的不可达对象。 为了确保三色标记的正确性,Golang 使用了**插入屏障**和**删除屏障**等机制来维护所谓的“三色不变式”,防止在并发环境中出现对象丢失的问题。 ## 3. 为什么说索引会加速mysql的查询?背后原理是什么? 索引在MySQL中可以显著加速查询的速度,其背后的原理主要依赖于它如何优化数据的访问路径。 ### 索引的工作原理 1. **减少I/O操作**:当没有索引时,MySQL需要扫描整个表(全表扫描)来找到符合条件的数据行,这可能涉及到大量的磁盘读取操作。而有了索引之后,MySQL可以通过索引来定位数据的位置,减少了不必要的磁盘I/O次数。 2. **二分查找法**:对于B-Tree或B+Tree类型的索引,它们允许使用二分查找算法,将每次查找的时间复杂度降低至O(log n),即对数级别。这意味着随着数据量的增长,查找所需的时间不会成比例增加。 3. **范围查询的支持**:某些类型的索引(如B-Tree/B+Tree)不仅支持等值匹配,还能够高效处理范围查询(例如`>`、`<`、`BETWEEN`等)。这是因为这些索引按照键值有序排列,使得范围内的记录可以连续地存放在一起。 4. **排序与分组优化**:如果查询包含`ORDER BY`或`GROUP BY`子句,并且所涉及的列已经建立了适当的索引,则MySQL可以直接利用索引顺序返回结果集,避免了额外的排序步骤。 5. **覆盖索引**:如果一个索引包含了查询所需的所有字段(即所谓的“覆盖索引”),那么MySQL可以直接从索引中获取数据,而不需要回表查找实际的数据行,进一步提高了性能。 6. **最左前缀原则**:对于复合索引(多列索引),MySQL会根据最左前缀原则来决定是否可以使用该索引。例如,如果你有一个`(col1, col2)`的复合索引,那么查询条件中只要包含了`col1`就可以用到这个索引;但如果只提供了`col2`作为查询条件,则无法使用此索引。 7. **减少锁争用**:通过减少需要扫描的数据量,索引还可以间接减少并发查询之间的锁冲突,提高系统的整体吞吐量。 ## 4. 讲一讲聚簇索引和非聚簇索引与回表查询的联系 **聚簇索引和非聚簇索引**是数据库中两种不同类型的索引,它们在数据存储方式以及查询性能上有着重要的区别。 **回表查询**是指当通过非聚簇索引查找数据时,如果索引中不包含查询所需的所有列,则需要再次访问实际的数据行来获取剩余的信息。 ### 聚簇索引 - **数据存储**:在一个表上只能有一个聚簇索引,因为聚簇索引决定了数据行在磁盘上的物理存储顺序。这意味着表中的记录按照聚簇索引键的排序进行物理存放。 - **查询效率**:对于基于聚簇索引键的查询,可以直接定位到相应的数据行,无需额外的I/O操作。因此,对于等值匹配、范围扫描等操作,聚簇索引通常能提供更高的查询性能。 - **回表查询**:由于聚簇索引包含了完整的行数据,所以在大多数情况下不需要回表查询,除非查询条件涉及到了不在聚簇索引中的列。 ### 非聚簇索引 - **数据存储**:非聚簇索引是独立于数据行存储的,它只包含索引列和指向对应数据行的指针(通常是主键或聚簇索引键)。一个表可以有多个非聚簇索引。 - **查询效率**:非聚簇索引能够加速对特定字段的查询,特别是当这些字段不是聚簇索引的一部分时。然而,当查询涉及到非索引列时,就可能需要进行回表查询。 - **回表查询**:当使用非聚簇索引来查找数据时,如果查询所需的全部列没有被包含在该索引内,MySQL必须先从非聚簇索引中找到对应的聚簇索引键(或行指针),然后再根据这个键去访问实际的数据行以获取其他信息。这种两次访问的过程就是所谓的“回表”。 ### 回表查询的影响 - **性能影响**:回表查询增加了额外的I/O开销,因为它要求数据库引擎执行两次查找:一次是在非聚簇索引中查找,另一次是从表中读取完整行数据。这会导致查询速度变慢,尤其是在高并发环境下。 - **优化策略**: - **覆盖索引**:尽量创建包含所有查询所需字段的复合索引,使得查询可以在索引层完成而不需要回表。 - **选择合适的索引列**:确保经常用于过滤、排序或分组的列被包含在索引中,以减少回表查询的可能性。 - **评估索引成本**:考虑到维护索引所带来的插入、更新和删除操作的成本,合理权衡索引的数量和类型。 ## 5. MYSLQ有哪些锁 MySQL 中主要的锁类型包括: - **表级锁(Table-level Locks)**:锁定整个表,分为读锁和写锁。读锁允许多个事务同时持有,但不允许写入;写锁则是排他的,确保只有一个事务可以写入。 - **行级锁(Row-level Locks)**:仅锁定被操作的数据行,适用于InnoDB存储引擎,提供更高的并发性。 - **页级锁(Page-level Locks)**:锁定数据页,介于表级锁和行级锁之间,较少使用,例如在BDB(Berkeley DB)存储引擎中。 - **意向锁(Intention Locks)**:表示事务想要在表中的某些行上获取锁,用于处理多粒度锁定问题。 - **元数据锁(Metadata Lock, MDL)**:控制对表结构的访问,防止在查询过程中表结构被修改。 - **间隙锁(Gap Locks)**、**临键锁(Next-Key Locks)** 和 **插入意向锁(Insert Intention Locks)**:这些是InnoDB特有的锁机制,主要用于防止幻读和其他并发问题。 ## 6. mysql事务隔离级别 **上一篇面经有个一模一样的问题**,想知道详细答案的可以看我上一篇分享的内容。 ## 7. redis实现分布式锁用哪些命令? 在 Redis 中实现分布式锁,通常会使用以下命令。这些命令能够确保锁的原子性和排他性,从而保证在分布式环境中只有一个客户端能获得锁。以下是常用的命令及其简要说明: 1. **SETNX (SET if Not eXists)**: - 用于尝试设置一个键值对,只有当键不存在时才会成功。这可以用来模拟加锁的行为。 - 命令格式:`SETNX key value` - 如果返回1表示成功获取锁;如果返回0表示已经有其他客户端持有该锁。 2. **EXPIRE** 或者 `SET ... EX`: - 设置或更新键的有效期(TTL, Time To Live),以防止死锁。可以在 `SETNX` 成功之后立即调用 `EXPIRE` 来设置过期时间,或者直接使用 `SET key value EX seconds` 在一个原子操作中完成这两步。 3. **GETSET**: - 获取当前键的值并设置新值。虽然这不是创建锁的标准方法,但在某些场景下可用于锁的续期或释放。 - 命令格式:`GETSET key value` 4. **DEL** 或 **UNLOCK Script**: - 释放锁。重要的是,在删除键之前应该检查这个锁是否是由当前客户端持有的(例如通过比较存储在键中的值)。为了安全地执行这个操作,通常会使用 Lua 脚本来确保这一过程是原子性的。 - 使用 Lua 脚本的例子如下: ```lua EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock_key client_value ``` 5. **EVAL / EVALSHA**: - 执行 Lua 脚本,常用于实现更复杂的锁逻辑,比如加锁、解锁和续期等操作。Lua 脚本提供了一种原子化的方式来进行多步骤的操作,确保了在高并发环境下的正确性。 ### Redlock 算法 对于更高的一致性和可靠性需求,Redis 还提供了 Redlock 算法,它是一个基于多个独立 Redis 实例的分布式锁算法。Redlock 的思想是在多个 Redis 实例上同时尝试获取锁,并根据多数原则决定最终是否成功获取锁。这增加了系统的容错能力。 ## 8. GOLANG的map该如何去解决并发安全问题? 在 Go 语言中,内置的 `map` 并不是线程安全的数据结构,即它不支持多个 goroutine 同时进行读写操作。如果多个 goroutine 同时对同一个 map 进行写入或更新,可能会导致竞争条件(race condition),进而引发程序崩溃或不可预测的行为。 为了确保 map 在并发环境下的安全性,可以采取以下几种方法: ### 1. 使用 sync.Map Go 标准库提供了 `sync.Map`,这是一个专门为并发访问设计的映射类型。它内部实现了锁机制来保证并发安全,适用于那些需要频繁读取但偶尔才更新的情况。`sync.Map` 提供了基本的键值对存储功能,并且比普通 map 更加高效地处理并发场景。 ### 2. 使用互斥锁 (Mutex) 对于更复杂的逻辑或者你需要控制多个资源之间的同步时,可以使用 `sync.Mutex` 或者 `sync.RWMutex` 来保护 map 的访问。这种方式允许你在代码中显式地定义哪些部分是临界区(critical section),从而避免竞态条件。 - **sync.Mutex**:提供了一个简单的互斥锁,一次只允许一个 goroutine 访问被锁定的代码段。 - **sync.RWMutex**:提供了读写锁,允许多个读者同时读取数据,但在有写者时会阻止所有其他读写操作。 ### 3. 使用 Channel 和 Select 如果你的应用场景允许,还可以通过 channel 和 select 结构来协调不同 goroutine 对共享资源的访问。这种方法虽然不如前两种直接,但在某些特定情况下可能更为合适。 ### 4. 封装成原子操作 对于简单的计数器或者其他可以表达为原子操作的需求,可以考虑使用 `atomic` 包提供的函数,尽管这并不直接适用于 map 类型。 ## 9. sync.map的实现原理了解吗? `sync.Map` 是 Go 标准库提供的一个并发安全的映射类型,其设计目的是为了在高并发环境下提供高效的读写操作。它内部实现了一些优化措施来减少锁争用,并且针对不同的访问模式进行了专门的设计。下面是 `sync.Map` 的一些关键实现原理: ### 1. 内部结构 `sync.Map` 的核心是由多个字段组成的结构体,其中包括: - **`mu`** :互斥锁(`Mutex`),用于保护对整个 `sync.Map` 的修改。 - **`reads`** 和 **`writes`** :两个缓存层,分别用于存储读取和写入的数据项。这两个字段都是无锁的数据结构,旨在提高并发读取时的性能。 - **`dirty`** :一个未同步的 map (`map[interface{}]*entry`),用来存放最近更新或新插入的键值对。`dirty` 中的元素最终会被迁移到 `reads` 中。 - **`misses`** :计数器,记录自上次清理以来未命中次数。当未命中的次数达到一定阈值时,触发一次迁移操作。 ### 2. 读路径 对于读操作,`sync.Map` 首先尝试从 `reads` 缓存中查找数据。如果找不到,则会检查 `dirty` 地图。如果仍然找不到并且存在未命中情况,那么会在持有全局锁的情况下进行一次完整的查找。为了避免频繁的全表扫描,`sync.Map` 采用了一种懒惰的方式处理未命中——只有在累积了一定量的未命中之后才会执行全面搜索并更新 `reads` 缓存。 ### 3. 写路径 写操作总是通过获取全局锁来进行,以确保线程安全。每次写入后,相应的键值对会被添加到 `dirty` 地图中。随着写操作的增加,`dirty` 地图逐渐变大。当 `dirty` 地图变得足够大或者发生了多次未命中时,`sync.Map` 会将 `dirty` 地图的内容复制到一个新的 `reads` 缓存中,同时清空 `dirty` 地图,以此来保证后续读操作能够快速定位到最新的数据。 ### 4. 清理与迁移 `sync.Map` 有一个内置的机制来定期清理过期的数据以及将 `dirty` 地图中的最新数据迁移到 `reads` 缓存中。这个过程是自动完成的,不需要开发者手动干预。迁移操作会在适当的时候触发,例如当检测到较多的未命中时。 ### 5. 空间换时间 `sync.Map` 采用了空间换时间的策略,即牺牲一定的内存占用来换取更好的并发性能。比如,它允许 `dirty` 地图和 `reads` 缓存之间存在一定的时间差,这期间可能会有重复的数据存在,但这样可以显著减少全局锁的使用频率,从而提高了整体效率。 ## 10. 讲讲map的扩容机制 当 `map` 中的元素数量增加到一定程度时,它会触发扩容操作以适应更多的键值对存储需求。 ### 初始分配 - 当你创建一个新的 `map` 时,Go 运行时会为其分配一个初始的桶(bucket)数组。每个桶可以容纳若干个键值对(通常是8个)。这个初始大小是根据估计的使用情况来决定的,但如果不确定具体的大小,Go 会选择一个较小的默认值。 ### 桶与溢出桶 - 每个 `map` 内部由一系列的桶组成,每个桶负责存储一定范围内的键值对。如果一个桶装满了(即达到了最大容量),而又有新的键值对要插入,则该桶会产生一个或多个溢出桶(overflow buckets),这些溢出桶链接在一起形成链表。 ### 扩容条件 - **负载因子**:当 `map` 的负载因子(即所有桶中存储的键值对总数除以桶的数量)超过某个阈值时,就会触发扩容。通常情况下,当负载因子接近1或者更大时,意味着大部分桶都已经填满,此时进行扩容是有益的。 - **溢出桶过多**:如果某些桶产生了过多的溢出桶,即使整体负载因子还未达到阈值,也可能触发扩容。这是因为过多的溢出桶会导致性能下降,特别是在遍历 `map` 或者查找特定键时。 ### 扩容过程 - **复制数据**:在扩容过程中,`map` 会创建一个新的、更大的桶数组,并将现有数据重新分布到新数组中。这包括计算每个键对应的哈希值并确定其在新数组中的位置。为了保证扩容期间的线程安全,Go 在扩容时会对 `map` 加锁。 - **两倍增长**:每次扩容后,`map` 的容量通常会翻倍。例如,如果原来的桶数组大小为8,扩容后的大小将是16,以此类推。这种指数级的增长有助于减少频繁的扩容操作。 - **渐进式迁移**:从 Go 1.12 版本开始,引入了渐进式迁移策略。这意味着并不是一次性完成所有数据的迁移,而是随着新键值对的插入逐步完成。这样可以在一定程度上缓解扩容带来的性能冲击。 ## 11. 遇到panic异常怎么解决?怎么去定位Panic? 在 Go 语言中,`panic` 是一种非正常程序终止的方式,通常用于处理不应该发生的情况。当一个 `panic` 被触发时,程序会立即停止当前的执行流程,并开始回溯调用栈,直到遇到 `recover` 语句或者程序退出。为了有效地解决和定位 `panic` 异常,你可以采取以下几个步骤: ### 解决 Panic 1. **使用 recover 捕获 panic**: - 在可能引发 `panic` 的代码段外围包裹一层 `defer` 和 `recover`,以便能够在不期望的情况下捕获异常并优雅地处理它。 ```go func safeCall() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() // 可能引发 panic 的代码 } ``` 2. **避免不必要的 panic**: - 检查代码逻辑,尽量减少可能导致 `panic` 的操作,比如数组越界、nil 指针解引用等。通过提前检查条件或使用更安全的方法来替代。 3. **确保资源正确释放**: - 如果 `panic` 发生在持有锁或其他资源的情况下,确保这些资源能够被正确释放,以防止资源泄露。可以使用 `defer` 来保证资源的释放。 4. **编写健壮的错误处理机制**: - 不要依赖 `panic` 来进行常规错误处理;相反,应该设计良好的错误返回路径,并在必要时向上传播错误信息。 ### 定位 Panic 1. **启用调试模式**: - 使用 `-race` 标志编译和运行你的程序,可以帮助检测并发相关的竞争条件,这可能是导致 `panic` 的原因之一。 2. **查看堆栈跟踪信息**: - 当 `panic` 发生时,Go 会打印出详细的堆栈跟踪(stack trace),包括函数调用链和每层调用的位置。仔细阅读这些信息有助于快速定位问题所在。 3. **日志记录**: - 在关键位置添加详细的日志输出,特别是围绕那些容易出现问题的地方。这不仅有助于理解程序的执行流,还可以为后续分析提供线索。 4. **单元测试与集成测试**: - 编写全面的测试用例覆盖各种边界情况,确保所有代码路径都能正常工作。对于已知会导致 `panic` 的场景,可以在测试中模拟并验证是否得到了适当的处理。 5. **工具辅助**: - 利用静态分析工具如 `golangci-lint` 或者动态分析工具如 Delve 调试器,它们可以帮你发现潜在的问题点,甚至直接指向引起 `panic` 的具体行号。 6. **二分法排查**: - 如果你无法立即找到问题的原因,可以尝试移除部分代码或将代码分割成更小的部分逐步测试,以此缩小可疑范围。 7. **环境一致性**: - 确保开发、测试和生产环境尽可能一致,因为某些 `panic` 可能是由于环境差异引起的。 ## 12. 举例docker的使用场景 Docker 是一种容器化平台,它允许开发者将应用程序及其依赖打包到一个独立的、轻量级的容器中,从而确保应用程序在任何环境中都能一致地运行。以下是 Docker 的一些典型使用场景: ### 1. 开发环境一致性 - **问题**:不同开发者的机器配置各异,导致“在我的机器上能正常工作”的问题频繁出现。 - **解决方案**:通过 Docker,可以创建统一的开发环境镜像,确保每个开发者使用的工具和依赖版本完全相同。这不仅提高了团队协作效率,还减少了因环境差异带来的调试时间。 ### 2. 持续集成与持续部署(CI/CD) - **问题**:自动化测试和部署过程中需要频繁切换不同的环境设置。 - **解决方案**:Docker 容器可以在 CI/CD 流水线中快速启动和销毁,提供一致且隔离的测试环境。每次构建都可以从相同的基线开始,保证了结果的可重复性和可靠性。 ### 3. 微服务架构 - **问题**:随着系统规模扩大,管理多个相互依赖的服务变得复杂。 - **解决方案**:每个微服务可以被打包成独立的 Docker 容器,简化了服务之间的部署、扩展和维护。借助 Docker Compose 或 Kubernetes 等工具,还可以轻松定义和管理多容器应用。 ### 4. 应用程序隔离 - **问题**:在同一台物理服务器上运行多个应用程序时,可能会发生资源竞争或冲突。 - **解决方案**:Docker 提供了进程级别的隔离,使得不同应用可以在各自的容器中安全地运行,彼此之间不会互相干扰。同时,Docker 还支持对 CPU、内存等资源进行细粒度控制,确保公平分配。 除此之外还有其他的用法,我就不一一举例了。 ## 代码题: leetcode 3218.切蛋糕的最小总开销 I > 力扣上面有详细的答案,我这里就不多解释了。 ## 欢迎关注 ❤ 我们搞了一个**免费的面试真题共享群**,互通有无,一起刷题进步。 **没准能让你能刷到自己意向公司的最新面试题呢。** 感兴趣的朋友们可以加我微信:**wangzhongyang1993**,备注:面试群。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

115 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2025-01-01 18:07:33
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传