Skip to main content

Interview

Post
KIGA
Author
KIGA
This is a personal blog, intended for sharing.
Table of Contents

Summarize
#

主要语言栈golang,主要技术方微服务开发。 有微服务实践经历,了解cicd流程,求职意向基础架构研发、运维研发之类的(主要还是研发方向)。

项目方向:Tips 项目细节自己肯定清楚,如果项目中不是自己做的部分,不要在简历上写太多,写清楚自己做了什么,容易被抠细节问,项目一般都会抠细节,特别细的那种!!!

语言栈:Go

gin框架路由怎么实现的,具体正则怎么匹配?
#

Gin框架的路由是通过一个叫做httprouter的库实现的。httprouter是一个高效的HTTP路由库,它通过使用一个叫做Radix Tree的数据结构来存储和匹配路由。

在Gin中,你可以使用gin.EngineGET, POST, PUT, DELETE, PATCH, OPTIONSHEAD方法来添加路由。这些方法接受一个路径和一个处理函数作为参数。路径可以包含参数,参数可以是:name形式的命名参数,或者*action形式的通配符参数。

以下是一个简单的示例:

router := gin.Default()
router.GET("/user/:name", func(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
})
router.Run(":8080")

在这个例子中,/user/:name是一个路由路径,:name是一个命名参数。当你访问/user/john时,john就会被作为name参数的值。

httprouter使用的是最长前缀匹配,而不是正则表达式匹配。这意味着它会找到与请求路径最匹配的路由。例如,如果你有/user/new/user/:name两个路由,当你访问/user/new时,会匹配到/user/new路由,而不是/user/:name路由。

限流中间件怎么实现?
#

使用令牌桶算法实现限流器,设置速率及最大突发数,运行时使用limiter.Allow()方法检查是否处理新的请求根据返回的布尔值表示是否达到速率的限制。

package main

import (
	"fmt"
	"net/http"

	"golang.org/x/time/rate"
)

const (
	rateLimit  = 1
	burstLimit = 3
)

var limiter = rate.NewLimiter(rateLimit, burstLimit)

func middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !limiter.Allow() {
			http.Error(w, "Too many requests", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	http.Handle("/", middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// time.Sleep(1 * time.Second) // Simulate an expensive operation
		w.Write([]byte("Done"))
	})))
	fmt.Println("Server is listening on port 8080")
	http.ListenAndServe(":8000", nil)

	select {}
}

go的slice与数组的区别,slice的实现原理,源码
#

Go 语言中的数组和切片是两种不同的序列类型。

  1. 数组:数组是具有固定长度的数据类型,长度是数组类型的一部分。一旦定义,数组的长度不能更改。

  2. 切片:切片是对数组的抽象。切片的长度可以在运行时更改,最大可以扩展到底层数组的长度。

切片的实现原理:切片是一个包含指向数组的指针、长度和容量的数据结构。当你更改切片的长度时,Go 会创建一个新的数组,并将原始数据复制到新数组中,然后更新切片以引用新数组。

切片的源码实现在 Go 的源码库中的 runtime/slice.go 文件中。这个文件包含了切片的内部结构定义和一些操作切片的函数。

以下是切片的内部结构定义:

// runtime/slice.go

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

这个结构体包含了一个指向数组的指针、切片的长度和切片的容量。所有的切片操作,如 appendcopy 等,都是通过操作这个结构体来实现的。

golang的协程调度,gpm模型。协程调度过程中的锁。
#

在Go语言中,协程的调度是基于GPM模型的。GPM模型包含三个主要的组成部分:

  • G:Goroutine,即协程,每个Goroutine对应一个函数,是执行任务的最小单位。
  • P:Processor,处理器,每个P管理一组G,并决定哪个G应该被执行。
  • M:Machine,机器,每个M对应一个内核线程,是真正执行任务的实体。

在GPM模型中,P是G和M之间的桥梁,每个M必须拥有一个P才能执行G。P的数量决定了系统并发的数量,通常等于机器的CPU核数。

在协程调度过程中,锁主要用于保护共享资源,防止并发访问导致的数据竞争。Go语言提供了sync包来支持锁的操作,包括互斥锁(sync.Mutex)和读写锁(sync.RWMutex)。

以下是一个使用互斥锁的例子:

package main

import (
	"fmt"
	"sync"
)

var counter int
var lock sync.Mutex

func main() {
	for i := 0; i < 1000; i++ {
		go increment()
	}

	// Wait for all goroutines to finish.
	// This is a simple way to wait, not recommended for production code.
	for counter < 1000 {
	}

	fmt.Println("Final Counter:", counter)
}

func increment() {
	lock.Lock()
	defer lock.Unlock()

	counter++
}

使用互斥锁来保护counter变量,确保在并发环境下,每次只有一个协程可以修改counter的值。

golang的channel实现,channel有缓存和无缓存
#

在Go语言中,channel是一种特殊的类型,用于在不同的Goroutine之间传递数据。channel可以有缓存,也可以没有缓存。

无缓存的channel是同步的,发送和接收操作都会阻塞,直到另一方准备好。这意味着,如果一个Goroutine尝试从一个空的channel接收数据,它将会阻塞,直到另一个Goroutine发送数据到这个channel。同样,如果一个Goroutine尝试向一个无缓存的channel发送数据,它将会阻塞,直到另一个Goroutine从这个channel接收数据。

有缓存的channel是异步的,发送和接收操作不会阻塞,只有当缓存满时,发送操作才会阻塞,只有当缓存空时,接收操作才会阻塞。

无缓存channel的例子:

package main

import "fmt"

func main() {
	ch := make(chan int)

	go func() {
		ch <- 1  // Send a value into the channel.
	}()

	val := <-ch  // Receive a value from the channel.
	fmt.Println(val)
}

有缓存channel的例子:

package main

import "fmt"

func main() {
	ch := make(chan int, 2)  // Create a buffered channel with a capacity of 2.

	ch <- 1  // Send a value into the channel.
	ch <- 2  // Send another value into the channel.

	fmt.Println(<-ch)  // Receive a value from the channel.
	fmt.Println(<-ch)  // Receive another value from the channel.
}

golang的关键字defer、recover、pannic之类的实现原理。
#

defer、recover和panic是用于处理异常和清理资源的关键字。

defer用于延迟执行一个函数,通常用于释放资源或者记录日志。defer语句会在函数返回之前执行,即使函数发生了异常,也会执行defer语句。

recover用于从panic中恢复,防止程序崩溃。recover只能在defer语句中使用,用于捕获panic的值,并返回该值。如果没有发生panic,recover会返回nil。

panic用于引发一个运行时错误,导致程序崩溃。当程序发生严重错误时,可以使用panic来终止程序,防止程序继续执行。

package main

import "fmt"

func main() {
    // 注册一个延迟函数,这个函数会调用recover来捕获可能发生的panic
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Recovered from", err)
        }
    }()

    fmt.Println("Start")
    panic("Something bad happened")
    fmt.Println("End")  // This will not be reached.
}

sync包里面的锁、原子操作、waitgroup
#

Go语言的sync包提供了多种并发原语,如互斥锁、读写锁、条件变量、WaitGroup等,以及一些用于进行原子操作的函数。

  1. 锁:sync包提供了Mutex(互斥锁)和RWMutex(读写锁)两种锁。互斥锁用于在代码中创建一个临界区,保证同一时间只有一个goroutine可以执行临界区的代码。读写锁与互斥锁类似,但是它允许多个goroutine同时读取数据。
var mutex sync.Mutex

func increment() {
	mutex.Lock()
	// Critical section of code.
	mutex.Unlock()
}
  1. 原子操作:sync/atomic包提供了一些函数,可以进行原子的内存操作,这些函数可以在并发环境中安全地修改或读取变量。
var counter int32

func increment() {
	atomic.AddInt32(&counter, 1)
}
  1. WaitGroup:sync.WaitGroup用于等待一组goroutine完成。主goroutine调用Add来设置应等待的goroutine的数量。然后每个goroutine运行并在完成时调用Done。同时,主goroutine通过调用Wait来阻塞,直到所有的goroutine都完成。
var wg sync.WaitGroup

wg.Add(1)
go func() {
	// Do some work.
	wg.Done()
}()
wg.Wait()

make和new的区别
#

在Go语言中,newmake都是用于内存分配的内建函数,但它们的用途和行为是不同的。

  • new(T)new函数接受一个类型作为参数,分配足够的内存来存储该类型的一个值,并返回指向该值的指针。这个值会被初始化为零值。例如,对于int类型,零值是0;对于指针类型,零值是nil。
p := new(int)   // p is *int and points to an unnamed int variable
fmt.Println(*p) // prints 0
  • make(T, args)make函数只能用于创建切片、映射和通道,并返回一个初始化的(非零)的T类型的值(而不是*T)。make函数的目的是初始化内部的数据结构,准备好值以供使用。
s := make([]int, 10)    // s is []int and has length 10
m := make(map[int]int)  // m is map[int]int and is empty
c := make(chan int, 10) // c is chan int and has buffer size 10

new返回指针,对应的类型为指针;make返回初始化后的值,对应的类型为该类型本身。

引用类型和非引用类型,值传递之类的
#

在编程中,数据类型可以分为两种:值类型和引用类型。

  1. 值类型:这些类型的变量直接存储值,内存通常在栈上分配。当你把一个值类型的变量赋值给另一个变量时,复制的是变量的值。因此,如果你改变一个变量的值,不会影响其他变量。Go 语言中的基本类型(如 int、float、bool、string)、数组和结构体都是值类型。

  2. 引用类型:这些类型的变量存储的是一个指向值的指针,内存通常在堆上分配。当你把一个引用类型的变量赋值给另一个变量时,复制的是指针,两个变量都指向同一个值。因此,如果你改变一个变量的值,会影响到其他变量。Go 语言中的切片、映射(map)、通道(channel)、接口(interface)和函数类型都是引用类型。

值传递:无论是值类型还是引用类型,函数参数都是通过值传递的。也就是说,当你把一个变量传递给函数时,函数会收到这个变量的一个副本。如果函数改变了副本的值,不会影响原来的变量。但是,如果变量是引用类型,副本的值就是一个指针,这个指针指向的值是可以被函数改变的。

操作系统
#

进程、线程、协程间的区别以及他们间的切换
#

进程、线程和协程是操作系统和编程语言中的基本概念,它们之间的主要区别在于资源分配和执行方式。

  1. 进程:进程是操作系统资源分配的基本单位,每个进程都有自己的内存空间和系统资源。进程间的切换需要操作系统介入,开销较大。

  2. 线程:线程是操作系统任务调度的基本单位,同一进程内的线程共享进程的资源。线程间的切换开销小于进程,但仍需要操作系统介入。

  3. 协程:协程是用户级的轻量级线程,不需要操作系统进行调度,切换开销非常小。协程的切换完全由用户控制,可以在任何地方进行切换。

io复用、用户态/内核态转换
#

IO复用和用户态/内核态转换是操作系统中的两个重要概念。

  1. IO复用:IO复用是指一个进程在等待一个IO操作完成的同时,可以处理其他的IO操作。这样可以提高系统的并发性能。常见的IO复用技术有select、poll和epoll。

  2. 用户态/内核态转换:在操作系统中,为了保护系统的安全,将操作模式分为用户态和内核态。用户态是应用程序的运行模式,内核态是操作系统代码的运行模式。当应用程序需要调用系统资源时,需要从用户态切换到内核态。这个切换过程会有一定的开销,所以在设计系统时,会尽量减少这种切换。

进程间通信方式,管道、共享内存、消息队列、信号量、套接字
#

进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传递数据的各种技术方法。以下是一些常见的IPC方法:

  1. 管道(Pipe):管道是最早的IPC形式之一,通常只能在具有共同祖先的两个进程之间使用。数据流动是单向的。

  2. 共享内存(Shared Memory):两个或多个进程可以访问同一块内存空间。它是最快的IPC方式,但需要解决进程间的同步和协调问题。

  3. 消息队列(Message Queue):消息队列是一种列表结构,允许进程将消息发送到队列中,其他进程可以从队列中读取或删除消息。

  4. 信号量(Semaphore):信号量主要用于同步和互斥,它是一个整数值,表示可用资源的数量。

  5. 套接字(Socket):套接字可以用于不同机器之间的进程通信,也可以用于同一机器的进程通信。它支持TCP和UDP协议。

每种IPC方式都有其优点和缺点,选择哪种方式取决于具体的应用需求。

awk命令
#

awk是一种编程语言,用于在Linux和其他Unix-like操作系统上处理文本。

特别适合处理结构化的文本文件,如表格和数据库输出。awk命令可以用来添加、删除、替换文本,创建报告等。

awk 'pattern {action}' file

在这里,pattern是匹配的文本,action是当找到匹配项时你想要执行的操作,file是处理的文件。

包含以下内容的文件test.txt

1:apple
2:banana
3:cherry

你可以使用以下命令来打印所有包含"apple"的行:

awk '/apple/ {print}' test.txt
# 1:apple

linux查看端口占用
#

在Linux中,可以使用netstatlsof命令来查看端口的占用情况。

  1. 使用netstat命令:
netstat -tuln

在这里,-t表示查看TCP端口,-u表示查看UDP端口,-l表示查看监听的服务状态,-n表示以数字形式显示地址和端口。

查看特定端口(例如8080)的占用情况,可以使用grep命令:

netstat -tuln | grep 8080
  1. 使用lsof命令:
lsof -i:8080

k8s & 容器
#

什么是云原生、什么是k8s、容器,容器与虚机相比优势
#

  • 云原生:云原生是指构建和运行应用程序的方法,这种方法充分利用了云计算交付模型的优势。它关注的是如何创建和部署应用程序,而不是在哪里。

  • Kubernetes (K8s):Kubernetes是一个开源平台,设计用于自动部署、扩展和操作应用程序容器。它将构成应用程序的容器分组成逻辑单元,以便于管理和发现。

  • 容器:容器是一个标准的软件单元,它将代码和所有依赖项打包在一起,使应用程序可以从一个计算环境快速可靠地运行到另一个计算环境。

  • 容器与虚拟机的优势:与虚拟机相比,容器更轻量级,启动更快,因为它们共享主机系统的内核,而不需要运行整个操作系统。容器之间的隔离性也比传统的虚拟化技术更强。此外,由于容器封装了整个运行时环境,因此在不同的运行环境之间移动容器变得更加简单。

docker是怎么实现的,底层基石namespace和cgroup
#

Docker 是一种开源的应用容器引擎,它允许开发者将应用及其依赖打包到一个可移植的容器中,然后发布到任何流行的 Linux 机器或 Windows 机器上,也可以实现虚拟化。Docker 的实现主要依赖于 Linux 内核的一些特性,主要包括 namespaces 和 cgroups。

  1. Namespaces:Namespaces 是 Linux 内核的一个特性,它可以为容器提供一个隔离的环境。每个容器都在各自的 namespace 中运行,一个 namespace 中的进程不能看到其他 namespace 的资源。Docker 使用了多种类型的 namespaces,包括 PID(进程)、NET(网络)、IPC(进程间通信)、MNT(文件系统挂载点)和 UTS(主机名和域名)。

  2. Cgroups(Control Groups):Cgroups 是 Linux 内核的一个特性,它用于限制和隔离一组进程对系统资源的使用,如 CPU、内存、磁盘 I/O 等。Docker 使用 cgroups 来控制容器的资源使用,确保每个容器都能得到它应得的资源,并防止单个容器使用过多的资源。

Docker 还使用了其他一些技术,如联合文件系统(UnionFS)来实现镜像的层次结构,以及容器的存储和分发。

以下是 Docker 工作的简化流程:

  1. Docker 客户端发出命令,如 docker run
  2. Docker 服务端接收到命令后,会创建一个新的容器。这涉及到创建一个新的文件系统,挂载读写层和只读层,设置网络等。
  3. Docker 服务端使用 namespaces 和 cgroups 创建一个隔离的环境,然后在这个环境中启动容器的主进程。
  4. 容器的主进程运行结束后,Docker 服务端清理容器,包括移除文件系统和释放资源。

计算机网络
#

tcp三次握手四次挥手,为什么不能是两次握手,三次挥手?握手和挥手过程中的状态。
#

TCP 三次握手和四次挥手的过程是为了建立一个可靠的连接,并确保数据的完整性和准确性。

TCP 三次握手

  1. 客户端发送 SYN 包(SYN=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认。
  2. 服务器收到 SYN 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(SYN=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态。
  3. 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

TCP 四次挥手

  1. 主动关闭方发送一个 FIN,用来关闭主动方到被动关闭方的数据传送,主动关闭方进入 FIN_WAIT_1 状态。
  2. 被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(ack=u+1),被动关闭方进入 CLOSE_WAIT 状态。
  3. 被动关闭方发送一个 FIN,用来关闭被动关闭方到主动关闭方的数据传送,被动关闭方进入 LAST_ACK 状态。
  4. 主动关闭方收到 FIN 后,进入 TIME_WAIT 状态,发送 ACK 给被动关闭方。

为什么不能是两次握手,三次挥手?

  1. 两次握手:两次握手无法解决"已失效的连接请求报文段"问题,可能会导致服务端错误地建立了连接。三次握手通过"不对初次接收到的 SYN 报文段进行确认"的方法,解决了这个问题。

  2. 三次挥手:TCP 是全双工的,关闭连接时需要保证双方的数据都能发送完毕,所以需要四次挥手。如果只有三次挥手,可能会导致一方的数据未发送完就被强制关闭。

2、time_wait作用,为什么是2msl,close_wait作用,time_wait过多怎么办? 3、http请求的过程,浏览器输入网址请求过程?dns解析的详细过程? 4、https与http 的区别,https第一次服务端回传是否加密? 5、tcp与udp区别,tcp怎么保证可靠性。 6、http请求头、分隔符、长连接怎么实现

数据库
#

mysql的事务,事务使用场景
#

MySQL 的事务是一种将一组 SQL 语句作为一个原子单元进行处理的方式。这组 SQL 语句要么全部执行成功,要么全部执行失败。事务主要用于处理操作量大,复杂度高的数据。事务处理可以保证数据库的一致性。

事务具有以下四个特性,通常被称为 ACID 属性:

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

以下是一个简单的 MySQL 事务的示例:

START TRANSACTION;
INSERT INTO table1 (column1) VALUES ('value1');
INSERT INTO table2 (column2) VALUES ('value2');
COMMIT;

事务的使用场景

事务主要用于需要保证一组操作要么全部成功,要么全部失败的场景,例如:

  • 银行转账:一个账户向另一个账户转账涉及到两个操作,扣除一个账户的金额和增加另一个账户的金额。这两个操作需要在一个事务中完成,要么都成功,要么都失败。
  • 电商下单:用户在电商平台下单涉及到减库存、创建订单、修改用户余额等操作,这些操作需要在一个事务中完成,要么都成功,要么都失败。

mysql的索引,什么情况下索引失效
#

MySQL 的索引是用来提高查询效率的一种数据结构。然而,在某些情况下,索引可能不会被 MySQL 的查询优化器使用,即索引"失效"。以下是一些常见的索引失效的情况:

  1. 使用了不等于(!= 或 <>)的查询条件:这种查询无法使用索引,因为索引是按照顺序存储的。

  2. 使用了 LIKE 操作符,但是通配符在前:比如 LIKE '%abc'。这种查询无法使用索引,因为索引是按照顺序存储的。

  3. 对列进行函数操作或表达式计算:比如 WHERE YEAR(date) < 2022WHERE age/2 = 15。这种情况下,MySQL 无法使用索引,因为它无法预知函数操作或表达式计算的结果。

  4. 使用 OR 连接的查询条件:如果 OR 连接的条件中有一项无法使用索引,那么整个查询都无法使用索引。

  5. 列类型不匹配:如果查询条件中的类型与列的类型不匹配,MySQL 会将列的类型转换为条件的类型,这种类型转换会导致索引失效。

  6. NULL 值:如果列中包含 NULL 值,那么这个列的索引可能不会被使用。

  7. 索引列的顺序:如果在创建复合索引时,索引列的顺序不正确,那么在查询时可能无法使用索引。

  8. 数据分布不均:如果索引列的数据分布非常不均匀,那么在某些情况下,MySQL 可能会选择全表扫描而不是使用索引。

聚簇索引与非聚簇索引,索引的存储b+树与b-树区别
#

聚簇索引与非聚簇索引

  1. 聚簇索引:在聚簇索引中,表记录的物理顺序与键值的逻辑(索引)顺序一致。一个表只能有一个聚簇索引。在 MySQL 中,InnoDB 存储引擎的主键索引就是聚簇索引。

  2. 非聚簇索引:非聚簇索引,也称为二级索引,表记录的物理顺序与键值的逻辑顺序不一致。一个表可以有多个非聚簇索引。非聚簇索引的叶子节点存储的是对应行数据的地址。

B树与B+树

  1. B树:B树是一种自平衡的树,可以用于保持数据有序。这种数据结构可以让查找数据、顺序访问、插入数据和删除数据等操作都在对数时间内完成。

  2. B+树:B+树是B树的一种变体,主要的不同在于:

    • B+树的所有键值都出现在叶子节点上,非叶子节点仅用来做索引,因此搜索的路径更加稳定。
    • B+树的叶子节点之间通过指针相连,这样可以提高范围查询的效率。

在数据库中,由于磁盘I/O操作的昂贵,读取操作的次数越少,性能就越好。B+树相比于B树更能减少磁盘I/O操作,所以在数据库中,B+树被广泛用作索引的数据结构。

join的内外连接,最左匹配原则。
#

在 SQL 中,JOIN 操作用于将两个或多个表根据某个相关列之间的关系组合起来。

内连接(INNER JOIN):只返回两个表中匹配的行。如果在一个表中存在匹配的行,而在另一个表中没有匹配的行,则不返回任何内容。

外连接(OUTER JOIN):除了返回两个表中匹配的行,还返回其中一个表中的所有行(全外连接),或者返回其中一个表中的行(左外连接或右外连接),即使在另一个表中没有匹配的行。

  • 左外连接(LEFT OUTER JOIN):返回左表的所有行,即使在右表中没有匹配的行。
  • 右外连接(RIGHT OUTER JOIN):返回右表的所有行,即使在左表中没有匹配的行。
  • 全外连接(FULL OUTER JOIN):返回左表和右表中的所有行。如果某行在另一个表中没有匹配的行,则结果集中的那部分列将包含 NULL。

最左匹配原则:在 MySQL 中,最左前缀匹配原则是指在进行查询时,从复合索引的最左边开始匹配 WHERE 子句中的条件,一旦遇到范围查询(>, <, BETWEEN, LIKE)就停止匹配,即使有更多的列索引也不会继续匹配。这是因为索引是按照列的顺序创建的,一旦遇到范围查询,就无法确定接下来的数据的顺序,因此只能停止匹配。

redis的数据结构,hmap怎么实现的,持久化怎么做,go操作redis的方式。
#

Redis 数据结构

Redis 支持多种类型的数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)等。

Hashmap 的实现

Redis 的 Hash 实现是一个字符串字段和字符串值之间的映射。内部使用哈希表(Hashtable)实现,可以通过字段名快速查找到对应的值。

Redis 持久化

Redis 提供了两种持久化方法:RDB 和 AOF。

  • RDB 持久化是通过创建数据的快照来实现的。在指定的时间间隔内,如果满足指定的写操作数量,Redis 就会在后台启动一个新的进程,将当前数据写入一个临时文件,写入完成后再替换旧的数据文件。

  • AOF 持久化记录服务器接收到的所有写操作命令,并在服务器启动时通过重新执行这些命令来还原数据。AOF 文件中的写命令都以 Redis 协议的格式保存,新的写命令会被追加到文件的末尾。Redis 还可以根据配置文件的设置,同步地或异步地将写命令追加到 AOF 文件。

Go 操作 Redis 的方式

在 Go 语言中,可以使用 go-redis 这个库来操作 Redis。以下是一个简单的示例:

package main

import (
	"fmt"
	"github.com/go-redis/redis"
)

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	pong, err := client.Ping().Result()
	fmt.Println(pong, err)
}

创建一个新的 Redis 客户端,然后调用 Ping 方法来检查是否可以成功连接到 Redis 服务器。

数据结构与算法
#

倒排索引和B+树
#

倒排索引

倒排索引是一种索引方法,被广泛应用于全文搜索。一个倒排索引由一系列的单词和出现这些单词的文档列表构成。在查询时,可以通过单词快速找到包含这个单词的文档,因此倒排索引非常适合用于搜索引擎。

例如,假设我们有以下两个文档:

  • 文档1:I love coding
  • 文档2:I love reading

倒排索引如下:

  • I:文档1,文档2
  • love:文档1,文档2
  • coding:文档1
  • reading:文档2

B+树

B+树是一种自平衡的树,可以用于保持数据有序。这种数据结构可以让查找数据、顺序访问、插入数据和删除数据等操作都在对数时间内完成。B+树的所有键值都出现在叶子节点上,非叶子节点仅用来做索引,因此搜索的路径更加稳定。B+树的叶子节点之间通过指针相连,这样可以提高范围查询的效率。

在数据库中,由于磁盘I/O操作的昂贵,读取操作的次数越少,性能就越好。B+树相比于B树更能减少磁盘I/O操作,所以在数据库中,B+树被广泛用作索引的数据结构。

倒排索引和B+树都是索引的实现方式,但它们适用的场景不同。倒排索引主要用于全文搜索,而B+树主要用于数据库的索引。

判断链表是否有环,时间复杂度要求0(1)
#

对于判断链表是否有环的问题,时间复杂度要求 O(1) 是不可能的。因为至少需要遍历链表一次才能确定是否存在环。所以,最低的时间复杂度应该是 O(n),其中 n 是链表的长度。

一个常用的解决方案是使用两个指针,一个快指针和一个慢指针。快指针每次移动两个节点,慢指针每次移动一个节点。如果链表中存在环,那么快指针和慢指针最终会在环内的某个位置相遇。

以下是使用这种方法的 Go 语言代码示例:

type ListNode struct {
    Val  int
    Next *ListNode
}

func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil {
        return false
    }
    slow, fast := head, head.Next
    for fast != nil && fast.Next != nil {
        if slow == fast {
            return true
        }
        slow = slow.Next
        fast = fast.Next.Next
    }
    return false
}

hasCycle 函数接受一个链表的头节点作为参数,如果链表中存在环,函数返回 true,否则返回 false

什么是平衡二叉树、最小堆
#

平衡二叉树

平衡二叉树(Balanced Binary Tree)是一种特殊的二叉树,它的每个节点的左子树和右子树的高度差最多为1。平衡二叉树可以保证树的高度在对数级别,从而使得在树上进行插入、删除和查找操作的时间复杂度为 O(log n)。AVL 树和红黑树都是平衡二叉树的实现。

最小堆

最小堆(Min Heap)是一种特殊的完全二叉树。在最小堆中,父节点的值总是小于或等于其子节点的值。这意味着树的根节点是所有节点中的最小值。最小堆常常被用来实现优先队列,它可以在 O(log n) 的时间复杂度内插入新元素和删除最小元素。

大文件的top10问题
#

大文件的 Top10 问题通常指的是如何从一个非常大的文件中找出出现次数最多的10个元素。由于文件可能非常大,无法全部加载到内存中,因此需要使用一些特殊的方法来解决这个问题。

一种常见的解决方案是使用哈希函数和最小堆:

  1. 使用哈希函数将大文件的数据分割成多个小文件,使得每个小文件都可以加载到内存中。哈希函数需要设计得足够好,以确保同一个元素不会被分到不同的小文件中。

  2. 对每个小文件进行处理,统计每个元素的出现次数,并保存到一个哈希表中。

  3. 使用一个大小为10的最小堆,遍历每个小文件的哈希表,维护堆中的元素。堆中始终保持出现次数最多的10个元素。

  4. 最后,堆中的10个元素就是整个大文件中出现次数最多的10个元素。

这个方法的时间复杂度是 O(n),空间复杂度取决于哈希表的大小和堆的大小,但由于我们只需要维护一个大小为10的堆,因此空间复杂度可以认为是常数级别的。

golang实现栈、队列
#

在 Go 语言中,可以使用切片(Slice)来实现栈和队列。

栈是一种后进先出(LIFO)的数据结构。可以使用 append 函数来实现入栈操作,使用切片的索引来实现出栈操作。

type Stack []int

func (s *Stack) Push(v int) {
    *s = append(*s, v)
}

func (s *Stack) Pop() int {
    res := (*s)[len(*s)-1]
    *s = (*s)[:len(*s)-1]
    return res
}

func (s *Stack) IsEmpty() bool {
    return len(*s) == 0
}

队列

队列是一种先进先出(FIFO)的数据结构。可以使用 append 函数来实现入队操作,使用切片的索引来实现出队操作。

type Queue []int

func (q *Queue) Enqueue(v int) {
    *q = append(*q, v)
}

func (q *Queue) Dequeue() int {
    res := (*q)[0]
    *q = (*q)[1:]
    return res
}

func (q *Queue) IsEmpty() bool {
    return len(*q) == 0
}

在这两个代码中,StackQueue 都是基于切片的自定义类型。PushEnqueue 方法用于添加元素,PopDequeue 方法用于移除元素,IsEmpty 方法用于检查栈或队列是否为空。

脏读、不可重复读、幻读 区别
#

  1. 脏读(Dirty Read):是指一个事务读取了另一个事务未提交的数据,导致读取到了脏数据。脏读破坏了数据的一致性和完整性,因此需要使用事务来避免脏读问题。
  2. 不可重复读(Non-repeatable Read):是指一个事务执行了两次相同的查询操作,但在两次查询之间,另一个事务修改了满足该查询条件的数据,导致两次查询返回的结果不一致。不可重复读破坏了数据的一致性和完整性,因此需要使用锁机制或MVCC机制来避免不可重复读问题。
  3. 幻读(Phantom Read):是指一个事务执行了两次相同的查询操作,但在两次查询之间,另一个事务插入了新的数据或删除了已有的数据,导致两次查询返回的结果不一致。幻读破坏了数据的一致性和完整性,因此需要使用锁机制或MVCC机制来避免幻读问题。

带应届生,怎么带,有什么工程经验可以分享
#

如果我要带一个应届生,我会考虑以下几个方面:

  1. 帮助应届生了解公司文化和业务:首先,我会让应届生了解公司的文化、价值观和业务方向,以便他们更好地融入公司,并理解公司的业务需求。我会向他们介绍公司的产品、服务、客户和竞争对手等方面的知识,并帮助他们了解公司的组织结构和团队文化。

  2. 提供实践机会和培训计划:其次,我会为应届生提供充足的实践机会,让他们能够参与到真正的项目中,亲身体验公司的业务流程和工作方法。同时,我会制定一份详细的培训计划,帮助他们提升技术能力和解决问题的能力。

  3. 提供反馈和指导:在应届生的工作过程中,我会定期与他们进行沟通,并提供反馈和指导。我会给他们提供具体的问题解决方案,帮助他们理解问题的本质和解决方法。同时,我也会鼓励他们提出问题和意见,以便更好地改进工作流程和团队合作。

  4. 分享工程经验:最后,我会分享我自己的工程经验,包括技术选型、工作流程、时间管理等方面的经验。我会鼓励应届生参与到技术讨论中,让他们能够了解最新的技术趋势和行业动态,提高自己的技术视野。

在我自己的工程经验中,我认为以下几个方面是比较重要的:

  1. 技术选型的重要性:在一个项目中,选择适合的技术栈是非常重要的。技术选型的好坏会直接影响项目的成功与否。因此,我们需要在选择技术时,考虑项目的需求、团队的技术水平、技术的成熟度和可维护性等方面。

  2. 代码质量的重要性:在项目开发过程中,保证代码质量是非常重要的。良好的代码质量可以提高代码的可读性和可维护性,减少后期维护成本。因此,在项目开发过程中,我们需要注重代码风格的统一性、编码规范的遵守、重构的及时性等方面。

  3. 团队协作的重要性:在一个项目中,团队协作是非常重要的。良好的团队协作可以提高项目的效率、减少沟通成本,同时也可以提高团队成员的工作积极性和满意度。因此,在项目开发过程中,我们需要注重团队协作的组织和管理,建立有效的沟通机制和协作流程。

  4. 持续学习的重要性:在技术领域,持续学习是非常重要的。技术的发展非常快速,我们需要不断学习新的知识和技能,以适应不断变化的市场需求。因此,在工作中,我们需要注重自我学习和提升,参加技术培训和交流活动,不断提高自己的技术水平和工作能力。

Redis 缓存淘汰有哪些
#

Redis缓存淘汰指的是在Redis缓存中当缓存空间已满或者快要满时,根据一定的策略将一些缓存数据删除的过程。以下是Redis支持的缓存淘汰策略:

  1. LRU(Least Recently Used):最近最少使用策略,根据数据的访问时间,删除最近最少使用的数据。

  2. LFU(Least Frequently Used):最近最不经常使用策略,根据数据的访问频率,删除最不经常使用的数据。

  3. TTL(Time To Live):过期时间策略,根据数据的过期时间,删除已经过期的数据。

  4. Random(随机策略):随机删除一些数据。

除了上述策略外,Redis还提供了一些其他的淘汰策略:

  1. Maxmemory-policy noeviction:不删除策略,当内存空间已满时,缓存写入操作会直接返回失败。

  2. Maxmemory-policy allkeys-lru:所有键按照LRU策略淘汰。

  3. Maxmemory-policy allkeys-random:所有键按照随机策略淘汰。

需要注意的是,这些淘汰策略并不是绝对的,当Redis内存不足时,Redis可能会根据自己的算法调整淘汰策略来适应当前的内存情况。因此,在使用Redis时,需要根据实际情况选择合适的淘汰策略,并根据业务需求设置合理的缓存过期时间和内存限制,以保证系统的稳定性和性能。

⾼并发与性能的关系
#

高并发与性能密切相关,高并发环境下的性能问题往往是开发人员需要面对和解决的挑战之一。

并发是指多个用户或进程同时访问同一个系统或资源的情况,而高并发则是指同时访问的用户或进程数量非常大。在高并发环境下,如果系统的性能不能满足用户的需求,就会出现响应时间过长、系统崩溃等问题,严重影响用户体验和系统稳定性。

为了提高系统的性能以应对高并发的挑战,需要采取一系列措施,如:

  1. 优化算法和数据结构:通过优化算法和数据结构,提高代码的执行效率,减少系统的负载,从而提高系统的性能。

  2. 增加系统资源:增加系统的硬件资源,如CPU、内存、磁盘等,以提高系统的处理能力和响应速度。

  3. 使用缓存技术:使用缓存技术,将经常访问的数据缓存到内存中,减少对数据库的访问,从而提高系统的性能。

  4. 采用分布式架构:采用分布式架构,将系统拆分成多个子系统,分散负载,提高系统的并发能力和可扩展性。

  5. 使用异步编程:使用异步编程技术,如多线程、协程、异步IO等,提高系统的并发处理能力,减少等待时间,从而提高系统的性能。

需要注意的是,高并发环境下的性能问题是一个复杂的系统工程,需要综合考虑多个因素,如软硬件设备、网络带宽、负载均衡、缓存策略、数据库设计等,从而实现最优的性能优化方案。同时,我们还需要进行系统性能测试和监控,及时发现和解决问题,保证系统的稳定性和可用性。

缓存和数据库⼀致性如何保证的
#

缓存和数据库一致性是一个非常重要的问题,因为缓存中的数据可能会与数据库中的数据不一致,导致系统出现错误。为了保证缓存和数据库一致性,可以采用以下几种方案:

1.读写缓存时进行锁定:在读写缓存时,可以使用锁机制来保证缓存和数据库的一致性。例如,可以使用读写锁或互斥锁来控制对缓存的读写,以保证缓存和数据库的数据一致。

2.更新缓存和数据库时采用事务:在更新缓存和数据库时,可以使用事务来保证数据的一致性。例如,可以使用数据库事务将缓存和数据库的更新操作放在一个事务中,以保证缓存和数据库的数据一致。

3.使用缓存失效机制:当数据库中的数据发生变化时,可以使用缓存失效机制使得缓存中的数据失效,下次请求时从数据库中读取最新的数据。例如,可以使用Redis的发布订阅机制来实现缓存的失效通知。

4.使用写回策略:在更新数据时,不直接更新数据库,而是先将数据写入缓存,并在一定时间内将缓存中的数据批量写回数据库。例如,可以使用Redis的持久化功能将缓存中的数据写回数据库。

需要注意的是,不同的方案适用于不同的场景,选择合适的方案需要综合考虑多个因素,如系统的性能、可靠性、一致性需求等。同时,我们还需要进行系统性能测试和监控,及时发现和解决问题,保证系统的稳定性和可用性。

项⽬架构中如何做技术选型
#

在项目架构中进行技术选型是一个非常重要的决策,选择合适的技术可以大大影响项目的成功与否。以下是一些可能有用的步骤和建议:

  1. 定义需求和目标:在进行技术选型之前,需要明确项目的需求和目标,包括功能需求、性能需求、可扩展性需求、安全需求等。这些需求和目标将有助于确定选择合适的技术。

  2. 进行市场调研:在技术选型之前,需要进行市场调研,了解当前市场上的主流技术和工具,以及各种技术的优缺点。这些信息将有助于快速评估各种技术的适用性。

  3. 进行技术评估和比较:在确定候选技术后,需要进行技术评估和比较,以确定哪个技术最适合项目。评估和比较的指标包括性能、可扩展性、易用性、可维护性、安全性等。

  4. 进行原型开发和测试:在确定选型后,需要进行原型开发和测试,以验证选择的技术是否满足项目的需求和目标。原型开发和测试还有助于发现技术选型中可能存在的问题和风险。

  5. 考虑团队技能和经验:在选择技术时,还需要考虑团队成员的技能和经验,以确保项目的成功。如果团队成员对某种技术不熟悉,可能需要进行培训或寻找外部专家的帮助。

最后,技术选型是一个持续的过程,需要不断评估和优化。在项目的不同阶段,可能需要重新评估技术选项,以确保项目始终采用最优的技术。

Interview
#

MySQL 隔离级别
#

有以下四种隔离级别:

  1. 读未提交(Read Uncommitted):一个事务可以读取另一个事务未提交的数据,导致脏读、不可重复读和幻读。
  2. 读已提交(Read Committed):一个事务只能读取另一个事务已提交的数据,可以避免脏读,但是可能出现不可重复读和幻读。
  3. 可重复读(Repeatable Read):一个事务只能读取另一个事务已提交的数据,同时,一个事务在执行过程中多次读取同一数据,返回的结果是一致的,可以避免脏读和不可重复读,但是可能出现幻读。
  4. 串行化(Serializable):一个事务只能读取另一个事务已提交的数据,同时,一个事务在执行过程中多次读取同一数据,返回的结果是一致的,可以避免脏读、不可重复读和幻读,但是会导致大量的超时和锁竞争。

MySQL 锁
#

MySQL 中的锁主要有以下几种:

  1. 全局锁:全局锁对整个数据库实例进行加锁,通常用于对整个数据库进行备份或者升级操作。全局锁的开销大,影响性能。

  2. 表锁:表锁是 MySQL 中最基本的锁策略,是针对整个表进行加锁。表锁分为读锁和写锁,读锁之间不互斥,读锁和写锁、写锁和写锁之间互斥。

  3. 行锁:行锁是最细粒度的锁,它锁定的是数据表中的行。行锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。

  4. 间隙锁:间隙锁是在 InnoDB 存储引擎中为了防止幻读而提出的一种锁机制。间隙锁锁定的是一个范围,但不包括记录本身。

  5. 意向锁:意向锁是一种表级锁,用于告知其他事务,一个事务将要在表中的行上加共享锁或者排他锁。这是为了在请求行锁之前,先在表上设置对应的意向锁,然后再在具体的行上加行锁。

  6. 记录锁:记录锁是一种行锁,可以锁定数据表中的一行记录。

  7. Next-Key Lock:Next-Key Lock 是 InnoDB 存储引擎特有的,它是行锁和间隙锁的结合,不仅锁定一个行记录,而且也锁定了记录之间的间隙。

  8. 自增锁:自增锁是 InnoDB 存储引擎中自增列的一种特殊的表级锁,每次插入数据,InnoDB 存储引擎会自动给表加自增锁,插入完成后释放自增锁。

MySQL 存储结构(b+树)
#

MySQL 的主要存储引擎 InnoDB 使用 B+ 树作为其索引和数据的存储结构。

B+ 树是一种自平衡的树,可以保持数据有序。这种数据结构可以保持数据行的存储顺序,并允许高效的插入、删除和查找操作。

B+ 树与 B 树的主要区别在于,B+ 树的所有值都存在叶子节点(也就是说,内部节点不存储数据),并且叶子节点之间通过指针相连,这样可以提供更高效的范围查询。

在 InnoDB 中,表是根据主键顺序存储在 B+ 树中的,每个叶子节点都包含了一行数据和一个指向下一行的指针。如果表有多个索引,那么每个索引都会有一个自己的 B+ 树,其中叶子节点包含了索引字段和一个指向对应行的指针。

这种存储结构使得 InnoDB 能够在处理大量数据时,提供良好的性能和可扩展性。

索引 回表 是什么
#

在 MySQL 中,“索引回表"是指在通过索引查找数据后,再通过索引中的数据回到主表中获取需要的数据的过程。

kafka 如何保证可靠性(生产者可靠性、消费者可靠性、存储可靠性)
#

Kafka 提供了一系列的机制来保证生产者、消费者和存储的可靠性:

  1. 生产者可靠性:Kafka 生产者在发送消息时,可以选择是否等待服务器的确认。这是通过设置 acks 参数来实现的。如果 acks=0,生产者不会等待服务器的确认,这种情况下可能会丢失数据。如果 acks=1,只要至少有一个副本收到消息,生产者就会得到服务器的确认。如果 acks=all,所有副本都需要确认收到消息,这种情况下可以提供最高的数据可靠性。

  2. 消费者可靠性:Kafka 消费者从服务器读取数据后,需要向服务器发送确认信息,表示已经成功处理了这些数据。这是通过提交偏移量(offset)来实现的。如果消费者处理数据出现问题,可以重新处理这些数据。此外,Kafka 还支持消费者组,可以实现消费者的负载均衡和故障转移。

  3. 存储可靠性:Kafka 通过副本机制来保证存储的可靠性。每个主题可以配置多个副本,副本之间可以进行数据同步。如果某个副本失败,其他副本可以接管其工作。此外,Kafka 还支持数据压缩和持久化,可以有效地提高存储效率和保证数据安全。

// 生产者设置示例
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all"); // 设置 acks 参数
Producer<String, String> producer = new KafkaProducer<>(props);

// 消费者设置示例
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test"); // 设置消费者组
Consumer<String, String> consumer = new KafkaConsumer<>(props);

线上是如何分表分库的,用什么做分表分库的策略,跨表查询
#

在大型系统中,为了处理大量的数据和高并发的请求,通常需要进行数据库的分库分表。分库分表可以提高系统的扩展性和性能。

分库分库的策略通常有以下几种:

  1. 垂直分库分表:根据业务模块的不同,将不同的表放在不同的数据库中。这种方式简单直观,但是可能会导致某些数据库过于繁忙,而其他数据库相对空闲。

  2. 水平分库分表:根据表中数据的键值(例如用户 ID、时间戳等)将数据分散到不同的数据库或表中。这种方式可以有效地分散数据库的负载,但是实现起来相对复杂。

  3. 基于范围的分库分表:例如,用户 ID 1-1000 在数据库 A,用户 ID 1001-2000 在数据库 B。

  4. 基于哈希的分库分表:例如,根据用户 ID 的哈希值决定存储在哪个数据库。

跨表查询(Join 操作)在分库分表后会变得复杂,因为数据可能分布在不同的数据库或表中。一种解决方案是在应用层进行 Join 操作,即分别从各个表中查询数据,然后在应用层将数据合并。另一种解决方案是使用支持分布式 Join 操作的数据库中间件。

在实际应用中,可能会根据具体的业务需求和数据特性,使用一种或多种策略。例如,阿里巴巴的分库分表中间件 ShardingSphere,就支持多种分库分表策略,并且可以处理分布式事务和跨库 Join 操作。

线上 Redis 用的是什么模式
#

线上环境中,Redis 可以使用多种模式,具体取决于应用的需求和环境。以下是一些常见的模式:

  1. 单实例模式:这是最简单的模式,只有一个 Redis 服务器提供服务。

  2. 主从复制模式:这种模式下,有一个 Redis 服务器作为主服务器,其他的服务器作为从服务器。所有的写操作都在主服务器上进行,读操作可以在任何一个从服务器上进行。

  3. 哨兵模式:哨兵模式是主从复制模式的升级版,它可以自动处理主服务器宕机的情况。当主服务器宕机时,哨兵会自动从从服务器中选举出一个新的主服务器。

  4. 集群模式:Redis 集群是多个 Redis 节点的集合,数据会被分片存储在这些节点上。Redis 集群可以提供高可用性和高性能。

具体使用哪种模式,需要根据应用的读写负载、数据一致性要求、可用性要求等因素来决定。

缓存热 key 怎么办
#

处理 Redis 缓存中的热键(被频繁访问的键)可以有以下几种策略:

  1. 分片:将热键的数据分散到多个 Redis 实例中,以减少单个实例的负载。

  2. 复制:创建热键的多个副本,并将读取请求分散到这些副本中。

  3. 缓存穿透:如果热键的数据经常变化,可以考虑使用缓存穿透,即直接从数据库中读取数据,而不是从缓存中读取。

  4. LRU 缓存:如果内存不足,可以使用 LRU(最近最少使用)策略来删除不常用的键,以留出空间给热键。

  5. 使用第三方工具:例如 Redisson,它提供了 RMapCache 对象,可以设置最大容量和过期时间,当达到最大容量时,会根据 LRU 策略移除最少使用的元素。

  6. 优化数据结构:根据具体的业务需求和数据特性,选择合适的数据结构,例如使用哈希表来存储大量的键值对。

以上策略的选择和实施需要根据具体的业务需求和系统环境来决定。

defer 、go 继承,手写快排
#

在 Go 语言中,defer 是一种用于延迟执行函数或方法的机制,通常用于清理资源或解锁等操作。defer 语句会在包含它的函数结束时执行。

Go 语言没有类似 Java 或 C++ 的继承机制,但是它有接口(interface)和组合(composition)机制,可以实现类似的功能。

快速排序(QuickSort)是一种常用的排序算法,基于分治的思想。

以下是使用 Go 语言实现快速排序的代码:

package main

import "fmt"

func quickSort(arr []int, low, high int) {
    if low < high {
        pi := partition(arr, low, high)
        quickSort(arr, low, pi-1)
        quickSort(arr, pi+1, high)
    }
}

func partition(arr []int, low, high int) int {
    pivot := arr[high]
    i := low - 1
    for j := low; j <= high-1; j++ {
        if arr[j] < pivot {
            i++
            arr[i], arr[j] = arr[j], arr[i]
        }
    }
    arr[i+1], arr[high] = arr[high], arr[i+1]
    return i + 1
}

func main() {
    arr := []int{10, 7, 8, 9, 1, 5}
    n := len(arr)
    quickSort(arr, 0, n-1)
    fmt.Println("Sorted array: ", arr)
}

登录流程,JWT、session、cookie
#

登录流程通常涉及到以下几个步骤:

  1. 用户在前端输入用户名和密码,然后发送到后端服务器。
  2. 后端服务器验证用户名和密码的正确性。如果验证通过,服务器会生成一个标识用户身份的凭证(如 JWT、Session ID)。
  3. 服务器将这个凭证发送回前端,前端将其存储起来(通常存储在 Cookie 中)。
  4. 之后,每次前端发送请求时,都会在请求中包含这个凭证,服务器通过验证这个凭证来确认用户的身份。

其中,JWT、Session 和 Cookie 是实现这个流程的常用技术:

  • JWT(JSON Web Token):是一种自包含的 token,可以包含用户的身份信息。服务器生成 JWT 后发送给前端,前端每次请求时将 JWT 发送给服务器,服务器通过验证 JWT 来确认用户身份。

  • Session:是服务器用来跟踪用户状态的一种技术。服务器为每个用户生成一个唯一的 Session ID,并将其发送给前端。前端每次请求时都会发送这个 Session ID,服务器通过 Session ID 来获取用户的状态信息。

  • Cookie:是一种存储在用户浏览器上的小型数据文件,可以用来保存用户的登录状态。服务器可以设置 Cookie,浏览器在每次请求同一服务器时都会自动发送这个 Cookie。

这三种技术可以组合使用,例如,服务器可以生成一个 JWT,然后将其存储在 Session 中,然后将 Session ID 存储在 Cookie 中,这样就可以在保证安全性的同时,实现用户的自动登录。

缓存一致性,Redis key 统计,Redis 单线程,io 多路复用
#

Redis 是一个单线程的内存数据库,但是它可以通过 I/O 多路复用技术来处理并发连接。

  1. Redis 单线程:Redis 的操作都是单线程的,这意味着在任何时刻,Redis 都只会使用一个 CPU 核心。Redis 的单线程设计使得开发者无需担心常见的多线程编程问题,如竞态条件、死锁等。

  2. I/O 多路复用:尽管 Redis 是单线程的,但是它可以通过 I/O 多路复用技术来同时处理多个网络连接。I/O 多路复用是一种可以让单个线程监视多个 I/O 描述符(如 socket)的技术。当某个 I/O 描述符就绪(如有数据可读),则通知程序进行相应的读写操作。

关于 Redis 缓存一致性,Redis 本身并不保证与数据库的一致性。在使用 Redis 作为缓存时,需要开发者自己处理缓存和数据库的一致性问题。常见的策略有:缓存穿透、缓存击穿、缓存雪崩等。

Key统计,Redis 提供了 INFO 命令,可以获取 Redis 服务器的各种信息和统计,包括 key 的数量。例如,INFO keyspace 命令可以获取数据库的统计信息,包括 key 的数量。

127.0.0.1:6379> INFO keyspace
# Keyspace
db0:keys=10,expires=0,avg_ttl=0

数据库 0 中有 10 个 key,没有设置过期时间的 key,平均 TTL(Time To Live)为 0。

Redis slowlog 原理
#

Redis 的 Slowlog 是一种用于记录执行时间较长的命令的日志系统。当一个命令的执行时间超过了设定的阈值,这个命令就会被记录到 Slowlog 中。

Slowlog 的工作原理如下:

  1. 当一个命令开始执行时,Redis 会记录下当前的时间。
  2. 当命令执行完毕,Redis 会再次获取当前的时间,并计算出命令的执行时间。
  3. 如果命令的执行时间超过了设定的阈值,Redis 会将这个命令及其执行时间、客户端信息等记录到 Slowlog 中。

Slowlog 的相关配置参数包括:

  • slowlog-log-slower-than:设置 Slowlog 的时间阈值,单位是微秒。只有执行时间超过这个阈值的命令才会被记录到 Slowlog。如果设置为 -1,表示关闭 Slowlog;如果设置为 0,表示所有命令都会被记录到 Slowlog。
  • slowlog-max-len:设置 Slowlog 的最大长度。当 Slowlog 的长度超过这个值时,最旧的记录会被删除。

可以通过 SLOWLOG GET 命令获取 Slowlog,通过 SLOWLOG RESET 命令清空 Slowlog。

需要注意的是,Slowlog 是在 Redis 服务器内存中维护的,不会持久化到磁盘,也不会在主从复制时传播。因此,每个 Redis 服务器都有自己独立的 Slowlog。

go 协程机制
#

Go 语言中的协程(goroutine)是一种轻量级的线程,由 Go 运行时管理。协程相比于传统的系统线程和进程,其创建和销毁的开销更小,内存占用更少,更适合在 I/O 密集型任务中使用。

以下是 Go 协程的一些主要特性:

  1. 轻量级:Go 协程的创建和销毁的开销非常小,内存占用也比线程小得多。一个 Go 程序可以同时运行数以万计的协程。

  2. 简单的并发模型:Go 语言提供了 go 关键字来创建协程,使得并发编程变得非常简单。同时,Go 语言的 channel 提供了一种在协程之间进行通信的机制,避免了复杂的锁操作。

  3. 由 Go 运行时管理:Go 协程的调度是由 Go 运行时进行的,而不是由操作系统内核进行。Go 运行时可以在用户态进行协程的调度,无需系统调用,因此效率更高。