Golang

Golang除了加Mutex锁之外还有哪些方式安全读写共享变量
  • Goroutine可以通过channel进行安全读写
  • 可以通过原子操作进行
无缓冲的chan的发送和接收是否是同步的
make(chan int)make(chan int, 1)
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中常用的并发模型
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中没有数据,就会一直阻塞,直到有值
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()
}
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()
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控制的,协程的切换是由用户控制的

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中的锁
// 首次使用后不得复制互斥锁
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()
如何限制goroutine的数量

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

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)
    }
}
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能不能比较
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延迟函数,可以处理易忽略的问题,如资源释放、连接关闭等

// 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逻辑

// 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之间请求域的数据、取消信号、截止时间等相关操作

context.Background()withCancelwithTimeoutwithDeadlinecontext.TODO
Go的slice如何扩容

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

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

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

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

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

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

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

channel[]*strng
值接收者和指针接收者的区别

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

函数和方法值接收者指针接收者
值类型调用者方法会使用调用者的一个副本使用值的引用来调用方法,p.get(),相当于(&p).get()
指针调用者指针被解引用为值,p.get()相当于(*p).get()实际上也是传值,方法里的操作会影响到调用者,类似于指针传参
原始的本质
Go对象在内存中如何分配
mspan512G/(4*8B)=16G512G/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包实现的原理
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行的写锁,直到这一行数据被成功更新
索引类型
like%
联合索引最左匹配原则
  • 最左优先,在检索数据时从联合索引的最左边开始匹配,一直向右直到遇到范围匹配就停止
    • 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

where num = 1 或 where num ="1"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有什么不同
逻辑日志物理日志逻辑日志
读写分离与主从复制
  • 原理
    • 主库将变更写入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 updatelock in share mode

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是不可靠信道
三次握手、四次挥手
SYN=1 ACK=0seq=xSYN=1 ACK=1ack=x+1seq=yack=y+1seq=x+1
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申请,一般需要收费