golang后端面试宝典

golang以及中间件相关

1、sync.Map与普通map区别:

  1. 并发安全性

    • sync.Map是为并发访问设计的,它内部使用读写锁(RWLock)或其他机制来保证并发操作的安全性。
    • 普通map在并发环境中使用时,需要外部同步,如使用互斥锁(Mutex)来避免并发读写导致的数据竞争。
  2. 读写时的锁策略

    • sync.Map在读操作时不需要加锁,它允许多个读操作并发进行,只有在写操作时(如添加、删除或更新键值对)才会加锁。
    • 普通map在进行读写操作时,通常需要全局加锁,这可能会降低并发性能。
  3. 性能

    • sync.Map在读多写少的场景下性能较好,因为读操作不需要加锁。
    • 普通map在并发读写时,由于需要外部同步,可能会因为锁竞争而导致性能下降。
  4. 迭代器

    • sync.Map不保证迭代顺序,且迭代过程中对sync.Map的并发修改可能会导致迭代器失效。
    • 普通map也不保证迭代顺序,但在迭代过程中,如果对map进行了并发的修改,可能会导致运行时 panic。
  5. 内置函数

    • sync.Map提供了LoadStoreDeleteRange等方法,这些方法都是并发安全的。
    • 普通map的操作方法不是线程安全的,需要额外的同步措施。
  6. 内存使用

    • sync.Map可能会比普通map使用更多的内存,因为它需要存储额外的锁信息和可能的多个副本(为了减少锁争用)。
  7. 使用场景

    • sync.Map适合在高并发的应用程序中使用,尤其是在需要频繁进行并发读写操作的场景。
    • 普通map适合在并发需求不高或可以通过其他方式保证同步的场景。

2、数组跟切片的区别:

数组(Array):

  1. 固定长度:数组的长度是固定的,在声明时必须指定数组的长度,例如 arr [5]int 表示一个包含5个整数的数组。

  2. 类型不变:数组的类型在声明时确定,并且在整个数组中是一致的。

  3. 值语义:数组作为值传递时,会复制整个数组数据,这可能导致性能问题,尤其是在处理大型数组时。

  4. 无内置方法:数组本身没有内置的方法,如追加、删除等操作。

  5. 栈上分配:数组通常在栈上分配,这限制了它们的大小时和生命周期。

  6. 声明var arr [5]intarr := [5]int{1, 2, 3, 4, 5}

切片(Slice):

  1. 动态长度:切片的初始长度可以是零,可以在运行时增长和缩减。

  2. 可变:切片可以被重新切片(slice),以创建一个包含原始切片一部分的新切片。

  3. 引用语义:切片是引用类型,它们指向底层数组,对切片的修改会影响底层数组。

  4. 内置方法:切片提供了多种内置方法,如 append(), delete(), copy(), len()cap()

  5. 堆上分配:切片背后的数组通常在堆上分配,这允许更灵活的内存管理。

  6. 声明var s []ints := []int{1, 2, 3, 4, 5}

一些关键点:

  • 性能:对于小型数据集,使用数组可能更高效,因为它们在栈上分配,访问速度更快。对于大型数据集,切片可能更合适,因为它们可以动态地增长。

  • 灵活性:切片比数组更灵活,因为它们可以动态地调整大小,并且可以很容易地通过内置方法进行操作。

  • 传递函数:当需要传递一个可变长度的序列给函数时,使用切片更合适,因为它们可以通过引用传递,减少了复制的开销。

  • 数组转换为切片:可以通过 arr[:n] 将数组的前n个元素转换为切片,其中n是数组的长度。

  • 内存管理:虽然切片在内存管理上更灵活,但这也意味着开发者需要更小心地管理切片的生命周期,以避免内存泄漏。

3、go语言中,如何去管理控制协程并发数量? 

1. 同步等待组(sync.WaitGroup)

sync.WaitGroup 是Go语言提供的一个同步原语,可以用来等待一组并发操作完成。通过Add方法增加计数,然后在每个goroutine中调用Done方法减少计数,主goroutine通过调用Wait方法阻塞等待直到计数为零。

var wg sync.WaitGroup

// 启动一定数量的goroutine
for i := 0; i < maxGoroutines; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // 执行任务
    }()
}

wg.Wait() // 等待所有goroutine完成

2. 通道(Channels)和缓冲区

使用通道(Channels)可以控制协程的启动数量。通过设置通道的缓冲区大小,可以限制同时运行的goroutine数量。

ch := make(chan struct{}, maxGoroutines)

for i := 0; i < totalTasks; i++ {
    go func() {
        ch <- struct{}{} // 进入通道,如果缓冲区满了,这里会阻塞
        defer func() { <-ch }() // 完成后从通道中移除
        // 执行任务
    }()
}

3. 带缓冲区的通道作为信号量

同样,可以使用带缓冲区的通道来实现一个简单的信号量,控制并发执行的goroutine数量。

sem := make(chan struct{}, maxGoroutines)

for i := 0; i < totalTasks; i++ {
    go func(task int) {
        <-sem // 等待获取信号量
        defer func() { sem <- struct{}{} }() // 完成任务后释放信号量
        // 执行任务
    }(i)
}

4. 并发控制库

Go标准库中的sync包提供了一些并发控制的工具,例如sync.Mutex(互斥锁)和sync.RWMutex(读写锁),可以用来控制对共享资源的访问。还有一些第三方库,如concurrencylimiter,提供了更高级的并发控制功能。

  1. 互斥锁(sync.Mutex): 互斥锁用于保护对共享资源的访问,确保同时只有一个 goroutine 可以执行临界区的代码。它通过 Lock()Unlock() 方法来控制对共享资源的访问。

    var mu sync.Mutex
    var shared int

    func main() {
        for i := 0; i < 10; i++ {
            go func(i int) {
                mu.Lock()
                shared++
                mu.Unlock()
            }(i)
        }
    }

  2. 读写锁(sync.RWMutex): 读写锁是互斥锁的一个变种,它允许多个读操作同时进行,但在写操作时,会阻止其他所有读和写操作。这在读取操作远多于写入操作的情况下很有用。

    var rwmu sync.RWMutex
    var data map[string]int
    
    func read(key string) int {
        rwmu.RLock()
        defer rwmu.RUnlock()
        return data[key]
    }
    
    func write(key string, value int) {
        rwmu.Lock()
        defer rwmu.Unlock()
        data[key] = value
    }

4、go的垃圾回收机制的底层实现原理,以及为什么go的垃圾回收机制对并发友好

Go语言的垃圾回收(Garbage Collection,GC)机制是一种自动内存管理策略,其底层实现原理主要包括以下几个方面:

  1. 标记-清除(Mark-Sweep)算法: 初始阶段,Go使用传统的标记-清除算法。这个算法分为两个主要阶段:标记阶段和清除阶段。在标记阶段,GC从根对象开始遍历所有可达对象,将它们标记为活跃的。在清除阶段,未被标记的对象被认为是垃圾,随后被回收。

  2. 分代回收: 为了解决标记-清除算法存在的问题,Go采用了分代回收策略。它将内存分为多个代,新创建的对象会被分配到新生代,而存活时间较长的对象会晋升到老生代。GC会优先处理新生代对象,这样可以减少GC的次数和时间。

  3. 并发垃圾回收: Go从1.5版本开始引入了并发垃圾回收机制,将垃圾回收任务分为两个阶段:扫描和清理。在扫描阶段,GC标记所有被引用的对象;在清理阶段,并发执行清理任务,回收未被引用的对象。这种并发执行减少了程序暂停的时间。

  4. 三色标记法: 为了避免长时间STW(Stop-The-World)问题,Go使用三色标记法,将对象分为白色(潜在垃圾)、黑色(确定存活)和灰色(待确认,有指向白色对象的指针)。通过这种分类,GC可以并发地进行对象的标记和回收,而不会影响程序的执行。

  5. 写屏障(Write Barrier): 为了维护三色不变性,在并发垃圾回收中,Go使用写屏障技术。写屏障是一种在执行写操作时插入的额外代码,它确保了在并发环境下,对象图的变动不会干扰垃圾回收的标记过程。Go 1.8版本引入了混合写屏障,结合了插入写屏障和删除写屏障的优点,以保证在并发环境下垃圾回收的正确性。

  6. 优化策略: Go的GC还包括了一系列优化措施,如快速垃圾回收,它通过优化标记-清除算法来减少GC的暂停时间和降低对程序执行的影响。

Go的垃圾回收机制对并发友好的原因主要包括:

  • 低停顿时间:通过并发执行GC任务,减少了程序因GC而产生的停顿。
  • 写屏障的应用:保证了在并发环境下对象图的一致性,避免了STW问题。
  • 三色标记法:允许GC与程序的其他部分同时运行,而不会相互干扰。
  • 内存分配策略:Go的内存分配策略(如使用sync.Pool)可以减少因内存分配导致的GC压力。

总的来说,Go的垃圾回收机制通过一系列优化措施和并发算法,实现了高效率的内存管理和对并发执行的友好支持。

5、再以操作系统的层面上说一下,go与java的并发的区别,以及go的高并发为啥比java快

在操作系统层面上,Go(Golang)和Java在并发模型上有几个关键的区别,这些区别影响了它们在高并发场景下的性能表现:

  1. 并发模型

    • Go 使用的是**协程(goroutine)**模型,它是一种轻量级的线程,由Go运行时(runtime)管理。Go的goroutine比传统操作系统线程更轻量,创建和销毁的开销小得多。
    • Java 使用的是线程模型,每个线程由操作系统管理,创建和销毁线程的开销相对较大。
  2. 调度器

    • Go 的调度器是Go运行时的一部分,它负责goroutine的调度。Go的调度器设计得非常高效,能够在用户态进行goroutine的创建、销毁和调度,减少了操作系统内核态切换的开销。
    • Java 的线程是由操作系统的调度器管理的,涉及到更多的上下文切换和内核态切换,这些操作相对更重。
  3. 并发控制

    • Go 提供了丰富的并发原语,如channel、sync.Mutex、sync.WaitGroup等,它们都是语言层面支持的,可以方便地在goroutine之间进行通信和同步。
    • Java 虽然也提供了并发控制的类和接口,如java.util.concurrent包中的类,但它们的使用相对更复杂一些,且需要开发者有较强的并发编程经验。
  4. 内存管理

    • Go 的垃圾回收机制对并发友好,它使用并发标记和并发清除,减少了垃圾回收对程序执行的影响。
    • Java 的垃圾回收通常会引起应用程序的暂停,尤其是在使用某些垃圾回收器时,如Serial GC或Parallel GC。
  5. 系统调用和上下文切换

    • Go 的goroutine上下文切换是在用户态进行的,不需要操作系统介入,因此上下文切换的开销小。
    • Java 的线程上下文切换通常涉及到操作系统内核,开销更大。
  6. 栈大小

    • Go 的goroutine默认栈大小较小(通常是2KB),但可以根据需要动态调整。
    • Java 的线程栈大小通常较大(如1MB或2MB),这也增加了线程创建和维护的开销。
  7. 生态和库

    • Go 的标准库中包含了许多针对并发设计的包和函数,如net/http用于编写高并发的网络服务器。
    • Java 虽然也有丰富的并发库,但使用起来可能更复杂,且需要更多的样板代码。

Go的高并发性能之所以比Java快,主要是因为它在设计上就考虑了并发和网络编程的需求,提供了轻量级的goroutine、高效的调度器和丰富的并发原语,这些都使得Go在高并发场景下的性能表现更优。而Java虽然也支持多线程和并发,但由于线程模型的开销、垃圾回收的暂停以及上下文切换的开销,其性能在高并发场景下可能不如Go。

6、git常见命令 

初始化和配置

  1. 初始化仓库

    git init

  2. 克隆仓库

    git clone [url]

  3. 设置用户信息

    git config --global user.name "[name]" git config --global user.email "[email address]"

文件操作

  1. 添加文件到暂存区

    git add [file]

  2. 从暂存区移除文件

    git reset [file]

  3. 删除文件

    git rm [file]

  4. 重命名文件

    git mv [old] [new]

提交和分支

  1. 提交到仓库

    git commit -m "commit message"

  2. 查看提交历史

    git log

  3. 创建新分支

    git branch [branch-name]

  4. 切换到分支

    git checkout [branch-name]

  5. 合并分支

    git merge [branch-name]

  6. 删除分支

    git branch -d [branch-name]

  7. 查看分支状态

    git status

远程仓库

  1. 添加远程仓库

    git remote add [shortname] [url]

  2. 查看远程仓库

    git remote -v

  3. 从远程仓库拉取代码

    git fetch [remote]

  4. 从远程仓库拉取特定分支

    git pull [remote] [branch]

  5. 推送到远程仓库

    git push [remote] [branch]

  6. 从远程仓库获取最新分支列表

    git fetch --all --prune

代码合并和解决冲突

  1. 解决合并冲突

    • 手动解决冲突后,使用 git add 添加更改,并使用 git commit 提交解决。
  2. 查看差异

    git diff [branch1] [branch2]

  3. 撤销工作目录中的未提交更改

    git checkout -- [file]

  4. 撤销上一次提交(不删除更改):

    git reset --soft HEAD~

  5. 撤销上一次提交并删除更改

    git reset --hard HEAD~

  6. 查看文件的详细更改历史

    git blame [file]

  7. 重写提交历史(慎用,可能影响他人工作):

    git rebase -i HEAD~N

  8. 清理不必要的文件和提交

    git gc --prune=now

  9. 创建标签

    git tag [tag-name]

  10. 推送标签到远程仓库

    git push --tags

这些命令涵盖了Git的大部分基本操作,包括版本控制、分支管理、远程协作等。掌握这些命令对于后端程序员来说非常重要,可以帮助你更高效地进行团队协作和代码管理。

7、go中的select的使用场景是什么,有什么作用 

  1. 多个 Channel 的通信: 当有多个 channel 需要并发操作时,select 可以同时等待多个 channel 上的发送或接收操作。一旦其中的某个操作可以进行,select 语句就会执行相应的分支。

  2. 非阻塞操作select 可以用于实现非阻塞的 channel 操作。如果没有 channel 操作可以立即进行,select 会执行 default 分支(如果提供了的话),这使得程序可以在等待 channel 操作的同时执行其他逻辑。

  3. 超时控制: 通过 select 结合 time.Sleepdefault 分支,可以实现对 channel 操作的超时控制。

  4. 死锁避免select 可以避免死锁的发生。在某些情况下,如果所有的 channel 操作都被阻塞,而程序没有其他可执行的路径,这可能导致死锁。selectdefault 分支提供了一种避免这种情况的方法。

  5. 复杂的并发模式: 在复杂的并发场景中,select 可以用于实现状态机、超时重试、多路复用等模式。

以下是一些 select 的使用示例:

示例 1:多路复用

c1 := make(chan int)
c2 := make(chan int)

go func() {
    c1 <- 1
}()

go func() {
    c2 <- 2
}()

select {
case v1 := <-c1:
    fmt.Println("Received", v1, "from c1")
case v2 := <-c2:
    fmt.Println("Received", v2, "from c2")
}

示例 2:超时控制

c := make(chan int)
time.AfterFunc(5*time.Second, func() {
    close(c)
})

select {
case v := <-c:
    fmt.Println("Received", v, "from c")
case <-time.After(1 * time.Second):
    fmt.Println("Timed out waiting for c")
}

示例 3:死锁避免

c1 := make(chan int)
c2 := make(chan int)

go func() {
    c1 <- 1
}()

select {
case v := <-c1:
    fmt.Println("Received", v, "from c1")
    c2 <- 2 // Only executed if the receive from c1 succeeds
default:
    fmt.Println("No data received from c1")
    c2 <- 2 // Safe to send to c2 because c1 receive didn't block
}

示例 4:非阻塞操作

c := make(chan int, 1)
c <- 1

select {
case v := <-c:
    fmt.Println("Received", v, "from c")
default:
    fmt.Println("No data available to receive from c")
}

计算机网络相关

1、服务端什么时候会出现大量的TIME WAIT 状态

服务端出现大量TIME_WAIT状态的情况通常与以下几个因素有关:

高并发短连接场景:在高并发的环境下,尤其是当服务端处理的是大量的短连接时,每次连接完成后都会进入TIME_WAIT状态。这是因为在TCP四次挥手过程中,主动关闭连接的一方(可以是服务端或客户端)需要等待足够的时间来确保最后一个ACK报文能够被对方正确接收,以完成连接的可靠关闭。

HTTP请求中的连接管理:在HTTP请求中,如果Connection头部被设置为close,则服务端在处理完请求后会主动关闭TCP连接,从而导致TIME_WAIT状态的产生。HTTP默认的Connection值为close,意味着服务端在完成请求后会发起关闭连接的操作,这在高并发场景下可能导致大量TIME_WAIT状态的累积。

TCP四次挥手关闭机制:根据TCP协议的规定,主动关闭连接的一方在发送最后一个ACK后会进入TIME_WAIT状态,以确保所有数据都能够被对方正确接收,并处理可能出现的延迟数据包。这个状态会持续2倍的最大报文段生存时间(2MSL),通常为2分钟。如果在这段时间内,服务端持续创建新的连接并主动关闭它们,就可能积累大量的TIME_WAIT状态。

端口资源耗尽:由于每个TCP连接都使用一个唯一的端口号,当服务端产生大量TIME_WAIT状态时,可能会导致可用端口资源的耗尽。这会影响到服务端创建新的连接,进而影响业务的正常运行。

服务端程序的行为:服务端程序的设计和实现也会影响TIME_WAIT状态的产生。例如,如果服务端程序没有正确管理连接,或者在关闭连接时存在延迟,也可能导致TIME_WAIT状态的累积。

2、tcp的三次握手过程:

客户端——发送带有SYN标志的数据包——服务端一次握手Client进入syn_sent状态;

服务端——发送带有SYN/ACK标志的数据包——客户端二次握手服务端进入syn_rcvd;

客户端——发送带有ACK标志的数据包——服务端三次握手连接就进入Established状态;

为什么三次:

主要是为了建立可靠的通信信道,保证客户端与服务端同时具备发送、接收数据的能力。

为什么两次不行?

1、防止已失效的请求报文又传送到了服务端,建立了多余的链接,浪费资源。

2、两次握手只能保证单向连接是畅通的。(为了实现可靠数据传输,TCP协议的通信双方,都必须维护一个序列号,以标识发送出去的数据包中,哪些是已经被对方收到的。三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤;如果只是两次握手,至多只有连接发起方的起始序列号能被确认,另一方选择的序列号则得不到确认)。

  1. SYN:初始同步

    • 客户端选择一个初始序列号(seq=x)并将其放入一个SYN(同步序列编号)标志的TCP段中,然后发送给服务器。
  2. SYN-ACK:同步和确认

    • 服务器收到这个SYN标志的TCP段后,如果同意建立连接,则会发送一个TCP段作为响应。这个TCP段包含服务器自己的初始序列号(seq=y),以及对客户端初始序列号的确认(ack=x+1),并设置SYN和ACK(确认)标志。
  3. ACK:确认

    • 客户端收到服务器的SYN-ACK响应后,会发送一个确认段(ACK标志设置),包含客户端的确认序列号(ack=y+1),表明连接建立成功。
  4. 三次握手的目的是确保双方都准备好发送和接收数据,防止旧的连接请求突然传送到服务器,从而产生错误。例如,如果一个旧的连接请求被传送到了服务器,那么服务器可能认为客户端希望建立一个新的连接,但是由于客户端并没有发送新的连接请求,这将导致错误。

3、TCP四次挥手过程:

客户端——发送带有FIN标志的数据包——服务端,关闭与服务端的连接,客户端进入FIN-WAIT-1状态。

服务端收到这个FIN,它发回⼀个ACK,确认序号为收到的序号加1,服务端就进入了CLOSE-WAIT状态。

服务端——发送⼀个FIN数据包——客户端,关闭与客户端的连接,客户端就进入FIN-WAIT-2状态。

客户端收到这个FIN,发回ACK报⽂确认,并将确认序号设置为收到序号加1,TIME-WAIT状态。

为什么四次:

因为需要确保客户端与服务端的数据能够完成传输。

CLOSE-WAIT:

这种状态的含义其实是表示在等待关闭。

TIME-WAIT:

为了解决网络的丢包和网络不稳定所带来的其他问题,确保连接方能在时间范围内,关闭自己的连接。

TCP(传输控制协议)四次挥手过程是终止一个TCP连接时的步骤。这个过程涉及到客户端和服务器之间的四次交互,以确保双方都同意结束连接,并且所有未完成的数据都被发送和接收。以下是四次挥手的详细步骤:

  1. FIN (结束)

    • 假设客户端想要关闭连接,它发送一个FIN(结束)标志的TCP段,用来关闭从客户端到服务器的数据传输。客户端进入FIN-WAIT-1状态。
  2. ACK (确认)

    • 服务器收到这个FIN后,会发送一个ACK(确认)作为回应,确认收到了FIN。
  3. FIN (结束)

    • 服务器发送自己的FIN,请求关闭从服务器到客户端的数据传输。客户端进入FIN-WAIT-2状态。
  4. ACK (确认)

    • 客户端收到服务器的FIN后,发送一个ACK作为回应。一旦服务器接收到这个ACK,它就进入了CLOSED(关闭)状态。客户端在发送完ACK后也会等待一段时间(称为2MSL,即最大报文段生存时间的两倍),以确保服务器接收到了最终的ACK。如果在2MSL时间内没有收到服务器的重传FIN,客户端也会进入CLOSED状态。

在TCP四次挥手过程中,每个方向的连接都独立地关闭,这就是为什么需要两个FIN和一个ACK的原因。以下是每个状态的解释:

  • FIN-WAIT-1:等待服务器的FIN。
  • FIN-WAIT-2:等待服务器的FIN的确认。
  • CLOSE-WAIT:等待客户端的FIN。
  • LAST-ACK:等待客户端FIN的确认。
  • TIME-WAIT:确保服务器接收到了最终的ACK。
  • CLOSED:连接完全关闭。

四次挥手的目的是确保TCP连接的双方都有机会完成所有未完成的数据传输,并且释放分配给连接的资源。如果在挥手过程中某一方没有正确接收到FIN或ACK,那么它可能会重发FIN,直到收到确认或者超时后放弃连接。

4、如何查看TIME-WAIT状态的链接数量?

netstat-an|grep TIME_WAIT|wc -l查看连接数等待time_wait状态连接数。

5、OSI与TCP/IP模型

OSI七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP/IP五层:物理层、数据链路层、网络层、传输层、应用层

6、常见网络服务分层

应用层:HTTP、SMTP、DNS、FTP

传输层:TCP、UDP

网络层:ICMP、IP、路由器、防火墙

数据链路层:网卡、网桥、交换机

物理层:中继器、集线器

7、TCP与UDP区别及场景

c6ac37a141194b30a8c484aa9af6acde.png

基于UDP的协议:RIP、DNS、SNMP

基于TCP的协议:HTTP、FTP、SMTP

8、TCP滑动窗口,拥塞控制

TCP通过:应用数据分割、对数据包进行编号、校验和、流量控制、拥塞控制、超时重传等措施保证数据的可靠传输;

拥塞控制目的:为了防止过多的数据注入到网络中,避免网络中的路由器、链路过载。

拥塞控制过程:TCP维护一个拥塞窗口,该窗口随着网络拥塞程度动态变化,通过慢开始、拥塞避免等算法减少网络拥塞的发生。

9、TCP粘包原因和解决方法

TCP粘包是指:发送方发送的若干包数据到接收方接收时粘成一包。

发送方原因:

TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量):

收集多个小分组,在一个确认到来时一起发送、导致发送方可能会出现粘包问题。

接收方原因:

TCP将接收到的数据包保存在接收缓存里,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

解决粘包问题:

最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪,通过使用某种方案给出边界,例如:

  • 发送定长包。每个消息的大小都是一样的,接收方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。

  • 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。

  • 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。

10、TCP、UDP报文格式

TCP报文格式:

58872881a3af5badea8a6301276925d7.png

源端口号和目的端口号:

用于寻找发端和收端应用进程。这两个值加上ip首部源端ip地址和目的端ip地址唯一确定一个tcp连接。

序号字段:

序号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达2^32-1后又从0开始。

当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号。

确认序号:

既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加1。只有ACK标志为1时确认序号字段才有效。发送ACK无需任何代价,因为32 bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。

首都长度:

首部长度给出首部中32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此TCP最多有60字节的首部。然而,没有任选字段,正常的长度是20字节。

标志字段:在TCP首部中有6个标志比特。它们中的多个可同时被设置为1.

URG紧急指针(u rgent pointer)有效
ACK确认序号有效
PSH接收方应该尽快将这个报文段交给应用层
RST重建连接
SYN同步序号用来发起一个连接。这个标志和下一个标志将在第18章介绍
FIN发端完成发送任务

窗口大小:

TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个16 bit字段,因而窗口大小最大为65535字节。

检验和:

检验和覆盖了整个的TCP报文段:TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。

紧急指针:

只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。

选项:

最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。

UDP报文格式:

9806f6251ec8fb243a2240415bad0c07.png

端口号:

用来表示发送和接受进程。由于IP层已经把IP数据报分配给TCP或UDP(根据IP首部中协议字段值),因此TCP端口号由TCP来查看,而UDP端口号由UDP来查看。TCP端口号与UDP端口号是相互独立的。

长度:

UDP长度字段指的是UDP首部和UDP数据的字节长度。该字段的最小值为8字节(发送一份0字节的UDP数据报是OK)。

检验和:

UDP检验和是一个端到端的检验和。它由发送端计算,然后由接收端验证。其目的是为了发现UDP首部和数据在发送端到接收端之间发生的任何改动。

IP报文格式:普通的IP首部长为20个字节,除非含有可选项字段。

33f1cedd03411df3fd0fa8bb6b4a84f7.png

4位版本:

目前协议版本号是4,因此IP有时也称作IPV4.

4位首部长度:

首部长度指的是首部占32bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部长度最长为60个字节。

服务类型(TOS):

服务类型字段包括一个3bit的优先权字段(现在已经被忽略),4bit的TOS子字段和1bit未用位必须置0。4bit的TOS分别代表:最小时延,最大吞吐量,最高可靠性和最小费用。4bit中只能置其中1比特。如果所有4bit均为0,那么就意味着是一般服务。

总长度:

总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节。当数据报被分片时,该字段的值也随着变化。

标识字段:

标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。

生存时间:

TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送 ICMP报文通知源主机。

首部检验和:

首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。

以太网报文格式:

7e6a5ab0156ae27f357924085778ad7f.png

目的地址和源地址:

是指网卡的硬件地址(也叫MAC地址),长度是48位,是在网卡出厂时固化的。

数据:

以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面补填充位。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由到拨号链路上,数据包度大于拨号链路的MTU了,则需要对数据包进行分片fragmentation)。ifconfig命令的输出中也有“MTU:1500”。注意,MTU个概念指数据帧中有效载荷的最大长度,不包括帧首部的长度。

11、HTTP协议1.0_1.1_2.0_3.0的区别:

HTTP1.0:服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态);

HTTP1.1:KeepAlived长连接避免了连接建立和释放的开销;通过Content-Length来判断当前请求数据是否已经全部接受(有状态);

HTTP2.0:引入二进制数据帧和流的概念,其中帧对数据进行顺序标识;因为有了序列,服务器可以并行的传输数据。

http1.0和http1.1的主要区别如下:

1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match);
2、网络连接的优化:1.1支持断点续传;
3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态;
4、Host头处理:支持Host头域,不在以IP为请求方标志;
5、长连接:减少了建立和关闭连接的消耗和延迟。

http1.1和http2.0的主要区别:

1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式;
2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成正常的请求);
3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的hearder大小;
4、服务端推送:同google的SPDUY(1.0的一种升级)一样;

HTTP/1.0

  • 发布时间:1996年。
  • 连接使用:默认每个请求/响应对都使用一个单独的TCP连接,请求完成后立即断开(无持久连接)。
  • 管道化:虽然HTTP/1.0支持管道化(pipelining),即在同一个TCP连接上发送多个请求,但大多数浏览器实现时遇到了问题,导致顺序问题和性能瓶颈。
  • Host头:不包含Host头,这意味着同一个IP地址可以托管多个域名,但服务器无法区分请求是针对哪个域名的。

HTTP/1.1

  • 发布时间:1999年。
  • 持久连接:默认支持持久连接(Connection: keep-alive),允许在一个TCP连接上发送多个请求和响应,减少了建立和关闭连接的开销。
  • 管道化:理论上支持管道化,但由于实现和浏览器行为的问题,管道化并没有得到广泛采用。
  • Host头:引入了Host请求头,允许服务器区分同一个IP地址上的不同域名。
  • 分块传输编码:支持分块传输编码,允许发送不完整的消息。
  • 范围请求:支持范围请求,允许客户端请求文件的一部分。
  • 缓存改进:引入了更多的缓存控制选项。

HTTP/2

  • 发布时间:2015年。
  • 二进制协议:HTTP/2是一个二进制协议,与HTTP/1.x的文本协议相比,二进制协议解析更高效,减少了解析时间和潜在的错误。
  • 多路复用:允许在同一个TCP连接上同时发送多个请求和响应,解决了HTTP/1.x中头阻塞(head-of-line blocking)的问题。
  • 服务器推送:服务器可以主动向客户端发送资源,而不需要客户端明确请求,这可以提前加载资源,减少页面加载时间。
  • :HTTP/2中的数据传输是流的形式,每个流可以包含消息,而消息可以来自不同的请求或响应。
  • 流优先级:客户端可以为每个流设置优先级,这样服务器就可以优先处理更重要的请求。
  • 头部压缩:使用HPACK压缩算法对请求头进行压缩,减少了冗余头信息的大小。
  • 流量控制:使用窗口大小和流控制来管理数据的传输,防止拥塞。

HTTP/3

  • 发布时间:2019年(作为RFC 9114发布)。
  • 基于QUIC:HTTP/3不再基于TCP协议,而是使用QUIC协议,它在UDP上提供了类似TCP的可靠传输,同时减少了连接建立的延迟。
  • 改进的拥塞控制:QUIC提供了更有效的拥塞控制算法,可以更好地利用网络带宽。
  • 连接迁移:允许在不同的网络条件下无缝迁移连接,例如,当用户从Wi-Fi切换到移动网络时。

随着时间的推移,HTTP协议不断演进,以适应网络环境的变化和提高网络应用的性能。HTTP/2和HTTP/3的引入,特别是多路复用和流控制,显著提高了网页的加载速度和用户体验。

12、HTTP与HTTPS之间的区别:

d84ea43afc8e9e49658f22a7d8077745.png

HTTPS链接建立的过程:

1.首先客户端先给服务器发送一个请求;

2.服务器发送一个SSL证书给客户端,内容包括:证书的发布机构、有效期、所有者、签名以及公钥;

3.客户端对发来的公钥进行真伪校验,校验为真则使用公钥对对称加密算法以及对称密钥进行加密;

4.服务器端使用私钥进行解密并使用对称密钥加密确认信息发送给客户端;

5.随后客户端和服务端就使用对称密钥进行信息传输;

对称加密算法:

双方持有相同的密钥,且加密速度快,典型对称加密算法:DES、AES

非对称加密算法:

密钥成对出现(私钥、公钥),私钥只有自己知道,不在网络中传输;而公钥可以公开。相比对称加密速度较慢,典型的非对称加密算法有:RSA、DSA

13、Get和Post请求区别:

HTTP请求:

328694539e8b5619d545a4a36a9fc157.png

get和Post区别:

90de85efa9e7d935d9f428afa9d946cd.png

14、HTTP常见响应状态码:

100:Continue---继续。客户端应继续其请求。

200:OK---请求成功。一般用于GET与POST请求。

301:Moved Permanently---永久重定向。

302:Found---暂时重定向。

400:Bad Request---客户端请求的语法错误,服务器无法理解。

403:Forbideen---服务器理解请求客户端的请求,但是拒绝执行此请求。

404:Not Found---服务器无法根据客户端的请求找到资源(网页)。

500:Internal Server Error---服务器内部错误,无法完成请求。

502:Bad Gateway---作为网关或者代理服务器尝试执行请求时,从远程服务器接收到了无效的响应。

15、重定向和转发区别:

重定向:redirect:

地址栏发生变化

重定向可以访问其他站点(服务器)的资源

重定向是两次请求。不能使用request对象来共享数据

转发:forward:

转发地址栏路径不变

转发只能访问当前服务器下的资源

转发是一次请求,可以使用request对象共享数据

16、Cookie和Session区别:

Cookie和Session都是用来跟踪浏览器用户身份的会话方式,但两者有所区别:

Cookie数据保存在客户端(浏览器端),Session数据保存在服务器端。

cookie不是很安全,别人可以分析存放在本地的COOKIE并进行欺骗,考虑到安全应当使用session。

Cookie⼀般⽤来保存⽤户信息,Session的主要作⽤就是通过服务端记录⽤户的状态

17、浏览器输入URL过程:

过程:DNS解析、TCP连接、发送HTTP请求、服务器处理请求并返回HTTP报文、浏览器渲染、结束。

d4f2dd1cbd61beb0009bfc2e4fe05ef8.png

18、gRPC 和 RESTful API 都是服务通信,这两个有什么区别 

gRPC:

  1. 定义方式:使用Protocol Buffers(protobuf)定义服务接口,它是一种与语言无关的接口定义语言(IDL),用于序列化结构化数据。

  2. 性能:gRPC使用Protocol Buffers作为默认的序列化格式,通常比JSON等格式更小、更快。

  3. 传输协议:基于HTTP/2设计,支持全双工通信,允许同时发送多个请求和响应,减少了网络延迟。

  4. 流式传输:支持双向流式传输,适合需要实时数据传输的场景。

  5. 语言支持:由Google开发,有广泛的语言支持,但主要针对C++和Java等语言进行了优化。

  6. 服务发现:通常不包括服务发现机制,可能需要与服务发现工具(如Consul或Etcd)结合使用。

  7. 安全性:gRPC内置支持TLS/SSL加密,可以很容易地实现安全的通信。

  8. 使用场景:适合内部系统、微服务架构中的服务间通信,特别是需要高性能、低延迟的场景。

RESTful API:

  1. 定义方式:使用HTTP方法(GET, POST, PUT, DELETE等)和URI来定义操作。

  2. 性能:通常使用JSON作为数据交换格式,比Protocol Buffers体积大,速度慢。

  3. 传输协议:基于HTTP/1.1,每个TCP连接只能顺序发送一个请求和响应(队头阻塞)。

  4. 流式传输:不支持双向流式传输,通常用于请求-响应模式的通信。

  5. 语言和工具:几乎所有编程语言和工具都支持RESTful API,易于使用和集成。

  6. 服务发现:更容易与服务发现和API网关等工具集成。

  7. 安全性:通常使用HTTPS实现加密,还可以结合OAuth、JWT等机制实现认证和授权。

  8. 使用场景:适合公开API、跨语言和跨平台的服务通信,以及浏览器端和移动应用的后端服务。

总结:

  • 性能:gRPC通常性能更优,适合需要快速、实时通信的场景。
  • 语言无关性:虽然两者都支持多种语言,但gRPC需要使用protobuf定义接口,而RESTful API使用HTTP,更通用。
  • 流式传输:gRPC支持双向流式传输,适合需要实时数据传输的应用。
  • 易用性:RESTful API更容易理解和使用,适合跨平台和跨语言的通信。
  • 安全性:两者都支持加密通信,但gRPC的安全性更内置和一致。

选择gRPC还是RESTful API取决于具体的应用场景、性能要求、开发资源和团队熟悉度。在某些情况下,两者也可以结合使用,例如,使用gRPC进行内部服务间通信,而将RESTful API作为对外的公开接口。

数据库相关

1、mysql数据库的左连接和右连接查询有什么区别:

左连接(LEFT JOIN)

  • 在左连接中,结果集将包含左表(即第一个使用JOIN的表)的所有行,即使右表(第二个表)中没有匹配的行。
  • 右表中没有匹配的列将使用NULL填充。
  • 左连接保证了左表的所有行都会出现在结果集中。

右连接(RIGHT JOIN)

  • 在右连接中,结果集将包含右表的所有行,即使左表中没有匹配的行。
  • 左表中没有匹配的列将使用NULL填充。
  • 右连接保证了右表的所有行都会出现在结果集中

2、Elasticsearch为啥能比普通的关系型数据库搜索效率高? 

Elasticsearch(简称ES)之所以在搜索效率上比传统的关系型数据库高,主要是因为它使用了不同的数据结构和算法来优化搜索操作。以下是几个关键因素:

  1. 倒排索引(Inverted Index)

    • Elasticsearch使用倒排索引,这是一种优化搜索的数据结构,它将文档中出现的每个单词映射到出现该单词的所有文档列表。这种结构使得根据关键词搜索文档变得非常快速3。
  2. 分词(Tokenization)

    • 在索引构建过程中,Elasticsearch会对文档内容进行分词处理,将文本拆分成独立的单词或词组,并为它们建立索引。这使得搜索可以基于文档内容的单词或词组进行3。
  3. 存储优化

    • 倒排列表存储在磁盘上,并进行优化操作,如压缩、合并等,以减少存储空间并提高查询效率3。
  4. 查询处理

    • 当用户发起搜索请求时,Elasticsearch会对查询语句进行分词,并在倒排索引中查找对应的倒排列表,通过交集运算快速找到包含所有查询词的文档3。
  5. 支持复杂查询

    • Elasticsearch支持布尔查询、短语查询、模糊查询等复杂查询操作,这些在关系型数据库中难以实现或效率较低3。
  6. 分布式架构

    • Elasticsearch天然支持分布式,可以在多个节点上分布数据和查询负载,这使得它可以水平扩展以处理大数据量的搜索需求3。
  7. 缓存机制

    • Elasticsearch拥有强大的缓存机制,包括字段值缓存、过滤器缓存和查询结果缓存,这些缓存可以显著提高重复搜索的性能5。
  8. 并行处理

    • 它利用多核CPU的并行处理能力,可以在不同的处理器核心上并行执行查询操作5。
  9. 评分机制

    • 根据相关性对搜索结果进行评分和排序,返回最相关的结果,这是关系型数据库通常不支持的2。
  10. 数据结构选择

    • Elasticsearch使用的数据结构(如B-Tree、哈希表等)适合快速查找和匹配关键词,而关系型数据库的B-Tree索引更适合基于范围的查询操作3。
  11. 专门的搜索引擎优化

    • Elasticsearch专为搜索和数据分析设计,而关系型数据库则为事务性操作和数据结构化设计2。
  12. 索引策略

    • Elasticsearch允许更灵活的索引策略,如自定义分析器和多字段索引,这些在关系型数据库中配置起来更复杂5。

由于这些特性,Elasticsearch在处理全文搜索、复杂查询和大规模数据集时,通常比关系型数据库更加高效。然而,这也意味着Elasticsearch在某些非搜索场景下可能不如关系型数据库高效,因此在实际应用中,两者往往根据具体需求配合使用。

3、MySQL间隙锁的概念: 

MySQL中的间隙锁(Gap Locks)是InnoDB存储引擎在可重复读(Repeatable Read)隔离级别下为了解决幻读问题而引入的一种锁机制。间隙锁锁定的是记录中的间隙,即两个已有记录之间的空间,或者是记录与表边界之间的空间。它的主要目的是防止在事务的执行期间,其他事务插入新的行到这些间隙中,从而保证事务在其执行期间看到的数据保持一致。

实现原理:

间隙锁通常与行锁一起使用,形成next-key lock,后者是行锁和间隙锁的组合。在可重复读隔离级别下,next-key lock可以防止幻读,即防止其他事务在间隙中插入新的行,从而保证在一个事务的执行期间,行的集合保持不变。

作用:

间隙锁主要用于以下场景:

  1. 防止幻读。
  2. 维护可重复读的隔离级别。

举例说明:

假设有一个user表,其中包含以下列:id, name, age,并且id是主键。表中已有如下记录:

id | name | age
---|------|----
1  | aaa  | 2
2  | bbb  | 1
3  | ccc  | 3
7  | ddd  | 7
10 | eee  | 10

在这个表中,存在三个间隙:(3, 7)(7, 10)(10, ∞)

现在,一个事务(称为事务A)开始并执行以下操作:

  1. 锁定id为8的记录(这个记录实际上不存在)。

SELECT * FROM `user` WHERE id = 8 FOR UPDATE;

事务A现在持有从id为7到id为10的间隙锁(假设是(7, 10))。这意味着在这个事务的上下文中,不允许其他事务在这个间隙中插入新的记录。

接着,另一个事务(称为事务B)尝试插入一个id为8的记录:

INSERT INTO `user` ( `id`, `name`, `age` ) VALUES( 8, 'fff', 8 );

由于事务A持有该间隙的锁,事务B的插入操作将会被阻塞,直到事务A提交或回滚,释放了间隙锁。

说明与注意事项:

  • 间隙锁只在可重复读隔离级别下生效。
  • 在读提交(Read Committed)隔离级别下,间隙锁不会生效,因为该级别下锁定的范围更小,主要依赖行锁。
  • 间隙锁可能会影响并发性能,因为它们限制了其他事务在锁定间隙中的操作。

4、数据库索引有哪几种?分别的作用是什么 

  1. 主键索引(PRIMARY KEY)

    • 作用:唯一标识表中的每条记录,不允许重复和空值。
    • 举例:用户表中的用户ID。
  2. 唯一索引(UNIQUE)

    • 作用:确保列中的所有值都是不同的,类似于主键,但允许有空值。
    • 举例:电子邮件地址,每个电子邮件地址在表中只能出现一次。
  3. 普通索引(INDEX)

    • 作用:加速查询,没有唯一性的限制。
    • 举例:学生表中的学生姓名,可以有多个同名的学生。
  4. 复合索引(COMPOSITE INDEX)

    • 作用:在两个或多个列上创建索引,提高查询效率。
    • 举例:订单表中同时对订单日期和订单号进行索引。
  5. 全文索引(FULLTEXT)

    • 作用:用于对大量文本数据进行搜索,支持模糊查询。
    • 举例:文章内容的搜索,可以快速检索包含特定单词的文章。
  6. 空间索引(SPATIAL)

    • 作用:用于地理空间数据类型,优化地理空间数据的查询。
    • 举例:地图服务中,根据地理位置查询附近的餐厅。
  7. 外键索引(FOREIGN KEY)

    • 作用:用于维护两个表之间的链接,确保引用的数据的完整性。
    • 举例:订单表中的用户ID外键索引,引用用户表中的主键。
  8. 分区索引(PARTITIONed INDEX)

    • 作用:对大型表进行分区,以提高查询效率和数据管理。
    • 举例:按年份分区的交易记录表,可以快速查询特定年份的交易数据。
  9. 覆盖索引(COVERING INDEX)

    • 作用:一个索引包含查询所需的所有列,避免了回表查询。
    • 举例:查询用户的所有信息,如果查询列都在索引中,则无需访问数据表。
  10. 函数索引(FUNCTIONAL INDEX)

    • 作用:对列的表达式结果进行索引,比如对字符串的某个函数操作结果进行索引。
    • 举例:对字符串列进行大小写转换后的索引,以支持不区分大小写的查询。

每种索引类型都有其特定的使用场景,选择合适的索引类型对于优化数据库性能至关重要。在设计数据库时,应该根据数据的使用模式和查询需求来创建索引。过多的索引会增加维护成本和写操作的开销,而适当的索引可以显著提高查询效率。

5、索引失效的场景,索引优缺点?什么场景适合索引? 

索引失效的场景:

根据提供的搜索结果,以下是一些常见的索引失效场景:

  1. 不满足最左匹配原则:在使用联合索引时,查询条件需要满足最左前缀匹配原则,否则索引可能不会生效。

  2. 使用了SELECT *:如果查询使用了SELECT *,则可能不会使用覆盖索引,因为覆盖索引只包含查询所需的列。

  3. 索引列参与运算:如果SQL语句中索引列参与了运算,如id + 1 = 2,则索引可能不会生效。

  4. 索引列使用了函数:例如使用SUBSTRCONCAT等函数处理索引列,会导致索引失效。

  5. 错误的LIKE使用:如果模糊查询的LIKE语句中通配符%位于查询条件的左侧,则索引不会生效。

  6. 类型隐式转换:参数类型与字段类型不匹配,导致类型发生了隐式转换,索引可能失效。

  7. 使用OR操作:如果查询条件中使用了OR,并且不是所有相关的列都有索引,则索引可能失效。

  8. 两列做比较:在查询条件中对两个索引列进行比较操作(如id > age),索引可能失效。

  9. 不等于比较:使用<>!=进行比较时,如果查询结果集占比较大,索引可能会失效。

  10. IS NOT NULL:使用IS NOT NULL作为查询条件时,索引可能不会生效。

  11. NOT INNOT EXISTS:使用这些语句时,如果列不是主键或没有索引,索引可能会失效。

  12. ORDER BY导致索引失效:如果ORDER BY子句中的列没有在索引中出现,或者没有使用LIMIT,索引可能不会生效。

  13. 参数不同导致索引失效:某些参数条件下,优化器可能认为全表扫描比使用索引更快,因此不会使用索引。

索引的优缺点:

优点

  1. 提高查询速度:索引加快了数据检索速度,尤其是对于大量数据的表。
  2. 确保数据唯一性:唯一索引确保列中的所有值都是不同的。
  3. 加速表连接:在使用多表连接查询时,可以加快连接速度。
  4. 减少分组和排序时间:索引可以减少数据分组和排序的时间。
  5. 使用优化隐藏器:提高查询性能。

缺点

  1. 维护成本:索引需要额外的存储空间,并且在插入、删除和更新操作时需要维护索引。
  2. 减缓数据输入:创建索引可能会降低数据的维护速度,因为每次数据变更都需要更新索引。
  3. 占用物理空间:索引会占用额外的磁盘空间。

适合使用索引的场景:

  1. 经常作为查询条件的列:这些列上创建索引可以显著提高查询效率。
  2. 主键列:自动创建唯一索引,强制数据的唯一性。
  3. 外键列:用于实现数据的参照完整性。
  4. 经常需要排序的列:如果经常需要根据某个列进行排序,该列适合创建索引。
  5. 经常用于范围搜索的列:如时间列或具有顺序的数据列。
  6. 联合搜索条件的列:如果经常使用多个列作为搜索条件,可以考虑创建复合索引。

6、如何设计索引,有哪些原则:

  1. 最左前缀原则:在使用联合索引时,应确保查询条件符合最左前缀原则,即查询条件中使用的列顺序应该与索引列顺序的前缀部分相匹配。

  2. 选择性原则:索引列的选择性高,即列中不同值的比例越高,索引效果越好。避免在具有大量重复值的列上建立索引。

  3. 唯一性原则:唯一索引可以避免数据重复插入,适合那些需要保证数据唯一性的列。

  4. 常用字段索引:对于经常作为查询条件的列,建立索引可以提高查询效率。

  5. 覆盖索引:如果一个查询只需要从索引中就能够获取所有需要的数据,而不需要回表查询,这种索引被称为覆盖索引,它通常可以提高查询性能。

  6. 避免过度索引:每增加一个索引,都会增加数据库的维护成本。需要权衡索引带来的查询性能提升与维护成本之间的关系。

  7. 索引列上避免计算:避免在索引列上进行函数计算或表达式操作,这会导致索引失效。

  8. 避免使用全模糊和前导通配符:在使用LIKE语句进行模糊查询时,避免使用前导通配符(如%keyword),这会导致索引失效。

  9. 考虑排序和分组:如果列经常用于ORDER BYGROUP BY子句,考虑为其建立索引。

  10. 避免大型索引:大型索引(宽索引)可能会占用大量空间,并且可能会降低写操作的性能。

  11. 考虑索引的维护:了解索引对INSERTUPDATEDELETE操作的影响,并考虑其维护成本。

  12. 使用EXPLAIN分析查询:使用EXPLAIN关键字分析查询语句,以了解查询的执行计划和索引使用情况。

  13. 避免冗余索引:如果一个索引可以被另一个索引的前缀替代,那么这个索引可能是冗余的。

  14. 考虑查询模式:根据实际的查询模式和访问路径设计索引,不同的应用和查询模式可能需要不同的索引策略。

  15. 定期审查索引:随着时间的推移,数据和访问模式可能会变化,定期审查索引的有效性并进行调整。

  16. 索引命名规范:使用有意义的索引名称,以便于管理和理解。

  17. 考虑使用复合索引:对于经常一起使用的列,考虑建立复合索引。

  18. 注意索引的顺序:在复合索引中,列的顺序很重要,应该将最常用于查询条件的列放在前面。

7、mvcc的原理:

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种用于数据库系统中的技术,旨在提高并发性能并解决事务间的隔离问题。MVCC通过在数据的多个版本之间提供隔离,允许在不锁定资源的情况下执行读取和写入操作,从而减少锁争用并提高系统性能。

MVCC的原理和关键组成部分:

  1. 版本化:在MVCC系统中,数据行通常包含版本信息,例如时间戳或事务ID。每当数据行被修改时,系统会创建该行的新版本,并保留旧版本。

  2. 快照:事务在开始时会创建一个“快照”,它是数据库在某一时刻的一致性视图。即使其他事务在快照创建后修改了数据,快照仍然可以访问到事务开始时的数据状态。

  3. 隔离级别:MVCC允许在不同的隔离级别下运行事务,从读未提交(Read Uncommitted)到串行化(Serializable),不同的隔离级别对应不同的可见性和并发性规则。

  4. 版本可见性规则:事务可以看到哪些版本的数据取决于其隔离级别和版本可见性规则。例如,在可重复读(Repeatable Read)隔离级别下,事务只能看到在事务开始前已经提交的行版本。

  5. 写入冲突检测:当一个事务试图修改一个已经被其他事务锁定的行时,MVCC系统会检测到冲突并采取相应的措施,如回滚事务或延迟操作直到冲突解决。

  6. 垃圾回收:旧版本的数据需要被定期清理以释放存储空间。这通常由一个后台进程负责,它会安全地删除那些不再被任何事务引用的旧版本。

MVCC的工作流程:

  1. 开始事务:事务开始时,创建一个快照,记录当前时间点的数据库状态。

  2. 读取数据:事务读取数据时,MVCC根据隔离级别和事务的快照来决定哪些版本的数据对当前事务可见。

  3. 写入数据

    • 如果事务是读取操作,它将读取快照中的数据版本。
    • 如果事务是写入操作,它将创建数据的新版本,并为其打上当前事务的标记。
  4. 事务提交

    • 如果事务成功提交,新版本的数据将变得对其他事务可见。
    • 如果事务失败或被回滚,其创建的所有新版本都将被丢弃。
  5. 垃圾回收:系统定期清理不再需要的旧数据版本,释放存储空间。

MVCC的优点:

  • 提高并发性:通过允许非锁定读取,MVCC减少了锁争用,提高了并发性能。
  • 快照一致性:事务可以在整个事务期间看到一致的数据视图,即使其他事务在这期间修改了数据。
  • 灵活的隔离级别:MVCC支持不同的隔离级别,提供了在性能和数据一致性之间的灵活选择。

MVCC的缺点:

  • 存储开销:由于需要存储数据的多个版本,MVCC可能会增加存储需求。
  • 复杂性:MVCC增加了数据库系统的复杂性,尤其是在冲突检测和版本管理方面。
  • 垃圾回收开销:旧版本的清理可能会对系统性能产生影响,尤其是在高并发系统中。

MVCC是现代数据库系统中实现高并发和事务隔离的关键技术之一,广泛应用于关系型数据库(如PostgreSQL、MySQL的InnoDB存储引擎)和一些NoSQL数据库中。

8、redolog、undolog、binlog的作用?三个log写入格式?

Redo Log(重做日志)

作用

  1. 保证事务的持久性。如果数据库崩溃,可以使用redo log恢复未提交的事务所做的修改。
  2. 提高事务提交的速度,通过减少磁盘I/O操作。

写入格式

  • Redo log是物理日志,记录数据页的物理修改,例如表空间号、数据页号、偏移量和具体修改的数据。它是顺序写入磁盘的物理文件中,并且通常在内存中有一个redo log buffer,事务提交时会将buffer中的内容写入磁盘上的redo log file。

Undo Log(回滚日志)

作用

  1. 保证事务的原子性。如果一个事务需要回滚或数据库需要恢复到某个一致的状态,undo log会被用来撤销已经进行的修改。
  2. 支持多版本并发控制(MVCC),允许在保持读一致性的同时进行非锁定读取。

写入格式

  • Undo log是逻辑日志,记录数据的逻辑变化,例如每行记录在事务开始前的状态。它允许系统在出错时将数据恢复到事务执行前的状态。

Binlog(二进制日志)

作用

  1. 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
  2. 用于数据库的基于时间点的还原。

写入格式

  • Binlog是逻辑日志,记录了所有的DDL和DML语句(除了查询语句),以事件形式记录。它包括执行的SQL语句的反向信息,意味着可以用于数据恢复和复制。

总结

  • Redo log是InnoDB存储引擎特有的,主要关注于物理数据页的修改,用于保证数据的持久性,并且通过减少磁盘I/O操作提高性能。
  • Undo log同样属于InnoDB存储引擎,用于事务的回滚和MVCC,记录数据的逻辑变化。
  • Binlog是MySQL Server层的日志,记录了所有数据库的变更操作,用于复制和恢复。

 9、数据库事务的ACID是什么,以及如何实现的?

  1. 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的某个操作失败,整个事务将回滚到事务开始前的状态,就像这个事务从未执行过一样。

  2. 一致性(Consistency):事务必须使数据库从一个一致的状态转移到另一个一致的状态。一致状态的含义是数据库中的数据应满足完整性约束。

  3. 隔离性(Isolation):并发执行的事务之间相互不受影响,一个事务的中间状态对其他事务不可见,事务执行期间的中间数据对其他事务是不可见的。

  4. 持久性(Durability):一旦事务提交,则其所做的修改会永久保存在数据库中。即使系统发生故障,这些修改也不会丢失。

此外,还有一个与事务密切相关的概念:

  • 连续性(Non-stop Availability):确保在任何时候系统都可提供服务,这通常需要通过数据库的高可用性解决方案来实现,如故障转移、集群等。

在数据库系统中,为了实现ACID属性,通常会使用到锁机制、日志记录(如redo log和undo log)、事务隔离级别等技术手段。例如:

  • 原子性:通过undo log来实现,确保事务可以回滚到事务开始前的状态。
  • 一致性:依赖于数据库的完整性约束和事务的逻辑来确保。
  • 隔离性:通过锁机制、MVCC(多版本并发控制)等技术来实现。
  • 持久性:通过redo log来确保,即使发生崩溃,已提交的事务修改也可以恢复。

10、数据库表设计的几大范式,以及设计表的时候需要遵循的哪些规则

数据库表设计是数据库建模过程中的一个重要环节,它影响着数据库的性能、数据的完整性和易用性。在设计数据库表时,通常遵循一些规范,这些规范被称为数据库范式。下面是几种基本的数据库范式和设计表时应遵循的一些规则:

数据库范式

  1. 第一范式(1NF)

    • 要求数据库表的每一列都是不可分割的基本数据项,即每个字段都是原子性的,不可以再分解。
  2. 第二范式(2NF)

    • 在第一范式的基础上,要求表中的每个实例或行必须可以被唯一地区分,即表必须有一个主键,并且非主键字段必须完全依赖于主键,没有部分依赖。
  3. 第三范式(3NF)

    • 在第二范式的基础上,要求非主键字段之间不能相互依赖,即没有传递依赖。这意味着表中的数据只和主键直接相关,与其他非主键无关。
  4. BCNF(巴斯-科德范式,Boyce-Codd Normal Form)

    • 是第三范式的加强版,要求任何非主属性都不能对表中的候选键产生函数依赖,解决了3NF中存在的某些问题。
  5. 第四范式(4NF)

    • 要求表中不存在多值依赖,即一个表中不应该有两个或多个独立的多值事实关于同一个主键。
  6. 第五范式(5NF)

    • 又称完美范式(PJNF,Project-Join Normal Form),要求消除表中的所有连接依赖(Join Dependency),即表的任何非平凡且非隐含的连接依赖都必须是对候选键的依赖。

设计表时应遵循的规则

  1. 确定实体和关系

    • 在设计表之前,需要先识别出系统中的实体以及实体之间的关系。
  2. 使用主键

    • 每个表都应该有一个主键,它是唯一标识表中每条记录的字段。
  3. 避免不必要的复杂关系

    • 避免创建过于复杂的表关系,这会增加数据库的复杂性和查询难度。
  4. 数据完整性

    • 确保数据的准确性和一致性,包括实体完整性、参照完整性和用户定义的完整性。
  5. 避免冗余

    • 减少数据冗余,避免存储可以由其他数据计算得到的数据。
  6. 规范化

    • 尽量使数据库表设计符合上述提到的范式,以减少数据异常和维护成本。
  7. 索引

    • 为提高查询效率,在经常查询的列上创建索引,但要避免过度索引,因为索引会增加写操作的开销。
  8. 使用数据类型

    • 为每个字段选择合适的数据类型,以确保数据的准确性和存储效率。
  9. 避免不必要的空值

    • 空值可以表示缺失的数据,但有时它们会导致查询和统计分析的复杂化。
  10. 安全性

    • 考虑数据的安全性,对敏感数据进行加密存储,并且合理设置访问权限。
  11. 可扩展性

    • 在设计时考虑未来可能的扩展,避免未来需要大规模重构数据库。
  12. 文档化

    • 记录数据库设计决策,包括表结构、索引、视图和存储过程等。

遵循这些范式和规则有助于创建一个结构良好、易于维护和扩展的数据库。然而,实际应用中可能需要在范式和性能之间做出权衡,因为过于严格的范式化可能会导致不必要的复杂性和性能下降。

复制再试一次分享

11、什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?

缓存穿透

缓存穿透是指客户端请求访问缓存中不存在,且数据库中也不存在的数据。这种情况下,缓存不会对该请求进行拦截,请求直接穿透缓存访问数据库。如果这种请求很多,可能会导致数据库压力过大,甚至垮掉。

避免缓存穿透的策略包括:

  1. 缓存空值:当数据库查询结果为空时,也在缓存中存储这个结果,并设置一个较短的过期时间。

  2. 使用布隆过滤器(Bloom Filter):布隆过滤器是一种空间效率很高的数据结构,可以判断一个元素是否在一个集合中。可以在访问缓存和数据库之前,先通过布隆过滤器判断数据是否存在。

  3. 限制频率:对每个用户或每个IP进行请求频率限制,防止恶意攻击。

缓存雪崩

缓存雪崩是指在高并发系统中,缓存中大量数据在同一时间过期,导致大量请求穿透缓存直接访问数据库,造成数据库压力突增,严重时甚至会导致数据库宕机。

避免缓存雪崩的策略包括:

  1. 设置不同的过期时间:避免所有缓存数据同时过期,可以为不同的数据设置随机的过期时间。

  2. 使用互斥锁或队列:当缓存数据过期时,使用互斥锁或队列控制对数据库的访问,避免同时对数据库发起大量请求。

  3. 缓存预热:在系统上线前,提前加载缓存数据,避免上线后缓存缺失导致的雪崩。

  4. 使用缓存降级:当缓存服务器压力较大时,可以临时关闭缓存,直接查询数据库,以减轻缓存服务器的压力。

  5. 使用多级缓存:使用多级缓存,比如热点数据缓存、温点数据缓存和冷数据缓存,通过不同级别的缓存来分摊请求压力。

  6. 增加缓存冗余:通过主从复制或分布式缓存系统,提高缓存的可用性和容错性。

  7. 限流和降级:在系统面临巨大访问压力时,通过限流保护系统稳定运行,同时对一些服务进行降级处理。

  8. 使用分布式锁:当缓存失效时,通过分布式锁保证只有一个请求能够访问数据库并重建缓存。

操作系统相关

1、进程,线程,协程的区别:

进程:是资源分配的最小单位,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,不共享栈、程序计数器。

线程:是任务调度和执行的最小单位,线程并行执行存在资源竞争和上下文切换的问题。

协程:是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。

进程(Process)

  • 定义:进程是操作系统进行资源分配和调度的一个独立单位。它是应用程序运行的实例,拥有独立的内存空间。
  • 资源拥有:每个进程都有自己独立的内存空间和系统资源,如文件句柄、I/O设备等。
  • 开销:创建和终止进程的开销相对较大,因为涉及到系统资源的分配和回收。
  • 通信方式:进程间通信(IPC)需要特定的机制,如管道、信号、共享内存、消息队列等。
  • 独立性:进程是系统资源的一个独立单位,一个进程崩溃不会直接影响到其他进程。

线程(Thread)

  • 定义:线程是进程中的一个实体,是CPU调度和执行的单位,是比进程更小的执行单元。
  • 资源拥有:同一进程内的线程共享进程的资源,包括内存空间和文件句柄等。
  • 开销:线程的创建和终止开销较小,因为它们共享进程的资源。
  • 通信方式:线程间通信较为容易,因为它们共享相同的内存空间,但这也可能导致数据共享和同步的问题。
  • 执行:在多核处理器上,线程可以并行执行,提高程序的执行效率。

协程(Coroutine)

  • 定义:协程是一种程序组件,它允许挂起和恢复执行,用于实现非阻塞的、协作的任务调度。
  • 资源拥有:协程通常存在于线程中,共享线程的资源。
  • 开销:协程的创建和切换开销非常小,因为它们避免了线程切换的开销。
  • 通信方式:协程间通信通常通过共享内存或特定的协程调度器来实现。
  • 执行:协程是协作式的多任务,它们在同一个线程内执行,通过挂起和恢复来实现任务的切换,而不是通过操作系统的抢占式调度。

主要区别:

  • 资源:进程拥有独立的内存空间和系统资源,线程共享进程资源,协程通常在线程内部实现,共享线程资源。
  • 开销:进程的创建和销毁开销最大,线程次之,协程最小。
  • 调度:进程由操作系统调度,线程可以由操作系统或用户调度,协程通常由用户或协程调度器调度。
  • 并发性:进程和线程可以实现并发执行,协程则实现并行执行,但它们之间的切换更轻量级。

在现代编程中,协程因其轻量级和高效的调度机制,被广泛应用于I/O密集型和高并发的应用程序中,如网络服务器和并发I/O操作。

2、进程间通信方式IPC :

管道pipe:

亲缘关系使用匿名管道,非亲缘关系使用命名管道,管道遵循FIFO,半双工,数据只能单向通信;

信号:

信号是一种比较复杂的通信方式,用户调用kill命令将信号发送给其他进程。

消息队列:

消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。

共享内存(share memory):

  • 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。

  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

信号量(Semaphores):

信号量是⼀个计数器,⽤于多进程对共享数据的访问,这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。

套接字(Sockets):

简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。

  1. 管道(Pipes)

    管道是一种半双工通信方式,允许一个进程和一个或多个进程进行通信。数据只能在一个方向上流动。
  2. 命名管道(Named Pipes)

    类似于管道,但它们拥有一个名字,并且可以独立于创建它们的进程而存在,允许不相关的进程之间的通信。
  3. 消息队列(Message Queues)

    消息队列允许进程以消息的形式交换数据,这些消息可以设置优先级,并被存储在队列中直到被接收。
  4. 信号(Signals)

    信号是一种简单的通信方式,用于发送通知,如中断或终止进程。它们不是用于传输大量数据。
  5. 共享内存(Shared Memory)

    共享内存是一种效率很高的IPC机制,它允许两个或多个进程共享一个给定的存储区。进程可以直接读写这块内存,但需要同步机制来避免竞态条件。
  6. 信号量(Semaphores)

    信号量是一种用于控制对共享资源访问的同步机制,通常与共享内存一起使用。
  7. 套接字(Sockets)

    套接字是一种网络通信方式,但也可用于本机进程间通信。它们支持面向连接(如TCP)和无连接(如UDP)的通信。
  8. 文件映射(Memory-mapped Files)

    文件映射是一种将文件内容映射到进程地址空间的IPC方式,允许多个进程访问同一文件的内容,从而实现数据共享。
  9. 远程过程调用(Remote Procedure Call, RPC)

    RPC允许一个进程调用另一个进程的函数或方法,就像它们是本地的一样,RPC隐藏了底层网络通信的复杂性。
  10. 分布式对象(Distributed Objects)

    在面向对象的系统中,分布式对象允许对象在不同的进程中存在,并通过某种机制(如COM或CORBA)进行通信。
  11. 事件(Events)

    事件是一种同步机制,允许一个进程通知另一个进程某个事件已经发生。
  12. 内存屏障(Memory Barriers)

    在多核系统中,内存屏障用于确保内存操作的执行顺序,从而实现进程间的同步。 

3、常见的linux操作系统的命令用法 

  1. 文件系统导航

    • cd:改变当前目录。
    • ls:列出目录内容。
    • pwd:显示当前工作目录。
    • mkdir:创建新目录。
    • rmdir:删除空目录。
    • touch:创建空文件。
  2. 文件操作

    • cat:查看文件内容。
    • less:分页显示文件内容。
    • more:分页查看文件。
    • head:查看文件的开始部分。
    • tail:查看文件的末尾部分,常用于查看日志文件。
    • cp:复制文件或目录。
    • mv:移动或重命名文件。
  3. 权限管理

    • chmod:改变文件或目录的权限。
    • chown:改变文件或目录的所有者。
  4. 文本处理

    • grep:搜索文本内容。
    • sed:流编辑器,用于文本处理。
    • awk:强大的文本分析工具。
  5. 网络操作

    • ping:检测网络连通性。
    • netstat:显示网络状态。
    • telnet:远程登录。
    • curl 或 wget:进行网络请求。
  6. 进程管理

    • ps:查看当前运行的进程。
    • top:实时显示进程的资源占用。
    • kill:发送信号给进程,常用于结束进程。
  7. 系统信息

    • df:查看磁盘空间使用情况。
    • du:查看文件或目录的磁盘使用情况。
    • free:查看内存使用情况。
    • uname:显示系统信息。
  8. 包管理(在基于Debian的系统如Ubuntu中):

    • apt-get 或 apt:软件包管理工具。
    • dpkg:低级包管理工具。
  9. 服务管理(特别是在使用systemd的系统中):

    • systemctl:管理系统和服务。
  10. 文本编辑

    • vi 或 vim:强大的文本编辑器。
    • nano:简单易用的文本编辑器。
  11. 定时任务

    • cron:设置定时任务。
  12. 用户管理

    • useradd:添加用户。
    • usermod:修改用户。
    • passwd:更改用户密码。

举例说明:

  • 查看当前目录下的文件和目录列表:ls
  • 查看某个文件的前几行:head filename
  • 查找包含特定文本的文件:grep "text" filename
  • 改变文件权限,使得所有用户都可以读取:chmod a+r filename
  • 杀死占用某个端口的进程:kill -9 $(lsof -t -i:port)
  • 查看系统资源使用情况:top
  • 安装一个新的软件包:sudo apt-get install packagename
  • 编辑一个文件:vi filename

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
学习 Golang 后端开发需要掌握以下几个方面: 1. Golang 语言基础:学习 Golang 的语法、数据类型、控制流、函数、接口等基础知识。 2. Web 开发框架:了解 Golang 常用的 Web 开发框架,如 Gin、Echo、Beego 等。 3. 数据库操作:学习 Golang 如何操作 MySQL、PostgreSQL、MongoDB 等数据库。 4. 缓存技术:了解 Redis 等常用缓存技术的使用和优化。 5. 消息队列:学习消息队列的使用,如 RabbitMQ、Kafka 等。 6. 微服务架构:了解微服务架构的设计和实现方式,如 gRPC、Consul、Zookeeper 等。 7. 安全性:了解如何保证 Golang 后端应用的安全性,包括数据传输的加密、防止 SQL 注入、XSS 攻击等。 具体的学习路线可以按照以下步骤进行: 1. 先学习 Golang 基础知识,可以参考《Go 语言圣经》或《Go 语言编程》等经典教材。 2. 掌握 Web 开发框架,可以从 Gin 或 Echo 开始,掌握基本的 API 开发方式。 3. 学习数据库操作,可以从 MySQL 开始,了解如何使用 Golang 连接数据库、执行 SQL 语句等。 4. 学习缓存技术和消息队列,可以从 Redis 和 RabbitMQ 开始,了解如何使用这些技术提高系统性能和可靠性。 5. 学习微服务架构,可以了解如何使用 gRPC、Consul、Zookeeper 等工具实现微服务架构。 6. 学习安全性,可以了解如何使用 TLS 加密数据传输、如何防止 SQL 注入、XSS 攻击等常见安全问题。 以上是一个简单的学习路线,具体的学习内容和顺序可以根据自己的实际情况进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EssRt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值