go面试

查看github

https://github.com/lifei6671/interview-go
https://github.com/lifei6671/interview-go/blob/master/base/go-scheduler-base.md

变量堆和栈

如果一个变量的地址被调用了,这个变量将会候选分配在堆上。
然而,一个基本转义分析会识别一些情况。在这些情况里,变量不会存活到函数返回,这些变量将分配到栈上。

并发限制线程多少

使用带缓冲的通道限制并发数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package golimit

type GoLimit struct {
ch chan int
}

func NewGoLimit(max int) *GoLimit {
return &GoLimit{ch: make(chan int, max)}
}

func (g *GoLimit) Add() {
g.ch <- 1
}

func (g *GoLimit) Done() {
<-g.ch
}

按允许最大并发数创建一个带缓冲的通道, 创建协程之前调用Add()往通道里写一个数据, 协程完成是调用Done()方法读取一个数据. 若无法往通道里写数据时, 表示通道已经写满, 也就是目前的协程并发数为允许的最大数量. Add()方法将被阻塞, 也就无法创建新的协程. 直到有协程运行完成, 调用Done()方法读取了通道了一个数据.

context作用,原理,超时控制

golang context的理解,context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。

切片和数组区别

sync.Map实现原理,适用的场景

  1. 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
  2. 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
  3. 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
  4. 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
  5. 对于删除数据则直接通过标记来延迟删除

go语言有什么优点和缺点

优势:容易学习,生产力,并发,动态语法。劣势:包管理,错误处理,缺乏框架。

channel 安全原因

Golang的Channel,发送一个数据到Channel 和从Channel接收一个数据都是原子性的,channel内部维护了一个互斥锁,来保证线程安全。

gc三色标记法清除法

  1. 程序创建的对象都标记为白色。
  2. gc 开始扫描所有可到达的对象,标记为灰色,
    从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色
  3. 监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在
  4. 此时发现没有对象可以置为灰色时候,所留下的白色变量就被清理掉。

go gc回收时机

  1. 每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动 GC。内存增长率由环境变量 GOGC 控制,默认为100,即每当内存扩大一倍时启动GC。阀值 = 上次GC内存分配量 * 内存增长率
  2. 默认情况下,最长2分钟触发一次GC。
  3. 程序代码中也可以使用 runtime.GC() 来手动触发GC。这主要用于GC性能测试和统计。

Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量

Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行.

无缓冲Chan的发送和接收是否同步

channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞

Golang并发机制以及它所使用的CSP并发模型.

在计算机科学中,通信顺序过程(communicating sequential processes,CSP)是一种描述并发系统中交互模式的正式语言,它是并发数学理论家族中的一个成员,被称为过程算法(process algebras),或者说过程计算(process calculate),是基于消息的通道传递的数学理论。

Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,类似于greenthread,go底层选择使用coroutine的出发点是因为,

它具有以下特点:

  1. 用户空间 避免了内核态和用户态的切换导致的成本.
  2. 可以由语言和框架层进行调度.
  3. 更小的栈空间允许创建大量的实例.

Golang中的Goroutine的特性?

Golang内部有三个对象: P对象(processor) 代表上下文(或者可以认为是cpu),M(work thread)代表工作线程,G对象(goroutine).
https://github.com/lifei6671/interview-go/blob/master/base/go-gpm.md

defer的用途和使用场景是什么?

defer作用:可用于捕获程序异常,在某个方法中,出现异常时,defer可捕获此异常并进行打印,使用关键字defer向函数声明退出调用,即主函数退出时,defer后的函数才被调用。defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。

defer的执行顺序是什么?

defer语句并不会马上执行,而是会进入一个栈,函数return前,会按先进后出的顺序执行。也说是说最先被定义的defer语句最后执行。

了解空指针吗?

当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

new 和 make 的区别

make 只用于 chan,map,slice 的初始化;
new 用于给类型分配内存空间,并且置零;
make 返回类型本身,new 返回指向类型的指针。

值传递和指针传递有什么区别

值传递:会创建一个新的副本并将其传递给所调用函数或方法 指针传递:将创建相同内存地址的新副本
需要改变传入参数本身的时候用指针传递,否则值传递
另外,如果函数内部返回指针,会发生内存逃逸

聊聊内存逃逸分析

Go的逃逸分析是一种确定指针动态范围的方法,可以分析程序在哪些可以访问到指针,它涉及到指针分析和状态分析。

当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它程序,或者去调用子程序。 如果使用尾递归优化(通常函数式编程是需要的),对象也可能逃逸到被调用程序中。如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方都可以访问。

如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况就是当前程序的指针逃逸。逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。

导致内存逃逸的情况比较多(有些可能官方未能够实现精确的逃逸分析情况的bug),通常来讲就是如果变量的作用域不会扩大并且行为或者大小能够在其编译时确定,一般情况下都分配栈上,否则就可能发生内存逃逸到堆上。

引用内存逃逸的典型情况: * 在函数内部返回把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此生命周期大于栈,则溢出

发送指针或带有指针的值到channel中 在编译时,是没办法知道哪个 goroutine 会在 channel上接受数据,所以编译器没办法知道变量什么时候释放。
在一个切片上存储指针或带指针的值 一个典型的例子就是 []*string,这会导致切片的内容逃逸,尽管其后面的数组在栈上分配,但其引用值一定是在堆上
slice 的背后数组被重新分配了 因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
在 interface 类型上调用方法 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。

go逃逸分析怎么回事,内存什么时候栈分配什么时候堆分配

在某个函数中new或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸。
func test() *User{
a := User{}
return &a
}
逃逸分析命令
go run -gcflags “-m -l” main.go

我们得出了指针必然发生逃逸的三种情况(go version go1.13.4 darwin/amd64):

  1. 在某个函数中new或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸(构造函数返回的指针变量一定逃逸);
  2. 被已经逃逸的变量引用的指针,一定发生逃逸;
  3. 被指针类型的slice、map和chan引用的指针,一定发生逃逸;

同时我们也得出一些必然不会逃逸的情况:

  1. 指针被未发生逃逸的变量引用;
  2. 仅仅在函数内对变量做取址操作,而未将指针传出;

有一些情况可能发生逃逸,也可能不会发生逃逸:

  1. 将指针作为入参传给别的函数;这里还是要看指针在被传入的函数中的处理过程,如果发生了上边的三种情况,则会逃逸;否则不会逃逸;

了解过golang的内存管理吗

内存池概述

Go语言的内存分配器采用了跟 tcmalloc 库相同的实现,是一个带内存池的分配器,底层直接调用操作系统的 mmpa 等函数。

作为一个内存池,它的基本部分包括以下几部分:

首先,它会想操作系统申请大块内存,自己管理这部分内存
然后,它是一个池子,当上层释放内存时它不实际归还给操作系统,而是放回池子重复利用
接着,内存管理中必然会考虑的就是内存碎片问题,如果尽量避免内存碎片,提高内存利用率,像操作系统中的首次适应,最佳适应,最差适应,伙伴算法都是一些相关的知识背景。
另外,Go语言是一个支持 goroutine 这种多线程的语言,所以它的内存管理系统必须要考虑在多线程下的稳定性和效率问题。

在多线程方面,很自然的做法就是每条线程都有自己的本地的内存,然后有一个全局的分配链,当某个线程中的内存不足后就向全局分配链中申请内存。这样就避免了多线程同时访问共享变量的加锁。

在避免内存碎片方面,大块内存直接按页为单位分配,小块内存会切成各种不同的固定大小的块,当申请做任意字节内存时会向上取整到最接近的块,将整块分配给申请者以避免随意切割。

Go语言中为每个系统线程分配一个本地的 MCahe,少量的地址分配就直接从 MCache 中分配,并且定期做垃圾回收,将线程的 MCache 中的空闲内存返回给全局控制堆。小于 32K为小对象,大对象直接从全局控制堆上以页(4k)为单位进行分配,也就是说大对象总是以页对齐的。一个页可以存入一些相同大小的小对象,小对象从本地内存链表中分配,大对象从中心内存对分配。

大约有 100 种内存块类别,每一个类别都有自己对象的空闲链表。小于 32KB 的内存分配被向上取整到对应的尺寸类别,从相应的空闲链表中分配。一页内存只可以被分裂成同一种尺寸类别的对象,然后由空间链表分配管理器。

大约有 100 种内存块类别,每一个类别都有自己对象的空闲链表。小于 32kB 的内存分配被向上取整到对应的尺寸类别,从相应的空闲链表中分配。一页内存只可以被分裂成同一种尺寸类别的对象,然后由空闲链表分配器管理。

分配器的数据结构包括: FixAlloc:固定大小(128kB)的对象的空闲链分配器,被分配器用于管理存储; MHeap:分配堆,按页的粒度进行管理(4kB); MSpan:一些由 MHeap 管理的页; MCentral:对于给定尺寸类别的共享的 free list; * MCache:用于小对象的每 M 一个的 cache。

我们可以将Go语言的内存管理看成一个两级的内存管理结构 MHeap 和 MCache。上面一级管理的基本单位是页,用于分配大对象,每次分配都是若干连续的页,也就是若干个 4KB 的大小。使用的数据结构是 MHeap 和 MSpan,用 BestFit 算法做分配,用位示图做回收。下面一级管理的基本单位是不同类型的固定大小的对象,更像一个对象池而不是内存池,用引用计数做回收。下面这一级使用的数据结构是 MCache。

线程有几种模型?Goroutine 的原理你了解过吗,将一下实现和原理

线程模型有

  1. 内核线程模型
  2. 用户级线程模型
  3. 混合型线程模型
    $goroutine的优势

上下文切换代价小:从GMP调度器可以看出,避免了用户态和内核态线程切换,所以上下文切换代价小
内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;
goroutine 什么时候发生阻塞

channel 在等待网络请求或者数据操作的IO返回的时候会发生阻塞
发生一次系统调用等待返回结果的时候
goroutine进行sleep操作的时候

协程和线程和进程的区别

进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程
线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

打印二叉树组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func levelOrder(root *TreeNode) [][]int {
res := [][]int{}
if root == nil {
return res
}
queue := []*TreeNode{root}
var Level int = 0
for len(queue) != 0 {
//利用临时队列,暂存每个节点的左右子树
temp := []*TreeNode{}
//只需考虑在同一层上追加元素
res = append(res, []int{})
for _, v := range queue {
res[Level] = append(res[Level], v.Val)
if v.Left != nil {
temp = append(temp, v.Left)
}
if v.Right != nil {
temp = append(temp, v.Right)
}
}
if Level % 2 != 0 {
Reverse(res[Level])
}
//层级加1,队列重新复制为队列的左右子树集
Level++
queue = temp
}
return res
}


//数组倒序函数
func Reverse(arr []int) {
var temp int
length := len(arr)
for i := 0; i < length/2; i++ {
temp = arr[i]
arr[i] = arr[length-1-i]
arr[length-1-i] = temp
}
}

beego 特性

beego是一个用Go开发的应用框架,思路来自于tornado,路由设计来源于sinatra,支持如下特性
MVC
REST
智能路由
日志调试
配置管理
模板自动渲染
layout设计
中间件插入逻辑
方便的JSON/XML服务

grpc 基础

设计 一个 gRPC鉴权方案
经群友建议,我们可以把该要鉴权,或者不需要鉴权的方法,根据方法名放在一个列表(我使用的是map)里。看上面的 UnaryServerInfo, 此方法可行。还有,与此配合的就是,把 access_token 放在metadata里(其实就是HTTP/2 里的header)传输。

rpc实现原理

RPC 的核心功能主要由 5 个模块组成,如果想要自己实现一个 RPC,最简单的方式要实现三个技术点,分别是:

  1. 服务寻址
  2. 数据流的序列化和反序列化
  3. 网络传输

RPC需要解决的三个问题

  1. Call ID映射。我们怎么告诉远程机器我们要调用哪个函数呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用具体函数,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,是无法调用函数指针的,因为两个进程的地址空间是完全不一样。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
  2. 序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
  3. 网络传输。远程调用往往是基于网络的,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

实现高可用RPC框架需要考虑到的问题

既然系统采用分布式架构,那一个服务势必会有多个实例,要解决如何获取实例的问题。所以需要一个服务注册中心,比如在Dubbo中,就可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用;
如何选择实例呢?就要考虑负载均衡,例如dubbo提供了4种负载均衡策略;
如果每次都去注册中心查询列表,效率很低,那么就要加缓存;
客户端总不能每次调用完都等着服务端返回数据,所以就要支持异步调用;
服务端的接口修改了,老的接口还有人在用,这就需要版本控制;
服务端总不能每次接到请求都马上启动一个线程去处理,于是就需要线程池;

pprof调试cpu运行情况

  1. 工具型应用
    将 runtime/pprof 导入 main.go
    import “runtime/pprof”
    其中 pprof 封装了很好的接口供我们使用,比如要想进行 CPU Profiling,可以调用 pprof.StartCPUProfile() 方法,它会对当前应用程序进行 CPU profiling,并写入到提供的参数中(w io.Writer),要停止调用 StopCPUProfile() 即可。
  2. 服务型应用
    服务型应用使用 net/http/pprof 库, 其中 net/http/pprof 使用 runtime/pprof 包来进行封装,并在 http 端口上暴露出来。

如果使用了默认的 http.DefaultServeMux(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),只需要添加一行:

import _ “net/http/pprof”
如果应用使用了自定义的 Mux,则需要手动注册一些路由规则:

r.HandleFunc(“/debug/pprof/“, pprof.Index)
r.HandleFunc(“/debug/pprof/cmdline”, pprof.Cmdline)
r.HandleFunc(“/debug/pprof/profile”, pprof.Profile)
r.HandleFunc(“/debug/pprof/symbol”, pprof.Symbol)
r.HandleFunc(“/debug/pprof/trace”, pprof.Trace)

Kafka

  1. 你对Kafka事务了解多少
  1. Producer 事务:

为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer 获得的 PID 和 Transaction ID 绑定。这样当 Producer 重启后就可以通过正在进行的 Transaction ID 获得原来的 PID。

为了管理 Transaction,Kafka 引入了一个新的组件 Transaction Coordinator。Producer 就 是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于 事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

  1. Consumer 事务:

上述事务机制主要是从Producer方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过offset访问任意信息,而且不同的 Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。

  1. 你们Kafka集群的硬盘一共多大?有多少台机器?日志保存多久?用什么监控的?

这里考察应试者对kafka实际生产部署的能力,也是为了验证能力的真实程度,如果这个都答不好,那可能就不会再继续下去了。

一般企业判断这些指标有一个标准:

集群硬盘大小:每天的数据量/70%*日志保存天数;

机器数量:Kafka 机器数量=2(峰值生产速度副本数/100)+1;

日志保存时间:可以回答保存7天;

监控Kafka:一般公司有自己开发的监控器,或者cdh配套的监控器,另外还有一些开源的监控器:kafkaeagle、KafkaMonitor、KafkaManager。

  1. Kafka分区数、副本数和topic数量多少比较合适?

首先要知道分区数并不是越多越好,一般分区数不要超过集群机器数量。分区数越多占用内存越大 (ISR 等),一个节点集中的分区也就越多,当它宕机的时候,对系统的影响也就越大。

分区数一般设置为:3-10 个。

副本数一般设置为:2-3个。

topic数量需要根据日志类型来定,一般有多少个日志类型就定多少个topic,不过也有对日志类型进行合并的。

  1. Kafka中的消息有序吗?怎么实现的?
    kafka无法保证整个topic多个分区有序,但是由于每个分区(partition)内,每条消息都有一个offset,故可以保证分区内有序。
  1. topic的分区数可以增加或减少吗?为什么?

topic的分区数只能增加不能减少,因为减少掉的分区也就是被删除的分区的数据难以处理。

6.Kafka中需要用到选举吗?对应选举策略是什么?

一共有两处需要用到选举,首先是partition的leader,用到的选举策略是ISR;然后是kafka Controller,用先到先得的选举策略。

分布式锁的实现方式

基于数据库实现分布式锁
基于Zookeeper实现分布式锁
基于reids实现分布式锁
mysql —-> 行锁 + 乐观锁。获取锁:select update ;执行业务;释放锁:本地事物提交。
Zookeeper —–> 公平锁。获取锁:创建节点(临时顺序);执行业务 ;释放锁:关闭连接,临时顺序节点自动删除。
redis ——–> 加锁 setnx orderlock A expire 设置过期时间; 释放锁:del orderlock。

负载均衡算法可以分为两类:

静态负载均衡算法,包括轮询、比率、优先权。
动态负载均衡算法,包括最少连接数、最快响应速度、观察方法、预测法、动态性能分配、动态服务器补充、服务质量、服务类型、规则模式。

mysql

3、mysql相关

Q:innodb和myisam区别(事务,索引,锁。。。)

A: 网上找

Q:B+树和B树区别,优缺点

A:B树每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null。只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针,顺序访问指针,也就是每个叶子节点增加一个指向相邻叶子节点的指针。

Q:B树和二叉查找树或者红黑色区别

A:基础数据结构问题

Q:聚簇索引什么特点,为什么这样,顺序查询的实现,回表查询,联合索引特性

A:

聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
Q:大表分页查询,10亿行数据,查找第N页数据,怎么优化

A: 根据查询的页数和查询的记录数可以算出查询的id的范围,可以使用 id between and 来查询。

Q:悲观锁和乐观锁,mysql相关锁说一下

A:说下概念,其他网上找:

乐观锁( Optimistic Locking):对加锁持有一种乐观的态度,即先进行业务操作,不到最后一步不进行加锁,”乐观”的认为加锁一定会成功的,在最后一步更新数据的时候再进行加锁。

悲观锁(Pessimistic Lock):悲观锁对数据加锁持有一种悲观的态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

Q:如何分库分表

A:

1)垂直分表

也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。

2)垂直分库

垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。切分后,要放在多个服务器上,提高性能。

3)水平分库分表

将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。

redis 相关

4、redis

Q:几种数据结构(list,set,zset,geohash,bitmap)实现原理

A:网上找

Q:pipline用来干嘛

A:pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回。

Q:事务

A: redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 

Q:备份(aof/rdb)原理,哪些参数可调

A:RDB是根据指定的规则定时将内存中的数据备份到硬盘上,AOF是在每次执行命令后命令本身记录下来,所以RDB的备份文件是一个二进制文件,而AOF的备份文件是一个文本文件。至于调参,网上可找。

Q:网络模型

A:redis网络模型,网上找,需要理解。

Q:为什么单线程就能hold住几万qps

A:I/O复用,Reactor 设计模式

Q:热点key怎么处理

A:1、热key加载到系统内存中,直接从系统内存中取,而不走到redis层。

2、redis集群,热点备份分布到集群中,避免单台redis集中访问。

Q:一致性hash解决什么问题

A:redis集群和负载均衡

Q:redis集群(主从,高可用,扩展节点)

A:网上找

Redis的五种数据结构
String:Redis最基本的数据类型,一个键对应一个值,一个键值最大存储512MB
set:是String字符串类型的无序集合,也不可重复
zset:是String类型的有序集合,也不可重复。有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序
hash:hash是一个键值对的集合,是一个String类型的field和value的映射表,适合用于存储对象
list:是redis的简单的字符串列表,按插入顺序排序

为什么使用Redis
完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。
数据结构简单,对数据操作也简单。Redis中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis正是依赖这些灵活的数据结构,来提升读取和写入的性能。
采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。
使用基于IO多路复用机制的线程模型,可以处理并发的链接

介绍什么是持久化机制
Redis是一个基于内存的数据库,所有的数据都存放在内存中,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制

Redis的持久化机制有两种,第一种是RDB快照,第二种是AOF日志

RDB快照

RDB快照就是把数据以快照的形式保存在磁盘上,是某个时间点的一次全量数据备份,以二进制序列化形式的文件存储,并且在存储上非常紧密。
RDB持久化是指在指定的时间间隔内将内存中的数据集以快照的方式写入磁盘,并保存到一个名为dump.rdb的二进制文件中,也是默认的持久化方式,它恢复时是将快照文件从磁盘直接读到内存里。

RDB来说持久化触发机制有三种:save、bgsave、自动化触发

linux 基础面试

Q:Select/epoll,IO多路复用,底层数据结构,epoll的几个函数,两种模式

A: Select/epoll 问题,网上很多

Q:抢占式调度是什么回事

A: 进程优先级和时间分片等方面理解

Q:用户态和内核态

A: 系统态(内核态),操作系统在系统态运行——运行操作系统程序

用户态(也称为目态),应用程序只能在用户态运行——运行用户程序

linux网络编程

  1. 为什么TCP连接的时候是3次?2次不可以吗?

因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。

如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

  1. Session、Cookie和Token的主要区别

HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。

什么是cookie

cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。

什么是session

session是依赖Cookie实现的。session是服务器端对象

session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。

cookie与session区别

存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制
占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。
什么是Token

Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位

session与token区别

session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
session存储在服务器端,token存储在客户端
token提供认证和授权功能,作为身份认证,token安全性比session好;
session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

  1. tcp结束连接怎么握手,time_wait状态是什么,为什么会有time_wait状态?哪一方会有time_wait状态,如何避免time_wait状态占用资源?
    tcp通过四次挥手结束连接。
    TIME_WAIT: 表示收到了对方的FIN报文,并且已经发送了ACK。这个时候实际上已经没有了报文交互,那么TIME_WAIT状态 下的TCP连接会等待2*MSL,然后回到CLOSE状态。(Max Segment Lifetime,最大分段生存期, 指一个TCP报文在Internet上的最长生存时间。每个具体的TCP协议实现都必须选择一个确定的MSL值, RFC 1122建议是2分钟,但BSD传统实现采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本机的这个值)。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。TIME_WAIT状态是发起断连的一端存在的状态。

TIME_WAIT状态存在的意义:1、实现可靠的全双工连接:因为在最后一次发送ACK报本可能丢失,则对端可能会希望本端复位重发,导致错误产生。2、允许老的重复的分段在网络上消失。

  1. 滑动窗口的实现机制

滑动窗口机制,窗口的大小并不是固定的而是根据我们之间的链路的带宽的大小、这个时候链路是否拥护塞、接受方是否能处理这么多数据而决定。 滑动窗口协议,是TCP使用的一种流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。

项目问题

Q:遇到过内存溢出吗?怎么解决

A:主要了解有没有处理过内存泄漏导致的问题,C/C++定位内存泄漏问题;Golang和JAVA主要与GC的工作机制有关,堆内存一直增长,导致应用内存溢出等。

算法题

  1. 两数之和
    给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
    你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
    你可以按任意顺序返回答案。

方法一:暴力法

暴力法很简单。遍历每个元素 x 并查找是否存在一个值与 target−x 相等的目标元素。

复杂度分析:

时间复杂度:
O(n 2), 对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费O(n) 的时间。因此时间复杂度为 O(n 2)。

空间复杂度:O(1)。

方法二:两遍哈希表

为了对运行时间复杂度进行优化,我们需要一种更有效的方法来检查数组中是否存在目标元素。如果存在,我们需要找出它的索引。保持数组中的每个元素与其索引相互对应的最好方法是什么?哈希表。

通过以空间换取速度的方式,我们可以将查找时间从 O(n) 降低到 O(1)。哈希表正是为此目的而构建的,它支持以 近似 恒定的时间进行快速查找。我用“近似”来描述,是因为一旦出现冲突,查找用时可能会退化到 O(n)。但只要你仔细地挑选哈希函数,在哈希表中进行查找的用时应当被摊销为 O(1)。

一个简单的实现使用了两次迭代。在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target−nums[i])是否存在于表中。注意,该目标元素不能是 nums[i] 本身!

复杂度分析:

时间复杂度:O(n), 我们把包含有 n 个元素的列表遍历两次。由于哈希表将查找时间缩短到 O(1) ,所以时间复杂度为 O(n)。
空间复杂度:O(n), 所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n 个元素。

方法三:一遍哈希表

事实证明,我们可以一次完成。在进行迭代并将元素插入到表中的同时,我们还会回过头来检查表中是否已经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。

复杂度分析:

时间复杂度:O(n), 我们只遍历了包含有
n 个元素的列表一次。在表中进行的每次查找只花费 O(1) 的时间。

空间复杂度:O(n), 所需的额外空间取决于哈希表中存储的元素数量,该表最多需要存储 n 个元素。

李冠军-go(4年,武汉)-2022/03/04 16:00 电话 - 通过
go基础goroute,channel,gc等理解还行,beego框架使用阶段
redis,mysql基础不错,对mysql调优分析有一定了解
对docker、docker-Compose,k8s 有一点的使用经验
表达清晰,对项目大局观有一定了解
能出差短期2-3月来杭州
整体感觉不错,建议二面

李必傲-go(4年,武汉)-2022/03/07 17:00 电话 - 不通过
go基础goroute,channel,gc等较差,redis,mysql基础一般
算法和数据结构能力较差,表达不清晰,不建议二面

石志航-go(3年,武汉)-2022/03/11 17:00 电话 - 通过
go基础不错,gin框架使用阶段
redis,kafka基础不错
对mysql调优分析熟悉
熟悉grpc,tcp/ip
有一定网络基础
表达清晰,整体感觉不错,建议二面

万千-go (3年,武汉) - 2022/03/16 17:00 电话 - 通过
go 基础扎实,gc原理,goroutine等理解清晰
redis,mysql基础不错,对mysql调优分析有一定了解
对docker、docker-Compose,k8s 有一点的使用经验
表达能力强,有底层漏洞扫描安全方面的经验,建议二面

詹冠冠-go(6年,武汉)-2022/03/16 19:00 电话 - 通过
go 基础gc,defer,channel理解的不错
sync.Map,context 常见包熟练使用,原理清晰
gin框架一定使用经验
redis基础知识和原理理解清晰
mysql的索引,调优了解
tcp/ip基础了解,grpc理解清晰
有业务微服务的改造经历
熟练使用docker,docker-compose命令部署
建议二面

杨艳华-测试(2.5年,武汉)-2022/03/30 19:30 电话 - 通过
熟悉mysql数据库的增删改查,更改表结构等操作
linux基础使用没问题
使用简单docker 命令run,pull,push等
计算机网络了解的一般

曹仕英-测试(3年,武汉)-2022/04/07 19:00 电话 - 通过
熟悉Oracle, mysql数据库的增删改查等操作
对数据查询性能分析方法了解$$
linux基础使用没问题
网络基础了解的一般
对压力测试,负载测试,瓶颈分析方法熟悉

代叶亮-测试(2年,武汉)-2022/04/12 19:00 电话 - 通过
熟悉mysql数据库的增删改查,更改表结构等操作
linux基础使用没问题
计算机网络了解的一般
能编写简单python脚本
了解python+selenium自动化框架