surongzhen / php-go-Interview

面试宝典

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GO相关

进程 线程 协程

  • 进程 即运行起来的程序, 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程至少有一个线程。进程切换只发生在内核态,是系统资源分配的最小单位
  • 线程 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,是由操作系统调度,是操作系统调度(CPU调度)执行的最小单位。对于进程和线程,都是由内核进行调度,有 CPU 时间片的概念, 进行抢占式调度。内核由系统内核进行调度, 系统为了实现并发,会不断地切换线程执行, 由此会带来线程的上下文切换,一个进程可以有多个线程,每个线程会共享父进程的资源
  • 协程 独立的栈空间, 共享堆空间,协程是由程序员在协程的代码中显示调度,属于用户态线程,协程是对内核透明的, 也就是系统完全不知道有协程的存在, 完全由用户自己的程序进行调度。在栈大小分配方便,且每个协程占用的默认占用内存很小,只有 2kb ,而线程需要 8mb,相较于线程,因为协程是对内核透明的,所以栈空间大小可以按需增大减小,轻量且开销较小。

解释GMP模型

GMP 模型是 Go 语言调度器采用的并发编程模型,它包含三个重要的组件:Goroutine(G)、逻辑处理器(P)和操作系统线程(M)。这些组件协同工作以实现 Go 程序的高效并发执行。

具体来说,

  • Goroutine (G) 是 Go 语言中轻量级的并发执行单元,类似于线程但比线程更小、更灵活。每个 goroutine 都有自己独立的堆栈和寄存器等信息,可以通过 go 关键字创建并发执行任务。
  • 逻辑处理器(P)是一个虚拟的执行单元,负责调度 goroutine 和执行 Go 代码。Go 程序中有多个 P,每个 P 可以运行多个 goroutine,因此可以实现真正的并发执行。
  • 操作系统线程(M)是实际的执行单元,负责将 goroutine 调度到逻辑处理器上执行。Go 程序中通常会创建多个 M,以便在多核 CPU 上实现并发执行。

GMP的调度流程

GMP 调度器采用抢占式的协作调度,具体调度流程如下:

  1. 主线程启动,在主线程中创建一个操作系统线程(M)和一个逻辑处理器(P)。
  2. 当有 goroutine 函数被调用时,它会被放入到一个全局队列中等待执行。
  3. P 从全局队列中获取任务并执行。如果 P 执行的 goroutine 阻塞(例如在等待 I/O 完成),则该 P 的所有 goroutine 都会被暂停,P 会将自己标记为阻塞状态并开始寻找其他可用的 P。
  4. 如果没有可用的 P,则 M 变为自由线程,并且会去创建一个新的 P,以便执行未完成的 goroutine。新的 P 将加入到一个全局 P 列表中,而 M 将继续尝试在列表中寻找可用的 P。
  5. 当 goroutine 阻塞时,Goroutine 在堆上分配一块内存来保存其状态,并被添加到相关的等待队列中。而主线程会进入休眠状态,等待唤醒事件发生。
  6. 当阻塞的 goroutine 可以继续执行时,调度器会将它从等待队列中移除,并将其重新添加到全局队列中,等待 P 来执行。
  7. 当程序结束时,所有未完成的 goroutine 都会被杀死,而 P 和 M 也会被回收。

P和M的个数

  • P: 由启动时环境变量 $GOMAXPROCS 或者是由 runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。
  • M:
    • Go 语言本身的限制:Go 程序启动时,会设置 M 的最大数量,默认 10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略。
    • runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量。
    • 一个 M 阻塞了,会创建新的 M。

M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。

P和M何时会被创建

P: 在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

M: 没有足够的 M 来关联 P 并运行其中的可运行的 G 时创建。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。

goroutine创建流程

Go 语言中,goroutine 的创建非常简单,只需要使用 go 关键字即可。go 关键字后面跟随一个函数调用(或者匿名函数),该函数就会在新的 goroutine 中并发执行。

具体的创建流程如下:

  1. 确定要并发执行的函数或匿名函数。
  2. 使用 go 关键字创建新的 goroutine 并启动并发执行。
  3. 调度器将新的 goroutine 分配给逻辑处理器,然后绑定到操作系统线程上执行。
  4. 新的 goroutine 开始执行,其状态和堆栈等信息由调度器自动管理。
  5. 当函数执行结束时,goroutine 自动终止,其状态和堆栈等资源也会被回收。

值得注意的是,Go 语言的调度器可以同时运行数百万个 goroutine,因此不必担心创建大量的 goroutine 会导致系统负荷过重。但是,如果你的程序中存在频繁创建和销毁 goroutine 的情况,应该考虑使用 sync.Pool 等技术来优化内存分配和回收效率。

goroutine什么时候会被挂起

goroutine 会在以下情况下被挂起:

  1. 发生阻塞,例如等待 I/O 操作的完成或者发送或接收通道上的数据时没有可用的对等方。
  2. 发生调用 runtime.Gosched(),让出 CPU 给其他 goroutine 执行。
  3. 发生同步操作,例如 sync.Mutex 或 sync.WaitGroup 的锁定和解锁操作。
  4. 发生垃圾回收(GC)。
  5. 发生错误,例如 panic 或者超时。

同时启动了一万个goroutine,会如何调度

一万个G会按照P的设定个数,尽量平均地分配到每个P的本地队列中。如果所有本地队列都满了,那么剩余的G则会分配到GMP的全局队列上。接下来便开始执行GMP模型的调度策略:

  • 本地队列轮转:每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队首中重新取出一个G进行调度。
  • 系统调用:P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。当该G即将进入系统调用时,对应的M由于陷入系统调用而进被阻塞,将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。
  • 工作量窃取:多个P中维护的G队列有可能是不均衡的,当某个P已经将G全部执行完,然后去查询全局队列,全局队列中也没有新的G,而另一个M中队列中还有3很多G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。

goroutine内存泄漏原因和处理

原因

Goroutine 是轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息,而这些内存在目前版本的 Go 中是不会被释放的。因此,如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。造成泄露的大多数原因有以下三种:

  • Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。
  • Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
  • Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。

解决方法

  • 使用channel
    • 1、使用channel接收业务完成的通知
    • 2、业务执行阻塞超过设定的超时时间,就会触发超时退出
  • 使用pprof排查
    • pprof是由 Go 官方提供的可用于收集程序运行时报告的工具,其中包含 CPU、内存等信息。当然,也可以获取运行时 goroutine 堆栈信息。
  • 使用 context 进行超时控制:context 包提供了超时控制等功能,可以避免 goroutine 在执行过程中发生死循环等问题。
  • 利用 Go 的垃圾回收机制:Go 运行时系统包含了自动垃圾回收机制,可以周期性地检查并回收不再使用的内存。

Go语言中的并发安全性是什么?如何确保并发安全性?

并发安全性是指在并发编程中,多个goroutine对共享资源的访问不会导致数据竞争和不确定的结果。

为了确保并发安全性,可以采取以下措施:

  • 使用互斥锁(Mutex):通过使用互斥锁来保护共享资源的访问,一次只允许一个goroutine访问共享资源,从而避免竞争条件。
  • 使用原子操作(Atomic Operations):对于简单的读写操作,可以使用原子操作来保证操作的原子性,避免竞争条件。
  • 使用通道(Channel):通过使用通道来进行goroutine之间的通信和同步,避免共享资源的直接访问。
  • 使用同步机制:使用同步机制如等待组(WaitGroup)、条件变量(Cond)等来协调多个goroutine的执行顺序和状态。

通过以上措施,可以确保并发程序的安全性,避免数据竞争和不确定的结果。

Go语言中的defer关键字有什么作用?

defer关键字用于延迟函数的执行,即在函数退出前执行某个操作。defer通常用于释放资源、关闭文件、解锁互斥锁等清理操作,以确保在函数执行完毕后进行处理。

也可以使用defer语句结合time包实现函数执行时间的统计。

什么是互斥锁(Mutex)?在Go语言中如何使用互斥锁来保护共享资源?

互斥锁是一种并发编程中常用的同步机制,用于保护共享资源的访问。

在Go语言中,可以使用sync包中的Mutex类型来实现互斥锁。通过调用Lock方法来获取锁,保护共享资源的访问,然后在使用完共享资源后调用Unlock方法释放锁。

我们定义了一个全局变量counter和一个sync.Mutex类型的互斥锁mutex在increment函数中我们使用mutex.Lock()获取锁对counter进行递增操作然后使用mutex.Unlock()释放锁通过使用互斥锁我们确保了对counter的并发访问的安全性package main

import (
 "fmt"
 "sync"
)
var (
 counter int
 mutex   sync.Mutex
)
func increment() {
 mutex.Lock()
 counter++
 mutex.Unlock()
}
func main() {
 var wg sync.WaitGroup
 for i := 0; i < 1000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   increment()
  }()
 }
 wg.Wait()
 fmt.Println("Counter:", counter)
}

原子操作和锁的区别是什么?

原子操作和锁是并发编程中常用的两种同步机制,它们的区别如下:

  1. 作用范围:
  • 原子操作(Atomic Operations):原子操作是一种基本的操作,可以在单个指令级别上执行,保证操作的原子性。原子操作通常用于对共享变量进行读取、写入或修改等操作,以确保操作的完整性。
  • 锁(Lock):锁是一种更高级别的同步机制,用于保护临界区(Critical Section)的访问。锁可以用于限制对共享资源的并发访问,以确保线程安全。
  1. 使用方式:
  • 原子操作:原子操作是通过硬件指令或特定的原子操作函数来实现的,可以直接应用于变量或内存位置,而无需额外的代码。
  • 锁:锁是通过编程语言提供的锁机制来实现的,需要显式地使用锁的相关方法或语句来保护临界区的访问。
  1. 粒度:
  • 原子操作:原子操作通常是针对单个变量或内存位置的操作,可以在非常细粒度的层面上实现同步。
  • 锁:锁通常是针对一段代码或一组操作的访问进行同步,可以控制更大粒度的临界区。
  1. 性能开销:
  • 原子操作:原子操作通常具有较低的性能开销,因为它们是在硬件级别上实现的,无需额外的同步机制。
  • 锁:锁通常具有较高的性能开销,因为它们需要进行上下文切换和线程同步等操作。

综上所述,原子操作和锁是两种不同的同步机制,用于处理并发编程中的同步问题。原子操作适用于对单个变量的读写操作,具有较低的性能开销。而锁适用于对一段代码或一组操作的访问进行同步,具有更高的性能开销。选择使用原子操作还是锁取决于具体的场景和需求。

需要注意的是,原子操作通常用于对共享变量进行简单的读写操作,而锁更适用于对临界区的访问进行复杂的操作和保护。在设计并发程序时,需要根据具体的需求和性能要求来选择合适的同步机制。

Go语言中的select语句是什么?请给出一个使用select语句的示例?

select语句是Go语言中用于处理通道操作的一种机制。它可以同时监听多个通道的读写操作,并在其中任意一个通道就绪时执行相应的操作。

slice扩容策略

Go 1.18版本后

新申请的容量如果大于当前容量的两倍,会将新申请的容量直接作为新的容量,如果新申请的容量小于当前容量的两倍,会有一个阈值,即当前切片容量小于1024时,切片会将当前容量的2倍作为新申请的容量,如果大于等于1024,会将当前的容量的1.25倍作为新申请的容量。

Go 1.18版本后**

新申请的容量如果大于当前容量的两倍,会将新申请的容量直接作为新的容量,如果新申请的容量小于当前容量的两倍,会有一个阈值,即当前切片容量小于256时,切片会将当前容量的2倍作为新申请的容量,如果大于等于256,会将当前的容量的1.25倍+192作为新申请的容量,扩容的时候更加平滑,不会出现从2到1.25的突变。

Go 的垃圾回收机制

  • 手动释放占用的内存空间 程序代码中也可以使用runtime.GC()来手动触发GC

  • 自动内存回收 内存分配量达到阀值触发GC 定期触发GC, 默认情况下,最长2分钟触发一次GC

  • 三色标记最大的好处是可以异步执行,以中断时间极少的代价或者完全没有中断来进行整个 GC。 Go 采用的是三色标记法,将内存里的对象分为了三种:

    • 白色对象:未被使用的对象;
    • 灰色对象:当前对象有引用对象,但是还没有对引用对象继续扫描过;
    • 黑色对象,对灰色对象的引用对象已经全部扫描过了,下次不用再扫描它了。

    只要是新创建的对象, 默认都会标记为白色, 当垃圾回收开始时,Go 会把根对象标记为灰色,其他对象标记为白色,然后从根对象遍历搜索,按照上面的定义去不断的对灰色对象进行扫描标记。当没有灰色对象时,表示所有对象已扫描过,然后就可以开始清除白色对象了。

channel 的内部实现是怎么样的

底层 hchan结构体的主要组成部分

  • 用来保存goroutine之间传递数据的循环链表-------->buf
  • 用来记录此循环链表当前发送或接收数据的下标值---------->sendx和recvx
  • 用于保存向该chan发送和从该chan接收数据的goroutine队列---------->sendq和recvq
  • 保证chan写入和读取数据时的线程安全的锁----------->lock

channel 内部通过队列实现, 有一个唤醒队列队列作为缓冲区,队列的长度是创建chan时指定的。维护了两个 goroutine 等待队列,一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。 从channel中读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞;向channel中写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。被阻塞的goroutine将会被挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒

直到有其他 goroutine 执行了与之相反的读写操作,将它重新唤起。

并且内部维护了一个互斥锁, 来保证线程安全, 即在对buf中的数据进行入队和出队操作时, 为当前channel使用了互斥锁, 防止多个线程并发修改数据

向channel写数据 在这里插入图片描述

  1. 如果recvq队列不为空,说明缓冲区没有数据或无缓冲区,且有等待取值的goroutine在排队, 此时直接从recvq等待队列中取出一个G,并把数据写入,最后把该G唤醒,结束发送过程;
  2. 如果缓冲区有空余位置,则把数据写入缓冲区中,结束发送过程;
  3. 如果缓冲区没有空余位置,将当前G加入sendq队列,进入休眠,等待被读goroutine唤醒;

从channel读数据 在这里插入图片描述

  1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq队列中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  2. 如果等待发送队列sendq不为空,说明缓冲区已满,从缓冲队列中首部读取数据,从sendq等待发送队列中取出G,把G中的数据写入缓冲区尾部,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;

对已经关闭的 channel 进行读写,会怎么样

Go语言中的通道(channel)是一种非常有用的特性,用于在不同的goroutine之间传递数据, 在使用通道时,需要根据实际情况判断何时关闭通道,以避免出现不必要的竞态和内存泄漏。

  • 当 channel 被关闭后,如果继续往里面写数据,程序会直接 panic 退出
  • 关闭已经关闭的channel会发生Panic
  • 关闭值为 nil 的channel会发生Panic
  • 通道关闭后, 读取操作仍然可以从通道中读取到之前写入的数据。这是因为通道中的数据并没有立即消失,而是在读取完毕后被垃圾回收器回收。当关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。
// 判断当前 channel 是否被关闭
if v, ok := <-ch; !ok {
        fmt.Println("channel 已关闭,读取不到数据")
    }
1234

还可以使用下面的写法不断的获取 channel 里的数据:

// range迭代从channel中读数据, 只有当channel关闭后才能退出循环, 否则没有数据了也会一直阻塞
   for data := range ch {
        // get data dosomething
    }
1234

使用for-range读取channel, 这样既安全又便利, 当channel关闭时, 循环会自动退出, 无需主动检测channel是否关闭, 可以防止读取已经关闭的channel, 造成读取数据为通道所存储类型的零值

并发安全性

并发安全性是指在并发编程中,多个goroutine对共享资源的访问不会导致数据竞争和不确定的结果。为了确保并发安全性,可以采取以下措施:

  1. 使用互斥锁(Mutex):通过使用互斥锁来保护共享资源的访问,一次只允许一个goroutine访问共享资源,从而避免竞争条件。
  2. 使用原子操作(Atomic Operations):对于简单的读写操作,可以使用原子操作来保证操作的原子性,避免竞争条件。
  3. 使用通道(Channel):通过使用通道来进行goroutine之间的通信和同步,避免共享资源的直接访问。
  4. 使用同步机制:使用同步机制如等待组(WaitGroup)、条件变量(Cond)等来协调多个goroutine的执行顺序和状态。

runtime

runtime包是Go语言的运行时系统,提供了与底层系统交互和控制的功能。它包含了与内存管理、垃圾回收、协程调度等相关的函数和变量

指针

指针是一种变量,存储了另一个变量的内存地址。通过指针,我们可以直接访问和修改变量的值,而不是对变量进行拷贝。

指针在传递大型数据结构和在函数间共享数据时非常有用。

接口 interface

Go语言中的接口(interface)是一种非常重要的特性,用于定义一组方法

接口是一种动态类型,它可以包含任何实现了它所定义的方法集的类型。在使用接口时,需要注意以下几点:

  1. 接口是一种引用类型的数据结构,它的值可以为nil。
  2. 实现接口的类型必须实现接口中所有的方法,否则会编译错误。
  3. 接口的值可以赋给实现接口的类型的变量,反之亦然。
  4. 在实现接口的类型的方法中,可以通过类型断言来判断接口值的实际类型和值。

map 类型

map 是一种无序的键值对集合,也称为字典。map中的键必须是唯一的,而值可以重复。map 提供了快速的查找和插入操作,适用于需要根据键快速检索值的场景。 map 是使用哈希表、链表来实现的 我们从map中访问一个不存在的键时,它会返回该值类型的零值。 map是一种引用类型的数据结构,它的底层实现是一个哈希表。在使用map时,需要注意以下几点:

  1. map是无序的,即元素的顺序不固定。每次迭代map的顺序可能不同
  2. map的键必须是可以进行相等性比较的类型,如int、string、指针等。(通俗来说就是可以用 == 和 != 来比较的,除了slice、map、function这几个类型都可以)
  3. map的值可以是任意类型,包括函数、结构体等。
  4. map不是并发安全的, 在多个goroutine之间使用map时需要进行加锁,避免并发访问导致的竞态问题。

map 为什么是不安全的

Go 官方认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),所以决定了不支持并发安全。

因为它没有内置的锁机制来保护多个 goroutine 同时对其进行读写操作,而是会对某个标识位标记为 1,当多个 goroutine 同时对同一个 map 进行读写操作时,就会出现数据竞争和不一致的结果,当它检测到标识位为 1 时,将会直接 panic。

如何实现map线程安全

  1. 使用 map + 读写锁 sync.RWMutex
  2. 使用 sync.map sync.map是通过读写分离实现的,拿空间换时间, 通过冗余两个数据结构(read、dirty), 减少加锁对性能的影响, 可以无锁访问 read map, 而且会优先操作read map(不需要锁),倘若只操作read map就可以满足要求(增删改查遍历),那就不用去操作dirty map(它的读写都要加锁),所以在某些特定场景中它发生锁竞争的频率会远远小于方式1。

sync.Map 适合读多写少的场景, 且性能比较好,否则并发性能很差, 因为会动态调整,miss次数多了之后,将dirty数据提升为read

concurrent-map 提供了一种高性能的解决方案:通过对内部 map 进行分片,降低锁粒度,从而达到最少的锁等待时间(锁冲突)。, double-checking, 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据

map 的 key 为什么得是可比较类型的

map 的 key、value 是存在 buckets 数组里的,每个 bucket 又可以容纳 8 个 key-value 键值对。当要插入一个新的 key - value 时,会对 key 进行哈希计算得到一个 hash 值,然后根据 hash 值的低几位(取几位取决于桶的数量)来决定命中哪个 bucket。

在命中某个 bucket 后,又会根据 hash 值的高 8 位来决定是 8 个 key 里的哪个位置。若发生了 hash 冲突,即该位置上已经有其他 key 存在了,则会去其他空位置寻找插入。如果全都满了,则使用 overflow 指针指向一个新的 bucket,重复刚刚的寻找步骤。

从上面的流程可以看出,在判断 hash 冲突,即该位置是否已有其他 key 时,肯定是要进行比较的,所以 key 必须得是可比较类型的。像 slice、map、function 就不能作为 key。

遍历时, map 的 key 为什么是无序的

  • 首先, map 在扩容后,会发生 key 的迁移,原来落在同一个 bucket 中的 key,可能迁移到别的 bucket 中。即使按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。由于扩容导致 key 的位置发生变化,遍历 map 也可能不按原来的顺序了
  • 再者, 当遍历 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了

map的有序遍历

在Go语言中,map是无序的,每次迭代map的顺序可能不同。如果需要按特定顺序遍历map,可以采用以下步骤:

  1. 创建一个切片来保存map的键。
  2. 遍历map,将键存储到切片中。
  3. 对切片进行排序。
  4. 根据排序后的键顺序,遍历map并访问对应的值。

make 和 new 的区别

new

  • 分配内存。将会申请某个类型的内存, 内存里存的值是对应类型的零值
  • 只有一个参数。参数是分配的内存空间所存储的变量类型,Go语言里的任何类型都可以是new的参数,比如int, 数组,结构体,甚至函数类型都可以
  • 返回的是某类型的指针

make

  • 分配和初始化内存
  • 只能用于slice, map和chan这3个类型,不能用于其它类型。如果是用于slice类型,make函数的第2个参数表示slice的长度,这个参数必须给值
  • 返回的是原始类型,也就是slice, map和chan,不是返回指向slice, map和chan的指针

return 与 defer

return 并不是原子操作, 底层是两个步骤

  1. 返回值赋值, 返回值有匿名返回值, 具名返回值
  2. 执行defer
  3. 执行RET指令, 函数携带当前返回值退出

REDIS 相关

Redis数据类型有哪些

string

支持对字符串, 位, 数的操作

  • 计数功能,比如点赞数、粉丝数的操作, 计数器
  • 记录 session, token, 为服务无状态

hash

主要是用来存储对象 (整个对象进行存储,里面包含了多个字段)

  • 用户信息 用户信息序列化后的数据
  • 购物车 利用hash结构, 用户ID为 key, 商品ID 为 field, 商品数量为 value, 添加商品 hset cart:1001 10088 1, 增加数量 hincrby cart:1001 10088, 1. 优点: 同类数据归类整合存储, 方便数据管理 2. 相比 string 操作消耗CPU和内存更小, 性能更高且存储更节省空间, 缺点: 1. 过期功能不能使用在field上, 只能用在 key 上 2. Redis集群架构下不适合大规模使用(若值很大, 但key固定, 则只会存储到同某一个节点上, 没办法根据key做分片存储, 导致数据过于集中)

list

字符串列表(双向列表),允许从两端进行 push,pop 操作,还支持一定范围的列表元素

  • 消息队列 利用 list, Stack 栈 (先进后出 FILO) = LPUSH + LPOP, Queue 队列 (先进先出) = LPUSH + RPOP, Blocking MQ 阻塞消息队列 = LPUSH + BRPOP, 获取已关注的微博消息和微信公众号消息, LPUSH msg:uid msgId, LRANGE msg:uid 0 4

sorted set

有序集合,在集合的基础上提供了排序功能,通过一个 score 属性来进行排序。

  • 排行榜/新闻热搜榜 有序集合(sorted set)每次写入都会进行排序,而且不含重复值,所以我们可以将用户的唯一标识,比如 userId 作为 key,分数作为 score,然后就可以进行 ZADD 操作,以得到排行榜。

set

一个不重复值的组合,提供了交集、并集、差集等操作

  • 参与并抽奖
  • 点赞与收藏
  • 关注模型
  • 电商标签筛选

GEO

  • 附近的人 Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作, geoadd:添加地理位置的坐标。georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

Redis为什么快

  • 纯内存操作, 读写不涉及磁盘IO
  • 在底层上, Redis 使用epoll多路复用 (IO管理, 不负责数据的读写, 只监听读写的事件, Redis服务端程序一直在循环监听, 有事件时再去内核读取数据) 的网络IO模型,能较好的保障吞吐量。(Redis-client 在操作时会产生具有不同事件类型的 Socket, 在服务端, 有一段IO多路复用程序, 将其置入队列之中, 事件分派器依次去队列中取, 转发到不同的事件处理器中)
  • redis 采用了单线程处理请求,串行执行指令, 避免了线程切换和锁竞争而带来额外的资源消耗。

Redis过期策略有哪些?

Redis 过期策略是:定期删除+惰性删除

**定期删除:**指的是 Redis 默认是每隔 100ms 就随机抽取⼀些设置了过期时间的 key,检查其是否过期,如果过期就删除。

假设 Redis ⾥放了 10w 个 key,都设置了过期时间,你每隔⼏百毫秒,就检查 10w 个 key,那 Redis 基本上就死了,

cpu 负载会很⾼的,消耗在你的检查过期 key 上了。注意,这⾥可不是每隔 100ms 就遍历所有的设置过期时间的

key,那样就是⼀场性能上的灾难。实际上 Redis 是每隔 100ms 随机抽取⼀些 key 来检查和删除的。

**惰性删除:**定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是

说,在你获取某个 key 的时候,Redis 会检查⼀下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时

就会删除,不会给你返回任何东⻄。

定期删除漏掉了很多过期 key,也没有惰性删除,大量key堆积内存咋整?

答案是:⾛内存淘汰机制

内存淘汰机制

Redis 内存淘汰机制有以下⼏个:

noeviction: 当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错,这个⼀般没⼈⽤吧,实在是太恶⼼了。

allkeys-lru:当内存不⾜以容纳新写⼊数据时,移除最近最少使⽤的 key(这个是最常⽤的)。

allkeys-random:当内存不⾜以容纳新写⼊数据时,在键空间中,随机移除某个 key,这个⼀般没⼈⽤吧,为啥要随机,肯定是把最近最少使⽤的 key 给⼲掉啊。

volatile-lru:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,移除最近最少使⽤的 key(这个⼀般不太合适)。

volatile-random:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,随机移除某个 key。

volatile-ttl:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

持久化的两种⽅式?

RDB:RDB 持久化机制,是对 Redis 中的数据执⾏周期性的持久化。

AOF:AOF 机制对每条写⼊命令作为⽇志,以 append-only 的模式写⼊⼀个⽇志⽂件中,在 Redis 重启的时候,可以通过回放 AOF ⽇志中的写⼊指令来重新构建整个数据集。

通过 RDB 或 AOF,都可以将 Redis 内存中的数据给持久化到磁盘上⾯来,然后可以将这些数据备份到别的地⽅去,AOF数据更加完整

RDB 优缺点

RDB 会⽣成多个数据⽂件,每个数据⽂件都代表了某⼀个时刻中 Redis 的数据,这种多个数据⽂件的⽅式,⾮常适合做冷备

RDB 对 Redis 对外提供的读写服务,影响⾮常⼩,可以让 Redis 保持⾼性能,因为 Redis 主进程只需要 fork ⼀个⼦进程,让⼦进程执⾏磁盘 IO 操作来进⾏ RDB持久化即可。相对于 AOF 持久化机制来说,直接基于 RDB 数据⽂件来重启和恢复 Redis 进程,更加快速。

RDB恢复数据完整性较AOF缺少上次备份到现在的数据

AOF 优缺点

AOF 可以更好的保护数据不丢失,⼀般 AOF 会每隔 1 秒,通过⼀个后台线程执⾏⼀次 fsync 操作,最多丢失 1秒钟的数据。

AOF ⽇志⽂件以 append-only 模式写⼊,所以没有任何磁盘寻址的开销,写⼊性能⾮常⾼,⽽且⽂件不容易破损,即使⽂件尾部破损,也很容易修复。

AOF ⽇志⽂件即使过⼤的时候,出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进⾏压缩,创建出⼀份需要恢复数据的最⼩⽇志出来。在创建新⽇志⽂件的时候,⽼的⽇志⽂件还是照常写⼊。当新的 merge 后的⽇志⽂件 ready 的时候,再交换新⽼⽇志⽂件即可。AOF ⽇志⽂件的命令通过可读较强的⽅式进⾏记录,这个特性⾮常适合做灾难性的误删除的紧急恢复。⽐如某⼈不⼩⼼⽤ flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发⽣,那么就可以⽴即拷⻉ AOF ⽂件,将最后⼀条 flushall 命令给删了,然后再将该 AOF ⽂件放回去,就可以通过恢复机制,⾃动恢复所有数据。

对于同⼀份数据来说,AOF ⽇志⽂件通常⽐ RDB 数据快照⽂件更⼤。AOF 开启后,⽀持的写 QPS 会⽐ RDB ⽀持的写 QPS 低,因为 AOF ⼀般会配置成每秒 fsync ⼀次⽇志⽂件,当然,每秒⼀次 fsync ,性能也还是很⾼的。(如果实时写⼊,那么 QPS 会⼤降,Redis 性能会⼤⼤降低)以前 AOF 发⽣过 bug,就是通过 AOF 记录的⽇志,进⾏数据恢复的时候,没有恢复⼀模⼀样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令⽇志 / merge / 回放的⽅式,⽐基于 RDB 每次持久化⼀份完整的数据快照⽂件的⽅式,更加脆弱⼀些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令⽇志进⾏ merge 的,⽽是基于当时内存中的数据进⾏指令的重新构建,这样健壮性会好很多。

RDB AOF 到底该如何选择

  1. AOF保证数据不丢失
  2. RDB做不同程度的冷备AOF ⽂件都丢失或损坏不可⽤的时候,还可以使⽤ RDB 来进⾏快速的数据恢复
  3. 建议两个都开启

Redis 集群方案

  • 主从复制 :高可用, 解决单点故障问题, 在不同的机器上部署着同一 Redis 程序。在这多台机器里,我们会选择一个节点作为主节点,它负责数据的写入。其他节点作为从节点,从节点负责读取, 且定时的和主节点同步数据。一旦主节点不能使用了,那么就可以在从节点中挑选一个作为主节点,重新上岗服务。
  • 主从+哨兵:利用哨兵监控来自动切换从为主, 即哨兵会不断的检测主从节点是否能正常工作, 当某个 master 不能正常工作时,Sentinel 会启动一个故障转移过程,将其中的一个副本提升为 master,并通知其他从节点对应新的 master 相关信息, 还会告知已连接过来的客户端程序关于主节点新的地址, 客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址
  • redis cluster

Redis Cluster 了解吗?

  1. ⾃动将数据进⾏分⽚,每个 master 上放⼀部分数据
  2. 提供内置的⾼可⽤⽀持,部分 master 不可⽤时,还是可以继续⼯作的

分布式寻址算法、hash 算法(⼤量缓存重建)、⼀致性 hash 算法(⾃动缓存迁移)+ 虚拟节点(⾃动负载均衡)、Redis cluster 的 hash slot 算法、Redis cluster 的⾼可⽤与主备切换原理,Redis cluster 的⾼可⽤的原理,⼏乎跟哨兵是类似的。

从节点选举

每个从节点,都根据⾃⼰对 master 复制数据的 offset,来设置⼀个选举时间,offset 越⼤(复制数据越多)的从节点,选举时间越靠前,优先进⾏选举。所有的 master node 开始 slave 选举投票,给要进⾏选举的 slave 进⾏投票,如果⼤部分 master node (N/2 + 1) 都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。从节点执⾏主备切换,从节点切换为主节点

Redis 的雪崩、穿透和击穿,如何应对?

缓存雪崩

Redis 雪崩"是指在某一时间段,缓存集中失效,导致大量请求直接走数据库,有可能对数据库造成巨大压力,甚至使其宕机,从而使整个服务瘫痪。

解决方案:

  • 使用互斥锁
  • 缓存的失效时间再加上一个随机值
  • Redis cluster,避免全盘崩溃

缓存穿透

主要指查询的数据在缓存和数据库中都不存在的情况。在这种情况下,客户端仍然会不断地发起请求,导致每次请求都会压向数据库

  • 将此key对应的value设置为一个默认的值,比如设置为空(NULL),并设置一个缓存的失效时间
  • 布隆过滤器
  • 使用互斥锁

缓存击穿

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。

  • 若缓存的数据是基本不会发⽣更新的,则可尝试将该热点数据设置为永不过期。

  • 使用互斥锁

Redis互斥锁

  1. 加锁: SETNX key value , 原子操作, 当key不存在时, 完成创建并返回成功, 否则返回失败, 获取锁成功后执行后续逻辑, 只加锁但未释放锁会出现死锁
  2. 释放锁: DEL key, 通过删除键值对来释放锁, 以便其他线程通过SETNX命令获得锁, 加锁后, 程序还没执行释放锁, 程序挂了, 会出现死锁
  3. 为锁设置过期时间: EXPIRE key timeout, 设置key的超时时间, 保证锁在没有被显式释放时, 也能在一定时间后自动释放, 避免资源被永久锁住, 当过期时间到达, Redis会自动删除对应的key-value, SETNX和EXPIRE非原子性, Redis支持nx和ex操作是同一原子操作 set key value [expiration EX seconds|PX milliseconds] [NX|XX]
  4. 锁误删除: 若线程A成功获得锁并设置过期时间30秒, 但线程A执行时间超过30秒, 锁过期自动释放, 此时线程B获得锁并设置过期时间, 随后线程A执行完成并通过DEL命令来释放锁, 但此时线程B加的锁还没有执行完成, 线程A实际释放了线程B的锁, 即避免超时时间设置不合理时, 自己的锁被其他线程释放掉, 导致锁一直失效, 通过在value中设置当前线程加锁的标识, 在删除之前验证key对应的value, 判断是否当前线程持有, 可生成一个UUID标识当前线程
  5. 超时解锁导致并发: 若线程A成功获得锁并设置过期时间30秒, 但线程A执行时间超过30秒, 锁过期自动释放, 此时线程B获得锁, 线程A和线程B并发执行, 使用Redission, 通过WatchDog机制为将要过期但未释放的锁增加有效时间
  6. Redis主从复制: 客户端A在Redis的master节点上拿到了锁, 但是这个加锁的key还没有同步到slave节点, master故障, 发生故障转移, 一个slave节点升级为master节点, 客户端B也可以获得同个key的锁, 这就导致多个客户端都拿到锁, 使用RedLock(不是Redis实现, 是client实现的算法), 利用多个Redis集群, 用多数的集群加锁成功, 减少Redis某个集群出故障造成分布式锁出现问题的概率(只要过半就可以获得锁)

redis 如何实现延迟队列?

利用有序集合的 score 属性,将时间戳设置到该属性上,然后定时的对其排序,查看最近要执行的记录,如果时间到了,则取出来消费后删除,即可达到延迟队列的目的。

秒杀系统设计

  • 业务特点: 1、瞬时并发量大,秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增几倍、甚至几十倍以上 2、库存量少,一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。 3、业务和流程较为简单,一般都是下订单、扣库存、支付订单。
  • 技术难点: 1、若秒杀活动若与其他营销活动同时进行,可能会对其他活动造成冲击,极端情况下可能导致整个服务宕机。 2、页面流量突增,秒杀活动用户访问量会突增。需确保访问量的突增不会对服务器、数据库、Redis等造成过大的压力。 3、秒杀活动库存量小,瞬时下单量大,易造成超卖现象
  • 架构设计** 1、限流:由于库存量很少,对应的只有少部分用户才能秒杀成功。所以要限制大部分用户流量,只准少量用户流量进入后端服务器。 2、削峰:秒杀开始瞬间,大量用户进来会有一个瞬间流量峰值。把瞬间峰值变得更平缓是设计好秒杀系统关键因素。一般的采用缓存和MQ实现流量的削峰填谷。 3、异步:秒杀可以当做高并发系统处理。即可以从业务上考虑,将同步的业务,设计成异步处理的任务。 4、缓存:秒杀瓶颈主要体现在下单、扣库存的数据操作中。关系型数据库写入和读取效率较低。若将部分操作放到缓存中能极大提高并发效率(如使用Redis操作库存)
  • 客户端优化 1、秒杀页面: 如果秒杀页面的资源,如:CSS、JS、图片、商品详情等都经后端,服务肯定承受不住。如果将这个页面进行静态化,秒杀时肯定能起到压力分散的作用。 2、防止提前下单: 使用JS控制提交订单按钮,如果秒杀时间,就不能点击该按钮。
  • 服务端优化 1、对查询秒杀商品进行优化 将首次查询到的商品信息进行数据放入缓存,后面再访问时直接返回缓存的信息。 2、对库存的优化 在设置秒杀活动时就将商品库存放于Redis中,在下单扣库存时,直接对Redis进行操作。 3、后端流量控制优化(参加用户量过大时) 使用消息队列、异步处理等方式解决。即超过系统水位线的请求直接拒绝掉。
  • 核心**: 1、层层过滤,逐渐递减瞬时访问,降低下游的压力,减少最终对数据库的冲击 2、充分利用缓存与消息队列,提高请求处理速度以及削峰填谷的作用

mysql

CHAR 和 VARCHAR 的区别是什么?

CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:**CHAR 是定长字符串,VARCHAR 是变长字符串。**CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。

VARCHAR(100)和 VARCHAR(10)的区别是什么?

VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。

MySQL 主要由下面几部分构成?

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
  • 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

MyISAM 和 InnoDB 有什么区别?

  • InnoDB 支持行级别的锁粒度,MyISAM 不支持,只支持表级别的锁粒度。
  • MyISAM 不提供事务支持。InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别。
  • MyISAM 不支持外键,而 InnoDB 支持。
  • MyISAM 不支持 MVCC,而 InnoDB 支持。
  • 虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
  • MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
  • InnoDB 的性能比 MyISAM 更强大。

Mysql事务有哪些?

ACID

  1. 原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  3. 隔离性Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

脏读(Dirty read)

一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据,这也就是脏读的由来。

例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。

不可重复读(Unrepeatable read)

指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。

幻读(Phantom read)

幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

例如:事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。

并发事务的控制方式有哪些?

MySQL 中并发事务的控制方式无非就两种:MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。

控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。

  • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。

读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为 表级锁(table-level locking)行级锁(row-level locking) 。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。

MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。

MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log

  • undo log : undo log 用于记录某行数据的多个版本的数据。
  • read view 和 隐藏字段 : 用来判断当前版本数据的可见性。

SQL 标准定义了哪些事务隔离级别?

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

MySQL 的隔离级别是基于锁实现的吗?

MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。

SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。

nnoDB 有哪几类行锁?

InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
  • 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
  • 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

如何分析 SQL 的性能?

我们可以使用 EXPLAIN 命令来分析 SQL 的 执行计划 。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。

EXPLAIN 并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。

EXPLAIN 适用于 SELECT, DELETE, INSERT, REPLACE, 和 UPDATE语句,我们一般分析 SELECT 查询较多。

我们这里简单来演示一下 EXPLAIN 的使用。

EXPLAIN 的输出格式如下:

mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | cus_order | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 997572 |   100.00 |Usingfilesort|
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

各个字段的含义如下:

列名 含义
id SELECT 查询的序列标识符
select_type SELECT 关键字对应的查询类型
table 用到的表名
partitions 匹配的分区,对于未分区的表,值为 NULL
type 表的访问方法
possible_keys 可能用到的索引
key 实际用到的索引
key_len 所选索引的长度
ref 当使用索引等值查询时,与索引作比较的列或常量
rows 预计要读取的行数
filtered 按表条件过滤后,留存的记录数的百分比
Extra 附加信息

mysql索引数据结构?

B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。

目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

B 树& B+树两者有何异同呢?

  • B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
  • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
  • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
  • 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。

综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。

image-20231121133656745

Mysql 索引类型有哪些?

按照数据结构维度划分:

  • BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样(前面已经介绍了)。
  • 哈希索引:类似键值对的形式,一次即可定位。
  • RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
  • 全文索引:对文本的内容进行分词,进行搜索。目前只有 CHARVARCHARTEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。

按照底层存储方式角度划分:

  • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
  • 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。注:二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。唯一索引,普通索引,前缀索引等索引属于二级索引。

按照应用维度划分:

  • 主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。
  • 普通索引:仅加速查询。
  • 唯一索引:加速查询 + 列值唯一(可以有 NULL)。
  • 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
  • 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
  • 全文索引:对文本的内容进行分词,进行搜索。目前只有 CHARVARCHARTEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。

聚簇索引的优缺点

优点

  • 查询速度非常快:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
  • 对排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。

缺点

  • 依赖于有序的数据:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
  • 更新代价大:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。

非聚簇索引介绍

非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。

非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。

优点

更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的

缺点

  • 依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
  • 可能会二次查询(回表):这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。

覆盖索引

索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引,我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!

正确使用索引的一些建议?

  • 使用合适的字段(不为 NULL 的字段,被频繁查询的字段,被作为条件查询的字段,频繁需要排序的字段,被经常频繁用于连接的字段)
  • 被频繁更新的字段应该慎重建立索引
  • 限制每张表上的索引数量,建议单张表索引不超过 5 个(影响更新,插入速度)
  • 尽可能的考虑建立联合索引而不是单列索引
  • 注意避免冗余索引
  • 避免索引失效
  • 删除长期未使用的索引

mysql索引失效的集中情况?

  • 字段类型不匹配
  • 查询条件中包含or
  • like 通配符% 错误使用(like a% 走索引)
  • 联合索引最左匹配原则
  • 索引列使用MySQL函数或计算,索引失效
  • 使用(!= 或者 < >,not in),导致索引失效
  • 使用is null, is not null,导致索引失效
  • 左连接、右连接关联字段编码不一致,索引失效
  • order by 使用不当

mysql使用建议

  • 数据库和表的字符集统一使用 UTF8
  • 所有表必须使用 InnoDB 存储引擎
  • 所有表和字段都需要添加注释
  • 尽量控制单表数据量的大小,建议控制在 500 万以内
  • 谨慎使用 MySQL 分区表 (分区表在物理上表现为多个文件,在逻辑上表现为一个表;谨慎选择分区键,跨分区查询效率可能更低;建议采用物理分表的方式管理大数据。)
  • 经常一起使用的列放到一个表中
  • 禁止在数据库中存储文件(比如图片)这类大的二进制数据
  • 尽可能把所有列定义为 NOT NULL
  • 每个 InnoDB 表必须有个主键
  • 避免数据类型的隐式转换
  • 避免使用子查询,可以把子查询优化为 join 操作
  • 避免使用 JOIN 关联太多的表
  • 减少同数据库的交互次数
  • 在明显不会有重复值时使用 UNION ALL 而不是 UNION
    • UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
    • UNION ALL 不会再对结果集进行去重操作

mysql日志类别

  • binlog: 二进制日志,记录了数据库对数据的修改记录,例如表的创建,数据更新等。但并不包括 select 这些查询语句。binlog 日志是属于逻辑语句的记录,可用于主从数据库的同步。
  • relay log: 中继日志,用于主从备份恢复使用的。有主服务器的 binlog 逻辑操作语句,以及当前的恢复位置。
  • 慢查询日志: 记录在 mysql 里执行时间超过预期值的耗时语句
  • redo log: redo log 是对加载到内存数据页的修改结果的记录,和 binlog 不同的是,binlog 记录的是逻辑操作语句,偏向于过程记录。而 redo log 是一个数据页的修改日志,偏向于结果的记录。redo log 在写 binlog 日志前会先记录 redo log,记录完后标记为 prepare 状态。当 binlog 也写入完成后,才将 redo log 标记为 commit 状态。只有当 redo log 是 commit 状态时,事务才能真正的 commit。这样能防止主从节点根据 binlog 同步有可能事务不一致的情况。
  • undo log: 回滚日志主要用于回滚数据,和 redo log 不一样的是,undo log 是逻辑日志,是一种相反操作的记录,比如在回滚时,如果是 insert 操作时,则会逆向为 delete,delete 操作时,逆向为 insert 操作,更新则恢复到当时的版本数据。

mysql 的主从复制

整体上来说,复制有3个步骤:

  1. master log dump 线程, 主从复制的基础是主库记录数据库的所有变更记录到 bin log (bin log 是数据库服务器启动的那一刻起, 保存所有修改数据库结构或内容的一个文件), 主节点 log dump 线程, 当 bin log 有变动时, log dump 线程读取其内容并发送给从节点
  2. slave I/O 线程, 从节点 I/O 线程接收 binlog 内容, 将将其写入到它的中继日志(relay log);
  3. slave SQL 线程, 从节点的 SQL 线程读取 relay log 文件内容, 对数据更改进行重做

mysql从库同步复制方式:

  • 全同步复制 当向主库写数据时, 只有等所有的 slave 节点将同步的bin log日志写入 relay log,并且响应 ack 确认后,此次的事务才会提交, 然后返回客户端。数据完整性高,但性能低
  • 半同步复制 当向主库写数据时, 只要有一个 salve 节点响应 ack 后就可以认为同步成功,但细分为了两种,一种是 AFTER_COMMIT:先在主库提交事务, 然后同步从库, 等待从库的 ack 确认才告诉客户端是否 Ok。另一种是 AFTER_SYNC:主库先不提交事务, 只有从库有 replay log ,回复了 ack 后才进行提交事务。后面一种数据一致性较高
  • 异步复制 (默认) 当向主库写数据时, 立刻返回客户端, 即一旦有需要复制的就通知 slave, 但不会等待确认成功才进行后续操作。

分库分表有哪些?有什么优缺点

分库:从业务角度进行切分 降低单库并发及压力 分表:将数据根据一定的规则落在多张表上。比如按时间范围来切分,或者通过对 ID 进行 Hash 来路由到对应的表上。提高单表sql速度 分库分表后使得数据不再集中到一张表上,但也带来了维护以及其他处理问题。比如原来的事务变为分布式事务;原来的 join 操作将要变为在应用程序做过滤;还有数据的后续迁移、扩容规划等。

分表如何处理id?

描述 优点 缺点
UUID 通用唯一标识码的缩写, 让分布式系统中的所有元素都有唯一的标识信息, 不需要**控制器来指定 1. 降低全局节点的压力, 使得主键生成速度更快 2. 生成的主键全局唯一 3. 跨服务器合并数据方便 1. 占用16个字符, 空间占用较多 2. 不是递增有序的数字, 数据写入IO随机性很大且索引效率降低
数据库主键自增 MySQL数据设置主键且主键自动递增 1. INT和BIGINT类型占用空间较小 2. 主键自动增长, IO写入连续性好 3. 数字类型查询速度优于字符串 1. 并发性能不高, 磁盘存储, 受限于数据库性能 2. 分库分表, 改造复杂 3. 自增会泄露数据量
Redis自增 Redis计数器, 原子性自增 使用内存, 并发性能好 1. 数据丢失 2. 自增会泄露数据量
雪花算法(snowflake) 分布式ID经典解决方案 1. 不依赖外部组件 2. 性能好 时针回拨

MySQL主从同步延时问题

  • 分库,将⼀个主库拆分为多个主库,每个主库的写并发就减少了⼏倍,此时主从延迟可以忽略不计。
  • 打开 MySQL ⽀持的并⾏复制,多个库并⾏复制。如果说某个库的写⼊并发就是特别⾼,单库写并发达到了2000/s,并⾏复制还是没意义。
  • 重写代码,写代码的同学,要慎重,插⼊数据时⽴⻢查询可能查不到。
  • 如果确实是存在必须先插⼊,⽴⻢要求就查询到,然后⽴⻢就要反过来执⾏⼀些操作,对这个查询设置直连主库不推荐这种⽅法,你要是这么搞,读写分离的意义就丧失了。

慢查询该如何优化?

  1. 检查是否走了索引, 如果没有则优化SQL利用索引
  2. 检查所利用的索引, 是否是最优索引
  3. 检查所查字段是否都是必须的, 是否查询了过多字段, 查出了多余数据
  4. 检查表中数据是否过多, 是否应该要进行分库分表了
  5. 检查数据库实例所在的机器的性能配置, 是否太低, 是否增加资源

网络

  • TCP(Transmission Control Protocol,传输控制协议 ):提供 面向连接 的,可靠 的数据传输服务。
  • UDP(User Datagram Protocol,用户数据协议):提供 无连接 的,尽最大努力 的数据传输服务(不保证数据传输的可靠性),简单高效。
  • HTTP(Hypertext Transfer Protocol,超文本传输协议):基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。
  • DNS(Domain Name System,域名管理系统): 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。

HTTP 和 HTTPS 有什么区别?

  • 端口号:HTTP 默认是 80,HTTPS 默认是 443。
  • URL 前缀:HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://
  • 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
  • SEO(搜索引擎优化):搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。

当在浏览器上输入一个网址,其内部发生了什么?

  1. DNS 解析
  2. TCP 连接
  3. 发送 HTTP 请求
  4. 服务器处理请求并返回 HTTP 报文
  5. 浏览器解析渲染页面
  6. 连接结束

http状态码有哪些?

  1. 1XX:消息状态码。
  2. 2XX:成功状态码。
  3. 3XX:重定向状态码。
  4. 4XX:客户端错误状态码。
  5. 5XX:服务端错误状态码。
  • 100:Continue 继续。客户端应继续其请求。
  • 101:Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到 HTTP 的新版本协议。
  • 200:OK 请求成功。一般用于 GET 与 POST 请求。
  • 201:Created 已创建。成功请求并创建了新的资源。
  • 202:Accepted 已接受。已经接受请求,但未处理完成。
  • 203:Non-Authoritative Information 非授权信息。请求成功。但返回的 meta 信息不在原始的服务器,而是一个副本。
  • 204:No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档。
  • 205:Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域。
  • 206:Partial Content 部分内容。服务器成功处理了部分 GET 请求。
  • 300:Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择。
  • 301:Moved Permanently 永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI 代替。
  • 302:Found 临时移动,与 301 类似。但资源只是临时被移动。客户端应继续使用原有URI。
  • 303:See Other 查看其它地址。与 301 类似。使用 GET 和 POST 请求查看。
  • 304:Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。
  • 305:Use Proxy 使用代理。所请求的资源必须通过代理访问。
  • 306:Unused 已经被废弃的 HTTP 状态码。
  • 307:Temporary Redirect 临时重定向。与 302 类似。使用 GET 请求重定向
  • 400:Bad Request 客户端请求的语法错误,服务器无法理解。
  • 401:Unauthorized 请求要求用户的身份认证。
  • 402:Payment Required 保留,将来使用。
  • 403:Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求。
  • 404:Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面。
  • 405:Method Not Allowed 客户端请求中的方法被禁止。
  • 406:Not Acceptable 服务器无法根据客户端请求的内容特性完成请求。
  • 407:Proxy Authentication Required 请求要求代理的身份认证,与 401 类似,但请求者应当使用代理进行授权。
  • 408:Request Time-out 服务器等待客户端发送的请求时间过长,超时。
  • 409:Conflict 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突。
  • 410:Gone 客户端请求的资源已经不存在。410 不同于 404,如果资源以前有现在被永久删除了可使用 410 代码,网站设计人员可通过 301 代码指定资源的新位置。
  • 411:Length Required 服务器无法处理客户端发送的不带 Content-Length 的请求信息。
  • 412:Precondition Failed 客户端请求信息的先决条件错误。
  • 413:Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个 Retry-After 的响应信息。
  • 414:Request-URI Too Large 请求的 URI 过长(URI通常为网址),服务器无法处理。
  • 415:Unsupported Media Type 服务器无法处理请求附带的媒体格式。
  • 416:Requested range not satisfiable 客户端请求的范围无效。
  • 417:Expectation Failed 服务器无法满足 Expect 的请求头信息。
  • 500:Internal Server Error 服务器内部错误,无法完成请求。
  • 501:Not Implemented 服务器不支持请求的功能,无法完成请求。
  • 502:Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应。
  • 503:Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中。
  • 504:Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求。
  • 505:HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理。

Tcp为什么3次握手?

三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

  1. 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
  2. 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
  3. 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

为什么要四次挥手?

TCP 是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。

举个例子:A 和 B 打电话,通话即将结束后。

  1. 第一次挥手:A 说“我没啥要说的了”
  2. 第二次挥手:B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话
  3. 第三次挥手:于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”
  4. 第四次挥手:A 回答“知道了”,这样通话才算结束。

常用的网络攻击手段有哪些?

1.1、什么是 SQL 注入攻击?如何防止 SQL 注入攻击?

SQL 注入攻击是指攻击者通过向 Web 应用程序的输入框中插入恶意 SQL 语句来执行未经授权的操作。防止 SQL 注入攻击的方法包括使用参数化查询和输入验证,以及避免使用动态 SQL 语句。

1.2、什么是跨站点脚本攻击(XSS)?如何防止 XSS 攻击?

跨站点脚本攻击是指攻击者通过向 Web 应用程序的输入框中插入恶意脚本来窃取用户数据或执行未经授权的操作。防止 XSS 攻击的方法包括对输入数据进行验证和转义、使用内容安全策略(CSP)以及限制 Cookie 的范围。

1.3、什么是跨站请求伪造(CSRF)攻击?如何防止 CSRF 攻击?

跨站请求伪造攻击是指攻击者利用用户已经通过身份验证的会话执行未经授权的操作。防止 CSRF 攻击的方法包括使用同步令牌和使用双重身份验证。

数据结构

  1. 数组(Array) 是一种很常见的数据结构。它由相同类型的元素(element)组成,并且是使用一块连续的内存来存储,我们直接可以利用元素的索引(index)可以计算出该元素对应的存储地址。

数组的特点是:提供随机访问 并且容量有限。

假如数组的长度为 n。
访问:O(1)//访问特定位置的元素
插入:O(n )//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时
删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时
  1. 链表(LinkedList) 虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。链表的插入和删除操作的复杂度为 O(1) 只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。

使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点。

常见链表分类:

  1. 单链表
  2. 双向链表
  3. 循环链表
  4. 双向循环链表
假如链表中有n个元素。
访问:O(n)//访问特定位置的元素
插入删除:O(1)//必须要要知道插入元素的位置

单链表 单向链表只有一个方向,结点只有一个后继指针 next 指向后面的节点。因此,链表这种数据结构通常在物理内存上是不连续的。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向 null。

循环链表 其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向 null,而是指向链表的头结点。

双向链表 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。

双向循环链表 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。

应用场景

  • 如果需要支持随机访问的话,链表没办法做到。
  • 如果需要存储的数据元素的个数不确定,并且需要经常添加和删除数据的话,使用链表比较合适。
  • 如果需要存储的数据元素的个数确定,并且不需要经常添加和删除数据的话,使用数组比较合适。

数组 vs 链表

  • 数组支持随机访问,而链表不支持。
  • 数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。
  • 数组的大小固定,而链表则天然支持动态扩容。如果声明的数组过小,需要另外申请一个更大的内存空间存放数组元素,然后将原数组拷贝进去,这个操作是比较耗时的!

About

面试宝典

License:MIT License