首页 > 其他分享 >腾讯互娱面经,希望别凉

腾讯互娱面经,希望别凉

时间:2024-05-11 10:41:56浏览次数:13  
标签:互娱 删除 索引 过期 别凉 面经 实现 func 使用

面试题详解

Go接口

接口在Golang中扮演着连接不同类型之间的桥梁,它定义了一组方法的集合,而不关心具体的实现。接口的作用主要体现在以下几个方面:

多态性:

接口允许不同的类型实现相同的方法,从而实现多态性。这意味着我们可以使用接口类型来处理不同的对象,而不需要关心具体的类型。

package main

import "fmt"

type Animal interface {
	Sound() string
}

type Dog struct{}

func (d Dog) Sound() string {
	return "Woof!"
}

type Cat struct{}

func (c Cat) Sound() string {
	return "Meow!"
}

func main() {
	animals := []Animal{Dog{}, Cat{}}

	for _, animal := range animals {
		fmt.Println(animal.Sound())
	}
}

在上面的示例中,我们定义了一个Animal接口,它包含了一个Sound()方法。然后,我们实现了Dog和Cat两个结构体,分别实现了Sound()方法。通过将Dog和Cat类型赋值给Animal接口类型,我们可以在循环中调用Sound()方法,而不需要关心具体的类型。这就体现了接口的多态性,不同的类型可以实现相同的接口方法。

解耦合:

接口可以将抽象与实现分离,降低代码之间的耦合度。通过定义接口,我们可以将实现细节隐藏起来,只暴露必要的方法,从而提高代码的可维护性和可读性。

package main

import "fmt"

type Printer interface {
	Print(string)
}

type ConsolePrinter struct{}

func (cp ConsolePrinter) Print(message string) {
	fmt.Println(message)
}

type FilePrinter struct{}

func (fp FilePrinter) Print(message string) {
	// 将消息写入文件
	fmt.Println("Writing message to file:", message)
}

func main() {
	printer := ConsolePrinter{}
	printer.Print("Hello, World!")

	printer = FilePrinter{}
	printer.Print("Hello, World!")
}

在上面的示例中,我们定义了一个Printer接口,它包含了一个Print()方法。然后,我们实现了ConsolePrinter和FilePrinter两个结构体,分别实现了Print()方法。通过将不同的结构体赋值给Printer接口类型的变量,我们可以在主函数中调用Print()方法,而不需要关心具体的实现。这样,我们可以根据需要轻松地切换不同的打印方式,实现了解耦合。

可扩展性:

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return 3.14 * c.Radius * c.Radius
}

func main() {
	shapes := []Shape{Rectangle{Width: 5, Height: 10}, Circle{Radius: 3}}

	for _, shape := range shapes {
		fmt.Println("Area:", shape.Area())
	}
}

在上面的示例中,我们定义了一个Shape接口,它包含了一个Area()方法。然后,我们实现了Rectangle和Circle两个结构体,分别实现了Area()方法。通过将不同的结构体赋值给Shape接口类型的切片,我们可以在循环中调用Area()方法,而不需要关心具体的类型。这样,当我们需要添加新的形状时,只需要实现Shape接口的Area()方法即可,而不需要修改已有的代码。这就实现了代码的可扩展性。

接口的应用场景

  1. API设计:接口在API设计中起到了至关重要的作用。通过定义接口,我们可以规范API的输入和输出,提高代码的可读性和可维护性。
  2. 单元测试:接口在单元测试中也扮演着重要的角色。通过使用接口,我们可以轻松地替换被测试对象的实现,从而实现对被测代码的独立测试。
  3. 插件系统:接口可以用于实现插件系统,通过定义一组接口,不同的插件可以实现这些接口,并在程序运行时动态加载和使用插件。
  4. 依赖注入:接口在依赖注入中也有广泛的应用。通过定义接口,我们可以将依赖对象的创建和管理交给外部容器,从而实现松耦合的代码结构。

空结构体的用途

不包含任何字段的结构体,就叫做空结构体。

空结构体的特点:

  1. 零内存占用
  2. 地址都相同
  3. 无状态

空结构体的使用场景

  • 实现set集合

在 Go 语言中,虽然没有内置 Set 集合类型,但是我们可以利用 map 类型来实现一个 Set 集合。由于 map 的 key 具有唯一性,我们可以将元素存储为 key,而 value 没有实际作用,为了节省内存,我们可以使用空结构体作为 value 的值。

package main

import "fmt"

type Set[K comparable] map[K]struct{}

func (s Set[K]) Add(val K) {
   s[val] = struct{}{}
}
func (s Set[K]) Remove(val K) {
   delete(s, val)
}

func (s Set[K]) Contains(val K) bool {
   _, ok := s[val]
   return ok
}

func main() {
   set := Set[string]{}
   set.Add("程序员")
   fmt.Println(set.Contains("程序员")) // true
   set.Remove("程序员")
   fmt.Println(set.Contains("程序员")) // false
}
  • 用于通道信号

空结构体常用于 Goroutine 之间的信号传递,尤其是不关心通道中传递的具体数据,只需要一个触发信号时。例如,我们可以使用空结构体通道来通知一个 Goroutine 停止工作:

package main  
  
import (  
   "fmt"  
   "time"  
)  
  
func main() {  
   quit := make(chan struct{})  
   go func() {  
      // 模拟工作  
      fmt.Println("工作中...")  
      time.Sleep(3 * time.Second)  
      // 关闭退出信号  
      close(quit)
   }()  
  
   // 阻塞,等待退出信号被关闭  
   <-quit  
   fmt.Println("已收到退出信号,退出中...")  
}
  • 作为方法接收器

有时候我们需要创建一组方法集的实现(一般来说是实现一个接口),但并不需要在这个实现中存储任何数据,这种情况下,我们可以使用空结构体来实现:

type Person interface {
   SayHello()
   Sleep()
}

type CMY struct{}

func (c CMY) SayHello() {
   fmt.Println("你好,世界。")
}

func (c CMY) Sleep() {
   fmt.Println("晚安,世界...")
}

Go原生支持默认参数或可选参数吗,如何实现

什么是默认参数

默认参数是指在函数调用时,如果没有提供某个参数的值,那么使用函数定义中指定的默认值。这种语言特性可以减少代码量,简化函数的使用。

在Go语言中,函数不支持默认参数。这意味着如果我们想要设置默认值,那么就需要手动在函数内部进行处理。

例如,下面是一个函数用于计算两个整数的和:

func Add(a int, b int) int {
   
    return a + b
}

如果我们希望b参数有一个默认值,例如为0,那么可以在函数内部进行处理:

func AddWithDefault(a int, b int) int {
   
    if b == 0 {
   
        b = 0
    }
    return a + b
}

上面的代码中,如果b参数没有提供值,那么默认为0。通过这种方式,我们就实现了函数的默认参数功能。

需要注意的是,这种处理方式虽然可以实现默认参数的效果,但会增加代码复杂度和维护难度,因此在Go语言中不被推荐使用。

什么是可选参数

可选参数是指在函数调用时,可以省略一些参数的值,从而让函数更加灵活。这种语言特性可以让函数更加易用,提高代码的可读性。

在Go语言中,函数同样不支持可选参数。但是,我们可以使用可变参数来模拟可选参数的效果。

下面是一个函数用于计算任意个整数的和:

func Add(nums ...int) int {
   
    sum := 0
    for _, num := range nums {
   
        sum += num
    }
    return sum
}

上面的代码中,我们使用...int类型的可变参数来接收任意个整数,并在函数内部进行求和处理。

如果我们希望b和c参数为可选参数,那么可以将它们放到nums可变参数之后:

func AddWithOptional(a int, nums ...int) int {
   
    sum := a
    for _, num := range nums {
   
        sum += num
    }
    return sum
}

上面的代码中,我们首先将a参数赋值给sum变量,然后对可变参数进行求和处理。如果函数调用时省略了nums参数,则sum等于a的值。

需要注意的是,使用可变参数模拟可选参数的效果虽然能够实现函数的灵活性,但也会降低代码的可读性和规范性。因此在Go语言中不被推荐使用。

defer执行顺序

在 Go 中,defer 语句用于延迟(defer)函数的执行,通常用于在函数执行结束前执行一些清理或收尾工作。当函数中存在多个 defer 语句时,它们的执行顺序是“后进先出”(Last In First Out,LIFO)的,即最后一个被延迟的函数最先执行,倒数第二个被延迟的函数次之,以此类推。

在 Go 中,defer 语句中的函数在执行时会被压入一个栈中,当函数执行结束时,这些被延迟的函数会按照后进先出的顺序执行。这意味着在函数中的 defer 语句中的函数会在函数执行结束前执行,包括在 return 语句之前执行。

协程之间信息如何同步

协程(Goroutine)之间的信息同步通常通过通道(Channel)来实现。通道是 Go 语言中用于协程之间通信的重要机制,可以安全地在不同协程之间传递数据,实现协程之间的信息同步。

一些常见的方法来实现协程之间的信息同步:

  1. 使用无缓冲通道:无缓冲通道是一种同步通道,发送和接收操作会在数据准备好之前被阻塞。通过无缓冲通道,可以实现协程之间的同步通信,确保数据的正确传递。

  2. 使用带缓冲通道:带缓冲通道允许在通道中存储一定数量的元素,发送操作只有在通道已满时才会被阻塞。通过带缓冲通道,可以实现异步通信。

  3. 使用同步原语:Go 语言中的 sync 包提供了一些同步原语,如互斥锁(Mutex)、读写锁(RWMutex)等,可以用于协程之间的同步访问共享资源。

  4. 使用select语句:select 语句可以用于在多个通道操作中选择一个执行,可以实现协程之间的多路复用和超时控制。

  5. 使用context包:context 包提供了一种在协程之间传递取消信号和截止时间的机制,可以用于协程之间的协调和同步。

GMP模型

GM模型开销大的原因?

最开始的是GM模型没有 P 的,是M:N的两级线程模型,但是会出现一些性能问题:

  • 全局队列的锁竞争。M从全局队列中添加或获取 G 的时候,都是需要上锁的(下图执行步骤要加锁),这样就会导致锁竞争,虽然达到了并发安全,但是性能是非常差的。
  • M 转移 G 会有额外开销。M 在执行 G 的时候,假设 M1 执行的 G1 创建了 G2,新创建的就要放到全局队列中去,但是这时有一个空闲的 M2 获取到了 G2,那么这样 G1、G2 会被不同的 M 执行,但是 M1 中本来就有 G2 的信息,M2 在 G1 上执行是更好的,而且取和放到全局队列也会来回加锁,这样都会有一部分开销。
  • 线程的使用效率不能最大化。M 拿不到的时候就会一直空闲,阻塞的时候也不会切换。也就是没有 workStealing 机制和 handOff 机制。

GMP

go生成一个协程,此时放在P中还是M中

如果在本地的队列中有足够的空间,则会直接进入本地队列等待M的执行;如果本地队列已经满了,则进入全局队列(在GMP模型中,所有的M都可以从全局队列中获取协程并执行)

G阻塞,M、P如何

当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand off,并寻找新的idle的M,若没有idle的M就会新建一个M

Redis与MySQL数据如何同步

这里提供几种方案:

  1. 定时任务同步:编写定时任务或脚本,定期从 MySQL 中读取数据,然后将数据同步到 Redis 中。这种方案简单直接,适用于数据量不大且同步频率不高的场景。

  2. 使用消息队列:将 MySQL 中的数据变更操作(如新增、更新、删除)通过消息队列(如 RabbitMQ、Kafka)发送到消息队列中,然后消费者从消息队列中读取消息,将数据同步到 Redis 中。这种方案实现了异步同步,降低了对 MySQL 的压力。

  3. 使用数据库触发器:在 MySQL 中设置触发器,当数据发生变更时触发触发器,将变更信息发送到消息队列或直接同步到 Redis 中。这种方案可以实现实时同步,但需要谨慎设计触发器逻辑,避免影响数据库性能。

  4. 使用数据同步工具:可以使用一些数据同步工具(如 Maxwell、Debezium)来实现 MySQL 和 Redis 数据的实时同步。这些工具可以监控 MySQL 数据库的变更,并将变更数据同步到 Redis 中。

  5. 使用缓存库:一些缓存库(如 Redisson、Lettuce)提供了与 MySQL 数据库的集成,可以通过配置简单地实现 MySQL 数据到 Redis 的同步。

Explain的字段

explain的用法:

explain select * from gateway_apps;

返回结果:


下面对上面截图中的字段一一解释:

1、id:select 查询序列号。id相同,执行顺序由上至下;id不同,id值越大优先级越高,越先被执行。

2、select_type:查询数据的操作类型,其值如下:

  • simple:简单查询,不包含子查询或 union
  • primary:包含复杂的子查询,最外层查询标记为该值
  • subquery:在 select 或 where 包含子查询,被标记为该值
  • derived:在 from 列表中包含的子查询被标记为该值,MySQL 会递归执行这些子查询,把结果放在临时表
  • union:若第二个 select 出现在 union 之后,则被标记为该值。若 union 包含在 from 的子查询中,外层 select 被标记为 derived
  • union result:从 union 表获取结果的 select

3、table:显示该行数据是关于哪张表

4、partitions:匹配的分区

5、type:表的连接类型,其值,性能由高到底排列如下:

  • system:表只有一行记录,相当于系统表
  • const:通过索引一次就找到,只匹配一行数据
  • eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常用于主键或唯一索引扫描
  • ref:非唯一性索引扫描,返回匹配某个单独值的所有行。用于=、< 或 > 操作符带索引的列
  • range:只检索给定范围的行,使用一个索引来选择行。一般使用between、>、<情况
  • index:只遍历索引树
  • ALL:全表扫描,性能最差

6、 possible_keys:显示 MySQL 理论上使用的索引,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。如果该值为 NULL,说明没有使用索引,可以建立索引提高性能

7、key:显示 MySQL 实际使用的索引。如果为 NULL,则没有使用索引查询

8、key_len:表示索引中使用的字节数,通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好 显示的是索引字段的最大长度,并非实际使用长度

9、ref:显示该表的索引字段关联了哪张表的哪个字段

10、 rows:根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好

11、filtered:返回结果的行数占读取行数的百分比,值越大越好

12、extra:包含不合适在其他列中显示但十分重要的额外信息,常见的值如下:

  • using filesort:说明 MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。出现该值,应该优化 SQL
  • using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。出现该值,应该优化 SQL
  • using index:表示相应的 select 操作使用了覆盖索引,避免了访问表的数据行,效率不错
  • using where:where 子句用于限制哪一行
  • using join buffer:使用连接缓存
  • distinct:发现第一个匹配后,停止为当前的行组合搜索更多的行

Redis过期删除策略

Redis 中提供了三种过期删除的策略

定时删除

在设置某个 key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

优点:

对 CPU 是友好的,只有在取出键值对的时候才会进行过期检查,这样就不会把 CPU 资源花费在其他无关紧要的键值对的过期删除上。

缺点:

如果一些键值对永远不会被再次用到,那么将不会被删除,最终会造成内存泄漏,无用的垃圾数据占用了大量的资源,但是服务器却不能去删除。

惰性删除

惰性删除,当一个键值对过期的时候,只有再次用到这个键值对的时候才去检查删除这个键值对,也就是如果用不着,这个键值对就会一直存在。

优点:

对 CPU 是友好的,只有在取出键值对的时候才会进行过期检查,这样就不会把 CPU 资源花费在其他无关紧要的键值对的过期删除上。

缺点:

如果一些键值对永远不会被再次用到,那么将不会被删除,最终会造成内存泄漏,无用的垃圾数据占用了大量的资源,但是服务器却不能去删除。

定期删除

定期删除是对上面两种删除策略的一种整合和折中

每个一段时间就对一些 key 进行采样检查,检查是否过期,如果过期就进行删除

1、采样一定个数的key,采样的个数可以进行配置,并将其中过期的 key 全部删除;

2、如果过期 key 的占比超过可接受的过期 key 的百分比,则重复删除的过程,直到过期key的比例降至可接受的过期 key 的百分比以下。

优点:

定期删除,通过控制定期删除执行的时长和频率,可以减少删除操作对 CPU 的影响,同时也能较少因过期键带来的内存的浪费。

缺点:

执行的频率不太好控制

频率过快对 CPU 不友好,如果过慢了就会对内存不太友好,过期的键值对不能及时的被删除掉

同时如果一个键值对过期了,但是没有被删除,这时候业务再次获取到这个键值对,那么就会获取到被删除的数据了,这肯定是不合理的。

Redis 中过期删除策略

上面讨论的三种策略,都有或多或少的问题。Redis 中实际采用的策略是惰性删除加定期删除的组合方式。

定期删除,获取 CPU 和 内存的使用平衡,针对过期的 KEY 可能得不到及时的删除,当 KEY 被再次获取的时候,通过惰性删除再做一次过期检查,来避免业务获取到过期内容。

Redis常用数据结构

Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

Zset使用场景, 具体实现

Zset的两种实现方式:

  1. ziplist:满足以下两个条件的时候
  • 元素数量少于128的时候
  • 每个元素的长度小于64字节
  1. skiplist:不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist
  • map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的分数
  • skiplist按从小到大的顺序存储分数
  • skiplist每个元素的值都是[score,value]对

skiplist优势

skiplist本质上是并行的有序链表,但它克服了有序链表插入和查找性能不高的问题。它的插入和查询的时间复杂度都是O(logN)

skiplist原理

普通有序链表的插入需要一个一个向前查找是否可以插入,所以时间复杂度为O(N),比如下面这个链表插入23,就需要一直查找到22和26之间。


如果节点能够跳过一些节点,连接到更靠后的节点就可以优化插入速度:


在上面这个结构中,插入23的过程是

  • 先使用第2层链接head->7->19->26,发现26比23大,就回到19
  • 再用第1层连接19->22->26,发现比23大,那么就插入到26之前,22之后

上面这张图就是跳表的初步原理,但一个元素插入链表后,应该拥有几层连接呢?跳表在这块的实现方式是随机的,也就是23这个元素插入后,随机出一个数,比如这个数是3,那么23就会有如下连接:

  • 第3层head->23->end
  • 第2层19->23->26
  • 第1层22->23->26

下面这张图展示了如何形成一个跳表


在上述跳表中查找/插入23的过程为:


总结一下跳表原理:

  • 每个跳表都必须设定一个最大的连接层数MaxLevel
  • 第一层连接会连接到表中的每个元素
  • 插入一个元素会随机生成一个连接层数值[1, MaxLevel]之间,根据这个值跳表会给这元素建立N个连接
  • 插入某个元素的时候先从最高层开始,当跳到比目标值大的元素后,回退到上一个元素,用该元素的下一层连接进行遍历,周而复始直到第一层连接,最终在第一层连接中找到合适的位置

使用场景:

  • 对有序数据进行排序,例如新闻排行榜或游戏排行榜。
  • 对数据进行分组,例如将所有评分在3.0 到4.0 之间的电影分为一组。
  • 对数据进行去重,例如将所有重复的单词从文本中删除。

早日上岸!

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

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

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

标签:互娱,删除,索引,过期,别凉,面经,实现,func,使用
From: https://www.cnblogs.com/wangzhongyang/p/18186035

相关文章

  • 一份透心凉的北森java冷面(面经)
    1.对于分布式的理解2.几台机器合作怎么保证高可用3.es打了几个节点4.为什么es快5.es的build和body的区别6.es想进行时间范围搜索,用到什么命令和接口7.es的索引有哪些8.redis为什么搜索快9.在什么地方使用了redis10.将数据直接放到本地内存里更快,为什么用redis11.分布式......
  • 互联网面经分享:一文教你读懂四种网络拓扑结构
    1.什么是星型拓扑?星型拓扑有哪些优缺点?a.星型拓扑,也称为星状拓扑,以中央节点(通常称为交换机或集线器)为核心,连接了所有其他节点。在星型拓扑中,所有其他节点都直接连接到中央节点,而不直接连接到彼此。b.星型拓扑优势:中央节点集中管理:中央节点在星型拓扑中起着重要的作用......
  • go语言面经
    目录=和:=的区别?答案:=声明+赋值=仅赋值```govarfooint```如何在Markdown中插入代码块?在Markdown中插入代码块,您可以使用三个反引号(```)来包围代码内容,如下所示:defhello_world():print("Hello,World!")上面的Markdown代码将会......
  • 牛客面经(2024-04-07)
    美团一面4.2 基本全程八股1.双亲委派,类加载,每种类加载器加载什么?双亲委派:启动类加载器、拓展类、应用程序..打破双亲委派机制类加载过程:加载、链接(验证准备解析)、初始化、使用、卸载 2.spring AOP,bean 基于动态代理实现,jdk代理和cglibjdk代理因为是要继承pr......
  • 每日面经分享(python进阶 part2)
    Python中的装饰器和上下文管理器区别是什么?它们分别适用于哪些场景?a.装饰器用于在函数或类的外部添加额外功能,而上下文管理器用于管理资源的获取和释放。b.装饰器是一种用于修改函数或类行为的技术。适用于需要在函数或类的外部添加额外功能的场景,比如日志记录、性能监......
  • 阿里巴巴实习面经
    本人bg:浙江大学,计算机研二,本科也是浙大计算机专业的。在阿里巴巴达摩院实习,算法岗,我是去年拿到的阿里巴巴达摩院的实习offer,这个过程还是比较惊心动魄,所以我称之为惊心动魄版本,我把这个经历和经验总结分享给大家,算是回馈牛客了。一、我是怎么拿到offer的这也就是为什么是惊......
  • 淘天测开一面面经
    淘天测开一面自我介绍研究生成绩+本科成绩什么编程语言为什么想要做这样一个项目(webserver)?初衷是什么?之后有没有用到?实验室项目,在这个项目中的职责,做了哪些工作?怎么决定使用哪个算法来训练你的模型的?你们的团队中是怎么协调的?在整个项目推进的过程中,怎么分......
  • 2024最新分享我的面经总结:Java面试技术点攻略(九大核心专题
    关于操作系统这一部分,其实问的内容并不多,主要是因为这一部分问来问去也都是那么几个同样的问题,例如线程通信,线程与进程区别,进程调度算法以及虚拟内存、物理内存等。所以,在这一方面,我也整理了一些相对核心的内容。核心三:MySQL=========MySQL就更不用多说了,数据库不问......
  • 小米面经
    先来看看这次的题目大纲(基本上全部围绕我一直给大家强调的Java后端四大件展开):newInteger(10)==newInteger(10)相等吗 常量池String是可变的吗,为什么要设计为不可变说一下HashMap数据库结构和一些重要参数为什么是2次幂到什么时候开始扩容扩容机制流程有哪些......
  • 每日面经分享(pytest入门)
    1.pytest具有什么功能a.自动发现和执行测试用例:pytest可以自动发现项目中的测试文件和测试函数,无需手动编写测试套件或测试运行器。b.丰富的断言函数:pytest提供了丰富的断言函数,方便地验证测试结果是否符合预期。断言函数可以比较值、集合、异常等。c.参数化测试:pyte......