知识复习汇总
知识复习汇总
项目相关
ReviewCloud 项目
- 谈谈
ReviewCloud这个项目你的主要工作是什么,难点在哪?- 参考开源项目,对服务架构设计与接口统一:基于 Kratos + Protobuf 定义 gRPC 与 HTTP 接口规范,保证 C/B/O 三端服务接口一致;
- 微服务注册与治理:引入 Consul 作为服务注册发现中心;
- 数据异步化与扩展能力建设:通过 Canal + Kafka 实现 MySQL Binlog 日志订阅;
- 流式数据处理模块(review-job):开发独立的流式处理服务
review-job,从 Kafka 消费数据库变更事件,实现评价数据的实时清洗与结构化处理,并同步到 Elasticsearch 以支持多维查询。 - 性能优化与缓存体系建设:使用 Redis 作为 Elasticsearch 查询缓存层,引入 singleflight 机制抑制并发请求的缓存击穿,使用 JMeter 压测 验证优化效果,QPS 提升约 18.6%。
- 难点:
- MySQL 与 Kafka 的一致性:通过 Canal 订阅 Binlog 实现异步一致性,保证消息可靠投递与下游实时性。
- 流式任务消费与 Elasticsearch 同步延迟:设计异步管道并引入分区并发消费机制,使延迟控制在百毫秒级。
- 高并发下缓存击穿问题:使用 singleflight 合并同 key 并发请求,减少 Redis 缓存重建压力。
ES前面的Redis缓存是怎么存的?- 把
storeID,offset,limit合并设置为缓存的key,返回的数据设置为value。
- 把
1 | |
ES是在哪里分词的?- 分词行为由
Elasticsearch的索引定义(mapping)决定。
- 分词行为由
Canal怎么知道传那个表的信息?- 在配置项中通过正则匹配的方式来进行配置。
gRPC和HTTP的区别?gRPC本质上是 Google 开发的一个高性能 RPC 框架,它在 HTTP/2 之上定义了自己的协议层,使用Protobuf来定义接口与序列化格式。- 而
HTTP则是基于URL的资源访问模型,传输格式通常是JSON。
- 序列化和非序列化的区别是什么?
- 程序运行时的数据是存放在内存里的(例如 Go 的 struct、Java 的对象、Python 的字典),它们的内存结构和不同语言的运行时相关,不能直接通过网络发送给另一个系统。
- 所以我们要序列化成中立的格式,比如 JSON / Protobuf / 二进制流,再发送给对方。
protobuf和JSON的序列化区别?JSON的序列化结果就是一段文本字符串,本质是把内存中的数据结构(如结构体、对象、数组)转换成一段可读的字符串格式,方便传输和存储。protobuf本质是把内存中的数据结构(如结构体、对象、数组)按照.proto文件中定义的字段编号和类型,转换成一种紧凑的二进制格式,以便在网络中高效传输或存储。
Kafka怎么保证不丢数据?- 生产端:幂等 + acks=all + 重试
- Broker:多副本 + 顺序写 + ISR 机制
- 消费端:offset 管理 + 事务
- 项目中的
Kafka有几个Topic和几个Partition?- 目前项目中只有一个
Topic和这个Topic下只有一个Partition来保证消息的有序性。
- 目前项目中只有一个
- 雪花算法的机制是怎么样的?
- “雪花算法(Snowflake Algorithm)” 是一种在 分布式系统中生成全局唯一 ID 的算法。它最早由 Twitter 提出,用来解决分布式环境下「高并发下生成唯一 ID」的问题。
- 雪花算法使用一个 64 位的 long 型整数 来表示一个唯一 ID,各部分含义如下:
| 1 位符号位 | 41 位时间戳 | 10 位机器标识 | 12 位序列号 | - 序列号每毫秒可生成 4096 个不同 ID(0~4095)
- 雪花算法相较于传统自增
id的区别在哪?- 雪花算法每台机器独立生成,带上时间戳 + 机器 ID,支持在分布式系统中既能保证唯一,又能支持按时间排序(日志、消息、订单等场景)。
- go 循环包依赖的引用问题怎么解决的?
- 解决思路是让依赖单向化:抽公共部分到第三方包
Links 短链接项目
- 谈谈
Links这个项目你的主要工作是什么,难点在哪?- 开发用户鉴权模块:基于 JWT(JSON Web Token) 实现对调用转链服务的用户身份认证,引入 Access Token + Refresh Token 双令牌机制,兼顾安全性与性能,设计令牌续签逻辑,确保用户会话安全有效。
- 设计实现了分布式短链生成模块:基于 MySQL 自增主键 设计发号器,利用
REPLACE+LAST_INSERT_ID()实现高效递增 ID,通过 62 进制编码 生成唯一短链 ID,保证短链生成全局唯一、无重复、无循环转链。 - 短链解析与重定向模块:实现短链到长链的查询和跳转逻辑,使用 HTTP 库中的
http.Redirect(w, r, resp.LongUrl, http.StatusFound)方法实现重定向跳转。 - 缓存与并发优化:针对读多写少的业务场景,引入 Redis 缓存层,使用 布隆过滤器(Bloom Filter) 防止缓存穿透,结合 singleflight 合并相同并发请求,避免缓存击穿。
- 难点:
- 采用
map遍历对比过滤敏感词和保留词等,转链时查询数据库是否包含原始链接,避免重复转链。 - 短链唯一性与分布式发号冲突:使用 MySQL 自增主键结合
REPLACE INTO语义,确保发号全局唯一;生成后转为 62 进制字符串。
- 采用
- go-zero 怎么重零开始实现
api?- 写 API DSL → goctl 生成骨架 → 在 logic 写业务 → 在 svc 注入依赖(DB/缓存/RPC)→ 配置启动 → 自测与加中间件。
数据结构
哈希相关
- 简单介绍一下哈希表?
- 哈希表是一种根据键(Key)直接计算出存储位置的数据结构。能够通过哈希函数 (Hash Function) 把键映射到一个数组的索引上,从而实现快速的增删查改操作。
- 什么是解决哈希冲突?
- 哈希表用哈希函数把键映射到桶(数组槽位)。不同键可能落到同一桶,这就是冲突。冲突不可避免,核心是:怎么在冲突出现时,仍保持增删查改接近 O (1) 的开销。
- 如何解决哈希冲突?
- 1、拉链法:每个桶保存一个“容器”(通常是链表、动态小数组或小型平衡树)。冲突的元素都挂到该桶的容器里。
- 2、线性探测法:在哈希表中,如果某个键经过哈希函数映射得到的槽位(bucket)已被占用,就按照一个固定的探测序列去找下一个空槽。
- 3、二次探测法:发生冲突时,它不像线性探测法一样一步步顺序探测,而是按照二次方步长往外扩展。
- 4、双重散列:当发生冲突时,不是固定加 1(线性探测),也不是平方步长(二次探测),而是用“第二个哈希函数”决定探测步长。
- 如果用散列链表解决哈希冲突,那么查找时间复杂度会变成 ,如何下降时间复杂度?
- 1、控制负载因子:让期望复杂度回到 ,负载因子 α = 元素数/桶数,当 α 逼近设定阈值的时候就扩容➕重新哈希
- 2、优化哈希函数:降低冲突几率
- 3、桶内结构升级:把链表换成平衡树
排序相关
- 稳定排序和不稳定排序
- 如果一个排序算法在对一组数据进行排序时,相等的元素在排序后仍然保持它们原来的相对顺序,那么这种算法就是稳定的。
- 若相等的元素在排序后相对顺序可能发生变化,这种算法就是不稳定的。
- 稳定排序算法

- 不稳定排序算法:

- 快排为什么不稳定?
- 快速排序的主要过程是:
- 选择一个基准(pivot);
- 将数组分为三部分:
- 左边放比基准小的元素;
- 中间放和基准相等的元素;
- 右边放比基准大的元素;
- 递归地对两部分进行排序。
- 快速排序的不稳定性来源于——“交换操作”可能跨越相等元素之间的原始相对顺序。
- 快速排序的主要过程是:
语言语法
Golang
GMP 相关
- 用自己的话讲讲 GMP:
- G:Go 的“协程”,存放执行的函数、栈、状态等;
- M:系统线程(OS Thread),真正被操作系统调度运行;
- P:调度器的逻辑处理器,持有本地 goroutine 队列,并控制 goroutine 的执行;
findRunnableG()的查找顺序是:- 1、当前 P 的本地队列(Local run queue):优先从自己 P 的本地队列里取 G,开销最低。
- 2、全局队列(Global run queue):如果本地队列为空,会从全局队列中取 G。
- 3、Work stealing(从其他 P 窃取):如果全局也为空,会随机选择其他 P,从它的本地队列里偷一半 G 过来,保持负载均衡。
- 4、NetPoll(网络事件轮询):如果前面都没有,就检查网络轮询器(比如 goroutine 在等待 I/O),是否有准备好的 G。
- 为什么有了本地队列,还要有全局队列?
- Go 调度器中每个 P 拥有一个本地运行队列(local runq),用于存放可运行的 goroutine,以减少全局锁竞争、提高并发效率。 但如果只靠本地队列,容易出现任务分布不均的问题,因此 runtime 还维护一个全局队列(global runq),用来接收新建或唤醒的 goroutine,并在调度时为各 P 提供任务补充。 调度器采用“局部优先、全局补充、任务窃取”的策略,在保证性能的同时实现全局负载均衡和公平调度。
- 当一个
goroutine进入系统调用被阻塞,底层线程与p如何解绑?- 在系统调用前,首先该线程
m的g0会保存当前g的执行环境 - 然后将
g和p的状态更新为syscall; - 解除
p和当前m之间的绑定,因为m即将进入系统调用而导致短暂不可用; - 最后将
p添加到当前m的oldP容器当中,后续m恢复后,会优先寻找旧的p重新建立绑定关系。
- 在系统调用前,首先该线程
- 当
m完成了内核态的系统调用之后,如何寻找p开始重新运作?- 方法执行之初,此时的执行权是普通
g倘若此前设置的oldp仍然可用,则重新和oldP绑定,将当前g重新置为_Grunning状态,然后开始执行后续的用户函数; old绑定失败,则调用mcall方法切换到m的g0,并执行exitsyscall0方法;- 将
g由系统调用状态切换为可运行态(_Grunnable),并解绑g和m的关系; - 从全局
p队列获取可用的p,如果获取到了,则执行g; - 如若无
p可用,则将g添加到全局队列,当前m陷入沉睡,直到被唤醒后才会继续发起调度。
- 方法执行之初,此时的执行权是普通
- 讲讲 Go 协程:
- 与线程存在映射关系,为
M:N,M可以大于N; - 创建、销毁、调度在用户态完成,对内核透明,足够轻便;
- 可利用多个线程,实现并行;
- 通过调度器
p的斡旋,实现和线程间的动态绑定和灵活调度; - 栈空间大小可动态扩缩,因地制宜。
- 与线程存在映射关系,为
- 讲讲 GMP 模型中
p的作用,一定需要p吗?p即processor,是golang中协程的调度器;p是gmp的中枢,借由p承上启下,实现g和m之间的动态有机结合;- 对
g而言,p是其cpu,g只有被p调度,才得以执行; - 对
m而言,p是其执行代理,为其提供必要信息的同时(可执行的g、内存分配情况等),并隐藏了繁杂的调度细节; p的数量决定了g最大并行数量,可由用户通过GOMAXPROCS进行设定(超过CPU核数时无意义)。- 在目前的 GMP 模型下,
p是必须的,当去掉p让m从全局队列中直接取g进行执行的话会导致无法高效控制并行度,导致运行效率大幅下降。
- Goroutine 有哪些状态,状态之间具体是怎么切换和调度的?
- goroutine 在 Go runtime 中有六种主要状态:
_Gidle(未初始化)、_Grunnable(可运行)、_Grunning(正在运行)、_Gwaiting(阻塞等待)、_Gsyscall(系统调用中)、_Gdead(已结束)。 - 调度器通过
gopark()和goready()控制状态切换:当 goroutine 因 IO、channel、锁等操作阻塞时,会从_Grunning转为_Gwaiting并挂起; - 当条件满足时转回
_Grunnable并重新入队等待调度。 - Go 使用 GMP 模型实现调度,M(线程)通过绑定 P(逻辑处理器)执行队列中的 G(goroutine),支持 work-stealing 和用户态抢占,从而实现百万级轻量并发的高效调度。
- goroutine 在 Go runtime 中有六种主要状态:
slice 相关
- 说一下
slice的扩容机制- 当需要
append的长度在原来slice的cap容量以内则不需要扩容; - 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量;
- 倘若原容量小于
256,则扩容后新容量为原容量的两倍; - 倘若老容量已经大于等于
256,则在老容量的基础上扩容1/4的比例并且累加上192的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止。
- 当需要
map 相关
- map 的底层是怎么实现的?
- Go 的 map 底层是一个基于哈希表的结构,采用 数组 + 桶(buckets)+ 溢出桶 实现。
- 每个桶最多存 8 个键值对,桶内数据连续存放以提高缓存命中率。
- 桶的查找通过哈希值低位确定位置,高 8 位存在
tophash中快速对比。 - 当负载因子过高或删除碎片多时,会触发渐进式扩容,保证性能平滑。
- map 非并发安全,需加锁或使用
sync.Map。
- 假如在函数内对 map 扩容了,在外面能看到吗?
- 在 Go 里,即使函数内触发了 map 扩容,外部依然能看到扩容后的内容和新增的键值对,因为:
map是引用类型;- 传参时传的是
hmap的引用; - 扩容时只是底层结构发生变化(
buckets指针换了),但这个变化发生在同一个 hmap 对象里。
- 在 Go 里,即使函数内触发了 map 扩容,外部依然能看到扩容后的内容和新增的键值对,因为:
channel 相关
- channel 的内部结构是怎么样的?
- 在 Go 语言中,channel 的底层是一个由锁保护的环形队列结构(
hchan)。 它内部包含缓冲区(buf)、当前元素数量(qcount)、队列容量(dataqsiz)、 发送与接收的索引(sendx、recvx),以及两个等待队列(sendq、recvq)用于存放被阻塞的发送和接收 goroutine。 所有操作都由一个互斥锁(mutex)保护以保证并发安全。 当缓冲区满时发送方阻塞,当缓冲区空时接收方阻塞,通过调度器唤醒实现同步通信。
- 在 Go 语言中,channel 的底层是一个由锁保护的环形队列结构(
- 介绍一下
channelchannel是 go 语言一种内置的同步原语,可以在多个并发执行的goroutine之间通过通信来共享数据,而不需要显式使用锁。channel分为有缓冲管道和无缓冲管道:- 其中无缓冲管道的发送和接收必须同时存在,发送操作会阻塞直到有接收者,适合用于同步通信。
- 而有缓冲管道可以存储一定数量的元素,当缓冲区未满时,发送不会阻塞;当缓冲区为空时,接收会阻塞;更适合异步任务队列。
- 管道是并发安全的,内部使用 锁和环形队列 实现;无需额外的
mutex就能保证安全通信。 - 管道的关闭只能由发送方关闭,接收方可以检测通道是否关闭,但发送方往关闭的管道发送数据会触发
panic。
select是怎么和channel配合的?select是 Go 专门用于同时等待多个 channel 操作的控制结构。它的作用类似于多路复用器,让goroutine能够「同时监听多个channel」,只要其中任意一个可以通信(发送或接收),就立即执行对应的分支。select的行为可以分为三种情况:- 至少有一个
case可执行,随机选择一个执行; - 所有通道都阻塞,阻塞等待直到某个通道可用;
- 有
default且所有通道都阻塞,立即执行default(非阻塞)。
- 至少有一个
- 读写无缓冲 channel 时,生产者和消费者 Goroutine 会发生什么,它们的状态会发生什么样的转换?
- 当读写 无缓冲 channel 时,发送和接收必须同时进行。如果发送方 goroutine 先执行,它会被挂起(状态变为 waiting),直到接收方到来;
- 若接收方先执行,同样会被阻塞等待发送方。 Runtime 在两者匹配时会直接在它们的栈之间复制数据,并通过
gopark()与goready()完成状态切换,使被阻塞的 goroutine 从 waiting 恢复为 runnable。 - 因此,无缓冲 channel 实现了 goroutine 间的同步通信。
Contex 相关
- 介绍一下
contex接口中的四个方法-
Deadline() (deadline time.Time, ok bool)
- 作用:返回
context的截止时间(Deadline),即何时会自动取消。用于限制某个操作的最长执行时间,比如 HTTP 请求或数据库查询。 - 返回值:
deadline: 截止时间;ok: 如果没有设置截止时间,返回false。
-
Done() <-chan struct{}
- 作用:返回一个只读
channel,当context被取消或超时时,该channel会被关闭。监听ctx.Done()的goroutine会在取消信号到来时被唤醒,用于清理、退出等。 - 特性:
- 通常用来检测“是否该退出”;
- 一旦
Done()关闭,表示应该立即结束当前操作。
-
Err() error
- 作用:返回
context结束的原因。Err()常用于在退出前打印或处理取消原因。 - 可能的返回值:
nil:context 仍然有效;context.Canceled:被手动取消;context.DeadlineExceeded:超过截止时间。
-
Value(key any) any
- 作用:在
context中存取键值对数据;传递请求范围内的数据(例如用户ID、请求ID、追踪信息); - 注意事项:
- 不建议滥用 context 存放大量数据;
- 只应用于跨 API、跨
goroutine传递请求级别元信息。
-
interface 相关
interface和interface{}的区别interface{}是可以接受任何类型的接口。Go 1.18 之后,any是interface{}的别名。- 例如:
1 | |
interface是“定义行为规范的接口类型”。- 例如:
1 | |
Gin 框架
- 介绍一下
Gin框架的路由注册Gin框架的路由注册本质是通过内部维护一个压缩前缀路由树来实现快速匹配。- 每个请求方法(如
GET、POST)都有一棵独立的路由树。- 例如:
GET请求 →r.trees["GET"]POST请求 →r.trees["POST"]
- 注册时会将路径分解为节点(node),构建成前缀树,以便快速查找。
- 例如:
- 内部逻辑:
- 拼接路径(加上分组前缀);
- 调用
engine.addRoute(method, path, handlers); - 在对应方法的路由树中插入节点。
- 查找请求时,
Gin按照路径层级匹配节点(包括参数与通配符),找到匹配的handler列表。
内存模型
- 介绍一下 Go 语言的内存模型与分配机制
- 内存分配机制的整个体系由三层组成:
MCache:每个 P(Processor)的本地缓存,访问时无锁,快速分配,用于管理小对象;MCentral:每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内,从8 B到32 KB不等。这样可以减少外部碎片化。MHeap:全局的内存起源,访问要加全局锁。
page:- 是 Go 中最小的内存分配单元是 8 KB(一个 page);
MHeap以页为单位管理内存;- 页可以被划分为不同大小的块(
span)。
Span(内存块组):Span是多个连续页的集合;- 每个
Span负责管理特定大小(size class)的对象; Span内部被切割成相同大小的对象块(object)。
- 内存分配机制的整个体系由三层组成:
GC 相关
- 介绍一下 GC 流程
- Golang 中的垃圾回收采用并发三色标记法+混合写屏障机制。
- 三色标记法的流程:
- 对象分为三种颜色标记:黑、灰、白
- 黑对象代表,对象自身存活,且其指向对象都已标记完成
- 灰对象代表,对象自身存活,但其指向对象还未标记完成
- 白对象代表,对象尙未被标记到,可能是垃圾对象
- 标记开始前,将根对象(全局对象、栈上局部变量等)置黑,将其所指向的对象置灰
- 标记规则是,从灰对象出发,将其所指向的对象都置灰. 所有指向对象都置灰后,当前灰对象置黑
- 标记结束后,白色对象就是不可达的垃圾对象,需要进行清扫
- 混合写屏障机制:
- 插入写屏障(Dijkstra):目标是实现强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障将白色对象置为灰色,再建立引用
- 删除写屏障(Yuasa barrier):目标是实现弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用
- 三色标记法的流程:
- Golang 中的垃圾回收采用并发三色标记法+混合写屏障机制。


sync.Pool
- 介绍一下
sync.Poolsync.Pool是 golang 标准库下并发安全的对象池,适合用于有大量对象资源会存在被反复构造和回收的场景,可缓存资源进行复用,以提高性能并减轻 GC 压力。sync.Pool缓存的是any类型的对象(通常是指针类型),由程序员决定具体类型。sync.Pool结构体的源码:
1 | |
noCopy防拷贝标志;local类型为[P]poolLocal的数组,数组容量 P 为 goroutine 处理器 P 的个数;victim为经过一轮 gc 回收,暂存的上一轮 local;New为用户指定的工厂函数,当 Pool 内存量元素不足时,会调用该函数构造新的元素。
1 | |
poolLocal为 Pool 中对应于某个 P 的缓存数据;poolLocalInternal.private:对应于某个 P 的私有元素,操作时无需加锁;poolLocalInternal.shared: 某个 P 下的共享元素链表,由于各 P 都有可能访问,因此需要加锁.
defer
- 介绍一下
defer以及执行顺序defer主要用在函数或者方法上面,作用是用于函数和方法的延迟调用。defer的执行顺序和栈一样,是先调用,后执行。defer有两个主要的实际用处:- 用于资源回收,根据其延迟调用的机制,可以优雅地处理资源回收问题。
- 可以配合
recover()一起处理panic。
defer和return的处理顺序:- 先设置返回值
- 执行
defer语句 - 将结果返回
- 注意下面这个例子输出是
2:
1 | |
GORM 相关
- 讲讲
gorm框架?gorm框架可以将数据库表和代码中的类(结构体)建立映射。其三大核心思想包括:数据库表和类(结构体)映射、表中列字段和类(结构体)属性映射、对象操作转换为 SQL 语句。
gorm框架为什么要使用反射?gorm框架不能在编译期硬编码针对每个struct的逻辑,反射让它可以在运行期做到“识别任意类型”,从而适配多种不同结构体的不同字段。
gorm对象与字段映射的底层实现?- 创建
schema.Schema对象,保存表与字段的映射,通过结构体反射 + tag 解析实现。reflect.TypeOf(User{})获取结构体类型;- 遍历字段;
- 解析每个字段的 tag(如
gorm:"primaryKey"、gorm:"column:email_address"); - 根据命名策略生成表名和列名;
- 构造
Schema,存入缓存。
- 创建
gorm标签底层怎么处理?- 通过
ParseTagSetting函数处理成map[string]string的形式,tag 中以分号;拆分 tag 内容 - 通过
:来分割key:value:- 如果有
:,如size:100→ key=size, value=100 - 如果没有
:,如primaryKey→ key=primaryKey, value=true
- 如果有
- 通过
gorm每次提取标签都要进行一次反射吗?- GORM 并不会每次都反射提取标签;
- 它只在第一次遇到某个模型类型时用反射解析所有字段和 tag,之后所有 SQL 构造和操作都直接复用缓存的
Schema信息,只在运行时通过轻量级反射访问字段值。
计网相关
HTTP 篇
- HTTPS 比 HTTP 安全在哪?证书在其中起到什么作用,如何验证证书?
- HTTP 是明文传输的,传输的数据未经过加密,而 HTTPS 在 HTTP 基础上引入了 SSL/TLS 协议,提供了三大安全能力(加密、数据完整性、身份验证);
- 证书由权威的 CA(证书颁发机构)签发,里面包含以下关键内容(服务器的公钥、服务器的域名信息、证书有效期、CA 的数字签名)
- 客户端拿到服务器的证书,沿着签发链(服务器证书 → 中级 CA → 根 CA)逐层验证签名是否有效。
- 检查证书中记录的域名是否与访问的网站一致。
- 确认证书是否在有效期内。
- 通过 CRL(证书吊销列表)或 OCSP 协议验证证书是否已被吊销。
- 如果以上都通过,浏览器才会信任这个证书,并继续进行 TLS 握手,协商出对称密钥来加密后续通信。
- 对称密钥和非对称的区别?
- 对称密钥:
- 加密和解密使用同一把密钥。通信双方需要共享一把密钥。
- 假如有 N 个用户相互通信,就需要 把不同的密钥,管理起来比较复杂。
- 加密解密速度快,适合处理大量数据。
- 缺点:如何安全地分发和保存密钥是个难题,如果密钥泄露,通信内容也就不安全了。
- 非对称加密:
- 加密和解密使用一对密钥:公钥 (Public Key) 和私钥 (Private Key)。公钥可以公开,用来加密数据。私钥必须保密,用来解密数据。
- 每个人只需要维护自己的一对密钥(公钥和私钥)。公钥可以对外公开,私钥由自己保管,密钥管理相对简单。
- 加密解密速度慢,性能消耗大,但安全性更高,因为私钥不需要传输。
- 通常用来做:
- 数字签名(保证身份和不可否认性)
- 密钥交换(先用非对称加密交换对称密钥,再用对称密钥加密数据)
- 对称密钥:
- TLS 的四次握手流程
- 1、客户端发起请求:告诉服务器自己支持的 TLS 版本、加密算法套件、随机数 (用于生产后续的会话密钥,也就是对称密钥)。
- 2、服务器回应:选择使用的 TLS 版本和加密算法、发送自己的随机数(也是用于生产后续的会话密钥,也就是对称密钥)、发送服务器证书(包含服务器公钥,用于身份认证)。
- 3、客户端再次发起请求做会话密钥协商:客户端生成一个“预主密钥” (Pre-Master Secret),用服务器的公钥加密后发给服务器。同时发送加密算法改变通知,表示后续信息都用会话密钥来加密。
- 4、服务端通过“预主密钥” (Pre-Master Secret)计算出会话密钥后向客户端发信息:发送加密算法改变的通知,表示后续信息都用会话密钥来加密。
- 什么是粘包,有什么解决方法?在 http 中是怎么解决的?
- 粘包是 TCP 通信中多个消息在接收方黏在一起,导致无法区分消息边界的现象,原因是 TCP 面向字节流,没有消息边界概念,系统可能将多次发送合并或拆分。解决方法是应用层自定义协议边界,比如:
- 固定长度;
- 特殊分隔符;
- 消息头携带长度;
- 或使用现成协议(HTTP、gRPC)。
- 而 HTTP 协议通过 Content-Length 文本长度或 chunked 分块传输 来标识消息边界,因此天然避免了粘包问题。
- 粘包是 TCP 通信中多个消息在接收方黏在一起,导致无法区分消息边界的现象,原因是 TCP 面向字节流,没有消息边界概念,系统可能将多次发送合并或拆分。解决方法是应用层自定义协议边界,比如:
- post 和 get 的区别
- GET 用于从服务器获取资源,参数放在 URL,通常可被缓存、可书签、幂等、安全;
- POST 用于向服务器提交数据,参数放在请求体中,不可缓存、不幂等,常用于创建或修改资源。
- 语义上 GET 是“安全读取”,POST 是“有副作用的提交”。
- 在 HTTP 中,它们的区别体现在语义、缓存策略、参数传递、安全性等多个方面。
- HTTP 的两种缓存机制是什么?
- 强制缓存(强缓存):浏览器在缓存未过期时直接使用本地副本,不与服务器通信;通过响应头
Expires或Cache-Control: max-age控制。
- 协商缓存(对比缓存):当强缓存失效后,浏览器带上
If-Modified-Since或If-None-Match与服务器确认资源是否更新;若未修改返回 304,继续使用缓存。
- 强缓存节省请求次数,协商缓存节省数据传输,两者结合使用可最大化性能与一致性。
- 强制缓存(强缓存):浏览器在缓存未过期时直接使用本地副本,不与服务器通信;通过响应头
- HTTPS 解决了 HTTP 的什么问题?
- HTTPS 提供了数据加密、身份认证、完整性保护三大安全能力,使网络通信安全可靠。
- websocket 的建立过程
- 1️⃣ 客户端通过 HTTP 发起 Upgrade 请求,携带
Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key等头; - 2️⃣ 服务器返回
101 Switching Protocols,并校验生成Sec-WebSocket-Accept; - 3️⃣ 双方协议升级,建立基于 TCP 的持久全双工连接;
- 4️⃣ 后续通信采用 WebSocket 帧结构进行消息双向传输(文本、二进制、心跳等)。
- 1️⃣ 客户端通过 HTTP 发起 Upgrade 请求,携带
TCP 相关
- TCP 三次握手状态转移图:

- TCP 四次挥手状态转移图:

- UDP 和 TCP 假如同时发一个 100 M 带宽的网络最终是一个什么情况?
- UDP 没有拥塞控制,会尽力占用全部 100 M;
- TCP 会检测到丢包率升高,RTT 延长,于是快速减小发送窗口;
- 结果:
- UDP 流量几乎占满整个链路;
- TCP 速率可能下降到非常低(几 kbps ~ 几 Mbps 不等)。
DNS 相关
- 在浏览器输入栏中输入
baidu.com发生了什么?- 为了找到
baidu.com对应的服务器 IP 地址,浏览器会按层次查找缓存:- 浏览器 DNS 缓存
- 若近期访问过,会直接命中缓存。
- 操作系统缓存(OS Cache)
- 否则,向操作系统查询本地缓存(例如 Windows 的
DNS Client Service)。
- 否则,向操作系统查询本地缓存(例如 Windows 的
- 本地 hosts 文件
- 检查系统文件
/etc/hosts(Linux/Mac)或C:\Windows\System32\drivers\etc\hosts是否有静态映射。
- 检查系统文件
- 本地 DNS 服务器(ISP 或路由器)
- 若都没有命中,系统会向配置的 DNS 服务器(例如电信、Google DNS 8.8.8.8)发起 DNS 查询请求。
- 浏览器 DNS 缓存
- 为了找到
- DNS 什么时候用递归查询,什么时候用迭代(反复)查询?
- 递归查询(客户端 → 本地 DNS 服务器),意思是:“帮我查出
baidu.com的 IP 地址,不要让我再管后面的细节。” - 迭代查询(本地 DNS → 其他上级 DNS 服务器),这些过程中,每一层上级 DNS 都不会代为继续查询,只告诉下一级地址。
- 递归查询(客户端 → 本地 DNS 服务器),意思是:“帮我查出
- DNS 根服务器会不会同时接收到大量并发查询请求?
- 是的:根服务器会接收到大量并发查询
- 为什么根服务器能扛住如此高并发?
- 分布式部署(Anycast 技术),全球共有 超过 1000+ 台根服务器实例(通过 Anycast 技术部署在各地)。
- 高缓存利用率,实际上,绝大多数 DNS 查询 根本不会真的到达根服务器,本地 DNS 服务器(ISP 或公司内部 DNS)会缓存查询结果;
.com、.net等 顶级域名服务器 也会被缓存;根域的权威信息(比如顶级域的 NS 记录)变化极少,TTL 时间长(通常为 48 小时甚至更长)。 - 根区数据非常小且易处理;
- 百度是如何处理大量对
baidu.com的并发请求的?-
- DNS 多层调度 + 全球加速:当你访问
baidu.com,其实并不止一个 IP。同一个域名对应多个服务器 IP;根据用户地理位置,返回距离最近的服务器地址;DNS 还会考虑各节点的负载情况、延迟、可用性。
- DNS 多层调度 + 全球加速:当你访问
-
- CDN(内容分发网络)缓存静态资源:静态资源(图片、CSS、JS、搜索页面模板等)被分发到全国各地的边缘节点。页面中的图片、脚本等资源就近从 CDN 节点获取;只有动态内容(如搜索结果)才需回源请求百度主站。
-
- 分布式缓存与异步队列、高可扩展的搜索索引集群、健壮的监控与容灾体系等方式应对大量并发请求。
-
加密相关
MD5是可逆的还是不可逆的?MD5是不可逆的,它是一种 单向哈希函数,可以轻松计算出某个输入的MD5值,但无法从一个MD5值反推出原始输入。
操作系统相关
- 什么是死锁,怎么解决?
- 当多个线程(或事务)互相持有对方需要的资源且都不释放,从而形成循环等待的状态,就叫“死锁”。
- 固定加锁顺序、尽量缩短锁持有时间、使用“尝试锁”/超时机制等
- 线程、进程、协程的区别,各自的优劣,各自是怎么创建的,底层是怎么样的数据结构?
- 进程:是操作系统资源分配的最小单位。拥有独立地址空间(代码段、堆、栈、文件描述符等)。不同进程之间内存隔离,通信需要 IPC(管道、共享内存、消息队列等)。在 Linux 下用
fork()(复制父进程); - 线程:是操作系统调度的最小执行单位。属于某个进程,与同进程内的线程共享内存与资源。多线程可并行(在多核 CPU 上),但共享资源容易引发 竞争、死锁。C 语言使用
pthread_create()来创建。 - 协程:是用户态的“轻量级线程”,由程序自己调度,不需要内核调度,也无需上下文切换到内核态,上下文切换成本更低。
- 进程:是操作系统资源分配的最小单位。拥有独立地址空间(代码段、堆、栈、文件描述符等)。不同进程之间内存隔离,通信需要 IPC(管道、共享内存、消息队列等)。在 Linux 下用
- 协程的提出是为了解决什么问题,为什么说它轻量级?
- 在不增加线程(或系统开销)的前提下,让程序能同时处理大量 I/O 并发任务,当一个协程等待 I/O 时,它“主动让出”执行权,CPU 去执行其他协程,I/O 完成后再恢复该协程。
- 从底层的角度来看,程序是如何读取一个变量的值的?如果是局部变量/全局变量呢?如果是值类型/引用类型呢?、
- 程序访问变量的本质是通过内存地址读取数据:
- 局部变量存放在栈上,通过栈帧基址偏移寻址;
- 全局变量存放在数据段,通过固定地址或符号表访问;
- 值类型变量直接存放数据,访问时一次寻址;
- 引用类型变量存放指针,访问时先取指针再解引用(两次寻址)。
- 在执行层面,CPU 通过寄存器 + 内存寻址方式完成变量读取,编译器可能将频繁访问的变量直接放入寄存器优化访问速度。
MySQL
- MySQL 常用的存储引擎
- innodb、MyISAM
- MySQL 中使用
limit导致慢查询的原因是什么?如何优化?- 原因
- 大偏移量(LIMIT offset, size):MySQL 在取
LIMIT 100000, 20时,通常会扫描并丢弃前 100000 行,再返回 20 行。即便这些行不返回,也要先找出来再跳过,代价高。 - 无法利用索引顺序(
ORDER BY与索引不匹配):ORDER BY的列没有合适索引时需要 filesort/临时表,先把候选行排序,再截取LIMIT,量一大就慢。 - 回表成本高(InnoDB): 即使能用到二级索引,但
SELECT *会为每一行再去回表拿主键以外的列。行很多时随机 I/O 多、CPU 也高。 - 过滤条件与排序列不协同:
WHERE过滤用到了一个索引,ORDER BY用到了另一个,二者不能同时利用同一复合索引 → 要么扫太多行,要么排序代价大。 - 深翻页需求本身不合理:真要跳到很深的页(例如第 5000 页),即使优化到极致,也会有“跳过大量记录”的本质成本。
- 大偏移量(LIMIT offset, size):MySQL 在取
- 解决方案:
- 对于排序慢的问题,让
WHERE+ORDER BY共享同一个复合索引,避免全表扫描。 - 当
offset过大,会引发深度分页问题,目前不管是mysql还是es都没有很好的方法去解决这个问题。只能通过限制查询数量或分批获取的方式进行规避。
- 对于排序慢的问题,让
- 原因
- MySQL 什么时候会加间隙锁?
- 在可重复读隔离级别下,查询为范围匹配(
>,<,BETWEEN)时会加间隙锁,查询为精确匹配(=)且有唯一索引时加的是记录锁。 - 记录锁:锁定索引上的单条记录
- 间隙锁:锁定两个索引记录之间的“间隙”,不包括记录本身
- 临键锁:锁定索引记录本身 + 前一个间隙(记录锁 + 间隙锁)
- 在可重复读隔离级别下,查询为范围匹配(
- sql 注入的原理和防范?
- SQL 注入是攻击者把恶意的 SQL 片段当做数据注入到应用与数据库交互的输入中,导致应用执行了攻击者构造的 SQL,从而泄露、篡改、删除数据或升级权限。防范核心是:永远把“代码”(SQL 语句)和“数据”分离,并做到最小权限 + 多层防护。
- 通过参数化查询来防范,把 SQL 语句的“代码部分”与“数据部分”分离,把用户提供的数据作为参数(绑定变量)传给数据库驱动/引擎,而不是把数据直接拼接到 SQL 字符串里执行。
- 红黑树的特性?
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 所有叶子节点(NIL/空节点)都是黑色
- 红色节点的子节点必须是黑色(即红节点不能相邻)
- 从任一节点到其所有叶子节点的路径上,黑色节点数相同
- 为什么 innodb 使用 B+树而不是红黑树?
- B+ 树的“多路结构”降低了树的高度,降低了磁盘 I/O 的次数,性能有所提升。
- 红黑树是二叉搜索树,节点之间的顺序关系只能通过中序遍历实现,遍历时需递归访问多个节点,可能跨磁盘页。而 B+ 树的所有数据都在叶子节点,并且叶子节点之间通过双向链表指针相连。
- 假如单表数据量过大你会怎么进行优化?
- 可以采用按照时间进行分表:
- 分区表(单表多分区):对应用最透明,按
created_at做 RANGE 分区,查询带时间条件自动“分区裁剪”。适合:单实例、以时间查询为主、追求改造成本最低。 - 物理分表(多张月/日表):真实多表,如
t_YYYY_MM;应用或中间件负责路由与跨表聚合。适合:需要强物理隔离、后续要跨库扩容、备份/迁移按月解耦。
- 分区表(单表多分区):对应用最透明,按
- 可以采用按照时间进行分表:
- SQL 语句查询很慢怎么解决?
- 通过
EXPLAIN可以判断 SQL 是否执行了全表扫描:主要看输出中的 type 列,如果显示为 ALL,表示优化器未使用任何索引,对整张表逐行扫描,是最慢的一种访问方式;理想情况下应看到 ref、range、const 等更高效的访问类型。此外还可以结合 rows(预估扫描行数)与 key(实际使用的索引)字段一起分析,若 key 为空且 rows 很大,就能确定该语句在执行全表查询,需要通过添加合适索引或优化条件来改善性能。
- 通过
- 简述向数据库发送 sql 到返回结果的过程
- 当客户端发送一条 SQL 到数据库时,主要经历以下过程:
- 1️、建立连接;
- 2️、数据库接收 SQL 并解析(语法与语义检查);
- 3️、优化器生成执行计划;
- 4️、执行器根据计划调用存储引擎;
- 5️、存储引擎从内存或磁盘读取数据,或写入日志与页;
- 6️、数据返回执行器,经网络返回客户端。
- 整个过程涵盖连接管理、解析、优化、执行、存储引擎访问与结果返回,是 SQL 查询性能优化的关键路径。
- 当客户端发送一条 SQL 到数据库时,主要经历以下过程:
- 数据库什么时候应该建立索引?
- 数据库应在以下场景建立索引:
- 经常出现在 WHERE、JOIN、ORDER BY、GROUP BY 的列;
- 高选择性(区分度高)的列;
- 频繁查询、读多写少 的表;
- 联合查询的常用组合列(建立联合索引)。
- 不宜建索引的情况包括:表很小、更新频繁、重复值多、临时字段等。
- 建立索引的目标是用最少的索引覆盖最常用的查询路径,平衡读写性能。
- 数据库应在以下场景建立索引:
- 如何检测并解决数据库死锁?
- 数据库死锁是多个事务因相互占用资源而循环等待的现象。InnoDB 会自动检测死锁并回滚其中一个事务,返回错误
1213。我们可以通过SHOW ENGINE INNODB STATUS查看详细死锁日志。解决办法包括:- 优化 SQL 与索引,减少锁范围;
- 控制事务粒度与执行顺序;
- 避免长事务;
- 必要时降低隔离级别;
- 在应用层捕获死锁异常并重试。
- 预防关键是:一致的资源访问顺序 + 精确索引 + 快速事务。
- 数据库死锁是多个事务因相互占用资源而循环等待的现象。InnoDB 会自动检测死锁并回滚其中一个事务,返回错误
Redis
- 缓存击穿和缓存雪崩的概念,以及怎么解决?
- 缓存击穿是指缓存中某个热点数据过期了,此时有大量请求访问这个数据,导致无法从缓存中直接读取,会去访问数据库,此时数据库很容易被高并发请求击垮,称为缓存击穿。解决方法是不给热点数据设置过期缓存,由后台异步更新缓存。
- 缓存雪崩是指缓存中有大量数据在同一时间段过期了,此时有大量的用户请求无法访问到缓存中的数据,会去访问数据库,此时数据库很容易被高并发请求击垮,称为缓存雪崩。解决方法是均匀设置缓存的过期时间,不要让大量数据在同一时间段过期。或者不给热点数据设置过期缓存,由后台异步更新缓存。
- 缓存一致性怎么解决?
- 采用旁路缓存,读的话先读缓存,缓存命中则返回。缓存未命中,则读数据库,然后将数据写入缓存,再返回。然后写操作: 先更新数据库,再删除缓存。
- 如何使用
redis来实现滑动时间窗口限流?ZSET精准滑动窗口- 用
ZSET存最近一段时间内的每次请求时间戳。 - 每次请求:
- 删除窗口外的旧数据
ZREMRANGEBYSCORE key 0 now-window - 插入当前请求
ZADD key now member - 统计窗口内数量
ZCARD key(或ZCOUNT)
- 删除窗口外的旧数据
- 超过
limit则拒绝;否则放行。设置EXPIRE key window避免长期留存。
- 用
- 优点:精确、并发安全(用 Lua 原子化)。
- 缺点:每次请求 1 条记录,QPS 极高时 ZSET 体积变大(可按业务限流关键点使用)。
- 不用布隆过滤器还有什么方法解决缓存穿透的问题?
- 缓存空对象:当缓存 miss,DB 也查不到数据时,仍然在缓存中写入一个特殊值(比如
"NULL"、空 JSON)。 - 参数校验 / 业务层防线:在进入缓存逻辑之前,对请求参数进行合法性检查。比如用户 ID、商品 ID 必须是正整数、存在于某个范围内、或者符合雪花算法规则。
- 接口限流 / 黑名单机制:针对特定 IP、用户、token 做访问速率限制。对高频访问不存在 key 的请求源,临时加入黑名单。
- 缓存空对象:当缓存 miss,DB 也查不到数据时,仍然在缓存中写入一个特殊值(比如
消息队列
Kafka
- Kafka 的相关术语:
- Messages And Batches:Kafka 的基本数据单元被称为 message (消息),为减少网络开销,提高效率,多个消息会被放入同⼀批次 (Batch) 中后再写入。
- Topic:⽤来对消息进⾏分类,每个进入到 Kafka 的信息都会被放到⼀个 Topic 下
- Broker:用来实现数据存储的主机服务器, kafka 节点
- Partition:每个 Topic 中的消息会被分为若干个 Partition,以提高消息的处理效率,一个 Partition 只能被一个消费者读,而一个消费者能够读多个 Partition
- Producer:消息的生产者
- Consumer:消息的消费者
- Consumer Group:消息的消费群组
- Kafka 怎么保证数据不丢失?
- 生产者:消息被可靠写入多个副本后再返回成功;失败要可重试且不重复。
- Broker / 主题(Topic)与集群层:副本冗余 + 正确的选主与同步策略,避免单点和“脏主”导致的数据缺失。
- 消费者(Consumer)与 Offset 提交:处理完再提交偏移,或者用事务做到精确一次(EOS)。
- Kafka 是怎么保证有序的?
- 在 Kafka 中,消息的有序性是以分区(Partition)为粒度来保证的。同一个分区里的消息按照写入顺序追加到日志文件中,消费者也是按偏移量(offset)顺序依次读取,因此单分区内消息的顺序是天然有序的。
- 为了维持业务上的顺序,Kafka 会在 生产者(Producer)端通过 分区策略来控制消息落到同一个分区:
- 如果发送消息时指定了 相同的 key(比如订单号、用户 ID),Kafka 会使用 key 的 hash 值来计算目标分区,使得相同 key 的消息总是进入同一个分区,从而保证这部分消息的先后顺序。(假设在一个 Topic 下有很多 partition);
- 若不指定 key,Kafka 会采用轮询策略(Round Robin),这样消息会分散到多个分区中,整体就无法保证全局有序。
git 相关
git分支的作用?git分支只是一个指向提交(commit)的指针,它没有复制文件或代码。不同分支的修改互不影响,开发完成后可以通过merge(合并)或rebase(变基)整合到主线。
git pull和git fetch的区别?git fetch仅仅把远程仓库的新提交取回到本地仓库,但不会改变你当前分支的状态。git pull实际上等价于fetch+merge(或rebase),会更新本地分支,并且可能修改工作区。
git checkout和git stash的区别?git checkout是切换分支或恢复文件到某个版本,可能会丢弃或覆盖当前修改。git stash是临时保存当前未提交的更改,会安全地保存修改并清理工作区。
git merge和git rebase的区别?git merge把两个分支的内容合并,保留原有提交历史,会生成一个新的“合并提交”。git rebase会重写提交历史,使之线性化,不会合并两个分支。
- 说一下
git cherrypickgit cherrypick是从其他分支中“挑选”一个或多个提交(commit),并把它们应用到当前分支上。
服务注册与服务发现
- 客户端 A 是怎么通过 Consul 向服务端 B 做服务发现的?
- A 向 Consul 查询服务名
"user-service"。 - Consul 返回所有健康实例(IP:Port)。
- A 从列表中选一个,向该健康实例(IP:Port)拨号连接得到对象 client,通过 client 调用相应的方法。
- A 向 Consul 查询服务名
知识复习汇总
http://example.com/2025/02/20/Interview/