golang面经主要参考知乎大佬总结的面试题:https://zhuanlan.zhihu.com/p/519979757
计算机网络参考:https://blog.csdn.net/justloveyou_/article/details/78303617
本文主要对面试中问到的,但是面经中没有的题目进行补充。
gin源码解析:https://www.liwenzhou.com/posts/Go/gin-sourcecode/
可以从这三个方面回答:
1、gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
2、gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。
3、借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。
参考博客:https://www.liwenzhou.com/posts/Go/rpc/
RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。
1、如何确定要执行的函数? 在本地调用中,函数主体通过函数指针函数指定,然后调用 add 函数,编译器通过函数指针函数自动确定 add 函数在内存中的位置。但是在 RPC 中,调用不能通过函数指针完成,因为它们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数。
2、如何表达参数? 本地过程调用中传递的参数是通过堆栈内存结构实现的,但 RPC 不能直接使用内存传递参数,因此参数或返回值需要在传输期间序列化并转换成字节流,反之亦然。
3、如何进行网络传输? 函数的调用方和被调用方通常是通过网络连接的,也就是说,function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。.例如,一些 RPC 框架使用 TCP 协议,一些使用 HTTP。
以往实现跨服务调用的时候,我们会采用RESTful API的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接收API接口返回的响应数据。
其中,gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。最初由谷歌进行开发。它使用HTTP/2作为传输协议
RPC跟HTTP不是对立面,RPC中可以使用HTTP作为通讯协议。RPC是一种设计、实现框架,通讯协议只是其中一部分。
http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了:
原文链接:https://blog.csdn.net/pearl8899/article/details/118640086
原文链接: https://blog.csdn.net/weixin_49848200/article/details/126969772
(1)为什么进程切换比线程切换代价大,效率低?
为了加速页表的读取,出现了一种存放 程序最常访问页表项的 Cache 高速缓存,称之为TLB,可以极大提高虚拟地址到物理地址的转换速度。
理解:TLB可以看作是一种硬件的哈希表,来快速查找 高速cache 中是否存在特定地址的数据,而其中应用到的内存淘汰策略则是常被提到的LRU内存淘汰策略。
作用:可以加速页表读取,极大提高虚拟地址到物理地址的转换速度。
(2)协程为什么比线程轻量级
(3)什么情况会出现线程阻塞
睡眠状态、等待状态(调用wait)、礼让状态(将执行权让给同等级或更高级线程优先执行)
原文链接:https://blog.csdn.net/qq_37102984/article/details/127949307
https://www.cnblogs.com/JonaLin/p/11570858.html
这个东西实际上是问你redis+队列的实现应用
用户的秒杀请求堆积到队列里面。redis存着商品数量。首先超出队列长度的请求可以抛弃,其次消费队列的请求时,同步减少redis的数量。这时反而还可以还用去操作数据库。因为redis的原子性和非常牛逼的内存读
单台redis和mq组件选择 都可以撑住10W并发的秒杀了
参考golang的读写锁底层
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
Mutex用来写协程之间的互斥等待,读协程使用readerSem等待写锁的释放,写协程使用writerSem等待读锁的释放,readerCount 记录读协程个数,readerWait记录写协程之前的读协程个数
channel 底层为环形队列(缓冲区),发送队列,接收队列,Mutex,close
当协程需要在发送队列或接收队列等待时,会被打包成 sudog 这样一个结构。一个 G可能被打包为多个 sudog (结构体)分别挂在不同的等待队列上:
channel 是 goroutine 之间数据通信桥梁,而且是线程安全的。
1、对未初始化(make)的 channel 进行读写关闭操作,会产生死锁deadlock
2、当 for range 遍历 channel 时,如果发送者没有关闭 channel 或在 range 之后关闭,会导致 死锁
3、对于无缓冲区channel,读取和写入要配对出现,并且不能在同一个 goroutine 里
4、对于有缓冲区的channel,读写操作如果在同一个 goroutine 里,写数据操作一定在读数据操作前
参考博客:https://blog.csdn.net/tool007/article/details/124329558
https://blog.csdn.net/diaosssss/article/details/92830782
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
golang 中的死锁是当 goroutine 被阻塞而没有任何可能被解除阻塞时发生的状态,goroutine 会产生死锁,要么是因为它正在等待管道消息,要么是因为它正在等待同步包中的锁,常见的死锁情形:
第一种情形:无缓存能力的管道,自己写完自己读
第二种情形:协程来晚了
第三种情形:管道读写时,相互要求对方先读/写
第四种情形:读写锁相互阻塞,形成隐形死锁
https://www.jb51.net/article/221391.htm#_label0
开放寻址法,再哈希法,拉链法
https://blog.csdn.net/qq_16570607/article/details/118659466
Golang中的map底层使用的数据结构是hash table,基本原理就和基础的散列表一致,重点是Golang在设计中采用了分桶(Bucket),每个桶里面支持多个key-value元素的这种思路,具体可以参考下面的图,可以看到图中的B就是Bucket,每个桶中会存储多组K/V,默认情况下map首先是指向一个桶的数组,每个桶中最多包含8个key-value对,**对于输入的key首先经过散列函数计算得出散列值,**其实就是1个数字,大部分计算机都是64位的,所以通常这是一个uint64的值,这个哈希值的低位用于选择桶,确定桶之后,哈希值的高位用于定位到桶中的条目,如果桶中的key-value满了,则会标记当前桶溢出同时链接到额外的新桶,将元素放进去。
如果两个不同的key被定位到同一个桶中,其实就可以认为出现了哈希冲突,
参考链接:https://www.cnblogs.com/freeweb/p/15898542.html#ref-1
扩容条件
(1)负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
(2)当溢出桶过多时:
满足以上条件均会触发扩容机制。
扩容方案
等量扩容:
增量扩容:
这种扩容发生在桶数量不够用时。具体步骤如下:
考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
参考:https://zhuanlan.zhihu.com/p/617371376
Golang 在运行时不允许对 map 的并发读写,需要在多个线程中读写 map 时,解决办法:
package main
import (
"sync"
)
func main() {
var counter = struct {
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
go func() {
for {
counter.RLock()
_ = counter.m["some_key"]
counter.RUnlock()
}
}()
go func() {
for {
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
}
}()
select {}
}
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
https://blog.csdn.net/weixin_38299404/article/details/126805554
对于一个channel来说,如果没有任何goroutine引用,gc会对其进行回收,不会引起内存泄露。
而当goroutine处于接收或发送阻塞状态,channel处于空或满状态时,一直得不到改变,gc则无法回收这类一直处于等待队列中的goroutine,引起内存泄露。
引起内存泄露的几种情况:
发送端channel满了,没有接收端
接收端channel为空,没有发送端
channel未初始化
Context类型是一个可以帮助我们实现多goroutine 协作流程的同步工具,不但如此,我们还可以通过此类型的值传达撤销信号或传递数据。
Context类型的实际值大体上分为三种,即:根Context值、可撤销的Context值和含数据的Context值。所有的Context值共同构成了一颗上下文树,这棵树的作用域是全局的,而根Context值就是这棵树的根,它是全局唯一的,并且不提供任何额外的功能。
参考原文链接:https://blog.csdn.net/lml200701158/article/details/119214198
G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。
关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议与CPU 数量有关,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P
队列轮转
总结
Go将对象按照大小分为3种,微小对象使用mcache,mcache中的mspan填满后,与mcentral交换新的,mcentral不足时,在heapArena开辟新的mspan,大对象直接在heapArena开辟新的mspan
不是所有变量都放在协程栈上,以下三种情况会触发内存逃逸
逃逸分析工具:go build -gcflags ‘-m’
Go 的 GC 回收有三次演进过程,
写屏障的作用:破坏强弱三色不变式之一即可保证GC时不丢失对象
GC触发时机:分为系统触发和主动触发。
1)gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。
2)gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。时间周期以runtime.forcegcperiod 变量为准,默认 2 分钟。
3)gcTriggerCycle:如果没有开启 GC,则启动 GC。
4)手动触发的 runtime.GC 方法。
sync.Pool 临时对象池,只有三个方法,New Get Put
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
Age int
}
func init() {
pool = &sync.Pool{
New: func() interface{} {
fmt.Println("creating a new person")
return new(Person)
},
}
}
func main() {
person := pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
person.Name = "my"
person.Age = 10
pool.Put(person)
person = pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
person = pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
}
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
golang接口的意义和其他所有编程语言都是一样的,为了抽象和多态
这样你就可以在某个函数(方法)入参或者结构体(类)属性处使用一个接口变量,这个变量包含了接口定义的行为。
分治法,即大数据里最常用的MapReduce。
a、将100亿个数据分为1000个大分区,每个区1000万个数据
b、每个大分区再细分成100个小分区。总共就有1000100=10万个分区
c、计算每个小分区上最大的1000个数。
为什么要找出每个分区上最大的1000个数?举个例子说明,全校高一有100个班,我想找出全校前10名的同学,很傻的办法就是,把高一100个班的同学成绩都取出来,作比较,这个比较数据量太大了。应该很容易想到,班里的第11名,不可能是全校的前10名。也就是说,不是班里的前10名,就不可能是全校的前10名。因此,只需要把每个班里的前10取出来,作比较就行了,这样比较的数据量就大大地减少了。我们要找的是100亿中的最大1000个数,所以每个分区中的第1001个数一定不可能是所有数据中的前1000个。
d、合并每个大分区细分出来的小分区。每个大分区有100个小分区,我们已经找出了每个小分区的前1000个数。将这100个分区的1000100个数合并,找出每个大分区的前1000个数。
e、合并大分区。我们有1000个大分区,上一步已找出每个大分区的前1000个数。我们将这1000*1000个数合并,找出前1000.这1000个数就是所有数据中最大的1000个数
金额用 decimal 6位精度
日期 date 到秒 datetime
长字符串text 超长字符串用long text
用户头像 Blob
如何防止超卖,即高并发下库存变为负数
原文链接:https://blog.csdn.net/baidu_27627251/article/details/88047466
很多时候,慢查询都是因为没有加索引。如果没有加索引的话,会导致全表扫描的。因此,应考虑在where的条件列,建立索引,尽量避免全表扫描。
宽表优化
参考 https://blog.csdn.net/m0_67698950/article/details/125050163
主键索引也被称为聚簇索引,叶子节点存放的是整行数据; 而非主键索引被称为二级索引,叶子节点存放的是主键的值.
如果根据主键查询, 只需要搜索ID这颗B+树
而如果通过非主键索引查询, 需要先搜索k索引树, 找到对应的主键, 然后再到ID索引树搜索一次, 这个过程叫做回表.
总结, 非主键索引的查询需要多扫描一颗索引树, 效率相对更低
参考原文链接:https://blog.csdn.net/fly_zhaohy/article/details/104014391
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替
参考:https://blog.csdn.net/Programer0/article/details/125665810
https://www.cnblogs.com/xiaomaomao/p/16211762.html
通过非主键索引查询要扫描两棵 B+Tree,而通过主键索引查询只需要扫描一棵 B+Tree,第一次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表
那么不用主键索引就一定需要回表吗?
不一定!
如果查询的列本身就存在于索引中,那么即使使用二级索引,一样也是不需要回表的
MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式。
MySQL主从复制涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点
详见:https://blog.csdn.net/s_frozen/article/details/124566733
1、redo log(重做日志)属于MySQL存储引擎InnoDB的事务日志,InnoDB 在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作 redo log(重做日志),在更新内存写完 redo log 后,就返回给客户端,本次更新成功。找时间把内存里的数据写入磁盘的过程,术语就是 flush。在这个 flush 操作执行之前,内存中的数据和磁盘中的数据是不一致的。当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
采用固定大小,循环写入的格式
2、undo log(回滚日志)也是属于MySQL存储引擎InnoDB的事务日志。 undo log属于逻辑日志,如其名主要起到回滚的作用,它是保证事务原子性的关键。记录的是数据修改前的状态,在数据修改的流程中,同时会记录一条与当前操作相反的逻辑日志到undo log中
3、**bin log(归档日志)**是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。bin log记录了数据库所有DDL和DML操作(不包含 SELECT 和 SHOW等命令,因为这类操作对数据本身并没有修改),主要应用于MySQL主从模式(master-slave)中,主从节点间的数据同步;以及基于时间点的数据还原。三种格式:
4、 relay log(中继日志)日志文件具有与bin log日志文件相同的格式,从上边MySQL主从复制的流程可以看出,relay log起到一个中转的作用,slave先从主库master读取二进制日志数据,写入从库本地,后续再异步由SQL线程读取解析relay log为对应的SQL命令执行。
5、慢查询日志(slow query log): 用来记录在 MySQL 中执行时间超过指定时间的查询语句,在 SQL 优化过程中会经常使用到。通过慢查询日志,我们可以查找出哪些查询语句的执行效率低,耗时严重。
6、 一般查询日志(general query log):用来记录用户的所有操作,包括客户端何时连接了服务器、客户端发送的所有SQL以及其他事件,比如 MySQL 服务启动和关闭等等。MySQL服务器会按照它接收到语句的先后顺序写入日志文件。
7、错误日志(error log): 应该是 MySQL 中最好理解的一种日志,主要记录 MySQL 服务器每次启动和停止的时间以及诊断和出错信息。
详情 https://blog.csdn.net/whatday/article/details/126297642
mysql数据量超过2千万会出现性能问题
select_type:查询类型。主要用来分辨查询的类型事普通查询还是联合查询还是子查询
simple:简单的查询,不包含子查询和 union
primary:查询中若包含任何复杂的子查询,最外层查询则被标记为 primary
type:访问类型,表示以何种方式去访问数据库,最容易想的是全表扫描,即直接暴力的遍历一张表去寻找需要的数据,效率非常低下。
访问的类型有很多,效率从最好到最坏依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system:表只有一行记录(等于系统表),这是 const 类型的特例,平时不会出现,不需要进行磁盘 io
const:最多只能匹配到一条数据,通常使用主键或唯一索引进行等值条件查询
eq_ref:当进行等值联表查询使用主键索引或者唯一性非空索引进行数据查找时(实际上唯一索引等值查询 type 不是 eq_ref 而是 const)
ref:使用了非唯一性索引进行数据的查找
ref_or_null:对于某个字段既需要关联条件,也需要 null 值的情况下,查询优化器会选择这种访问方式
index_merge:在查询过程中需要多个索引组合使用
unique_subquery:该连接类型类似于 index_subquery,使用的是唯一索引。大多数情况下使用 SELECT 子查询时,MySQL 查询优化器会自动将子查询优化为联表查询,因此 type 不会显示为 index_subquery,而是 eq_ref
index_subquery:利用索引来关联子查询,不再扫描全表。但是大多数情况下使用 SELECT 子查询时,MySQL 查询优化器会自动将子查询优化为联表查询,因此 type 不会显示 index_subquery,而是 ref
range:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了 index 的全索引扫描。适用的操作符:=, <>, >, >=, <, <=, is null, between,like, or, in
index:全索引扫描这个比 all 的效率要好,主要有两种情况,一种是当前的查询覆盖索引,即需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免了数据的重排序
all:全表扫描,需要扫描整张表,从头到尾找到需要的数据行。一般情况下出现这样的 sql 语句而且数据量比较大的话那么就需要进行优化
一般情况下,要保证查询至少达到 range 级别,最好能达到 ref
possible_keys:显示查询可能使用哪些索引来查找,即显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用
key:这一列显示 mysql 实际采用哪个索引来优化对该表的访问,即实际使用的索引,如果为 null ,则表示没有使用索引
mrr:当表很大且没有存储在存储引擎的缓存中时,在辅助索引上使用范围扫描读取行可能会导致对基本表的许多随机磁盘访问。
通过磁盘扫描多范围读取(MRR)优化,MySQL尝试通过只扫描索引并收集相关行的键来减少范围扫描的随机磁盘访问次数。然后对键进行排序,最后使用主键的顺序从基表检索行。
磁盘扫描MRR的动机是减少随机磁盘访问的数量,而实现对基本表数据的更连续的扫描。
原文链接:https://blog.csdn.net/qq_44734154/article/details/126739951
参考原文链接:https://blog.csdn.net/jiaomubai/article/details/126040506
RR级别的事务隔离可以解决脏读和不可重复读,他通过MVCC解决了快照读情况下的幻读问题,当前读下的幻读是以来Innodb的锁机制实现的。所以总结起来就是:
MySQL InnoDB 的MVCC(多版本并发控制)主要是为了提高数据库并发性能,处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的
详见:https://blog.csdn.net/bbj12345678/article/details/120780051
表锁
行锁
间隙锁
索引的数据结构
采用B+树
1、MySQL为什么不用跳表?
跳表比B+树层级更高,需要更多的磁盘IO
2、Redis中为什么不用B+树?
Redis是基于内存的数据库,不用考虑磁盘IO问题,采用跳表,不用考虑B+树页分裂等问题
默认参数
横表:通常指我们平时在数据库中建立的表,是一种普通的建表方式。特点是,一个ID对应所有的值信息,以行Key-Value1-Value2-Value3的方式存储
纵表:一般不多见,在表结构不确定的时候,如需增加字段的情况下的一种建表方式。特点是每行仅存储该ID的某一个类别字段的值,以行的方式存储Key-Value的方式存储
链接:https://www.cnblogs.com/wy123/p/6677073.html
关系型数据库:mysql oracle
非关系型数据库 redis mongodb
哨兵模式即为反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
①选择优先级高的从服务器作为主服务器(值越小,优先级越高)
②如果优先级相同,选择偏移量最大的从服务器作为主服务器
③如果优先级和偏移量都相同,选择runid最小的从服务器作为主服务器,每个redis实例启动后都会随机生成一个40位的runid。选择runid最小的
参考:https://blog.csdn.net/qq_46370017/article/details/126327979
Cache Aside Pattern(旁路缓存模式)是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景,步骤如下:
写:
读:
参考链接:https://blog.csdn.net/lans_g/article/details/124652284
设置了失效时间的主键和具体的失效时间全部都维护在 expires 字典表中
消极方法(passive way),在主键被访问时如果发现它已经失效,那么就删除它
积极方法(active way),周期性地从设置了失效时间的主键中选择一部分失效的主键删除
详见:https://blog.csdn.net/u013530955/article/details/74454174/
一个哈希表可以是一个数组,数组里面的每一个存储单元都叫做哈希桶(Bucket),比如数组第一个位置(索引为 0)被编为哈希桶 0,第二个位置(索引为 1)被编为哈希桶 1,以此类推。
当我们写入一个键值对的时候,会根据 key 和 value 的指针构建一个 dictEntry 结构体实例。然后通过对 key 进行哈希运算来计算出桶的位置,最后将 dictEntry 结构体实例的指针写入哈希表中。
string类型
假如存储的「字符串是一个字符串值并且长度大于32个字节」就会使用SDS(simple dynamic string)方式进行存储,并且encoding设置为raw;若是「字符串长度小于等于32个字节」就会将encoding改为embstr来保存字符串。
https://blog.csdn.net/qq_41071876/article/details/124422912
1、解决哈希冲突:这里采用的便是拉链法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突
2、扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
1)如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
2)重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
3)所有键值对都迁徙完毕后,释放原哈希表的内存空间。
渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的
zset的两种实现方式
ziplist(压缩链表):满足以下两个条件的时候
skiplist(跳跃链表):不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist
跳跃表是有序单链表的一种改进,其查询、插入、删除也是O(logN)的时间复杂度
跳表其实就是一种可以进行二分查找的有序链表,跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了
为什么采用跳表,而不使用哈希表或平衡树实现呢?
详见:https://blog.csdn.net/qq_42410605/article/details/122517927
Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失
1、RDB:指定时间间隔内将当前数据的快照写入磁盘,过程:写时复制技术,有一个fork子进程,先将数据集写入临时文件,再用临时文件替换原来的快照文件,然后子进程退出
优点:
缺点:
2、AOF:默认不开启,采用日志形式记录每个写操作,并追加到文件中
优点:
缺点:
3、RDB和AOF的对比
原文链接:https://blog.csdn.net/weixin_42601136/article/details/122759402
随着系统的运行,redis的数据越来越多,会导致物理内存不足。通过使用虚拟内存(VM),将很少访问的数据交换到磁盘上,腾出内存空间的方法来解决物理内存不足的情况。虽然能够解决物理内存不足导致的问题,但是由于这部分数据是存储在磁盘上,如果在高并发场景中,频繁访问虚拟内存空间会严重降低系统性能。
Redis内存不足的缓存淘汰策略提供了8种:
Redis客户端执行一条命令,分为4部分:发送命令=>命令排队=> 命令执行=> 返回结果
第一步:确定Redis是否真的变慢了
第二步:查看slowlog(慢日志)
参考链接:https://blog.csdn.net/feiying0canglang/article/details/106184505
查看CPU:top命令
查看内存:free
查看隐藏文件:ls -a
检查所有监听端口:netstat -tln
查看网络连接、哪些端口被打开: netstat -anp
Linux使用正则表达式:https://blog.csdn.net/qq_48391148/article/details/125566389
select==>时间复杂度O(n)
3 - 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
poll==>时间复杂度O(n)
epoll==>时间复杂度O(1)
原文链接:https://blog.csdn.net/weixin_52622200/article/details/112891861
用户态和内核态是操作系统的两种运行状态,操作系统主要是为了对访问能力进行限制,用户态的权限较低,而内核态的权限较高
内核态切换到用户态是通过设置程序状态字PSW
用户态到内核态的切换
线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。
进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。
进程通信
线程通信
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
原文链接:https://blog.csdn.net/weichi7549/article/details/107590240
原文链接:https://blog.csdn.net/qq_36221862/article/details/55670604
基本的操作系统进程调度算法包括先来先服务(first come first serve),时间片轮转(round robin),多级反馈轮转法(round robin with multiple feedback),优先级法(静态优先级法/动态优先级法),短作业优先法(shortest job first),最高响应比优先法(highest response_ratio next)。
虚拟内存技术:提供一种虚拟地址到实际物理地址的映射,将连续的虚拟地址暴露给程序,而实际上他们在物理内存(比如内存条)上面是不连续的。
虚拟内存能够很好的帮助程序员避免麻烦的内存管理与冲突等问题,并且将内存作为模块化独立出来。
内存分页:是把整个虚拟内存和物理内存空间切成一段段固定大小的尺寸。这样一个连续并且尺寸固定的内存空间叫做页(Page)。虚拟页和物理页的大小是一样的,通常为4KB。
页表:记录【进程 虚拟地址】与【内存 物理地址】的映射关系。每个进程都拥有自己的虚拟地址空间,也拥有一个页表。
对于进程来说,使用的都是虚拟地址。每个进程维护一个单独的页表。页表是一种数组结构,存放着各虚拟页的状态,是否映射,是否缓存。
为什么用虚拟地址
直接操作物理内存,这种方式存在几个问题:
参考:https://blog.csdn.net/weixin_39790760/article/details/110815922
分段:将程序分为代码段、数据段、堆栈段等;
分段地址通过段表,转换成线性地址; 分段地址包括段号和段内地址; 段表,包括短号、段长、基址;
分页:将段分成均匀的小块,通过页表映射物理内存;
分页机制就是将虚拟地址空间分为大小相等的页;物理地址空间也分为若干个物理块(页框);页和页框大小相等。实现离散分配; 分页机制的实现需要 MMU 硬件实现;负责分页地址转换; 页大小(粒度)太大浪费;太小,影响分配效率。
原文链接:https://blog.csdn.net/weixin_45304503/article/details/126690435
该算法包括 4个主要部分: 慢启动、拥塞避免、快重传、快恢复
TCP发送速率起始慢,但在慢启动阶段以指数增长。
慢开始:
不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小;
拥塞避免
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长
快重传
快重传要求接收方在收到一个 失序的报文段 后就立即发出 重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
快恢复
当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
(1)对于TCP的send系统调用发送数据,如果socket是阻塞的,我们需要这样理解:send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。
(2)对于TCP的send系统调用,如果socket是非阻塞的,send会立即返回。
原文链接:https://blog.csdn.net/singularity0001/article/details/127663650
客户端状态变为FIN_WAIT_1。
服务端状态由ESTABLISHED变为CLOSE_WAIT,并继续处理数据,
当客户端收到服务端发送的ACK之后,状态由FIN_WAIT_1变为FIN_WAIT_2
发送FIN报文后,服务端状态变为LAST_ACK
客户端接收到服务端发送的FIN后,状态变为TIME_WAIT,并且在等待2MSL后连接关闭。
中间状态time_wait,close_wait过多的危害
11种状态说明
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
原文链接:https://blog.csdn.net/ckckckcckckc/article/details/127839708
1)客户端发起一个http请求,告诉服务器自己支持哪些hash算法。
2)服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥,网站地址,证书颁发机构,失效日期等)。证书中有一个公钥来加密信息,私钥由服务器持有。
3)验证证书的合法性
客户端收到服务器的响应后会先验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
4)生成随机密码(RSA签名)
如果验证通过,或用户接受了不受信任的证书,浏览器就会生成一个随机的对称密钥(session key)并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
5)生成对称加密算法
验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
详情:https://zhuanlan.zhihu.com/p/60033345
ssl过程
数字签名
(1)HTTP1.0特点:无状态、短连接
HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(短连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)
(2)HTTP1.1特点:长连接、请求管道化、缓存处理、Host字段、断点传输
(3)HTTP2.0特点:二进制传输、多路复用、头部压缩、服务器推送
长连接: 客户端和服务端建立连接后不进行断开,之后客户端再次访问这个服务器上的内容时,继续使用这一条连接通道。
短连接: 客户端和服务端建立连接,发送完数据后立马断开连接。下次要取数据,需要再次建立连接
原文链接:https://blog.csdn.net/tdcqfyl/article/details/122923972
原文链接:https://blog.csdn.net/justloveyou_/article/details/78303617
HTTP Request Header 请求头
Accept:指定客户端能够接收的内容类型。
Accept-Charset:浏览器可以接受的字符编码集。
Accept-Encoding:指定浏览器可以支持的web服务器返回内容压缩编码类型。
Accept-Language:浏览器可接受的语言。
Accept-Ranges:可以请求网页实体的一个或者多个子范围字段。
AuthorizationHTTP:授权的授权证书。
Cache-Control:指定请求和响应遵循的缓存机制。
Connection:表示是否需要持久连接。(HTTP 1.1默认进行持久连接)
CookieHTTP:请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。
Content-Length:请求的内容长度。
Content-Type:请求的与实体对应的MIME信息。
Date:请求发送的日期和时间。
Expect:请求的特定的服务器行为。
From:发出请求的用户的Email。
Host:指定请求的服务器的域名和端口号。
If-Match:只有请求内容与实体相匹配才有效。
If-Modified-Since:如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码。
If-None-Match:如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变。
If-Range:如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。
If-Unmodified-Since:只在实体在指定时间之后未被修改才请求成功。
Max-Forwards:限制信息通过代理和网关传送的时间。
Pragma:用来包含实现特定的指令。
Proxy-Authorization:连接到代理的授权证书。
Range:只请求实体的一部分,指定范围。
Referer:先前网页的地址,当前请求网页紧随其后,即来路。
TE:客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息。
Upgrade:向服务器指定某种传输协议以便服务器进行转换(如果支持。
User-AgentUser-Agent:的内容包含发出请求的用户信息。
Via:通知中间网关或代理服务器地址,通信协议。
Warning:关于消息实体的警告信息
1)源端口和目的端口:用于寻找发端和收端的应用程序。这两个值加上IP首部的源端IP和目的端IP唯一确定一个TCP连接;
2)序号(Seq):标识从TCP发端向TCP收端发送的数据字节流,它标识在这个报文段中的第一个数据字节的序号。如果将字节流看作在两个应用程序间的单向流动,
则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2的32次方减一后又从0开始。SYN标志消耗一个序号;
3)确认序号(ACK):如果上次成功收到数据字节序号加一。只有ACK标志为1时确认序号才有效,ACK = Seq + 1;
4)数据偏移:标识该TCP头部有多少个32bit(4字节),4比特最大表示15,TCP头部最长为60字节;
5)窗口:TCP流量控制的手段,告诉对方,我的TCP接收端缓冲区还能容纳多少个字节,这样对方能控制发送数据的速度;
6)校验和:由发送端填充,接收端对TCP报文执行CRC算法,以检验TCP报文段是否损毁。不仅校验头部,还包括数据部分;
7)紧急指针:也称为紧急偏移。紧急指针是一个正的偏移量,和序号字段的值相加表示最后一个紧急指针的下一字节的序号。是相对于当前序号的偏移。紧急指针
是发送端向接收端发送紧急数据的方法;
8)六个标志位:
a)URG:表示紧急指针是否有效;
b)ACK:表示确认号是否有效,携带ACK标志的数据报文段为确认报文段;
c)PSH:提示接收端的应用程序应该立即从TCP接受缓冲区中读走数据,为接受后数据腾出空间;
d)RST:表示要求对方重新建立连接,携带RST标志位的TCP报文段称为复位报文段;
e)SYN: 表示请求建立一个连接,携带SYN标志的TCP报文段称为同步报文段;
f)FIN:通知对方本端要关闭了,带FIN标志的TCP报文段称为结束报文段;
9)TCP头部选项:头部选项是一个可变长的信息,这部分最多包含40个字节(前面20字节是固定的)
头部选项的实际运用:
a)最大报文传输段(Maxinum Segment Size——MSS,后续进行详解)
b)窗口扩大选项(window scaling)
c)选择确认选项(Selective Acknowledgements——SACK)
d)NOP
原文链接:https://blog.csdn.net/www_dong/article/details/113419548
UDP首部有8个字节,由4个字段构成,每个字段都是两个字节,
1.源端口: 主机的应用程序使用的端口号。
2.目的端口:目的主机的应用程序使用的端口号。
3.长度:是指UDP头部和UDP数据的字节长度。因为UDP头 部长度为8字节,所以该字段的最小值为8。
4.校验和:检测UDP数据报在传输中是否有错,有错则丢弃。
原文链接:https://blog.csdn.net/weixin_43142797/article/details/105648071
参考:https://www.cnblogs.com/makuo/p/16412689.html
(1). 浏览器查询 DNS,获取域名对应的IP地址;
(2). 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;
(3). TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求;
(4). 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
(5). 浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;
(6). 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
原文链接:https://blog.csdn.net/justloveyou_/article/details/78303617
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
跨域的解决方案
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
原文链接:https://blog.csdn.net/miachen520/article/details/125569792
https://blog.csdn.net/weixin_66375317/article/details/124545878
红黑树的性质
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN ),红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
https://blog.csdn.net/mz474920631/article/details/124982316
二叉树每个根节点只有两个子树,在数据量很大的时候,树的深度会很大。B树的存在解决了这个问题。
B树的节点包含键和值,是key-value的形式。一个节点可以包含多个key,并不确定,需要看具体实现
B+树和B树相似,不同的地方是只有叶子节点存储形式为key-value,其他节点存储的只有key值。树的所有叶节点构成一个有序链表,可以按照key排序的次序依次遍历全部数据。
B+ 树的优点在于:
B树的优点在于:
B+树的插入与删除操作都仅用O(1)的时间复杂度,查找的时间复杂度O(logN)
原文链接:https://blog.csdn.net/qq_57014350/article/details/118759152
https://blog.csdn.net/wufeifan_learner/article/details/109724836
数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
链表和数组一样,链表也是一种线性表。从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构
相同点:
不同点:
Solid设计原则有五个主要方面,分别是单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖反转原则(DIP)。