go面试相关

Golang
Golang除了加Mutex锁之外还有哪些方式安全读写共享变量
  • Goroutine可以通过channel进行安全读写
  • 可以通过原子操作进行
无缓冲的chan的发送和接收是否是同步的
  • make(chan int):无缓冲chan,发送和接收同步进行
  • make(chan int, 1):有缓冲chan,不要求发送和接收同时进行
  • 无缓冲通道,发送阻塞直到数据被接收,接收阻塞直到数据被发送(读到数据)
  • 有缓冲通道,当缓冲区满时,发送阻塞,当缓冲区为空,接收阻塞
Golang并发机制以及CSP并发模型

CSP(communicating sequential processes):通信顺序过程,是一种描述并发系统中交互模式的正式语言,被称为过程算法,是基于消息的通道传递的数学理论

CSP讲究以通信的方式共享内存,用于描述两个独立的并发实体通过共享的通讯channel进行通信的并发模型

CSP中channel是第一类对象,不关注发送消息的实体,而关注与发送消息时使用的channel

  • Golang中的channel是被单独创建而且可以在进程之间传递,一个实体通过将消息发送到channel中,然后又监听这个channel的实体处理,两个实体之间是匿名的,这样就实现实体之间的解耦,其中channel是同步的一个消息被发送到channel中,最终是一定要被另外的实体消费的,实现原理类似一个阻塞的消息队列

  • Goroutine是Golang实际并发执行的实体。底层使用coroutine实现并发,coroutine是一种运行在用户态的用户线程,coroutine特点:

    • 用户空间避免了内核态和用户态切换导致的成本
    • 可以由语言和框架进行调度
    • 更小的栈空间允许创建更多的实例
  • Goroutine特性

    • Golang内部有三个对象
      • P(Processor)代表上下文(可以认为是cpu)
      • M(work thread)代表工作线程
      • G(Goroutine)协程对象
  • 正常情况下:一个cpu对象启动一个工作线程对象,线程去检查并执行goroutine对象。遇到goroutine阻塞的时候,会启动一个新的工作线程,以充分利用cpu资源

    • G:每个goroutine对象中的sched保存其上下文信息
    • M:对os内核线程的封装,数量对应真实的cpu数
    • P:逻辑处理器,为G和M的调度对象,用来调度G和M之间的关联关系,数量可以通过GOMAXPROCS()设置,默认为系统核心数
  • 在单核情况下,所有的goroutine运行在同一个线程(M0)中,每一个线程维护一个上下文§,任何时刻,一个上下文只有一个goroutine,其他的goroutine在runqueue中等待

    • 一个goroutine运行完自己的时间片之后,让出上下文,回到runqueue中
    • 当正在运行的G0阻塞时,会在创建一个线程(M1),P转到新的线程去运行
    • 当M0返回时,会尝试从其他线程中偷一个上下文过来,若未偷到,会将goroutine放到global runqueue中,然后将自己放入线程缓存中,上下文会定时检查global runqueue
  • Golang的CSP并发模型是通过Goroutine和channel实现的

    • Goroutine是并发的执行单位
    • channel是各个goroutine之间的通信机制,通常channel是goroutine之间通信的管道
Golang中常用的并发模型
  • 通过channel通知实现并发控制

    • 无缓冲的通道指的是通道的大小为0,这种类型的通道在接收前没有能力保持任何值,要求发送和接收同时准备好,才可以完成操作

    • func main() {
          ch := make(chan struct{})
          go func(){
              fmt.Println("start")
              time.Sleep(time.Second * 1)
              ch <- struct{}{}
          }
      
          <-ch
          fmt.Println("finished")
      }
      // 当主goroutine运行到<-ch时,若channel中没有数据,就会一直阻塞,直到有值
      
  • 通过waitroup实现

    • goroutine是异步执行的,为了防止在结束main函数的时候结束goroutine,就需要同步等待(waitgroup),它会等待它收集的所有goroutine任务全部完成

      • Add:可以添加或减少goroutine的数量
      • Done:相当于Add(-1)
      • Wait:执行后会阻塞主线程,直到waitgroup中的值减为0
    • 在waitgroup第一次使用后,不能被拷贝,否则会导致panic

    • func main() {
          var wg sync.WaitGroup
          var name := []string{
              "zhangsan",
              "lisi",
              "wangwu",
          }
          for _, n := range name {
              wg.Add(1)
              go func(n string) {
                  defer wg.Done()
                  println(n)
              }(n)
          }
          wg.Wait()
      }
      
  • Context实现并发控制

    • context对象是线程安全的,可以把一个context对象传递给任意个数的goroutine,对它执行取消操作时,所有goroutine都会受到取消信号
Go对nil slice和空 slice的处理是一致的吗

Go的json标准库对其处理不一致

func main() {
    var s1 []int
    slice[1] = 0 // panic,因为只是声明了slice却并给实例化对象

    // 适合查询或者处理一个空列表的情形
    s2 := make([]int,1)
    s3 := []int{}
}
协程、进程、线程的区别
  • 进程
    • 具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是由系统进行资源分配和调度的一个独立单位
    • 每个进程都有自己的独立内存空间,不同进程通过进程间的通信来通信。由于进程比较重,占据独立的内存,所以上下文进程间(栈、寄存器、虚拟内存等)的切换开销比较大,但相对比较稳定安全
  • 线程
    • 是进程的一个实体,线程是内核态,而且是cpu调度和分配的基本单位,是比进程更小的能够独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行在必不可少的资源(如程序计数器等),但是他与同属一个进程的其他线程共享进程所有有的全部资源
    • 线程之间的通信主要依靠共享内存,上下文切换很快,资源开销较小,但是相比进程容易丢失数据不够稳定
  • 协程
    • 是用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的上下文和栈
    • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文切换非常快
  • 区别
    • 进程拥有自己独立的堆和栈,既不共享堆,也不共享栈,由操作系统调度
    • 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,有OS调度
    • 协程和线程一样共享堆不共享栈,协程由程序开发这在协程代码中显式调度
  • 为什么协程比线程轻量?
    • 协程调用比线程调用效率高
      • 线程并发执行流程:线程是内核对外提供的服务,应用程序可以通过系统调用让内核来负责线程调度和切换,线程在等待IO操作是线程变为unrunnable状态会触发上下文切换。现代操作系统一般都采用抢占式调度,上下文切换一般发生在时钟中断和系统调用返回前,调度器计算当前线程的时间片,如果需要切换就从运行队列中选出一个目标线程,保存当前线程的环境,并且恢复目标线程的运行环境,最典型的就是切换ESP指向目标线程内核堆栈,将EIP指向目标线程上次被调度出时的指令地址。
      • 协程执行流程:不依赖于OS和其提供的线程,go自己实现的CSP并发模型。go协程也叫做用户态线程,协程之间的切换发生在用户态。在用户态没有时钟中断、系统调用等机制、因此效率高
    • 协程占有内存少
      • 协程只需要极少的栈内存(4~5k),默认情况下,线程栈的大小为1M
Golang的内存模型中为什么小对象多了会造成GC压力

通常小对象过多会导致GC三色标记法消耗过多的GPU,可以适当减少小对象分配

Go数据竞争(data race)问题解决

使用互斥锁或CAS(原子操作)无锁并发解决

  • 使用go run -racego build -race进行静态检测,实现原理是在内部开启多个协程执行同一个命令,并记录每个变量的状态
Channel怎样做到线程安全
  • 发送一个数据到channel和从channel接收一个数据都是原子性的
  • 设计channel的主要目的就是在多个任务间传递数据的,本身就是安全的
GC触发条件
  • 主动触发,调用runtime.GC(),此调用阻塞式的等待当前GC运行完毕
  • 被动触发
    • 使用系统监控,当超过两分钟未产生任何GC时,强制触发
    • 使用步调算法,核心思想是控制内存增长的比例,当前内存分配到一定的比例触发
GMP调度

协程拥有自己的上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈。

协程能够保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

线程和进程操作是由程序触发系统接口,最后执行者是系统;协程的操作执行者是用户自身程序

  • Go的调度器
    • M:代表一个内核级线程,一个M就是一个线程,goroutine就是运行在M之上的;M是一个很大的结构,里面维护小对象内存cache、当前执行的goroutine、随机数发生器等信息
    • G:它有自己的栈,instruction pointer和其他信息(正在等待的channel),用于调度
    • P:逻辑处理器,用来执行goroutine,它维护一个goroutine队列,用来存储所有需要他来执行的goroutine
    • Sched:调度器,维护有存储M和G的队列以及调度器的一些状态信息
  • GMP的调度
    • 新建的goroutine会先存放在global runqueue中,等待Go调度器进行调度
    • Goroutine被分配给其中的一个逻辑处理器P。并放到这个逻辑处理器对应的local runqueue本地运行队列中,最终等待被逻辑处理器P执行即可
    • 在M与P绑定后,M会不断从P的local runqueue中无锁的取出G,并切换到G的堆栈执行
    • 当P的local runqueue队列没有G时,会从global runqueue中获取一个G
    • 当glocal runqueue队列也没有等待运行的G时,则尝试从别的P中窃取部分G来执行相当于P之间的负载均衡
  • 当一个os线程M0陷入阻塞时,P转而在M1运行,M1可能是正被创建或者从线程缓存中取出
  • 当M0返回时,必须尝试获取一个P执行goroutine,一般情况下,会从其他的OS线程中获取一个P,若未拿到,就将goroutine放到global runqueue中,然后自己放入线程缓存中。
  • 所有的P会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue中的goroutine永远无法执行
  • 当P所分配的任务G很快就执行完了,就导致P处于空闲状态,但是此时其他的P还有任务。此时若global runqueue中没有G,就会从其他P窃取一些G来执行
  • 若从其他P那里获取任务的话,一般就拿runqueue的一半,这就确保每个OS线程都能充分的使用
并发编程概念
  • 并行是指两个或多个事件在同一时刻发生,并发指的是两个或多个事件在同一时间间隔发生
  • 并行是在不同实体上的多个事件,并发是指在同一个实体上的多个事件,在一台处理器上同时处理多个任务,在多个处理器上同时处理都个事务
  • 并发偏重于多个任务交替执行,多个任务之间有可能是串行的,并行是真正意义上的同时执行
  • 并发编程是指在一台处理器上同时处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分利用处理器的每一个核,以达到最高的处理性能
goroutine和channel的作用

进程是内存资源管理和cpu调度的执行单元,为了有效利用多喝处理器,将进程进一步划分,允许一个进程里存在多个线程,多个线程共享一片内存空间,cpu调度的最小单元变成了线程

协程,可以看作为轻量级的线程,线程的切换是由OS控制的,协程的切换是由用户控制的

  • goroutine就是协程,可以实现并行,多个协程可以在多个处理器同时运行,而协程同一时刻只能在一个处理器上运行

  • 多个goroutine之间的通信是由channel实现,而协程通信是通过yield和resume操作

  • channel实际上是一个有类型的消息队列,遵循先进先出的原则(FIFO)

  • ch <- data:将data数据发送给ch;data <- ch:从ch中获取数据赋值给data

  • func main() {
        ch := make(chan int)
        go pump(ch) // 在接收到第一个元素之后就被阻塞了,直到主goroutine取走数据,最后打印结果为1
        <- ch
    }
    
    func pump(ch chan int) {
        for i := 0; i < 5; i++ {
            ch <- i
        }
    }
    // chan默认是没有缓冲区的,即通信阻塞,发送操作必须等到有消费者接收才算完成
    
    // 无缓冲的chan只能容纳一个元素,有缓冲的chan可以容纳N个元素以非阻塞的方式
    // 向有缓冲区的chan发送数据不会阻塞除非缓冲区已满
    // 从缓冲区中获取数据不会阻塞,除非chan为空
    
查看goroutine的数量

通过GOMAXPROCS(控制的是未被阻塞的所有goroutine)可以查看

Go中的锁
  • 互斥锁(sync.Mutex())

    • go并发程序对共享资源进行访问的主要手段

    • // 首次使用后不得复制互斥锁
      type Mutex struct {
          state int32
          sema uint32
      }
      
      type Locker interface {
          Lock()
          Unlock()
      }
      // 锁定当前互斥量,若锁已被使用,则阻塞goroutine直到互斥锁可用
      func (m *Mutex) Lock()
      // 对当前互斥量进行解锁,若进入解锁时未锁定m,则为运行时错误
      // 允许一个goroutine锁定Mutex然后安排另外一个goroutine去解锁
      func (m *Mutex) Unlock()
      
  • 读写锁(sync.RWMutex())

    • 多个写操作之间是互斥的
    • 写操作与读操作之间也是互斥的
    • 多个读操作之间不是互斥的
    • 读写锁可以大大降低性能损耗
    • Unlock方法会试图唤醒所有想进行读锁定而被阻塞的协程,而Unlock只会在已经没有任何读锁定的情况下,试图唤醒一个想进行写锁定而被阻塞的协程
    • 若对一个未被写锁定的读写锁进行解锁,会引发panic,读锁也是
    • 由于读写锁控制下的多个读操作之间不是互斥的,因此读解锁更容易被忽视,对于同一个读写锁,添加多少个读锁定,就必须有等量的读解锁
    • 在多个读线程可以同时访问共享数据,写线程必须等待所有的读协程都释放锁之后,才能获得锁
    • 读线程必须等待写线程释放锁之后,才能获取锁
    • 可以同时读,但是写写,读写都是互斥的
  • sync.Map()安全锁

如何限制goroutine的数量

goroutine数量太多会造成系统资源耗尽导致程序崩溃或CPU使用率过高导致系统忙不过来

  • 在每次执行go之前判断goroutine的数量,若超了,就阻塞go的执行

  • 通道方式

    • var ch chan int
      func test() {
          <- ch
          println("receive:",ch)
      }
      
      func main() {
          ch = make(chan int,5) // 限制协程数量为5,出现问题:goroutine并不能都执行完成
          for i:=0; i<10; i++{
              ch <- i
              println("send:",ch)
              go test()
              println("result:",i)
          }
      }
      
  • WaitGroup方式

    • type Pool struct {
          queue chan int
          wg *sync.WaitGroup
      }
      
      func NewPool(size int) *Pool {
          if size < 1 {
              size = 1
          }
          return &Pool{
              queue:make(chan int, size),\
              wg:&sync.WaitGroup{},
          }
      }
      
      // 新增一个执行
      func (p *Pool) Add(delta int) {
          for i:=0;i<delta;i++{ // delta为正数增加
              p.queue <- 1
          }
          for i := 0; i>delta;i-- {
              <- p.queue // delta为负数减少
          }
          p.wg.Add(delta)
      }
      
      // 减少
      func (p *Pool) Done() {
          <- p.queue
          p.wg.Done()
      }
      
      func (p *Pool) Wait() {
          p.wg.Wait()
      }
      
      // runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量
      func main() {
          // 限制5个并发
          pool := NewPool(5)
          println("begin:",runtime.NumGroutine())
          for i:=0;i<20;i++{
              pool.Add(1)
              go func(i int) {
                  time.Sleep(time.Second())
                  println("continue:",runtime.NumGroutine())
                  pool.Done()
              }(i)
          }
          pool.Wait()
           println("done:",runtime.NumGroutine())
      }
      
chan是同步还是异步的
  • channel是异步进行的,状态如下:
    • nil:未初始化,只进行了声明或手动赋值为nil
    • active:正常,可以读或写
    • closed:已关闭,关闭后的chan值不一定为nil
操作零值nil通道非零值已关闭通道非零值尚未关闭通道
关闭panicpanic成功关闭
发送数据阻塞panic阻塞或成功发送
接收数据阻塞不阻塞(数据为空则返回对应类型零值)阻塞或成功接收
goroutine和线程的区别
  • 从调度上看,goroutine的调度开销远小于线程的调度开销
    • OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发送到CPU,CPU调用一个调度器内核函数,这个函数暂停当前正在运行的线程,将其寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,在从内存中恢复线程的注册表信息,最后继续执行选中的线程,这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,在恢复另外一个线程的状态,最后更新调度器的数据结构
    • Go运行的时候包含一个自己的调度器,使用M:N的调度技术,M个goroutine到N个OS线程,Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发,不需要切换到内核,所以成本比较低
  • 从栈空间上看,goroutine的栈空间更加灵活动态
    • 每个OS线程都有一个固定大小的栈内存,通常是2M,栈内存用于保存在在其他函数调用期间哪些正在执行或者临时暂停函数的局部变量。
    • goroutine在生命周期开始只有一个很小的栈,典型情况下(2K),栈大小不固定,可以按需增大或减少,最大限制可以达到1G
Go的struct能不能比较
  • 相同的struct类型可以比较,不同的类型不可以,编译不通过,出现类型不匹配错误

  • func main() {
        type A struct {
            a int
        }
        type B struct {
            a int
        }
        a := A{1}
        //b := A{1}
        b := B{1}
        if a == b {
            fmt.Println("a == b")
        }else{
            fmt.Println("a != b")
        }
    } 
    // invalid operation a == b
    // 类型不匹配
    
Go的defer原理

defer延迟函数,可以处理易忽略的问题,如资源释放、连接关闭等

  • 若函数中有多个defer则遵循先进后出的原则,类似于栈

  • Go的函数返回值是通过堆栈返回的,return语句不是原子操作,而是被拆分

    • 给返回值赋值
    • 调用defer
    • 返回给调用函数
  • 若defer表达式有返回值则被丢弃

    • 闭包与匿名函数

      • 匿名函数:没有名字的函数
      • 闭包:可以使用另外一个函数作用域中的变量的函数
    • // 1
      func f() (res int) {
          defer func() {
              res++
          }
          return 0
      }
      
      // 5
      func f1() (r int) {
          t := 5
          defer func() {
              t = t+5
          }
          return t
      }
      
      // 1
      func f2() (r int) {
          defer func(r int) {
              r = r + 5
          }(r)
          return 1
      }
      
select可以做什么

监听多个chan,每一个case是一个事件,可以是读事件也可以是写事件,随机选取一个执行,可以设置default,当监听的多个事件阻塞时,执行那个default逻辑

  • 源码重点

    • pollorder:保存的是scase的序号,乱序是为了之后执行的随机性
    • lockorder:保存了所有case的chan地址,按照大小堆排序lockorder对于的这片连续内存。对chan排序是为了去重,保证之后对所有chan上锁时不会重复
  • goroutine的退出方式

    • // for range方式,range可以感知chan的关闭,当chan被发送数据的协程关闭时,range就会结束,退出循环
      go func (in <-chan int) {
          for x := range ch {
              println("Process:",x)
          } 
      }(in)
      
      // 使用for select退出
      // select可以让函数有持续多路处理多个chan的能力,但是不能感知chan关闭
      
      // 问题:
      // 1、继续在关闭的通道上读,会读到通道传输数据的零值,若是指针类型则为nil值,继续处理还会产生nil
      // 解决:通道只能由发送方关闭
      
      // 2、继续在关闭的通道上写,将会panic
      // 解决:使用返回值第二个参数检测是否关闭
      // 两种情况
      // 1、若通道关闭后,需要退出协程,直接return即可
      go func() {
          for {
              select {
                  case x, ok := <-in:
                  if !ok {
                      return
                  }
                  println("process: ",x)
                  count++
                  case <-t.C:
                  println("working,count=",count)
              }
          }
      }()
      // 2、若通道关闭,不再处理该通道而是处理其他case,退出是等待所有的可读通道关闭
      go func() {
          for {
              select {
                  case x,ok := <-in1:
                  if !ok {
                      in1 = nil
                  }
                  case y,ok := <-in2:
                  if !ok {
                      in2 = nil
                  }
                  case <-t.C:
                  println("working,count=",count)
              }
              if in1 == nil && in2 == nil {
                  return
              }
          }
      }()
      
      // 使用退出通道退出
      func worker(stopCh <-chan struct{}) {
          go func() {
              defer println("worker exit")
              for {
                  select {
                      case <- stopCh:
                      println("recv strop signal")
                      return
                      case <-t.C:
                      println("working")
                  }
              }
          }()
          return
      }
      // 循环监听一个chan,一般来说for循环放一个select监听chan以达到通知子goroutine的效果,在借助waitgroup,主协程可以等待所有协程退出后在结束自己的运行
      
      // 注意
      // 发送协程主动关闭通道,接收协程不关闭;技巧:将接收方的通道入参声明为只读,若接收协程关闭只读协程就会报错
      // 协程处理1个通道,并且是读是,协程优先使用for range
      // ok可以处理多个通道关闭,需要关闭当前使用的for select的协程
      // 显示关闭通道stopCh可以处理主动通知协程的场景
      
Context包的用处

专门用来简化对于处理单个请求的多个goroutine之间请求域的数据、取消信号、截止时间等相关操作

  • 方法
    • Done:返回一个chan,当该context被取消的时候,该chan会关闭,同时对应的使用该context的goroutine也应该结束并发回
    • Context中的方法是协程安全的,代表在父goroutine创建的context可以传递给任意数量的goroutine并让它们同时访问
    • Deadline会返回一个超时时间,goroutine获取超时时间后,可以对某些IO操作设置超时时间
    • Value可以让goroutine安全地共享一些数据
  • 在goroutine中使用context的时候,通常要新创建一个context,因为若直接传递外部context到协程中,一个请求可能在主函数已经结束,在协程中若还未结束的话,会直接导致协程中的运行被取消
  • context.Background():返回值是一个空的context,经常作为树的根节点,一般由接收请求的第一个goroutine创建,不能被取消、没有值、没有过期时间
  • withCancelwithTimeout会返回继承的context对象,这些对象可以比他们的父context更早的取消
  • 当请求处理函数返回时,与该请求相关的context会被取消,当使用多个副本发送请求时,可以使用withCancel取消多余的请求,withTimeout在设置对后端服务器请求截止时间时非常有效
  • withDeadline有时间参数,表示context的最大存活时间,若超过了这个时间,将会自动撤销他的子context
  • context中上下文的数据不是全局的,只查询本节点以及父节点的数据,不能查询兄弟节点的数据
  • 使用原则
    • 不要将context放入struct中,要以参数的方式传递
    • 以context作为参数的函数方法,应将context放在第一个参数位
    • 给一个函数方法传递context的时候,不要传nil,若不知传什么可以使用context.TODO
    • context的value相关方法应该传递必须的数据,不要什么数据都用其传输
    • context的线程安全的,可以放心在多个goroutine之间传递
Go的slice如何扩容

slice是对数组一个连续片的引用,所以其是一个引用类型,这个片段可以是整个数组或者是部分数组

  • 若切片的容量小于1024个元素,那么扩容的时候就翻倍,若超过1024个元素,每次增长0.25
  • 若扩容之后,还未触及原数组的容量,那么切片中指针指向的位置还是原数组,若扩容之后超过了原数组的容量,就会新开辟一块内存,将原来的值拷贝过来,这种情况下修改就不会影响到原数组了
map如何实现顺序读取
  • 先将map中的key通过sort包排序,在进行读取
CAS

CAS(compare and swap),是原子操作的一种,是一种有名的无锁算法。

无锁编程即不使用锁的情况下实现多线程之间的变量同步,也叫做非阻塞同步。

可用于多线程编程中实现不被打断的数据交换操作,从而避免多线程改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题

  • 该操作通过将内存中的值与指定数据比较,当数值一样时将内存中的数据替换为新的值
  • CAS借助CPU提供的原子性指令实现的,CAS操作的时候不需要对共享变量加锁,而是通过类似于乐观锁的方式进行检查,本质还是不断的占用CPU资源换取加锁带来的开销
  • 高并发情况下每个线程执行CAS并不是每次都成功的,失败的线程需要重写获取变量当前的值
逃逸分析

逃逸分析是确定指针动态范围的方法,可以分析而在程序的哪些地方可以访问到指针

  • 当一个变量或对象在子程序中被分配时,一个指向变量的指针可能逃逸到其他执行线程中,或者去调用子程序,若一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到,这样就是指针逃逸
  • 若变量的作用域不断扩大并且其行为或者大小能够在编译的时候确定,一般情况下都是分配到栈上,否则就可能发生内存逃逸到堆上
  • 内存逃逸情况
    • 发送指针的指针包含了指针到channel中,由于在编译阶段无法确定其作用域与传递路径,所以一般都会逃逸分配到堆上
    • slice中的值是指针的指针或包含指针字段,例如:[]*strng,这种情况总是导致slice逃逸,即使slice的底层数组仍在栈上,数据的引用也会转移到堆上
    • slice由于append操作超出其容量,会导致slice重新分配,这种情况下,由于编译时slice的初始大小已知情况下,将会在栈上分配。若slice的底层存储必须基于仅在运行时数据进行扩展,则将它分配到堆上
    • 调用接口类型方法。接口类型的方法调用是动态调度,实际使用的具体实现只能在运行时确定
    • 尽管能够符合分配到栈上的场景,但是其大小不能再编译时确定的情况,也会分配到堆上
值接收者和指针接收者的区别

在调用方法的时候,值类型既可以调用值接收者的方法也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法也可以调用值接收者的方法

函数和方法值接收者指针接收者
值类型调用者方法会使用调用者的一个副本使用值的引用来调用方法,p.get(),相当于(&p).get()
指针调用者指针被解引用为值,p.get()相当于(*p).get()实际上也是传值,方法里的操作会影响到调用者,类似于指针传参
  • 若实现可接收者是值类型的方法,会隐含的实现接收者是指针类型的方法
  • 若方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者
  • 若方法是指针接收者,则调用者修改的是指针指向的对象本身
  • 使用指针作为接收者的理由
    • 能够修改接收者指向的值
    • 可以避免在每次调用方法时复制该值,在值的类型为大结构体时,会更加有效
  • 若类型具备原始的本质,即它的成员都是go内置的原始类型,如string、int等,就定义为值接收者类型的方法,像内置的引用类型:slice、map、interface、channel,这些类型,也使用值接收者
  • 若类型具备非原始的本质,不能被安全的复制,这种类型应用指针接收者
Go对象在内存中如何分配
  • 大对象是指大小大于32K的对象,在mheap中分配
  • 小对象在mcache中分配
    • 小对象中又细分了一个tiny(16byte>tiny>1byte)对象,且不包含指针的对象
  • 内存分配原则
    • go在程序启动的时候,会先向OS申请一块内存(这是一段虚拟的地址空间,并不会真正分配内存),切成小块后自己管理
      • 申请的内存块在x64上分别是512M(spans)、16G(bimap)、512G(arena)大小
        • arena就是堆区,go动态分配的内存都是在这个区域,它把内存分割成8K大小的页,一些页组合起来称为mspan
        • bitmap标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息;bitmap中一个byte大小的内存对于arena区域中4个指针大小(指针大小为8b)的内存,所以bitmap的大小为512G/(4*8B)=16G
        • spans存放mspan的指针,每个指针对应一页,所以大小就是512G/8k*8b=512M
栈内存如何分配
  • 栈和堆只是虚拟内存上2块不同功能的内存区域
    • 栈在高地址,从高地址向低增长
    • 堆在地地址,从低地址向高增长
  • 栈的优势
    • 栈的内存管理相对简单,分配上比堆块
    • 栈的内存不需要回收,而堆需要,无论是主动释放还是被动垃圾回收都需花费额外的CPU
    • 栈上的内存有更好的局部性,堆上访问内存就没那么友好了,CPU访问两块数据区域可能在不同的页上,CPU访问数据的时间就增加了
堆内存管理如何分配
  • 堆内存管理分为
    • 分配内存块
    • 回收内存块
    • 组织内存块
  • 一个内存块包含3类信息:元数据、用户数据、对齐字段
    • 内存对齐是为了提高访问效率
  • 释放内存的实质是吧使用的内存块从链表中取出来,然后标记为未使用,当分配内存块的时候,可以从未使用内存块中优先查找大小相近的内存块,若未找到再从未分配的内存中分配内存
  • 随着内存不断申请和释放,内存上会存在大量的碎片,降低内存使用率,为了解决内存碎片,可以将两个连续的未使用的内存块合并,减少碎片
defer下列两种情况返回什么
a := 1
defer fmt.Println("value: ", a) // 1,参数值在defer语句出现时就已经确定了,无论后边如何修改都不会影响到延迟函数
a++
defer func() {
    fmt.Println("value: ",a) // 2,defer中函数参数早已确定,但是函数内部所使用的值需要在运行时才能确定
}()
go函数为什么会发生内存泄露

内存泄露指的是能够预期的能很快被释放的内存由于长时间附着在长期存活的内存上或生命周期意外延长,导致预计能够立即回收的内存而长时间得不到回收

  • go内存泄露除了附着在长期对象上之外还有以下几种形式
    • 预期能被快速释放的内存因被跟对象引用而没有得到迅速释放
      • 当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其释放,则该内存永远得不到释放
    • goroutine泄露
      • go作为一种逻辑上理解的轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息
        • 若一个程序不断的产生新的goroutine,且不结束已以创建的goroutine并复用这部分内存就会造成内存泄露现象
  • 检测:使用自带工具pprof或者Gops检测进程占用的资源
new和make的区别

值类型:int、float、bool、string、struct、array

变量直接存储值,分配栈区的内存空间,这些变量所占据的空间在函数被调用完后会自动释放

引用类型:slice、map、chan以及值类型对应的指针

变量存储的是一个地址,指针指向内存中真正存储数据的首地址。内存通常在堆上分配,通过GC回收

  • 对于引用变量不仅需要声明变量还需要手动分配空间
  • new方法的参数要求传入一个类型而不是一个值,它会申请一个该类型大小的内存空间,并初始化为对于零值,返回指向该内存空间的一个指针
  • make只用来引用对象的内存创建,返回类型本身
G0的作用
  • G0作为一个特殊的goroutine,为scheduler执行调度循环提供了场地(栈),对于一个线程来说,G0总是第一个创建的goroutine,之后会不断的寻找其他的goroutine来执行,直到进程退出
  • 当需要执行一些任务且不想扩栈时,可以使用G0,因为其栈空间比较大
  • G0的职责:创建goroutine、deferproc函数里新建_defer、垃圾回收相关工作(如stw、栈增长等)
锁如何实现

锁是一种同步机制,用于多任务环境中限制资源的访问,以满足互斥请求

map的实现

map是一个K/V对集合,底层使用hash table,用链表解决冲突,不是每一个key都申请一个结构通过链表串起来,而是以bmap为最小粒度挂载,一个bitmap可以放8个KV

  • map的底层结构是hmap,是由若干个结构为bmap的bucket组成的数组,每个bucket底层都采用链表结构
http包实现的原理
  • 处理HTTP请求主要和ServerMux和Handler有关
    • ServerMux本质上是一个http请求路由器,将收到的请求与一组预先定义的url路径列表做对比,然后匹配到路径的时候调用关联的处理器(Handler)
    • Handler负责输出HTTP响应头和正文,任何满足了http.Handler接口的对象都可作为一个处理器,即实现以下方法即可
      • ServerHTTP(http.ResponseWriter,*http.Request)
Mysql
Mysql索引使用的算法

使用B+树,查询效率高,B+树占用内存较高,平衡二叉树的高度太高,查找可能需要太多的磁盘IO

事务的基本要素
  • 原子性:事务是一个原子操作单元,对数据的修改,要么全部成功,要么全部失败
  • 一致性:事务执行前后,数据库的完整性约束未被破坏
  • 隔离性:同一时间只允许同一事务请求同一条数据,不同事务之间没有干扰
  • 持久性:事务完成后,对数据库做的所有操作都是永久性的,不能回滚
存储引擎
  • InnoDB
    • 支持事务,设计目标是面向在线事务处理的应用
    • 支持外键、行锁、支持非锁定锁,即默认读取操作不会产生锁
  • MyISAM
    • 不支持事务、外键、支持全文索引,索引文件和数据文件是分开的
事务隔离级别

Mysql默认隔离级别是可重复读

隔离级别脏读不可重复读幻读
read uncommitted👌👌👌
read committed👌👌
repeatable read👌
serializable
  • 读未提交
    • 一个事务可以读取另外一个事务还未提交的数据
  • 读已提交
    • 一个事务要等另一个事务提交后才能读取数据,可避免脏读发生
    • 可能出现不可重复读问题:在同一个事务执行完全相同的select语句时可能获取到不同的结果
      • 原因
      • 有一个交叉事务有新的commit,导致了数据的改变
      • 一个数据库被多个实例操作的同时,同一事务的其他实例在该实例处理期间可能有新的commit
  • 可重复读
    • 在开始读取数据时,不允许修改操作
    • 可能出现幻读:当用户读取某一范围的数据行时,另一个事务又在该范围内插入新行,当用户再次读取该范围的数据行时,会发现有新的行
  • 串行化
    • 事务串行化顺序执行
    • 通过强制事务排序,使之不可能相互冲突,从而解决幻读问题,就是在每个读的数据行上添加共享锁
    • 可能导致大量的超时现象和所竞争
utf8和utf8mb4区别
  • utf8mb4专门用来兼容4字节的unicode字符,utf8mb4是utf8的超集,除了编码改变外不需要做其它的转换
  • utf8编码的最大字符长度为3个字节,若遇到四字节的宽字符就会插入异常
悲观锁和乐观锁
  • 悲观锁
    • 就是很悲观,每次拿取数据时都认为会被别人修改,所以在每次拿去数据的时候都会上锁,这样别人想获取这个数据就会阻塞直到获取锁
  • 乐观锁
    • 每次拿去数据的时候都不认为别人会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制
    • 乐观锁适用于多读的应用类型,可以提高吞吐量,特点是:先进行业务操作,不到万不得已的情况下不会去拿锁。即认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿锁
    • 乐观锁在数据库上的实现是完全逻辑的,不需要数据库提供特殊支持,只需要在需要锁的数据上增加一个版本号或者时间戳
    • 在数据库内部update同一行的时候是不允许并发的,即数据库每执行一条update语句时会获取被update行的写锁,直到这一行数据被成功更新
索引类型
  • 类型
    • unique:不可以出现相同的值,可以有null值
    • index:允许出现相同的数据内容
    • primary key:主键所以,类似于unique
    • fulltext index:可以针对值中的某个单词,效率低
    • 组合索引:将多个字段建到一个索引中
  • 缺点
    • 降低更新表的速度、因为更新表时不仅要更新数据还要更新索引文件
    • 建立索引会占用磁盘空间
  • 技巧
    • 索引不会包含有null的列
      • 只要列中包含有null值,都将不会被包含到索引中,复合索引中只要有一列含有null值,那么对于此复合索引就是无效的
    • 使用短索引
      • 可以提高查询速度而且可以节省磁盘空间
    • 索引列排序
      • mysql查询只使用一个索引,因此若where子句已经使用索引的话,order by中的列是不会使用索引的,因此尽量少使用排序操作,不要包含多个列的排序
    • 不要使用like操作,使用的话也要使用like%格式
    • 不要在列上进行运算
    • 不要使用not in、<>、!=操作,但是> < <= >= = between in是可以使用索引的
    • 索引要建立在经常select查询的字段上
    • 索引要建立在值比较唯一的字段上
    • 对于定义text、image、bit数据类型的列不应该加索引,因为这些列要么数据量大,要么取值少
    • 在where和join出现的列需要添加索引
    • 若where子句的查询条件使用函数则无法使用索引
  • 组合索引的作用
    • 减少开销
      • 建立一个组合索引就相当于建立多个其他索引。每多一个索引都会增加写操作的开销和磁盘空间的开销
    • 覆盖索引
      • 通常一个查询语句的执行只用从索引中就能取得,不必从数据表中读取,也称之为覆盖索引
    • 效率高
      • 索引列越多,通过索引筛选出的数据越快
联合索引最左匹配原则
  • 最左优先,在检索数据时从联合索引的最左边开始匹配,一直向右直到遇到范围匹配就停止
    • a=1 and b= 2 and c > 1 and d=1,建立索引(a,b,c,d),d不能用到索引,若建立(a,b,d,c)索引,全部字段就都能使用索引,其中abd的顺序可以任意调整,因为查询优化其会重新编排
  • =和in可以乱序
聚簇索引和非聚簇索引

两种索引区别是:叶子节点是否存放一整行数据

  • InnoDB主键使用聚簇索引,MyISAM使用非聚簇索引
  • 对于聚簇索引表来说,表数据和主键是一起存储的,主键索引的叶子节点存储行数据包括主键值,二级索引的叶子节点存储行的主键值
  • 对于非聚簇索引,表数据和索引是分开存放的,主键索引和二级索引存储上没有区别,使用B+树作为索引的存储结构,所有的节点都是索引,叶子节点存储的是索引加上对应的记录的数据
  • 因此聚簇索引的叶子节点就是数据节点,非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据的指针
如何查询一个字段是否命中索引

explain

  • 当一个sql中索引字段的类型是int时,搜索条件where num = 1 或 where num ="1"都可以使用字段索引,当索引字段类型为varchar时,where num = 1 或 where num ="1",前者不可以使用索引,后者可以
什么情况下不会命中索引
  • 索引规范不合理
  • 表中索引是以数据量字段最多的建立的索引
  • bool字段作索引
  • 模糊查询
  • 索引列参与运算
  • 非最左前缀原则
  • where对null判断
  • where使用不等于
  • or操作至少有一个字段没有索引
  • 需要回表的查询结果集过大
MVVC

数据库并发控制,多版本并发控制

  • 悲观锁:当一个线程需要对共享资源进行操作的时候,首先对共享资源加锁,当该线程持有该资源锁的时候,其他线程对该资源进行的操作将会被阻塞

  • 乐观锁:当一个线程需要对共享资源进行操作的时候,不会加锁,而是在操作完成之后进行判断

  • mvvc两种读形式

    • 快照读
      • 读的只是当前事务的可见版本,不用加锁,select就是快照读
    • 当前读
      • 读取的是当前版本,比如特殊的读操作、更新/插入/删除操作
  • mvvc实现原理

    • 使用三个隐藏字段实现并发版本控制

      • RowIDDB_TRX_IDDB_ROLL_PTRidname
        自动创建的ID事务id回滚指针idname
      • rowid:隐藏的自增id,当建表没有指定主键,InnoDB会使用该rowid创建一个聚簇索引

      • db_trx_id:最近修改该记录的事务ID

      • db_roll_ptr:回滚指针,指向这条记录的上一个版本

      • 其实还有一个flag字段,用来判断该行记录是否已经被删除

MVVC和reao log、undo log、binlog有什么不同
  • mvvc
    • 多版本并发控制是基于mysql的乐观锁理论实现隔离级别的方式。用于读已提交和可重复读隔离级别的实现
    • 在mysql中,会在表中每一条数据后面添加两个字段,最近修改该上事务的id和只想该行回滚段的指针
    • read view判断行的可见性,创建一个新事务是,copy一份当前系统中活跃的事务列表
  • undo log
    • 回滚日志文件、主要用于事务执行失败、进行回滚,以及mvvc对于数据历史版本的查看
    • 由引擎层的InnoDB实现,是逻辑日志,记录数据修改被修改前的值
    • 当一条数据需要更新前,会先把修改前的记录存储在undo log中,若这个修改异常,则会使用undo日志实现回滚操作,保证事务一致性
    • 当事务提交之后,undo log不会立即删除,而是放到待清理链表中、待判断没有事务用到该版本的信息时才可以清理相应undolog
    • 保存了事务发生之前的数据的一个版本,用于回滚,同时可以提供多版本并发控制下的读,即非锁定读
  • redo log
    • 重做日志文件:记录数据修改之后的值,用于持久化到磁盘中
    • 包括两部分
      • 内存中的日志缓冲(redo log buffer):该部分日志容易遗失
      • 磁盘上的重做日志文件(redo log file):该部分日志是持久的
    • 由InnoDB实现,是物理日志,记录的是物理数据页修改的信息。当一条数据需要更新时,InnoDB会先将数据更新,然后记录在redo log内存中,之后找个时间将redo log的操作执行到磁盘上的文件
    • 不管是否提交成功都进行记录,若回滚,就将回滚的修改也记录下来。确保了事务的持久性
  • bin log
    • server层实现,是逻辑日志,记录的是sql语句的原始逻辑
    • bin log会写入指定大小的物理文件中,是追加写入的,当前文件写满将会创建新的文件写入
    • 产生:事务提交的时候,一次性将事务中的sql语句按照一定的格式记录到bin log中
    • 用于复制和恢复在主从复制中,从看利用主库上的binlog进行重播,实现主从同步。业务数据不一致或错了就使用binlog恢复
读写分离与主从复制
  • 原理
    • 主库将变更写入binlog日志后,然后从库连接到主库后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中,接着从库中有一个sql线程会从中继日志中读取binlog,然后执行binlog中的内容,也就是在自己本地在执行一次sql,就可以保证与主库的数据一致
  • 问题
    • 从库同步主库数据的过程是串行化的,也就是说主库上并行操作,在从库上会串行化执行,由于从库从主库拷贝日志以及串行化执行sql特点,在高并发情况下,从库数据一定比主库慢一点,是有延时的,所以经常出现,刚写入主库的数据可能读不到了,要过几十毫秒,甚至几百毫秒才能读取到。
    • 如果突然主库宕机了,然后恰巧数据还没有同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。所以mysql实际上有两个机制
      • 半同步复制,用来解决主库数据丢失问题
        • 指的是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log后,会接着返回一个ack给主库,主库收到至少一个从库ack之后才会认为写完成
      • 并行复制,用来解决主从同步延时问题。
        • 从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志
InnoDB关键特性
  • 插入缓冲:对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而实现判断插入的非聚集索引页是否在缓冲池中,若在则直接插入,若不在,则先放到一个insert buffer对象中。然后以一定的频率和情况进行inset buffer和辅助索引叶子节点的合并操作,这是通常可以将多个插入合并到一个操作中,大大提高插入性能
  • 两次写:提高数据页的可靠性
  • 自适应哈希索引:innoDB会监控对表上各索引页的查询,若观察到建立哈希索引可以带来速度提升,则建立哈希索引
  • 异步IO:为了提高磁盘操作性能
  • 刷新邻接页:当刷新一个脏页时,InnoDB会检测该页所在区的所有页,若是脏页,则一起进行刷新
如何保证数据一致性和持久性
  • 使用WAL(先写日志在写磁盘),redolog就是一种WAL的应用
  • 当数据库忽然断电,在进行重启,mysql可以通过redolog还原数据,也就是每次事务提交时,不用同步刷新到磁盘数据文件,只要同步刷新redolog文件即可
InnoDB行锁模式
  • 排他锁:用法for update,称为写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁
    • 若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。在没有索引的情况下,InnoDB只能使用表锁
  • 共享锁
    • lock in share mode,称为读锁,允许一个事务去读,阻止其他事务获得相同数据集的排他锁
    • 若事务T对数据对象A加上S锁,则事务T可以duquA但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改
redis
数据结构及使用场景
  • string
    • redis最基础的数据结构,键都是字符串类型,其他几种数据结构都是在字符串类型基础上构建的
    • set key value,常用于缓存、计数、session共享、限速等
  • hash
    • 指的是键值本身又是一个键值对结构,可以用来存放用户信息
  • list
    • 用来存储多个有序字符串,可以做简单的消息队列
  • set
    • 用来保存多个字符串元素,set中不允许有重复元素,且元素无序,不能通过索引下标获取元素
    • 利用set的交集并集差集等操作,可以计算共同喜好等
  • zset
    • 有序集合,可以做排行榜、获取top N操作
持久化方式

redis为了保证效率,数据缓存在内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化

  • RDB:快照形式是直接把内存中的数据保存到一个dump(转储)的文件中,定是保存,保存策略
    • 当redis需要做持久化时,redis会fork(分支)一个子进程,子进程将数据写到磁盘上的一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉
  • AOF:将所有的对redis服务器进行修改的命令都存到一个文件里,命令的集合
    • 使用AOF做持久化,每个命令都会被写到appenonly.aof文件
    • AOF策略是每秒钟fsync一次,在这种配置下就算发生故障停机,也最多丢失一秒种的数据
    • 缺点:对于相同的数据集来说,AOF文件体积通常要大于RDB文件体积。根据其使用fsync策略,AOF的速度可能要慢于RDB
  • redis默认是RDB持久化方式,对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB),之后进行增量同步(AOF)
redis的LRU具体实现(缓存淘汰算法)
  • 传统的LRU是使用栈的形式,每次都将最新使用的数据移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据
  • redis的实现
    • redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳
    • 首先第一次随机选取的key都会放入一个pool中,pool大小为16,pool中的key是按照lru大小顺序排列的,接下来每次随机选取的key lru值必须小于pool中最小的lru才会继续放入,直到pool满。放满之后,每次若有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰
单线程redis为什么快
  • 存内存操作
  • 避免频繁的上下文操作
  • 合理高效的数据结构
  • 采用非阻塞IO多路复用机制
redis数据过期策略
  • 定时删除
    • redis启动一个定时器定时监视所有的key,判断key是否过期,过期的话就删除
    • 可以保证过期的key最终都会被删除,但是会非常消耗CPU资源,并且当key已过期,但定时器还未处于唤起状态,这段时间内key仍然可以用
  • 惰性删除
    • 在获取key时先判断是否过期,过期则删除
    • 缺点:若这个key一直未被使用就一直存在内存中,其实它已经过期了,浪费大量内存
  • 内存淘汰机制
    • 当内存不足以容纳新写入的数据时,新写入操作会报错(redis默认)
    • 当内存不足以容纳新写入的数据时,在键空间中,随机移除某个key
    • 当内存不足以容纳新写入的数据时,在键空间中、移除最少使用的key
    • 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,移除最近少使用的key
    • 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,随机移除某个key
    • 当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
如何解决缓存雪崩问题
  • 使用redis集群
  • 缓存时间不一致,给缓存添加失效时间,加上一个随机值,避免集体失效
如何解决缓存穿透问题
  • 在接口层做校验
  • 存null值
  • 布隆过滤器拦截:将所有可能的查询key先映射到过滤器中,查询时先判断key是否存在过滤器中,存在则继续向下执行,否则返回
并发竞争key如何解决
  • 利用消息队列
  • 利用分布式锁和时间戳
主从模式、哨兵模式和集群模式的区别
  • 主从模式
    • 最简单的模式。主要基于redis的主从复制特性架构的。通常会设置一个主节点,N个从节点,主节点复制处理使用者的IO操作,从节点将会对主节点的数据进行备份,并且也会对外提供读操作处理
    • 特点
      • 主从模式下,当一个节点损坏时,因为其会将数据备份到其他redis实例上,可以很大程度上可以恢复丢失的数据
      • 可以保证负载均衡
      • 主节点和从节点读写是分离的,使用者不仅可以从主节点上读取数据也可以很方便的从从节点读取数据,缓解主机压力
    • 从节点也支持写入数据,只不过从节点写入的数据不会同步到主节点以及其他从节点
    • 在主从模式下,一旦主节点宕机,其他节点不会竞争成为主节点,此时redis将丧失写的能力
  • 哨兵模式
    • 核心还是主从复制,只不过多了一个竞选机制——从所有的从节点中选出现的主节点,依赖于sentinel进程
    • 特点
      • 监控:它会监听主服务器和从服务器之间是否在正常工作
      • 通知:能通过API告诉系统管理员或程序,集群中的某个实例出现问题
      • 故障转移:在主节点出问题的情况下,会在所有从节点中重新选取一个主节点
      • 提供主服务器地址:向使用者提供当前主节点的地址
    • sentinel可以通过发布与订阅来自动发现redis集群上的其他sentinel。sentinel在发现其他sentinel进程后,会将其放入一个列表,此列表存储所有已经发现的sentinel
    • 集群中的sentinel不会并发着对同一个主节点进行故障转移。故障转移只会从第一个sentinel开始,当第一个失败后才会尝试下一个
    • 当选择一个从节点作为主节点后,故障转移成功。在此过程中,若重启了旧的主节点,就会出现无主节点的情况,此情况下,就要重启集群
    • 当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被sentinel改写为旧的主节点的配置信息。完成改写之后,在将新的主节点的配置广播给所有的从节点
  • 集群模式
    • 提供在多个redis节点间共享数据的程序集,redis集群分为主节点和从节点。主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线时,代替下线的主节点继续处理命令请求
    • 通过分区提供一定程度上的可用性,优势:
      • 自动分割数据道不同的节点上
        • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令
    • redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置那个槽,集群的每个节点负责一部分hash槽
    • 这种结构很容易添加或删除节点,由于从一个节点将哈希槽移动到另一个节点不会停止服务,所以无论添加或改变某个节点的哈希槽数量都不会造成集群不可用的状态
事务

multi、exec、discard、watch是实现事务的基石

  • 特征
    • 在事务中的所有命令豆浆会被串行化的顺序执行,事务执行期间,redis不会再为其他客户端的请求提供任何服务,从而保证事务中的所有命令都被原子的执行
    • redis事务中若有某一条命令执行失败,其后的命令仍然会继续执行
    • 通过multi开启事务,在该语句之后执行的命令都被视为事务内的操作,执行exec进行提交,执行discard进行回滚
    • 事务开启之前、若客户端与服务器之间出现通信故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。若网络中短的时间是发生在客户端执行exec后,那么该事务中所有的命令都将会被服务器执行
    • 当使用append-only模式时,redis会通过调用系统函数write将该事务中的所有操作在本次调用中全部写入磁盘。若在写入的过程中出现崩溃,那么此时可能只有部分数据写入到磁盘,另一部分丢失
  • redis服务器会在重启时进行一系列的必要的一致性检测,一旦发现类似问题就会立即退出并展示相应错误信息
zset底层实现
  • 用跳表实现的
    • 跳表是一个随机化的数据结构,实质就是一个可以进行二分查找的有序链表
    • 跳表在原有的有序链表上加了多级索引,通过索引实现快速查找
    • 跳表不仅可以提高搜索性能,也可以提高插入和删除操作的性能
  • 跳表可以实现二分查找的有序链表
  • 每个元素插入时随机生成它的level
  • 最低层包含所有的元素
  • 若一个元素出现在level(x),那么它肯定出现在x以下的level中
  • 每个索引节点包含两个指针:一个向左、一个向右
  • 跳表的查询过程
    • 先从第一层查找,不满足就下沉到第二层查找,因为每一层都是有序的,所以时间复杂度是O(logN)
网络协议
tcp和udp的区别
  • tcp面向连接,udp是无连接的,即发送数据之前不需要建立连接
  • tcp提供可靠的传输服务,通过其传送的数据无差错、不丢失、不重复,且按序到达;udp不保证可靠交付
  • tcp面向字节流,实际上tcp把数据看成一连串无结构的字节流;udp是面向报文的
  • tcp连接是点对点的;udp可以是一对一、一对多、多对多、多对一的交互通信
  • tcp首部开销20字节;udp首部开销8字节
  • tcp的逻辑通信信道是全双工的可靠信道;udp是不可靠信道
三次握手、四次挥手
  • 三次握手A为客户端,B为服务端

    • 首先B处于listen状态,等待客户端的连接请求
    • A向B发送连接请求报文段,SYN=1 ACK=0,选择一个初始序号seq=x
    • B收到A的连接请求报文段,若同意建立连接,则向A发送连接确认报文段SYN=1 ACK=1,确认号为ack=x+1,同时也选择一个初始的序号seq=y
    • A收到B的连接确认报文段后,还要向B发出确认,确认号为ack=y+1序号为seq=x+1
    • A的TCP通知上层应用进程,连接已建立
    • B收到A的确认后,连接建立
    • B的TCP主机收到A的确认后,也通知其上层应用进程:tcp连接已建立
  • 四次挥手

    • 数据传输结束后,通信的双方都可以释放连接。现在A进程先向其TCP发出连接释放报文段,并停止在发送数据,主动关闭tcp连接
      • A把连接释放报文段FIN=1,序号seq=u,等待B的确认
      • B发出确认,确认号为ack=u+1,而这个报文段自己的序号为seq=v
      • 从A到B这个方向的连接就释放了,tcp连接处于半关闭状态。A不能向B发送数据。B若发送数据,A仍然接收
      • 当B不再需要连接是,发送释放请求报文段FIN=1
      • A收到后发出确认,进入time_wait状态,等待2MSL(4mins)时间后释放连接
      • B收到A的确认后释放连接
    • 原因:客户端发送FIN连接释放报文段之后,服务器收到了这个报文,就进入和close-wait状态,这个状态是为了让服务器端还为传送完毕的数据,传送完毕之后,服务器会发送FIN连接释放报文
301和302区别

301 moved permanently:被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一

320:临时重定向,客户端应当继续向原有地址发送以后的请求。

504和500的区别

500:由于服务器上代码出现错误或抛出异常

502:网关错误,通常是程序空指针错误

504:网关超时

http与https区别
  • 传输信息安全性不同
    • http,信息明文传输,若攻击者截取了web浏览器和服务器之间的传输报文,就可以直接读取其中的信息
    • https,是具有安全性的ssl加密传输协议,确保数据传输安全
  • 连接方式不同
    • http是无状态连接
    • https是由ssl+http协议构建的可进行加密传输、身份认证的网络协议
  • 端口不同
    • http 80
    • https 443
  • 证书申请方式不同
    • http:免费申请
    • https:需要到ca申请,一般需要收费
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值