腾讯的校招面试也没那么难嘛!

网络 网络管理
假设客户端主动发起连接关闭请求,他给服务端发起一包FIN包,标识要关闭连接,自己进入终止等待1装填,服务端收到FIN包,发送一包ACK包,标识自己进入了关闭等待状态,客户端进入终止等待2状态。

今天分享的是腾讯校招Golang后端面经,这位同学一面之后信心满满的来找我说:“腾讯的校招面试也没那么难嘛,也可能只是一面,后面才会放大招。”

TA复盘的关键面试题如下:

图片图片

我给大家整理一下考察的知识:

  • Go基础:数组和切片,结构体,逃逸分析,GC
  • 数据结构:B+树和B树
  • 缓存:持久化策略,缓存穿透
  • 计网:TCP/UDP
  • 算法:最长回文串

面试题详解

slice和数组的区别

这是一个经常问到的面试题。

slice 的底层数据是数组,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度
    cap   int // 容量
}

slice的数据结构如下:

图片图片

(注意: 底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。)

还有一个很重要的区别就是slice作为函数参数传递时为按引用传递的,函数内对其元素的修改将导致函数外的值也发生改变,不过需要注意的是传入的是一个指针的副本,如果对该指针进行修改,不会导致原本的指针发生变化。而数组是值传递,函数内对数组的值的改变不影响初始数组。

两个结构体可以进行等值比较吗?

可以试一下运行下列代码:

package main

import "fmt"

func main() {
 sn1 := struct {
  age  int
  name string
 }{age: 11, name: "qq"}
 sn2 := struct {
  age  int
  name string
 }{age: 11, name: "qq"}
 if sn1 == sn2 {
  fmt.Println("sn1 == sn2")
 }
 sm1 := struct {
  age int
  m   map[string]string
 }{age: 11, m: map[string]string{"a": "1"}}
 sm2 := struct {
  age int
  m   map[string]string
 }{age: 11, m: map[string]string{"a": "1"}}
 if sm1 == sm2 {
  fmt.Println("sm1 == sm2")
 }
}

参考答案:编译不通过  invalid operation: sm1 == sm2

解析:

  1. 结构体能比较是否相等,不能比较大小。
  2. 相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关,下面的sn3 与上面的 sn1 就是不同的结构体
sn3:= struct {
  name string
  age  int
}{age:11,name:"qq"}
  1. 如果 struct 的所有成员都可以⽐较,则该 struct 就可以通过 == 或 != 进⾏⽐较是否相等,⽐较时逐个项进⾏⽐较,如果每⼀项都相等,则两个结构体才相等,否则不相等;

那什么是可⽐较的呢,常⻅的有 bool、数值型、string、指针、数组等,像切⽚、map、函数等是不能 ⽐较的。

说说逃逸分析

要搞清楚GO的逃逸分析一定要先搞清楚内存分配和堆栈:

内存既可以分配到堆中,也可以分配到栈中。

GO语言是如何进行内存分配的呢?其设计初衷和实现原理是什么呢?

要搞清楚上面的问题,我们先来聊一下内存管理和堆、栈的知识点:

内存管理

内存管理主要包括两个动作:分配与释放。逃逸分析就是服务于内存分配的,而内存的释放由GC负责。

在Go语言中,栈的内存是由编译器自动进行分配和释放的,栈区往往存储着函数参数、局部变量和调用函数帧,它们随着函数的创建而分配,随着函数的退出而销毁。

Go应用程序运行时,每个 goroutine 都维护着一个自己的栈区,这个栈区只能自己使用不能被其他 goroutine 使用。栈是调用栈(call stack)的简称。一个栈通常又包含了许多栈帧(stack frame),它描述的是函数之间的调用关系

与栈不同的是,堆区的内存一般由编译器和工程师自己共同进行管理分配,交给 Runtime GC 来释放。在堆上分配时,必须找到一块足够大的内存来存放新的变量数据。后续释放时,垃圾回收器扫描堆空间寻找不再被使用的对象。

我们可以简单理解为:我们用GO语言开发过程中,要考虑的内存管理只是针对堆内存而言的。

程序在运行期间可以主动从堆上申请内存,这些内存通过Go的内存分配器分配,并由垃圾收集器回收。

为了方便大家理解,我们再从以下角度对比一下堆栈:

堆和栈的对比

加锁

  • 栈不需要加锁:每个goroutine都独享自己的栈空间,这就意味着栈上的内存操作是不需要加锁的。
  • 堆有时需要加锁:堆上的内存,有时需要加锁防止多线程冲突

延伸知识点:为什么堆上的内存有时需要加锁?而不是一直需要加锁呢?

因为Go的内存分配策略学习了TCMalloc的线程缓存思想,他为每个处理器分配了一个mcache,注意:从mcache分配内存也是无锁的。

性能

  • 栈内存管理 性能好:栈上的内存,它的分配与释放非常高效的。简单地说,它只需要两个CPU指令:一个是分配入栈,另外一个是栈内释放。只需要借助于栈相关寄存器即可完成。
  • 堆内存管理 性能差:对于程序堆上的内存回收,还需要有标记清除阶段,例如Go采用的三色标记法。

缓存策略

  • 栈缓存性能更好
  • 堆缓存性能较差原因是:栈内存能更好地利用CPU的缓存策略,因为栈空间相较于堆来说是更连续的。

逃逸分析

上面说了这么多堆和栈的知识点,目的是为了让大家更好的理解逃逸分析。

正如上面讲的,相比于把内存分配到堆中,分配到栈中优势更明显。

Go语言也是这么做的:Go编译器会尽可能将变量分配到到栈上。

但是,在函数返回后无法证明变量未被引用,则该变量将被分配到堆上,该变量不随函数栈的回收而回收。以此避免悬挂指针(dangling pointer)的问题。

另外,如果局部变量占用内存非常大,也会将其分配在堆上。

Go是如何确定内存是分配到栈上还是堆上的呢?

答案就是:逃逸分析。

编译器通过逃逸分析技术去选择堆或者栈,逃逸分析的基本思想如下:检查变量的生命周期是否是完全可知的,如果通过检查,则在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配。

逃逸分析原则

Go语言虽然没有明确说明逃逸分析原则,但是有以下几点准则,是可以参考的。

  • 不同于JAVA JVM的运行时逃逸分析,Go的逃逸分析是在编译期完成的:编译期无法确定的参数类型必定放到堆中;
  • 如果变量在函数外部存在引用,则必定放在堆中;
  • 如果变量占用内存较大时,则优先放到堆中;
  • 如果变量在函数外部没有引用,则优先放到栈中;

GC中的根节点是什么?

Go 基础知识中 GC 肯定是比较重要的部分,然而平时我们在看八股文的时候总会对文中所提到的“根节点”产生疑惑,那么到底什么是根节点呢?

在垃圾回收的上下文中,“根节点”是指程序中被直接或间接引用的对象集合。

针对 Go 语言,垃圾回收器会从程序的“根节点”开始遍历,找出所有可以被访问到的对象,并标记它们为可达对象。根据上述“根节点”定义,Go 程序的根节点通常包括以下几类对象:

  1. 程序的全局变量和静态变量:这些变量在整个程序执行过程中都可以被访问到,因此垃圾回收器会将它们作为根节点。
  2. 程序的调用栈中的变量:这些变量在函数调用过程中被创建,并在函数返回时被销毁。因此,在函数调用期间,它们被认为是根节点。
  3. 当前执行的Goroutine:在 Go 语言中,Goroutine 是轻量级的线程,它们可以独立地运行,因此当前执行的Goroutine也被认为是根节点。

垃圾回收器从这些根节点开始遍历,查找所有可以被访问到的对象,并标记它们为可达对象。而没有被标记为可达对象的对象就是垃圾对象,可以被回收。这个过程被称为可达性分析。

既然 GC 不在栈上起作用,那为什么根节点还包括程序的调用栈中的变量呢?

根节点是指程序中被直接或间接引用的对象集合,它们是垃圾回收器扫描堆中对象时的起点。程序的调用栈中的变量也可以被认为是根节点之一,因为它们可以被其他对象引用。

调用栈是用于存储函数调用信息的一种数据结构,它由多个帧组成,每个帧对应一个函数调用。每当一个函数被调用时,就会在调用栈中创建一个新的帧,并将该函数的参数、局部变量和返回地址等信息保存到帧中。当函数返回时,对应的帧就会被销毁,该函数的所有局部变量也随之被释放。虽然调用栈中的变量存储在栈上,但它们也可以被其他对象引用,例如一个函数返回一个指向调用栈中局部变量的指针。因此,当垃圾回收器扫描堆中对象时,它也需要考虑调用栈中的变量是否被其他对象引用,以便正确地标记和回收不再使用的对象。

综上所述,调用栈中的变量虽然存储在栈上,但它们也可以被认为是根节点之一,因为它们可以被其他对象引用。因此,在Go语言中,垃圾回收器需要扫描调用栈中的变量,以确保不会回收被其他对象引用的变量。

b树和b+树的区别,为什么索引使用b+树结构?

  1. B树的每个节点都存储了key和data,而B+树的data存储在叶子节点上。

B+树非叶子节点仅存储key不存储data,这样一个节点就可以存储更多的key。可以使得B+树相对B树来说更矮(IO次数就是树的高度),所以与磁盘交换的IO操作次数更少。

  1. B+树所有叶子节点构成一个有序链表,按主键排序来遍历全部记录,能更好支持范围查找。

由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

  1. B+树所有的查询都要从根节点查找到叶子节点,查询性能更稳定;而B树,每个节点都可能查找到数据,需要在叶子节点和内部节点不停的往返移动,所以不稳定。

redis持久化策略?

RDB持久化(全量备份)

RDB持久化是指在指定时间间隔内将内存中的数据集快照写入磁盘。实际上fork子线程,先将数据集写入临时文件,写入成功后,在替换之前的文件,用二进制压缩文件,RDB是Redis默认的持久化方式,会在对应目录下生产一个dump.rdb文件,重启会通过加载dump.rdb文件恢复数据

RDB优点:

  1. 方便持久化:只有一个dump.rdb文件;
  2. 容灾性好:一个文件可以保存到安全的磁盘;
  3. 性能好:fork子线程来完成写操作,主线程继续处理命令;
  4. 效率高:如何数据集偏大,RDB启动效率比AOF高

RDB缺点:

  1. 数据安全性低:因为RDB是每隔一段时间进行持久化,可能会造成数据丢失。
  2. 由于RDB是通过fork子线程协助完成数据持久化工作的,因此如果数据集较大时,可能会导致整个服务停止服务几百毫秒,甚至一分钟。

AOF持久化(增量备份)

AOF持久化是以日志的形式记录记录每一个增删操作然后追加到文件中。AOF的出现是为了弥补RDB备份的不足(数据不一致性)。

与RDB持久化相比,AOF的持久化实时性更好。

AOF的备份策略:Redis的配置文件中存在三种不同的AOF持久化方式:

  1. appendfsync always:每次有数据修改发生时都会同步。
  2. appendfsync everysec:每秒同步一次
  3. appendsync no:让操作系统决定何时进行同步。

AOF优点:

  1. AOF实时性哈好,数据安全性更高;
  2. AOF通过append模式写文件,即使中途服务器宕机,也可以通过redis-check-aof工具解决数据一致性问题。
  3. AOF机制的rewrite模式(文件过大会对命令进行合并重写),可以删除其中某些命令(比如误操作的命令)

AOF缺点:

  1. AOF文件比RDB文件大,且恢复慢;
  2. 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。

两者结合

将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。

混合持久化工作在 AOF 日志重写过程。

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。

缓存穿透,怎么解决?

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透的发生一般有这两种情况:

  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案,常见的方案有三种。

  • 第一种方案,非法请求的限制;
  • 第二种方案,缓存空值或者默认值;
  • 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;

第一种方案,非法请求的限制

当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

第二种方案,缓存空值或者默认值

当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

TCP/UDP 详解,区别

作用

首先,tcp和udp都是工作再传输层,用于程序之间传输数据的。数一般包含:文件类型,视频类型,jpg图片等。

图片图片

区别

TCP是基于连接的,而UDP是基于非连接的。

tcp传输数据稳定可靠,适用于对网络通讯质量要求较高的场景,需要准确无误的传输给对方,比如,传输文件,发送邮件,浏览网页等等。

udp的优点是速度快,但是可能产生丢包,所以适用于对实时性要求较高但是对少量丢包并没有太大要求的场景。比如:域名查询,语音通话,视频直播等。udp还有一个非常重要的应用场景就是隧道网络,比如:vpn,VXLAN。

以人与人之间的通信为例:UDP协议就相当于是写信给对方,寄出去信件之后不能知道对方是否收到信件,信件内容是否完整,也不能得到及时反馈,而TCP协议就像是打电话通信,在这一系列流程都能得到及时反馈,并能确保对方及时接收到。如下图:

图片图片

三次握手

当客户端向服务端发起连接时,会先发一包连接请求数据,过去询问一下,能否与你建立连接?这包数据称之为SYN包,如果对端同意连接,则回复一包SYN+ACK包,客户端收到之后,发送一包ACK包,连接建立,因为这个过程中互相发送了三包数据,所以称之为三次握手。

图片图片

为什么要三次握手而不是两次握手?

这是为了防止,因为已失效的请求报文,突然又传到服务器,引起错误,这是什么意思?

假设采用两次握手建立连接,客户端向服务端发送一个syn包请求建立连接,因为某些未知的原因,并没有到达服务器,在中间某个网络节点产生了滞留,为了建立连接,客户端会重发syn包,这次的数据包正常送达,服务端发送syn+ack之后就建立起了连接,但是第一包数据阻塞的网络突然恢复,第一包syn包又送达到服务端,这是服务端会认为客户端又发起了一个新的连接,从而在两次握手之后进入等待数据状态,服务端认为是两个连接,而客户端认为是一个连接,造成了状态不一致,如果在三次握手的情况下,服务端收不到最后的ack包,自然不会认为连接建立成功,所以三次握手本质上来说就是为了解决网络信道不可靠的问题,为了在不可靠的信道上建立起可靠的连接,经过三次握手之后,客户端和服务端都进入了数据传输状态。

四次挥手

图片图片

处于连接状态的客户端和服务端,都可以发起关闭连接请求,此时需要四次挥手来进行连接关闭。

假设客户端主动发起连接关闭请求,他给服务端发起一包FIN包,标识要关闭连接,自己进入终止等待1装填,服务端收到FIN包,发送一包ACK包,标识自己进入了关闭等待状态,客户端进入终止等待2状态。这是第二次挥手,服务端此时还可以发送未发送的数据,而客户端还可以接受数据,待服务端发送完数据之后,发送一包FIN包,最后进入确认状态,这是第3次挥手,客户端收到之后恢复ACK包,进入超时等待状态,经过超时时间后关闭连接,而服务端收到ACK包后,立即关闭连接,这是第四次挥手。

为什么客户端要等待超时时间?

这是为了保证对方已经收到ACK包,因为假设客户端发送完最后一包ACK包后释放了连接,一旦ACK包在网络中丢失,服务端将一直停留在 最后确认状态,如果等待一段时间,这时服务端会因为没有收到ack包重发FIN包,客户端会响应 这个FIN包进行重发ack包,并刷新超时时间,这个机制跟第三次握手一样。也是为了保证在不可靠的网络链路中进行可靠的连接断开确认。

本文转载自微信公众号「 程序员升级打怪之旅」,作者「小韬&王中阳」,可以通过以下二维码关注。

转载本文请联系「 程序员升级打怪之旅」公众号。

责任编辑:武晓燕 来源: 王中阳Go
相关推荐

2019-05-17 09:33:50

图像识别三维重建文本识别

2012-12-21 09:31:52

Windows 8

2014-11-03 18:22:53

2021-07-01 07:34:09

LinuxIO模型

2023-10-26 18:22:16

前端CSSFlex 布局

2015-07-13 09:45:32

阿里校招

2021-06-02 20:13:41

Matplotlib绘图工具

2012-08-29 10:48:37

Windows 8操作系统

2020-05-21 09:17:51

Vue 3Vue代码

2018-08-30 17:28:52

TCP三次握手HTTP

2015-08-24 14:36:53

2013-03-11 11:14:05

2018-11-27 15:55:21

TCP通讯协议

2015-04-30 10:12:13

开源云平台OpenStack

2024-07-04 13:29:06

2014-04-23 15:13:42

2018-10-19 11:15:34

云计算互联网数据中心

2012-08-22 09:13:05

程序员

2015-09-09 09:53:11

BAT移动互联网招聘

2014-03-21 15:30:06

产品经理PM能力
点赞
收藏

51CTO技术栈公众号