国庆假期刚结束,马上就收到了一位粉丝分享的腾讯音乐面试经历。这是一场质量很高的面试,大厂面试官确实实力强劲。

但是由于一直忙于交接和工作,他完全裸面,所以在面试后直呼:“想哭,面试中全程被面试官牵着走,没有掌握主动权将话题引向自己擅长的方向”

所以我建议大家在面试前一定要充分准备,特别是项目相关内容要深入理解每个决策背后的逻辑。

下面看一下他分享的内容吧:

面试内容

(一)开场

  1. 面试官介绍岗位
  • 岗位为腾讯音乐 - 全民K歌 - 国际版 - 直播歌房后台研发工程师,主要负责直播相关功能,开发语言为Go和C++。
  1. 关于求职者基本情况
  • 要求5分钟以内的自我介绍。
  • 被问到所在公司技术团队裁撤后为何会留下自己。
  • 岗位base深圳能否接受。

(二)项目相关

  1. 系统重构 - 数据迁移项目
  • 微服务相关
  • 如何理解微服务,为什么微服务能提升系统可拓展性。
  • 微服务相比于单体服务的缺点。
  • 服务间通信成本指的是什么,成本体现在哪。
  • 数据库迁移原因与成本评估
  • 把数据库从MongoDB迁移到MySQL的原因。
  • 如何评估迁移异构数据库(MongoDB到MySQL)的成本,是否仅考虑成熟度和被接受程度,因为迁移涉及数据同步工作量巨大。
  • 技术选型相关
  • 如果采用最终一致性方案,MySQL支持事务而MongoDB不支持事务,有什么原因不使用最终一致性方案。
  • 数据同步与异常处理
  • 迁移过程中数据同步怎么做(新数据库写MySQL,写脚本迁移Mongo数据到MySQL)。
  • 业务要读写新旧数据时怎么办。
  • 迁移比较暴力,如何发现数据异常,异构数据库迁移时如何保证每条数据正常迁移到MySQL。
  • 唯一键场景下(Mongo已有数据,切换到MySQL写时可能重复写入)的处理。
  1. 优化服务器项目
  • 分布式缓存实现
  • 优化服务器引入分布式缓存技术(具体是Redis),如何实现的。
  • 缓存与数据库一致性
  • 如何保证Redis缓存和数据库的数据一致性(回答缓存先写数据库后写存在问题)。
  • 写缓存成功数据库失败时缓存是否为脏数据。
  • 先写数据库成功再写Redis,缓存更新失败怎么办,如何知道何时将数据库数据同步到缓存及同步的做法。
  • 缓存是否有过期时间机制,有没有不过期一直生效的情况,有过期的话多久过期。
  • 缓存同时过期会有什么问题以及如何解决。
  1. 数据抓取业务项目
  • Kafka相关
  • Kafka在业务中的角色。
  • 业务为什么要经过Kafka这一层,账号量有多大。
  • 定时任务可分散执行时间,Kafka在这种情况下削峰意义不大,是否有其他考虑使用Kafka的因素。
  • 防止账号丢失已有确认机制,当作下游处理失败重新触发即可,为何还需要Kafka。
  1. 抖音微信小游戏归因业务项目
  • Token缓存相关
  • 微信小游戏的token存于sync.map(服务内存),为何使用服务内存而不使用Redis或其他外部缓存方案。
  • 用户量相关,当有一定数量实例和用户量时这种方案是否有缺陷(此处存在理解偏差导致回答失误)。

(三)Go和MySQL

1. MySQL相关

1. MySQL中索引如何实现,为什么MySQL里不使用B树。

在MySQL中,索引的实现主要依赖于特定的数据结构,以提高数据检索的速度。其中最常见的索引类型是基于B+树(B+Tree)的数据结构。

MySQL中的索引实现

  1. B+树索引:

    • MySQL中的大多数存储引擎(如InnoDB和MyISAM)默认使用B+树作为索引的数据结构。
    • B+树的所有叶子节点都处于同一层,且相互之间有指针相连,这样可以方便地进行范围查询。
    • 叶子节点包含了指向记录的指针,而非内部节点,这使得B+树更适合用于全键值、范围或排序操作。
  2. B树索引:

    • 虽然B树也可以用作索引,但在MySQL中使用较少。
    • B树的每个节点都可以存储数据,包括内部节点,这可能导致更多的磁盘I/O操作,因为每次查询可能需要遍历更多的节点。

为什么MySQL里不使用B树

尽管B树在理论上可以用来实现索引,但在MySQL中更倾向于使用B+树,主要原因包括:

  1. 磁盘I/O优化:

    • B+树的所有数据都在叶子节点上,这意味着对于非叶子节点来说,只需要存储键值和指向下一层节点的指针,这使得每个节点可以容纳更多的键值,进一步降低了树的高度,减少了磁盘I/O次数。
  2. 范围查询优化:

    • B+树的叶子节点之间有链接,这使得范围查询更加高效。执行范围查询时,可以从第一个满足条件的叶子节点开始,沿着节点间的链接快速访问后续节点,无需返回上层节点重新寻找下一个满足条件的节点。
  3. 插入删除效率:

    • 在B+树中,插入和删除操作通常只影响叶子节点,不会影响非叶子节点,这简化了树的维护过程,提高了操作效率。
  4. Go相关

2. 切片如何实现

一个切片实际上是一个包含三个字段的结构体:

  • 指针(Pointer):指向底层数组的一个指针。
  • 长度(Length):表示切片当前包含的元素个数。
  • 容量(Capacity):表示从切片指向的数组起始位置到数组末尾的元素个数。

底层结构:

type slice struct {
    array uintptr  // 指向底层数组的指针
    len  int      // 切片的长度
    cap  int      // 切片的容量
}
3. Go里并发读写map会出现问题,如何解决
  1. 使用互斥锁(Mutex)

使用 sync.Mutex 或 sync.RWMutex 对 map 的读写操作进行同步控制。这种方法适用于读写操作都较频繁的场景,示例:

import (
    "sync"
)

type SafeMap struct {
    mu sync.Mutex
    m  map[string]int
}

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = value
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    value, ok := sm.m[key]
    return value, ok
}
  1. 使用 sync.Map:

Go 1.9 版本引入了 sync.Map,这是一个并发安全的 map 实现。sync.Map 适用于读多写少的场景,因为它在读取时不加锁,而在写入时才加锁,从而减少了锁的竞争,示例:

import (
    "sync"
)

var sm sync.Map

func Set(key, value interface{}) {
    sm.Store(key, value)
}

func Get(key interface{}) (interface{}, bool) {
    return sm.Load(key)
}
  1. 使用通道(Channel):

通过通道协调对 map 的访问,确保同一时间只有一个goroutine可以读写 map。

4. Go里的sort排序如何实现

sort 包提供了多种排序功能,可以对不同类型的切片和自定义集合进行排序。sort 包的核心思想是通过实现 sort.Interface 接口来提供排序功能:

sort.Interface 是一个接口,定义了排序所需的三个方法:

  1. Len() :返回要排序的元素个数。
  2. Less(i, j int) :如果第 i 个元素应该排在第 j 个元素之前,则返回 true
  3. Swap(i, j int) :交换第 i 个元素和第 j 个元素的位置。
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

常见类型的排序:

sort 包提供了对常见类型的切片进行排序的便捷函数:

  1. Ints :对 []int 类型的切片进行升序排序。
  2. Float64s :对 []float64 类型的切片进行升序排序。
  3. Strings :对 []string 类型的切片进行升序排序。
5. 协程和线程的区别,线程开销大的原因,协程有上下文切换为何线程消耗更多CPU资源

协程和线程的区别

  1. 资源消耗

    • 线程:每个线程有自己的栈空间(几十KB到几MB),内存消耗大。
    • 协程:栈空间较小(几KB),动态调整,内存消耗小。
  2. 调度方式

    • 线程:由操作系统控制,抢占式调度,涉及内核态切换。
    • 协程:由应用程序控制,协作式调度,在用户态进行。
  3. 上下文切换

    • 线程:涉及内核态和用户态切换,开销大。
    • 协程:只在用户态进行,开销小。
  4. 并发粒度

    • 线程:适用于处理多个相对独立的任务。
    • 协程:适用于在单个线程内实现多个任务的协作和切换。

线程开销大的原因

  1. 内存分配

    • 每个线程需要固定大小的栈空间,占用大量内存。
  2. 上下文切换

    • 涉及内核态和用户态切换,需要保存和恢复大量寄存器状态和栈指针。
  3. 调度开销

    • 操作系统维护复杂的调度算法和数据结构,增加系统开销。
  4. 同步机制

    • 使用锁、信号量等机制,高并发场景下可能导致性能瓶颈。

协程消耗更少CPU资源的原因

  1. 上下文切换简单

    • 只在用户态进行,不需要内核态介入,开销小。
  2. 调度灵活

    • 开发者控制调度,按需切换,减少不必要的上下文切换。
  3. 内存管理高效

    • 栈空间小且动态调整,内存利用率高。
  4. 同步机制简单

    • 通常通过通道(如Go语言中的channel)实现同步,避免锁的竞争。

(四)算法与反问

  1. 算法手撕
  2. 反问

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以私信我。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐