最下面的面试题持续更新,不更新了就是找到工作了。。。
算法:
1.提取二进制位最右边的
r = i & (~i + 1)
2.树上两个节点最远距离,先考虑头结点参与不参与。
3.暴力递归改dp。
1.确定暴力递归方式。
2.改记忆化搜索
3.严格表方式:
分析可变参数变化范围,参数数量决定表维度、
标出计算的终止位置、
标出不用计算直接出答案的位置,根据暴力递归中的basecase、
推出普遍位置如何依赖其他位置、
确定依次计算的顺序,跟递归顺序一致。
4.得到精致版dp,如斜率优化。
4.动态规划尝试模型
从左往右,范围尝试
5.尝试的好坏:
1.单参的维度(如int类型和数组)。决定参数范围
2.参数的个数
5.有序表包括:
1.红黑树、2.avl树,3.sb树,4.跳表
6.绳子问题用滑动窗口
7.打表法。
循环参数,暴力打印结果,根据结果找到规律。憋出表达式。
8.预处理
9.二维表问题关注宏观设计,不要关心局部实现。
可以想象成要打印的数是一个一个的框,只需要记录两个顶点。顶点移动的过程中打印。
10.两个队列可以实现栈,两个栈可以实现队列。
11.动态规划dp表空间压缩技巧。
12.菲波拉契数列复杂度从n优化到logn。
相同类型问题通用。
几阶问题会生成几阶的行列式。
计算某数的次方。降为logn。
13.注意数字越界
math.MaxInt
14.假设答案法。
假设答案,用尽量简单的流程找到答案。
15.压缩数组
看到子矩阵,先想子数组。
16.优化动态规划
从数据状况入手,从问题本身入手。
17.递归死循环,要根据实际增加平凡边界。
18.看到子串和子数组问题,就想以每个字符结尾会怎样。
19.舍弃可能性
20.dp斜率优化,当有枚举行为时,使用临近格子的值代替枚举行为。(凑面值问题)
21.寻找等长有序的两个数组上中位数。
22.约瑟夫环问题,可推理数学公式:老的编号=(新的编号 + m -1)% i + 1
m:数到哪个编号淘汰
i:本轮一共多少人
23.有序表增删改查都是O(logn)
24.有单调性用滑动窗口。无单调性用 前缀和 map
25.动态规划压缩技巧,从dp表压缩到数组,从数组压缩到变量。
26.尝试模型
从左往右、范围上尝试、两个字符串对应
范围尝试模型,重点讨论开头和结尾
范围尝试模型指在这样的可能性分类下,能不能把整个域上的问题解决。
从i开始到j结尾的可能性讨论下,能不能解决该问题。
一般来说子序列问题。
27.bfprt算法
28.如果没有枚举行为,停留在记忆化搜索就行,如果有,则改为dp表,尝试进行斜率优化。
29.二叉树的递归套路,父节点找左右子树要信息,往上传递。
30.完美洗牌问题
找到共有多少组。将数分为1,2,4,8的数量段。再处理。
abcde123字符串,原地变为123 abcde:
abcde逆序,变为edcba;123逆序变为321;整体逆序,变为123abcde.
31.递归大问题调小问题,一定要保证,大问题所做决策的所有影响都已经体现在小问题的参数上了。让小问题再决策时有信息可用。(无后效性递归)
32.arr[start] 到 arr[i]的异或和 = arr[0] 到 arr[i] 的异或和 异或 arr[0] 到 arr[start] 的异或和。
33.前缀树
34.不会碰到的递归状态,dp填表的时候别填。
观察好,哪些位置不需要填。
初始位置根据basecase填好,终止位置是哪个。
普遍位置怎么依赖,规定好填写顺序。
35.滑动窗口+负债表
36.二维图形问题,转为一维线段问题求解。
golang:
map:
1.k是可比较类型,切片,func和map是不可比较的。struct中至少有一个字段是可比较的。
2.写数据时如果map没有初始化,将会panic,删除时如果没有初始化,不会报错。
3.map中有2的整数次幂的桶数组。桶中固定装8个kv,超过放在溢出桶。
4.解决哈希冲突时兼顾了拉链法和开放地址法。
5.扩容分为增量扩容和等量扩容。
增量扩容阈值:kv数量/桶数量 > 6.5时。
等量扩容:溢出桶数量 > 2^B ,B为桶数组长度的指数,2^B即桶的个数。
6.扩容时kv的迁移位置是固定的,要么在原位置,要么在原位置+原来桶长度的位置。
7.bucket中不仅存储kv,还存储了hash值的高8位。
8.初始化map时,会根据数组长度预先创建溢出桶。
9.如果正在扩容状态,先判断旧桶中的标识,是否迁移完。如果迁移完,去新桶中查询。
10.tophash有特殊标识,标识以后的位置都是空。
11.写或删除数据时如果正在扩容,进行辅助扩容。
12.写数据时判断是否需要进行增量或等量扩容。
13.桶中每个槽位都有tophash,emptyone(当前位置被删除)和emptyrest(之后的位置都为空)。这两个值都是删除的时候设置的。
14.如果正在迁移,先遍历新桶。
15.遍历时如果桶正在迁移,按照已经迁移完的顺序遍历。
16.根据oldbucket是否为空判断是否处于扩容阶段。
17.如果处于扩容阶段,每次写操作会帮助map进行扩容,1.帮助自己要写入或删除的桶,2.帮助索引最小的桶。
18.map不支持并发操作的主要原因是因为采用的渐进式扩容机制。
sync.map:
空间换时间
1.sync.map结构包括四个字段,锁,只读value,读写map,和miss次数。
2.entry的状态有3种,1:正常,2:软删除,3:硬删除。
软删除的作用是,如果刚删的数据要重新插入,不需要重新加锁,只需要对entry的指针进行原子操作即可。
3.miss次数>=写map的kv数,把写map覆盖读map。同时把读map中的amemded flag置为false。写map置为空。
4.因为读写map的value存储的都是指针,所以插入数据时,如果kv还存在是软删除状态,直接使用cas更新,不需要加锁。
5.插入完数据后,如果amended flag是true,就要将写map拷贝到读map。
6.插入数据时,如果写map是空,需要从读map拷贝一份到写map。
7.写map拷贝到读map时是直接全量覆盖,读map拷贝到写map时,需要过滤软硬删除的数据,量比较重。
8.写多读少时性能较低,写指的是插入,如果是更新性能还好。
9.删除如果读map中有,就置为软删除态。如果读map中没有且amended flag是true,就在写map中物理删除。也会触发misscount的增加。
10.执行遍历时,会判断读map的数据是不是完整的,如果不是,进行写map的覆盖。
11.软删除态和硬删除态:
软删除态是指读map和写map都有这个kv,只是v的指针指向软删除常量。
硬删除态是指读map中有这个kv,但是写map没有。
硬删除态只发生在写数据时,读map向写map拷贝数据,写map会过滤掉软硬删除态的数据,读map同时要把软删除态变为硬删除态。
12.通过读写map的相互复制,实现内存回收,避免内存泄露。
锁:
1.锁冲突解决方式:
阻塞/唤醒属于悲观锁。
自旋+CAS属于乐观锁。
2.golang是两种结合。
四次自旋未果之后,陷入阻塞。
或者cpu是单核的,直接进行阻塞/唤醒。
当前p后面还有待执行的g。
3.饥饿模式:
有g长时间得不到锁,将抢锁的流程从非公平模式转到公平模式。
进入条件:存在协程1ms没有获取到锁。
退出条件:没有阻塞队列,或者队列中没有等待时间超过1ms的协程。
4.正常模式:阻塞队列头部协程被唤醒,和刚到达的协程竞争锁。如果失败,重新回到队列头部。
5.饥饿模式:新进入的协程不能竞争锁,只从阻塞队列遵循先进先出。
6.饥饿模式会带来性能损耗。
7.先尝试使用cas加锁。bit整体是0,才会加锁成功。
8.当有等待协程被唤醒时,判断是否需要退出饥饿状态。
读写锁:
9.添加读锁是使用cas方法对readercount+1,如果加完是负值说明添加失败,说明有写锁。因为在添加写锁时readercount字段会直接减最大值。
10.解锁时对readercount字段-1,如果减完是负值,说明有写锁在等待,尝试唤醒。
11.释放写锁之后,唤醒所有读等待的协程。
12.如果有写锁阻塞,读锁也不可获取。
13.添加写锁会把当前读锁的数量放到readerwait中,此时readercount也被更新为负值,之后就不会有读锁进来。
切片:
1.在作为参数传递时,是进行的值拷贝,但是结构体内部存储的是指向数组的指针。
map也会如此:
2.切片的结构:
len:逻辑意义上切片中实际存放了多少个元素。
cap:物理意义上的容量。
3.没有初始化的切片也可以进行append。
4.截取切片为左闭右开。截取完之后的新切片,还是指向同一底层数组。
5.append操作在容量不足时才会扩容。
6.扩容时,需要的容量大于老容量的2倍,直接取新容量。
如果容量小于256,翻倍扩容。否则每次扩容1/4并累加上192,直到大于等于预期容量。
最后根据元素类型结合go的内存分配机制,向上取整,找到合适的容量。
将原数组拷贝到新数组,切换指针,完成拷贝。
7.切片的删除操作只能使用截取拼接实现。
8.使用系统方法copy可以实现切片的深拷贝。
context:
1.生命周期的终止事件传递的单调性。由上往下传递。
2.context.Backgroud()和context.todo()返回的都是空context。
3.cancelContext的核心为开启一个守护协程,在父终止时,终止子。
4.timerContext继承cancelContext。
5.valueContext只能存储一个key-value,获取时如果当前Context不存在会一直向父节点查找。
6.valueContext的读取代价很高,而且相同的key不会覆盖,在不同位置会取到不同的value。
gmp:
1.goroutine的栈可动态扩缩容。
2.g有自己的运行栈,状态及任务函数。
3.g需要绑定p才能执行,在g的视角,p就是他的cpu。
4.goroutine相对协程最大的优化在于,goroutine将协程和线程的强绑定关系给释放了。
5.因为p的本地队列有可能被窃取,所以也会有并发访问,不能完全做到无锁。
6.g结构中存有m的指针,但不是强绑定关系,受p的调度。
7.g有自己的生命周期。
8.m中存储特殊goroutine g0,属于顶级goroutine。
9.p结构中主要是环形队列,和指向首尾的指针,以及下一个可执行的g。
10.g0负责调度,执行一直在g0到g的切换。
11.调度类型:
主动调度:用户主动调用runtime.Gosched
被动调度:如互斥锁,channel等,后续需要被唤醒。
正常调度:
抢占调度:将由全局监控goroutine monitor完成。因为此时已经进入内核态,绑定的m也失去控制。
12.schedule负责找到下一个要被执行的goroutine。
13.先从本地队列取,取不到从全局队列取,再尝试获取io就绪态的goroutine,最后从其他队列窃取一半的goroutine,放到本地队列。
每第61次,会直接从全局队列取,避免goroutine饥饿。
14.窃取会尝试4次,随机选择其他队列。
15.窃取是拨动对方队列的头指针实现的。
16.用户主动让出执行权,会将执行权转移给g0,g0将该goroutine加入全局队列。
17.全局监控goroutine是main方法启动的,会定时检查p的状态。满足条件就会进行抢占。抽离当前的p,为他分配新的m。
channel:
1.读已关闭且缓冲区为空的channel,bool返回false。
2.读已关闭channel不会陷入阻塞。
3.向已关闭channel写入数据会panic。
4.channel的缓冲区是一个唤醒数组。
5.空status类型长度为0.
6.往没有初始化的channel中写数据,goroutine会陷入异常阻塞。
7.channel写入时,如果有阻塞读goroutine。会通过memmove element直接把数据拷贝给阻塞等待的goroutine。然后唤醒读取的goroutine。
8.对于用select的情况,在读写该陷入goroutine阻塞的情况,转而进入自旋。
9.当channel被关闭时,所有阻塞的读或写goroutine都会被唤醒。写goroutine会panic。
10.避免重复关闭channel,可以用sync包的once方法。
11.for range 循环遍历channel,只要channel没关闭,就会一直读取。
sync.WaitGroup:
等待聚合模式。
1.本质上是并发计数器。
2.可以被多个goroutine监听。即wait方法可以被多个goroutine调用。
3.尽量先调用add再调用wait。
4.waitGroup是防拷贝的。指在不同的方法内也应该传递指针,不应该传递值。
5.done方法本身也是调用的add,传-1.
6.如果done之后数量为0,则唤醒所有wait goroutine。
7.核心还是利用atomic包进行计数。
8.在执行过wait之后,add的操作不应该并发执行,注意waitgroup的轮次性。
内存管理&垃圾回收
1.虚拟内存最小单位为页,物理内存最小单位为帧。
2.单位太小会影响效率,太大会造成内存碎片。Linux为4kb。
3.golang内存分配核心:以空间换时间,一次申请多次复用。利用多级缓存,实现无锁||细锁化。
4.golang页的大小为8kb。
5.mspan为最小的管理单元,大小为8kb的整数倍。从8到80被划分成67个类型。
6.mspan会减少外部内存碎片,且细化锁的粒度。
7.mspan分配的内存页都是连续的。
8.同等级的mspan属于同一个mcentral,会被前后指针组织成双向链表。
9.mspan内部会使用bitmap辅助寻找空闲的内存页。
10.mspan有隐藏等级0,上不封顶的容量。
11.spanClass有8个比特位,高7位对应等级,最低位表示nocan。nocan与垃圾回收有关。
12.每个p对应的mcache
13.每个中心缓存mcentral都对应一类spanClass。
14.全局堆缓存mheap,负责将连续页组装成span。通过heapArena记录页和span的映射关系。
15.通过空闲页基数树辅助快速寻找空闲页。
16.内存不够时,向操作系统申请,单位是heapArena,即64M。
17.每个基数树可以管理16G空间内存的占用情况,用于帮助mheap快速找到连续的未使用的内存空间。mheap一共持有2^14个基数树。
分别标识,从起点开始最大连续空闲页、最大连续空闲页、尾端最大连续空闲页。
寻找时相邻的pallocSum可以拼接。如尾部有3个空闲连续页可以和下一个头部的空闲页相加。
18.对象类型:
微对象tiny 0-16kb,小对象small 16-32kb,大对象>32kb。
19.内存分配函数也是一个触发垃圾回收的入口。
20.gc由守护goroutine来执行。
21.经典垃圾回收算法有:标记-清除、标记-压缩、半空间复制、引用计数等。
22.golang从1.8之后的垃圾回收算法为:并发三色标记法+混合写屏障机制。
黑色:自身可达且指向对象标记完成。
灰色:自身可达但指向对象未标记完成。
白色:尚未被标记,可能是垃圾。
23.使用广度优先遍历。
24.分级别的内存管理机制会将垃圾回收产生的碎片控制在内部。
25.golang的内存逃逸机制,判断对象的生命周期如果更长,就会转移到堆上。生命周期短的会被分配到栈上,随方法结束被回收。
26.强弱三色不变式。
27.插入写屏障保证强三色不变式。
28.删除写屏障实现若三色不变式。
29.因为屏障有性能损耗,所以无法用于栈对象,因栈是轻量级,且变化比较频繁。
30.混合写屏障。
31.跨栈的对象会进入堆中。
32.gc触发分为三类,1是定时触发,两分钟一次,2是对象分配时达到堆内存阈值,3是手动调用。
33.标记准备工作
34.开启的标记goroutine数量占p资源的25%,开启的标记goroutine会放到池子中。
如果p能被4整出,则按p的数量进行计算,否则按执行时间计算。
35.stop the world 将所有的p设置为stop状态。
36.开启写屏障后,对象会被放入缓冲队列中,在标记完成前取出置灰。
37.p在获取可执行goroutine时,会判断是否在gc阶段,保证p只会获得一次gc goroutine。
38.每种颜色的对象都在一个队列里。
39.每次弹出一个灰色对象,把他指向的对象变成灰色,把自己变成黑色。
40.标记模式:
41.对象是否在使用的标识利用mspan的bitmap。
42.标记的本质是修改mspan中bitmap的标记位。
43.扫描根对象是扫描每个goroutine对应的方法栈,会根据函数的入参进行扫描,如果是引用类型,才需要去判断其指向的对象。非指针类型利用函数销毁即可完成回收,指针类型才有可能引用堆上的对象。
44.每个函数栈都有一个bitmap,来标识参数。
45.通过heapArena可以实现从页到mspan的映射。
46.辅助标记,当goroutine有负债的时候,会先尝试从全局资产中窃取,如果窃取不到,就要进行辅助标记进行偿还。
47.标记终止。
48.清扫goroutine会在main方法中初始化,陷入阻塞,被终止标记唤醒,清扫完一个goroutine后就会让出p,直到所有goroutine清扫完成后重新陷入阻塞。
49.重新设置下次触发gc的阈值,阈值可通过用户设置,默认为100%。
50.将内存还给操作系统:runtime会启动一个回收协程,以1%cpu的利用率运行,持续回收内存。
利用基数树辅助查询。
51.GC 阶段中的 STW 操作
Golang 的垃圾回收在开始和结束时都有一个短暂的 STW 阶段:
-
GC 开始阶段:当垃圾回收器开始标记对象时,STW 会暂停 Goroutine,以确保 GC 能够安全地扫描所有栈和堆,获取根对象。
-
GC 结束阶段:标记完成后,再次需要 STW 来处理最终的清理和调整操作,确保所有引用都已经正确处理。
string:
1.string会被分配到只读内存段,不可被修改,即使切换到unsafe类型。修改会重新分配内存。
2.stringstruct包含两个字段,str和len,str是内存的起始位置,len表示长度为多少字节。
mysql:
1.page页大小为4kb。mysql一次取16kb。
2.mysiam引擎叶子节点存储指针,类似于非聚簇索引,好处在于不会频繁导致页分裂,从而降低增删数据成本。
高性能MYSQL(学习笔记)—索引篇3_剧组索引-CSDN博客
3.mysql数据量到达两千万或三千万后性能会急剧下降,因为树的深度变身,io次数增加。
4.innodb三大特性:1.自适应哈希、2.buffer poll、双写缓冲区。
5.自适应哈希:1.不需要手动添加,2.底层是散列表,3.只使用等值查询。
6.buffer pool即缓冲池,由缓存数据页和数据块组成。默认大小128m。
7.page页大小为16kb,数据块为数据页的5%,大概800字节。
8.mysql通过哈希表判断数据页是否在缓冲池中。
9.配置页会根据状态分为三种,通过三个链表进行管理。
freelist表示空闲缓冲区,管理freepage,只保存对应的控制块。
flushlist表示需要刷新到磁盘的缓冲区。管理dirty page。指被修改过的page页。
lrulist表示正在使用的缓冲区,用来管理没有被修改的数据页cleanpage和dirtypage。
10.写缓冲区change buff是针对二级索引页的更新优化。占用的buffer pool的空间。
如果要更新的页没有在缓存中,就会先把更新操作缓存起来。查询时如果更新操作在缓存中,就更新到页离。
11.写缓冲区只适用于非唯一索引,因为唯一索引需要做唯一性校验,需要io页。
12.传统lru算法存在的问题是,当进行大范围数据查询时,会将真正的热数据替换掉。mysql的预读也会影响真正的热数据。
13.优化过的lru分为热数据和冷数据区,数据新进来的时候插入到冷数据的头部。
如果存在时间超过1秒钟,就会将它移动到热数据的头部。
14.索引最好不要超过五个字段。
15.创建索引的原则。
16.page页分为几种:数据页,undo页,系统页,事务数据页等。
fileheader:文件头,描述页信息。
pageheader:记录页的状态。
infimun和supermum records用来记录比行记录主键都小的值和比行记录主键都大的值。
user records:记录的是实际行的记录。
free space:记录空闲空间。
page directory:查找page页记录的信息。
file trailer:检测数据完整性。
17.页整体分为三个部分,通用部分(文件头和文件尾)、存储记录空间、索引部分。
通用部分有指向上一页和下一页的指针。
18.数据页中的行记录由记录头使用单向链表串联起来,page directory实现了目录功能,可以使用二分查找。
19.mysiam的索引都是指向数据存储的地址的,因此也不存在回表。
20.聚簇索引和非局促索引的区别。
21.全文索引
22.联合索引创建时,mysql会根据最左边的字段进行排序,所以要查询时要遵循最左原则。
23.索引下推:在遍历的过程中进行判断,减少回表次数。
24.想解决%在左边的模糊查询索引失效问题,使用覆盖索引。会从全文扫描变成全索引扫描。
25.自增id的缺点:高并发情况下竞争自增锁。
26.页插入大小到15/16时就会插入下一个页,留下的空间为以后更新用。
27.b树主要用于文件系统及部分数据库索引,如mogodb。
28.根节点保存在内存中,其余节点保存在磁盘。
29.一个b+树能存储的最大数据量。
30.explain用于模拟sql的执行过程。
31.分页查询在偏移量固定时,随返回条数性能下降,同理,在条数固定时,随偏移量增加性能也下降。因为分页查询每次都是从第一条开始扫描。
32.优化方案:1.利用滚动翻页。2.利用子查询,实际上与1一致。
33.排序有全字段排序和rowid排序两种。
34.间隙锁用于范围查询和范围删除,解决幻读问题。在可重复度和串行化时生效。
35.间隙锁唯一目的是防止其他事务插入间隙,造成幻读现象。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
redis:
sds:
1.redis中的string为sds类型,即动态字符串。
2.redis不使用c中的字符串是因为:1.获取长度困难,2.非二进制安全,3.不可修改。
3.sds可以进行扩容。
intset:
4.intset是set集合的一种实现方式,基于整数数组实现,具备长度可变和有序性。
5.contents存储的是指针,真正的数字大小由encoding决定。
6.存储的元素所占字节一样,是为了方便寻址。
7.超过预定大小会进行类型升级,整个数组都要统一类型,倒序重新放置。
8.插入时使用的是二分查找,找到要插入的位置,后面的元素后移。
dict:
9.dict由3部分组成:1.哈希表,2.哈希节点,3.字典。
求一个数的余数,相当于&该数-1。sizemask的用处。
10.如果有hash冲突,新来的元素会占据头位置。
11.dict在每次新增时会检查是否需要扩容,根据元素个数/数组长度来计算出负载因子。
12.如果是新建的hash表,初始大小为4.
13.扩容会在原基础上+1,寻找到大于该数的2的n次方的数。
14.在删除元素的时候会检查用不用收缩,负载因子小于0.1进行收缩。
15.收缩最小不能小于初始化长度4.
16.dict中有两个hash表,扩容时会创建新的放到ht[1],等迁移完成后,重新放到ht[0]。
17.扩容时渐进式进行的,每次增删改查时,迁移一个位置。
ziplist:
18.ziplist是压缩列表,使用连续的内存模仿双端链表,可以省去指针占用的内存。
19.entry的长度是不固定的,以此来达到节省空间的目的。
20.查询数据必须遍历,会有性能的损耗。所以ziplist长度不宜过长。
21.连锁更新也会造成性能损耗。
quicklist:
22.结构为双端列表,用于解决ziplist的长度限制问题,每个节点都是一个ziplist。
skiplist:
23.最多允许32级指针。
redisObject:
24.redis中任意的键值都会被封装成redisObject。
25.每个对象头都有固定的空间占用,存储多个string占用的空间比用list要大。
string:
26.string有三种编码方式,如果存储的是数字,则不需要sds对象。如果是字符串,有raw和embstr两种编码方式,raw编码时对象头和sds的内存是不连续的,通过指针指向。embstr编码时,内存是连续的。
27.选择44字节进行判断是为了方便内存分配,object信息+存储的信息=64,正好是内存的一个分片,可以避免内存碎片。
28.使用object encoding命令可以查看key的编码方式。
list:
29.list类型可以从首尾操作元素,使用quicklist实现。
30.默认ziplist为8kb,压缩等级为1,首尾各有一个节点不压缩。
set:
31.单列集合,元素唯一,不保证顺序,可以求交并集。
32.set需要经常判断元素存不存在,所以链表结构不适合查询。
33.使用哈希表,即dict。value统一放null。
34.如果存入的都是整数,且元素数量不超过阈值,使用intset。
35.intset编码后续有可能转为dict。
zset:
36.zset同时使用dict和skiplist。
37.当数据量过小时,会使用ziplist作为底层结构。
hash:
38.hash和zset一样,在数据量小的时候用ziplist,数据量超过阈值,使用dict。
内核态和用户态:
39.提高io效率核心要解决的问题是等待时间和拷贝时间。
io模型:
40.五种io模型
阻塞io:在数据没有到达时会陷入阻塞。
非阻塞io:循环调用数据有没有到达,到达后拷贝数据也会陷入阻塞。
io多路复用:利用单线程同时监听多个fd,即文件描述符。
select、poll在有数据到达时不知道具体的fd是哪个,epoll是直接知道的。
41.select有数量限制,poll没有数量限制。
42.select底层使用的bitmip上线1024,poll底层使用的链表,虽然无上限,但是过长会影响性能。epoll底层使用红黑树。
43.异步io也是比较好的方式,只是用户代码实现需要考虑并发问题。
44.redis多路复用会监听3个时间,客户端连接,客户端可读,客户端可写。
45.单线程模型的瓶颈主要在读数据。引入多线程进行读写客户端请求和解析命令。
过期策略:
46.每个redis的db都是一个redisDb对象,里面包含多个dict字典。
47.redis过期使用惰性删除。访问key的时候如果过期则删除。
48.周期删除:定时抽样部分过期数据进行删除。
49.周期删除有两种模式。
淘汰策略:
50.redis是在每次处理命令之前,检查内存。
51.每个redisObject对象中都会记录key的访问时间或访问次数,根据根据配置不同,记录信息不同。
52.redis使用的不是严格的lru和lfu,每次是挑选样本进入淘汰池,从淘汰池中淘汰。因为池子会满,所以最后会趋近lru和lfu。
哨兵模式:
53.redis快的原因,纯内存操作、单线程无上下文切换、渐进式rehash、缓存时间戳、多路复用。
扩容也是两倍扩容,利用空间换时间,有两个数组,交换使用。
获取时间戳是系统调用,所以redis有定时任务来缓存时间戳。
54.redis为什么不用多线程:1.redis的性能瓶颈不在cpu,在内存和网络。使用redispipeline可达到每秒钟百万请求。
55.redis高级功能:1.慢查询,2.pipeline,3.watch,4.lua脚本
56.redis的事务是弱事务,不支持回滚操作,只能做语法判断。
57.看门狗问题。
58.redis和memcache。redis是支持集群的。memcached是预申请内存。
59.过期key有可能造成主从不同步。
60.hyperloglog的每个输入都会进行hash转换成二进制串。
每一个二进制串,我们都可以看作是一个伯努利过程。
每个二进制位可以当做硬币的正反面,通过出现1的次数,估算进行了多少次投币。
【Redis笔记】一起了解 Redis 中的 HyperLogLog 算法?-CSDN博客
61.rdb为快照,不适合实时持久化,有数据丢失问题。
aof的效率没有rdb高。
生产可以使用混合模式。
62.mysql和redis数据一致性的保证,容易出现的问题。
63.redis集群功能的限制。
64.集群不可用情况。
65.redis分槽使用的是一致性哈希的改进,虚拟一致性哈希。
66.redis槽一共16384个,不设计更多的槽是因为在1000个节点的情况下,16384个槽就够用了,如果节点过多,节点间通信会占用大量的网络带宽。
67.redis的哈希槽使用bitmap进行存储。
68.redis集群写入数据有可能丢失,redis不提供数据一致性保证。
69.提升redis性能。
主节点不开启持久化,从节点进行持久化。数据比较重要,主节点就使用aof。
主从复制速度的保证,尽量在同一内网。
70.数据更新之前被度去过两次,可以成为热数据。
71.redis阻塞的情况:1.来自客户端的命令执行耗时,如keys*,hgetall等。2.大key的获取和删除。3.清空库,4.aof同步写。
etcd:
raft协议:
1.etcd是一个键值对存储组件,可以实现配置共享和服务发现。
2.etcd满足cap理论的c和p指标。
3.etcd内部通信使用grpc。
4.使用mvcc将数据记录在boltDB中。
5.etcd中的数据提交前都会先记录到日志中。
6.利用日志和快照机制实现故障恢复。
7.raft算法需要超过半数节点同意,不包括半数。
8.预写日志保证了操作顺序,保证最终一致性的核心。
9.数据足够新的follower才能竞选leader。
10.leader在发送预写日志时,会带上上一个预写日志索引。
11.索引包含任期和索引。
12.如果缺少索引,会向主节点请求。
13.如果日志索引超前,从节点就会删除超前日志,以主节点为准。
14.索引有两个,一个是apliedindex,一个是commitindex。
15.解决读不一致性的方案,1:都从leader节点读,2:写入时主节点返回最新的index,读的时候带上。
16.写记录现在leader执行,然后通知follower,得到响应后提交。
17.如果有两个leader,在收到消息后,会判断任期,如果自己更靠前,则拒绝,让另一个leader退位。
18.follower中会有定时器,一段时间没有收到leader的心跳,就会发起选举。
19.发起选举时,如果任期相同,会判断索引大小,索引大于等于自己,就会同意,否则拒绝。
20.如果拉票超时,就会增加任期值,从新发起。
21.配置变更和增加节点都需要leader节点同步。
22.节点变更期间的选举,只有老节点可以参与投票,处理请求也是如此。
23.为了解决瓜分选票造成的无限循环问题,在心跳超时和选举超时时间上会加上扰动值。
24.每个leader上任后,必须提交一笔自己任期内的写数据操作。
25.leader向follower同步数据,超时会进行重发。
26.通过幂等处理leader成功提交,但未完成响应客户端的情况。
27.为了规避无意义竞选,follower在发起竞选前,会提前试探,有多数派回复,确认自己网络没有问题,才会竞选。
28.ack重试机制保证客户端数据不丢失,幂等序列号,保证请求的唯一性。
etcd实现:
29.etct结构分为应用层和算法层。
30.应用层和算法层都使用for+select监听对方的消息。
31.算法层本质上是一个节点状态机。
32.两次提交指的是预写日志提交和真实提交。
33.预写日志有两种类型,一种是配置变更如节点增加或减少,另一种是正常写请求。
34.预写日志会被封装到message中。message有多种类型。
35.算法层只能对持久化日志进行查询,真正的持久化是在应用层做的。
36.算法层会将预写日志先缓存在内存中,收到应用层可提交的消息后,通知应用层持久化。
37.node是应用层和算法层通信的入口。
38.强制读主的方式需要leader去自证自己是主节点,没有出现分裂。
39.raft是算法层的核心类。
40.应用层是提供服务的入口。
41.etcd默认是线性读。
42.数据持久在boltDB中。
43.mvcc是一种乐观锁。不能解决更新丢失问题。
44.读请求会请求leader拿到最新的index,然后判断自己的节点是否和最新的一致。
45.在 etcd v3 中引入 treeIndex 模块正是为了解决这个问题,支持保存 key 的历史版本,提供稳定的 Watch 机制和事务隔离等能力
。
46.etcd 在每次修改 key 时会生成一个全局递增的版本号(revision)。
然后通过数据结构 B-tree 保存用户 key 与版本号之间的关系;
再以版本号作为 boltdb key,以用户的 key-value 等信息作为 boltdb value,保存到 boltdb。
47.当etcd服务器重启后,内存中的treeIndex数据确实会丢失,因为treeIndex是内存中的数据结构,用于快速索引和查找键值对。但是,etcd具有持久化存储机制,它将数据写入磁盘以在重启后恢复数据。
具体来说,etcd使用一种称为WAL(Write-Ahead Log)的机制来持久化数据
。每当在etcd中进行更改(比如添加、更新或删除键值对)时,这些更改会被追加到WAL日志中。WAL日志是一个顺序写入的日志文件,它确保了数据的持久性和一致性。
在etcd启动时,它会读取WAL日志文件并重新构建内存中的树状结构
。
48.在treelndex中,每个节点的 key是一个keyIndex结构,etcd就是通过它保存了用户的key 与版本号的映射关系。
49.那么 key 打上删除标记后有哪些用途呢?什么时候会真正删除它呢?
-
一方面删除 key 时会生成 events,
Watch 模块根据 key 的删除标识,会生成 对应的 Delete 事件
。 -
另一方面,当你重启 etcd,遍历 boltdb 中的 key 构建 treeIndex 内存树时, 你需要知道哪些 key 是已经被删除的,并为对应的 key 索引生成 tombstone 标 识。
而真正删除 treeIndex 中的索引对象、boltdb 中的 key 是通过压缩 (compactor) 组件异步完成
。
正因为 etcd 的删除 key 操作是基于以上延期删除原理实现的,因此只要压缩组件未回收历 史版本,我们就能从 etcd 中找回误删的数据。
50.在 etcd v3 中,为了解决 etcd v2 的以上缺陷,使用的是基于 HTTP/2 的 gRPC 协议,双向流的 Watch API 设计,实现了连接多路复用。
在 HTTP/2 协议中,HTTP 消息被分解独立的帧(Frame),交错发送,帧是最小的数据单位。每个帧会标识属于哪个流(Stream),流由多个数据帧组成,每个流拥有一个唯一的 ID,一个数据流对应一个请求或响应包。
50.etcd 的确使用 map 记录了监听单个 key 的 watcher,但是你要注意的是 Watch 特性不仅仅可以监听单 key,它还可以指定监听 key 范围、key 前缀,因此 etcd 还使用了如下的区间树。
当收到创建 watcher 请求的时候,它会把 watcher 监听的 key 范围插入到上面的区间树中,区间的值保存了监听同样 key 范围的 watcher 集合 /watcherSet。
51.定时自动淘汰(leader做的)
leader 在 etcd server 启动时会启动一个 goroutine RevokeExpiredLease() , 每 500ms 检查一次。
使用 最小堆 管理 lease,按到期时间升序排序,每次检查时从堆顶取出元素,检查lease过期时间
52.Checkpoint 检查点机制(leader做的)
leader 在 etcd server 启动时会启动一个 goroutine CheckpointScheduledLeases(),每 500ms 将 lease 的 ttl 通过 raft 同步给 follower
RabbitMQ:
1.4种交换机类型。
2.消息可靠性保证。
3.生产者确认有两种方式,同步和异步。
4.如果没有开启消息持久化,当内存满了,mq要将一部分数据迁移到磁盘,会造成阻塞。
5.数据可靠性保证有两种方式1:数据持久化,2:lazyqueue。
5.消费者确认机制。
6.消息失败可以先在本地重试,不要直接重新投递给mq。
7.利用死信交换机实现延迟队列。
8.底层使用最小堆实现。
9.RabbitMQ 的元数据都是存在于 Erlang 自带的分布式数据库 Mnesia 中的。
10.在实现上,每台 Broker 节点都会保存集群所有的元数据信息。当 Broker 收到请求后,根据本地缓存的元数据信息判断 Queue 是否在本机上,如果不在本机,就会将请求转发到 Queue 所在的目标节点。
11.Mnesia 本身是一个分布式的数据库,自带了多节点的 Mnesia 数据库之间的同步机制。
12.如果消费者(处理消息的程序)连接到节点 B 来读取队列的消息,节点 B 会自动把这个请求转发到节点 A,因为队列存在于 A。
13.集群模式可以为节点添加镜像队列节点,同步节点的数据。
镜像队列可以让队列的数据在多个节点之间复制。例如,你可以让队列的副本同时存在于节点 A 和 B 上。这样,即使节点 A 挂了,节点 B 依然有该队列的完整副本,可以继续处理消息。
14.不支持有且仅有一次的保证。
15.RabbitMQ 从队列中取出待处理的消息,并将其分发给消费者 A
。
消息的状态在分发后会被标记为“正在处理中”,即消息进入了一个冻结状态。
16.RabbitMQ 使用文件来存储持久化消息。这些文件通常位于 RabbitMQ 的数据目录(如 /var/lib/rabbitmq/mnesia/
)中,文件格式通常是二进制格式,专门设计用于高效存取和存储消息。
17.队列元数据:队列的元数据也会被持久化到磁盘中,包括队列的定义、绑定信息等。这些数据存储在 Mnesia 数据库中,这是 RabbitMQ 用来管理内部状态的数据库。
es:
1.非关系型文档数据库。
2.pb级数据秒级查询。
3.稳当是json格式。
4.mapping中的类型,数字类型,keyword:精确查找不进行分词,text:文本类型会被分词,时间类型,alias:别名。另外还有结构类型,如坐标类型。
5.可以不定义结构直接写入数据,会动态判断自动创建。
6.映射方式分为自动和手动两种。
7.全文检索就是会被分词的检索。
8.检索和搜索的区别,检索是指相关度。
9.全文索引步骤:切词、规范化、去重、字典序。
10.将用户输入进行分词,每个词进行查询,命中次数最多的id,相关度就高。
11.should在和must或者filter同时出现时,should会失效。
12.filter是不计算相关度的,效率相对高一些。
13.嵌套查询:object、nested、join。
14.按查询准确度可以分为:1.全文检索match、2.精确查找term、模糊匹配:suggester、通配符正则等。搜索提示是基于suggester实现的。
15.match会进行分词搜索,term不会。
16.如果使用.keyword,查询的就是不分词的。
17.match和term是针对输入词的,keyword是针对源数据的。
18.评分算法:BM25和tf-idf。
19.倒排索引的存储结构为fst。
20.倒排表使用压缩算法:稠密数组for,稀疏数组roaring bitmaps。
21.通用最小化算法。
22.fst 和 trie字典树又称前缀树。
23.数据模型。
- 倒排索引:用于存储词项到文档 ID 的映射,优化了全文搜索。采用压缩的索引结构,如跳跃表、B+ 树或哈希表。
- 文档 ID 到文档的映射:存储字段以列式存储方式存储,确保快速提取所需字段。
- DocValues:为排序和聚合操作设计的列式存储结构,提供高效的数据访问。
- 段:包含上述所有结构的基本单元,每个段都是独立的索引,段合并优化了存储和查询效率。
24.段和文档的关系,段是不可修改的。
段存储的是文档数据,因此它们之间的关系可以这样概括:
- 文档被写入 Elasticsearch 时,Lucene 会将其组织到段中。段中包含多个文档,这些文档会按照倒排索引的方式被存储,便于高效检索。
- 每个段包含文档的多个部分,包括:
- 倒排索引(Inverted Index):文档中的词项(Token)到文档 ID 的映射,用于支持全文搜索。
- 存储字段(Stored Fields):文档的实际内容(如 JSON 格式的数据),以便在搜索时返回文档的原始数据。
- DocValues:用于排序和聚合的字段值。
- 删除标记(Delete Markers):如果文档被删除,则该文档不会立即被物理删除,而是在段中添加删除标记,直到段合并时才真正删除这些标记的文档。
25.FST 的作用是:
- 通过快速查找定位词项在倒排索引中的位置;
- 一旦定位到词项,倒排索引会提供对应的文档 ID 列表,这个列表可以包含与该词项相关的所有文档。
26.每个文档存储在某个 段(Segment) 中,每个段会存储文档的元数据以及文档的实际内容。Elasticsearch 会根据文档 ID 查找到具体的段文件,然后在段文件中读取该文档的内容。
27.每个文档都有一个全局的文档 ID(用户提供的 ID),而 Lucene 内部为每个段中的文档分配一个局部的 DocID,这个 DocID 仅在段内部有效。
28.查询示例。
- 段扫描顺序:通常是从旧到新,或者从小到大,但实际操作中可能会并行扫描多个段。
- 并行扫描:多个线程可以并行扫描不同的段,以提高查询效率。
- FST 和倒排索引:在每个段中,首先使用 FST 查找关键词,然后使用倒排索引获取匹配的文档 ID。
- 结果合并:最终结果会在所有段的匹配结果中合并、去重和排序后返回给用户。
29.数据先写入内存buffer中,当到达时间阈值或空间阈值时,会写入到索引段文件中,段文件将数据放入到OS Cache中。
30.当数据放入到OS Cache中后,索引段文件将处于open状态,此时数据就可被查到了。
31.当OS Cache数据达到阈值,会fsync到磁盘中。
32.读写分离的意义在于减小磁盘的读写压力。
33.commit point会将段文件进行合并。
34.数据写入内存buffer中时,会同步写入到tranlog中,以保证数据的完整性。
35.es多节点写入流程。请求的节点会找到写入数据对应的主节点,写入主节点之后,主节点同步副节点,副节点完成写入后,通知主节点,主节点将结果通知请求节点,请求节点响应客户端。
36.拼写纠错和模糊查询利用fuzzy实现。fuzziness步长最大为2。不指定的话为auto,会根据字符总长度来给定。
37.距离算法为damerau-莱文斯坦距离。
38.多个分片组成一个index,每个分片都是一个单独的lucene实例。
39.主分片可读可写,副本分片是只读的。
40.分片创建策略:分片在创建之前要指定分片的数量和大小。
分片的分配策略:要尽可能均匀的分配到不同的节点上。
再分配策略:当有节点加入或离开时,会重新分配。
延迟分配策略:再分配会延迟一分钟执行。
分片的数量策略:不宜太多或太少。
分片的大小策略:10-50g
41.1gb堆内存能管理的分片在20左右。
42.两个节点不具备选举能力,稳定的集群至少需要三个节点。
43.索引的组成部分。
44.索引的分片的数量和副本的数量是在settings中设置的。
45.索引有三种含义:1.es的index,2.索引文件,3.动词写入数据。
46.es会有两个进程定时检查主节点和副节点的存活状态。masterFD和nodesFD。通过其他节点给master节点发送请求。
47.当副节点发现master节点失联,或者主节点能联系到的副节点小于n/2+1会发起选举。
48.从配置了master角色的节点中选举。
49.fd发起请求,找到响应请求的所有节点,不包括发起的节点。经过过滤找到符合选举的节点。
如果已经存在活跃master,而且不是自己,则停止。
判断候选节点是否满足指定票数。
50.选举完成后要将节点信息分发到每个节点。
51.集群节点数量为偶数时,es会使一个节点失去选举权,避免脑裂。
grpc:
1.四种模式:单一模式,客户端流,服务端流,双向流。
2.基于http2实现。http支持流和帧的多路复用。
分布式:
1.补偿性事务:
TCC的核心思想是:针对每一个操作都需要注册一个和其相对应的确认和补偿的操作,他分为三个阶段Try、Confirm和Cancel
2.分布式事务使用base理论,实现最终一致性。
3.分布式唯一id:
uuid、数据库子增、批量生成id、redis生成id、雪花算法snowflflake。
4.负载均衡算法:
轮询、加权轮询、随机轮询、最少链接(适用redis实现)、源地址散列(适用session和长链接)
5.计数器,即固定窗口算法。
适用redis的incrby和过期时间实现。
风险在于无法应对突增。如服务器限流60,在00:50有50个请求,在01:00有50个请求。
6.滑动时间窗口
将窗口分为小粒度的几个固定时间窗口,进行计算。
7.漏桶算法
8.令牌桶算法
9.数据库处理大数据
分区、分库、分表、读写分离。
10.服务熔断,服务熔断为了解决服务雪崩,如a->b->c->d,如果d有问题卡住,调用链路都会卡住。
触发熔断之后需要添加标记,后续走降级策略。
11.降级和熔断
熔断会触发降级,但降级还有多种情况。
12.提升系统并发能力
分流:负载均衡、消息队列、数据库拆分
导流:缓存、cdn
13.微服务划分:准确识别系统隔离点也就是系统边界。
14.最大努力消息通知
消息重复通知、消息可查询校对。
内部系统可使用消息队列,外部系统暴露接口。
15.解决缓存不一致问题(cache-asid),使用read/write through,即写入数据时直接写入到缓存中,再由缓存同步数据库。
项目启动时先从数据库读取到缓存中。
gin:
1.基于http/net包实现。
2.gin的特点。支持中间件headlerschain,方便的使用gin.context 并发安全的,路有数radix tree。
3.gin和http包的关系。
蓝色代表gin的功能,红色代表gin的结构,黄色代表http包。
4.gin.engine结构。
5.gin.engine是实现了http接口的实例。
6.sync.pool中包含Context,用来复用。每个请求都会分配一个Context。
7.如果池子里有Context,将会把数据清空,用来复用。
8.自动清理的能力,当两轮gc之后,还未被用到的Context就会被清除。
9.routergroup中所有的配置会被这个组中所有的成员共同使用。会在自己路径之前拼接上routergroup的路径。
10.routergroup中的中间件也会被成员共同使用。
11.engine中会有九棵压缩树,对应http的九种请求,节点会根据路径挂载在树上,当请求到达时,再通过树去查找对应的headers。
12.所有实现了http包handler方法的结构体,都可以被注入到net的框架中。
13.routergroup中表示是否是根节点。默认分配为是。
14.在engine被创建出来时,会初始化Context池。
15.使用中间件会将中间件添加到group所对应的handler的切片中。
16.注册handler流程。
17.核心就是把自己当做http的实现类。
18.http启动之后会循环监听对应的端口,每次调用端口端口监听器的accept方法。使用epoll技术。
19.针对到达的请求会分配goroutine去服务,找到启动时注入的handler,也就是gin的实现。
20.gin框架处理流程。
21.Context用完之后没有进行清理,获取时才执行清理。
22.根据请求的方法找到对应的请求压缩前缀树,从前压缩前缀树中找到对应的处理链条。
23.获取到链路后会挂载到对应的Context上。承载请求参数和处理链条。
24.gin为什么使用压缩前缀树,map适合单点操作,无法应付模糊。
25.补偿机制:每次都是从节点的左边开始遍历子树。
26.Context结构。
27.gin使用sync.pool实现Context复用。
28.sync.pool是物理意义上的缓存,但内存回收不稳定,需要两轮gc。
29.Context中index字段表示遍历到了哪个handlers。
30.最多只能注册62个handler。
31.用户可以在某个handler中调用Context.about。该方法会将index值设置为63。
redigo:
1.连接池使用双端链表存储。
2.获取时从头部获取。
3.close方法是将链接放回链接池,并不是关闭。
gorm:
1.内部使用很多反射,性能较低。编译阶段不容易发现问题。
2.builder设计模式,分为存储数据+处理数据两步。
3.高频重复拼接sql。
docker:
k8s:
1.kube-proxy ipvs和iptables的异同。
都是通过netfilter内核进行转发的。
iptables是为防火墙设计的,ipvs是专门用于高性能负载均衡。使用更高效的hash表结构。
ipvs有更好的性能和扩展性,支持更复杂的负载均衡算法,支持服务健康检测和连接重试的功能。
支持动态修改ipset设置。
网络:
1.tcp/ip网络分层架构。
2.tcp四次挥手。
如果减少为3次,服务端将客户端fin的响应和自己fin的响应合二为一,中间可能会有延迟,因为http是可靠传输,客户端会不停地发起fin请求,造成资源浪费。
3.凡是对端的确认,无论客户端确认服务端,或者服务端确认客户端,都要消耗tcp报文的序列号。因为有重试的行为。
4.半连接队列和syn flood洪泛攻击,
5.tcp fast open利用cookie减少一次请求的往返。
6.tcp报文中时间戳的作用。计算往返时间和解决序列号重用问题。
7.重试时间如何计算,
平滑往返时间。根据上次的重传时间进行计算。适用于波动比较小的时候。
8.tcp流量控制。对于接收方和发送发,都需要把数据放在自己的缓冲区。交互时会将自己的缓冲区大小告诉对方。
缓冲区使用滑动窗口进行。
9.keep alive会定时检查链接存活状态,但是需要7200s,一般不用。
10.端口在传输层的头里面。一个是源端口,一个是目标端口。
临时端口是从48152-65535的。
11.tcp的确认号计算。
12.对应的协议。
13.消息边界
14.netstat用于显示网络连接信息。
15.命令行抓包工具tcpdump。
16.len长度为0的数据是握手和挥手数据。
17.windows抓包工具winreshark。
18.tcp和udp的区别。
19.http是无连接的,指的是交互完成后链接就断开了。
20.http是无状态的。
21.https是http+ssl/tls。
22.ssl+tls处于tcp/ip协议和应用层协议中间的位置。也可以说属于应用层。
23.服务端证书相当于公钥。
24.http1.0只能保持短暂链接,发送大文件需要多次握手。需要提供规范,connection,最常见的为keep-alive。只有get和post两种请求方式。
http 1.1是使用最广泛的协议,支持保持链接,默认包含keep-alive。不需要等待服务端返回就可以发送下一次请求。支持缓存的控制。
25.http2.0 支持多路复用,二进制分帧。
使用二进制传输。
首部压缩。第一次发送头部,后面发送头部的差异就行。
允许服务端推送。
26.cdn内容分发网络,中心平台服务器进行分发,分配到离你最近的边缘节点服务器。
27.在解析域名时,指向的是cname,即cdn的节点,而不是源服务器。
28.cdn全局负载均衡,会根据用户ip找到用户的真实地址,根据用户的真实地址,运营商和节点的压力,为用户分配合适的节点。
29.cdn缓存,如果发现用户请求的资源在缓存中有,会直接返回。命中率在90以上。
30.域名。
面试题:
1.mysql同一个事务里语句不同表的执行顺序会影响效率吗。
据说有锁释放时机的问题。
2.mysql索引失效。
要尽量满足全值匹配
要满足最佳左前缀法则
主键插入顺序尽量自增
计算、函数导致索引失效
类型转换导致索引失效
范围条件右边的列索引失效
没覆盖索引时,“不等于”导致索引失效
没覆盖索引时,is not null、not like导致索引失效
没覆盖索引时,左模糊查询导致索引失效
“OR”前后存在非索引列,导致索引失效
不同字符集导致索引失败,建议utf8mb4
3.redis删除大key,hash类型。不知道有哪些成员。
使用hash的迭代器,一点一点的删除成员。
4.查询字符串类型的字段where条件不加单引号,和查询int类型,where条件加单引号。
前者会失效,后者不会。
5.有序数组的归并。
func mergeSortedSlice2(l, r []int) []int {
lOldLen := len(l)
l = append(l, make([]int, len(r))...)
i := lOldLen - 1
j := len(r) - 1
index := len(l) - 1
for {
if i > -1 && j > -1 {
if l[i] > r[j] {
l[index] = l[i]
i--
} else {
l[index] = r[j]
j--
}
index--
continue
}
break
}
if i == -1 {
for i := j; i >= 0; i-- {
l[i] = r[i]
}
}
return l
}
6.切片和map作为入参时,在调用函数内修改,是否影响原值。
会影响原值,map是引用类型,传参虽然是复制,但复制的是地址。
切片虽然是值类型,但是是指向底层数组的指针。
注意,如果触发扩容则不会,传参前的切片指向的数组和内部函数修改完切片指向的数组不一样。
7.redis节点宕机。
如果负责分片迁移的节点宕机了,Redis 集群有一些机制来确保数据的一致性和可用性,但具体恢复和处理方式会依赖于集群的配置,尤其是是否启用了主从复制(replication)以及集群的故障转移机制(failover)。以下是Redis集群中常见的处理方式:
1. 主从复制与故障转移(Failover):
Redis集群通常会配置主从复制(master-slave replication)来增强容错性。如果负责迁移的节点(master)宕机,但该节点有从节点(slave)可以接管,那么Redis集群会触发故障转移(failover)机制。
-
主节点宕机:当一个负责迁移的主节点宕机,Redis集群中的从节点将会通过Raft或Gossip协议进行选举,以选出一个新的从节点来接管这个主节点的角色。
-
分片迁移继续:在新的主节点被选出后,分片迁移可以恢复。如果迁移的数据还没完全完成,Redis会继续在新的主节点上处理分片的迁移。客户端可能会短暂收到
CLUSTERDOWN
错误,但在故障恢复后,集群会恢复正常。 -
数据一致性:Redis在迁移过程中,通过保存的迁移状态和
MOVED
、ASK
等机制,确保当一个节点宕机后,客户端能够正确地被重定向到新的目标节点或负责分片的其他节点。因此,数据的写入和读取请求会被重新路由,保障一致性。
2. 没有从节点(没有冗余):
如果宕机的节点是一个没有从节点的孤立主节点,且在没有冗余节点的情况下宕机,那么可能出现以下几种情况:
-
分片不可用:如果负责某些哈希槽的节点宕机,并且该节点没有从节点可以接管,这些哈希槽(slots)暂时不可用。Redis集群将返回
CLUSTERDOWN
错误,表示集群处于不可用状态,直到问题解决或该节点被恢复。 -
数据丢失:如果宕机节点没有从节点,且节点无法恢复,分片上未迁移的数据可能会丢失。在这种情况下,你需要从备份中恢复数据。
-
人工干预:管理员可能需要手动重启宕机节点或重新分配这些哈希槽到其他可用的节点。在这种情况下,迁移会重新开始,新的节点将接管原来宕机节点的哈希槽。
3. 分片迁移状态的保存:
Redis集群在分片迁移时,会保存分片迁移的进度和状态。如果在迁移过程中节点宕机,当节点恢复后,它会重新尝试完成未完成的迁移任务。
-
迁移恢复:一旦宕机节点恢复,它会检查之前迁移任务的进度,并继续从中断的地方开始迁移。
-
Redis的持久化机制:如果Redis启用了RDB(快照)或AOF(Append Only File)持久化机制,即使节点宕机,也可以通过这些持久化文件来恢复数据状态,并在恢复后继续进行分片迁移。
4. 客户端的处理:
在迁移过程中,如果节点宕机,客户端可能会遇到以下情况:
-
短暂不可用:客户端在尝试访问宕机节点时,可能会短暂收到
CLUSTERDOWN
错误,提示集群不可用。 -
请求重定向:如果Redis集群成功故障转移或分片被其他节点接管,客户端请求会通过
MOVED
或ASK
指令被重新定向到新的目标节点。
总结
如果负责分片迁移的节点宕机,Redis集群通过以下机制来处理:
- 主从复制和故障转移:如果有从节点,集群会选举新的主节点继续迁移。
- 分片重定向:Redis会通过
MOVED
或ASK
指令重定向客户端请求到正确的节点,确保数据一致性。 - 数据恢复:节点恢复后,迁移任务会继续从中断的地方开始。
如果没有从节点或持久化机制,可能需要人工干预或从备份中恢复数据。
8.在缓存系统中,多个 goroutine 可能会同时请求相同的缓存数据。例如,多个用户请求相同的热门文章,如果每个请求都触发一次数据库查询,可能会导致大量的重复请求。
现场写的实现:
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
sMap := sync.Map{}
waitGroup := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
waitGroup.Add(1)
go func(i int) {
str, err := doFind("")
sMap.Store(i, str)
fmt.Println(i)
fmt.Println(str)
fmt.Println(err)
waitGroup.Done()
}(i)
}
waitGroup.Wait()
var i int
var j int
sMap.Range(func(key, value any) bool {
i++
if str, ok := value.(string); ok && str == "test" {
j++
}
return true
})
fmt.Println(i)
fmt.Println(j)
time.Sleep(10 * time.Second)
}
//在缓存系统中,多个 goroutine 可能会同时请求相同的缓存数据。例如,多个用户请求相同的热门文章,如果每个请求都触发一次数据库查询,可能会导致大量的重复请求
var i int32
func doFind(key string) (string, error) {
str, err := findCache()
if err != nil {
return str, err
}
if str != "" {
return str, err
}
do:
{
flag := atomic.CompareAndSwapInt32(&i, 0, 1)
if flag {
return updateData()
}
flag = atomic.CompareAndSwapInt32(&i, 1, 0)
}
for j := 0; j < 3; j++ {
str, err := findCache()
if err != nil || str != "" {
return str, err
}
}
goto do
}
var data string
func findCache() (string, error) {
return data, nil
}
func updateData() (string, error) {
time.Sleep(2 * time.Second)
data = "test"
return data, nil
}
gpt的回答:
在这种情况下,多个 Goroutine 同时请求相同的缓存数据,确实可能导致重复的数据库查询,影响性能。为了解决这个问题,可以引入单飞(single-flight)模式来避免重复请求。Go 标准库中的 sync/singleflight
包为这种场景提供了一个解决方案。
什么是 single-flight 模式?
Single-flight 是一种模式,确保当多个 Goroutine 请求相同的数据时,只有一个 Goroutine 触发实际的查询或操作,其他 Goroutine 等待第一个 Goroutine 完成查询并共享其结果,从而避免重复请求。
sync/singleflight
的用法
Go 的 sync/singleflight
包通过 singleflight.Group
实现了这种机制。下面是一个简单的使用示例:
package main
import (
"fmt"
"golang.org/x/sync/singleflight"
"sync"
"time"
)
var (
sfGroup singleflight.Group
cache = make(map[string]string) // 模拟缓存
mu sync.Mutex
)
func queryFromDatabase(key string) string {
// 模拟查询数据库
time.Sleep(2 * time.Second)
return "Value for " + key
}
func getValue(key string) string {
mu.Lock()
value, ok := cache[key]
mu.Unlock()
if ok {
// 如果缓存中有,直接返回
return value
}
// 使用 singleflight 防止重复查询
value, _, _ = sfGroup.Do(key, func() (interface{}, error) {
// 触发数据库查询
v := queryFromDatabase(key)
mu.Lock()
cache[key] = v // 将结果存入缓存
mu.Unlock()
return v, nil
})
return value
}
func main() {
var wg sync.WaitGroup
keys := []string{"article-1", "article-1", "article-2", "article-1"}
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
fmt.Printf("Fetching %s: %s\n", k, getValue(k))
}(key)
}
wg.Wait()
}
自己实现类似于 Go 的 sync/singleflight
的功能,核心思想是当多个 Goroutine 请求相同的数据时,确保只有一个 Goroutine 进行实际的查询操作,其他 Goroutine 等待该查询完成,并共享其结果。
我们可以通过以下步骤实现这个功能:
- 存储查询状态:我们需要一个全局的状态来跟踪当前是否有相同的请求正在进行,或者该请求已经完成。
- 同步机制:使用
sync.Mutex
来保护对查询状态的访问,防止并发读写时出现数据竞争。 - 等待机制:当有多个 Goroutine 等待相同数据时,它们需要等待第一个 Goroutine 完成操作,可以用
sync.Cond
或channel
实现这种等待机制。
package main
import (
"fmt"
"sync"
"time"
)
// 查询结果结构体
type result struct {
val string
err error
}
// 自定义的 single-flight 实现
type call struct {
wg sync.WaitGroup
val interface{}
err error
}
// 单飞缓存管理
type Group struct {
mu sync.Mutex // 用于保护calls
m map[string]*call
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
// 如果有相同的请求正在进行,等待现有的请求
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait() // 等待其他 Goroutine 完成查询
return c.val, c.err
}
// 否则,发起新的查询
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
// 调用真正的查询函数
c.val, c.err = fn()
// 查询完成,唤醒其他等待的 Goroutine
c.wg.Done()
// 查询完成后删除 call
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
// 模拟数据库查询
func queryFromDatabase(key string) string {
time.Sleep(2 * time.Second) // 模拟查询耗时
return "Value for " + key
}
func main() {
var g Group
var wg sync.WaitGroup
keys := []string{"article-1", "article-1", "article-2", "article-1"}
for _, key := range keys {
wg.Add(1)
go func(k string) {
defer wg.Done()
val, _ := g.Do(k, func() (interface{}, error) {
return queryFromDatabase(k), nil
})
fmt.Printf("Fetched %s: %s\n", k, val)
}(key)
}
wg.Wait()
}
8.es索引重建,和别名。
-
比如 order_202201,order_202202,order_202203 索引 起个别名 order,这样好处就是 可以关闭历史索引,加快查询数据。
-
比如 需求变更 导致 索引 mappings 调整,mappings 变更 添加还好,如果 更新 就必须删除索引重来,同步数据 (数据量较大) 耗时,如果删除 索引重建 会导致 上一个版本 服务暂时不可用。这个时候 如果使用 别名 就可以 无感知切换,索引重建到另一个索引上面,索引重建完成后 发布版本完成 在通过别名切换。这样就可以省去 同步 es 时候的 服务不可用问题。
9.将数组右移k位,比如[1,2,3,4,5]右移2位变成[4,5,1,2,3]
10.gorm连接大量失效。
网络波动、gorm设置的过期时间超过mysql本身的。
11.gorm连接池打满可能:
使用事务没有关闭、使用rows方法要关闭连接、使用事务,创建新变量。
tx := conn.Begin()
//下面一定使用tx,不要用conn了!!!!
12.es优化
尽量使用filter,不使用query。
去掉_all字段可以节省内存,es查询默认第一页。
索引压缩。
开启预热。
使用深分页。
13.keyword不支持聚合查询。支持模糊和精确查询。
14.es中任意一个节点都可以作为协调节点。
根据id查询流程:
根据字段查询流程:
15.text不支持排序和聚合,keyword支持。
16.优化写速度。对于日志系统,增加同步oscache的时间。
17.分段的好处。
18.分片过多会影响查询和写入性能。
19.在p数量固定为1时的协程输出。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
waitGroup := sync.WaitGroup{}
waitGroup.Add(10)
for i := 0; i < 10; i++ {
// time.Sleep(1 * time.Second)
go func() {
fmt.Println(i)
waitGroup.Done()
}()
}
waitGroup.Wait()
//waitGroup := sync.WaitGroup{}
//waitGroup.Add(10)
//for i := 0; i < 10; i++ {
// go func(i int) {
// fmt.Println(i)
// waitGroup.Done()
// }(i)
//}
//waitGroup.Wait()
}
p为1的情况主要还是看时间片的调度。
20.mysql间隙锁
间隙锁用于范围查询和范围删除,解决幻读问题。在可重复度和串行化时生效。
间隙锁唯一目的是防止其他事务插入间隙,造成幻读现象。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
21.那么何时使用行锁,何时产生间隙锁
只使用唯一索引查询,并且只锁定一条记录时,innoDB会使用行锁。
只使用唯一索引查询,但是检索条件是范围检索,或者是唯一检索但检索结果不存在(试图锁住不存在的数据)时,会产生 Next-Key Lock(临键锁)。
使用普通索引检索时,不管是何种查询,只要加锁,都会产生间隙锁(Gap Lock)。
同时使用唯一索引和普通索引时,由于数据行是优先根据普通索引排序,再根据唯一索引排序,所以也会产生间隙锁。
22. [4,5,1,2,3] 输出位移了几位。
遍历数组,找到值的下降处。
23.数组求乘积最大的三个数的积。
寻找最大的3个值,和最小的两个值,如果有两个负数值,考虑负负得正的情况。
24.求π值。
使用无穷级数来计算π值:这是数学中常用的方法。例如,格雷戈里-莱布尼茨无穷级数:π = (4/1) - (4/3) + (4/5) - (4/7) + (4/9) - (4/11) + (4/13) - (4/15) ... 通过不断迭代,这个级数可以越来越接近π的精确值。
25.桶里有100个黑球和100个白球,每次取两个,如果颜色相同放回去一个黑球,不同则放回去一个白球,求最后一个球是黑球的概率。
有的人上来就想该如何计算概率,实际上这是一道思维题,一种方式是我们可以通过找规律来得到结果。可以分析每种情况:
如果取到的全是黑球,需要放进去一个黑球,相当于黑球-1,白球-0。总球数-1。
如果取到的全是白球,需要放进去一个黑球,相当于黑球+1,白球-2。总球数-1。
如果取到的是一黑一白,需要放进去一个白球,相当于黑球-1,白球-0。总球数-1。
所以每次操作,桶里的球的总数总会-1,到最后桶里肯定剩1个球。
白球要么数量不变,要么减少2个。那么到最后剩的那个球一定是黑球。所以最后剩黑球的概率是1。
26.最大回文子串。
暴力破解、马拉车算法、逆序之后寻找公共子串。
12.基础提升 KMP、Manacher算法等_哔哩哔哩_bilibili
27.kmp算法,寻找子串
11.基础提升 有序表、并查集等_哔哩哔哩_bilibili
28.zset存储排行榜,先按收礼数量排序,再按更新时间排序。
将送礼数量作为高位,送礼时间作为低位。这样可以确保在送礼数量相同时,根据送礼时间进一步排序。
但是将不能使用incrby更新,可以使用lua脚本或者redis事务。
29.context。
context是一个树形结构,子上下文继承父上下文。
30.context的取消
取消信号的传播机制基于树状结构的遍历。每个上下文都会存储一个指向其子上下文的列表,当上下文取消时,会递归取消其所有子上下文。整个取消过程如下:
- 当调用
cancel()
时,首先关闭当前上下文的done
channel。 - 遍历
children
列表,递归调用子上下文的cancel()
,传递取消信号。 - 子上下文通过
parentDone
channel 感知到父上下文的取消,从而同步自身状态。
- 当创建子上下文时(如通过
WithCancel
等),子上下文会通过propagateCancel
函数注册到父上下文的children
列表中。 - 父上下文一旦被取消,会遍历
children
列表,递归取消所有子上下文。 - 当子上下文取消时,也会自动从父上下文的
children
列表中移除,确保不会出现资源泄漏。
子上下文同时监听父上下文的 done
channel 和自己的 done
channel。这种设计确保了子上下文能及时感知到父上下文的状态变化,同时也能够响应自身的取消请求。这样可以确保整个上下文树的取消信号能够有效地传播,避免资源泄漏和未终止的 goroutine。
31.golang error处理方式的优化。
error机制:
优点:显式错误处理、轻量级、可控性高、没有异常捕获的开销。
缺点:冗长的代码、容易被忽略、错误传递可能复杂。
panic机制:
优点:简洁、减少错误处理开销、适用于不可恢复的错误。
缺点:破坏正常的控制流、不适合常规错误处理、影响可读性和调试、recorver机制复杂。
优化error的使用。
使用自定义错误类型、使用error.is和error.as、简化错误传递、结合defer、结合panic、链式处理和result模式、使用第三方库
32.为什么要使用channel进行协程间通信。
以通信方式共享内存,不要以共享内存方式通信
本质上还是让资源去调度请求,而不是让请求去调度资源。
-
避免竞争条件:通过消息传递,协程之间不会直接访问共享内存,减少了数据竞争的风险。
-
更清晰的设计:使用消息传递,可以清晰地定义各个协程的输入和输出,使系统架构更加直观。
-
增强模块化:协程通过发送和接收消息进行通信,可以将其设计为独立的模块,便于维护和扩展。
-
提高安全性:由于不直接共享内存,数据的安全性得到提高,减少了错误的发生。
-
适应性强:通过消息传递的方式可以更容易地适应不同的并发模型,比如生产者-消费者模式或工作池模式。
33.有学生表和成绩表,查询学生的所有成绩,学生没有参加考试的记录不会出现。
说直接用left join可以实现?
34.任务表有任务类型和任务开始时间,查询对应任务开始时间范围的数据,索引怎么创建。
以使用 (start_time, task_type)
的索引,但效率可能不如 (task_type, start_time)
。具体来说:
-
前缀字段:只有当查询的条件以索引的前缀字段为基础时,索引才会被完全利用。在你的情况中,如果首先过滤
task_type
,再用start_time
,那么前者在索引中处于第一位会更有效。 -
范围查询影响:如果使用
(start_time, task_type)
索引,范围查询会影响后续字段的过滤效果,因为在范围条件下,MySQL 可能无法有效利用后面的字段。
总结来说,虽然可以使用 (start_time, task_type)
索引,但在你的特定查询中,建议使用 (task_type, start_time)
以获得更好的性能。
35.单例模式
单例模式的类提供了一种访问其唯一对象的方法,该对象可以直接访问,无须实例化。单例模式保证了一个类的对象只存在一个,同时维护一个对其对象的全局访问点。
饿汉式、懒汉式、sync.once式
36.泛型
既实现了方法的复用,又能对数据类型做一致性限制
当构建通用工具、通用算法、通用数据结构,就是需要复用的时候请考虑泛型,什么时候需要复用,就是当考虑通过规模化降低成本的时候。
37.go-zero
38.es加入新分片。
39.map扩容,channel。
40.约瑟夫环。求每轮的编号。
41.翻转链表。
42.zset
43.假设我现在有送礼服务,送礼调用金融在完成用户扣金币之后,将数据丢进消息队列,以供后续榜单、积分等服务消费。假如扣完金币之后,出现了故障,怎么处理。
分布式事务(两阶段提交和三阶段提交)、幂等和重试、补偿机制,除了这三种还有。
使用本地事务 + 事件驱动设计
-
金融扣款和消息队列各自保证自己的本地事务。
-
当金融系统扣除金币成功后,记录一个待发送事件,此事件表示金融系统已经扣款完成,但消息还未成功发送。
-
后续通过事件表或者类似机制,异步地将扣款成功的事件消息推送到消息队列。
-
如果消息队列出现问题,可以通过重试机制或补偿机制来保证消息最终能够到达队列。
-
这种方式通常称为最终一致性,可以利用消息中间件的消息确认机制(例如 Kafka、RabbitMQ 的消费确认机制)来确保消息消费成功。
-
使用事务消息(事务消息队列)
-
某些消息队列(如 RocketMQ、RabbitMQ 支持事务消息),在扣款成功后,消息会首先被标记为“预发送”状态。只有当金融系统成功扣款后,才会将预发送的消息进行确认,最终发送到队列。
-
如果扣款失败,预发送的消息则会被丢弃。
-
这种方式可以保证金融扣款和消息队列的消息推送在一个事务中。
44.cond
WaitGroup 可以实现当所有子协程完成之后, 主协程再继续执行。
如上的场景是主协程等待子协程达到某个状态再继续运行。
但是反过来怎么操作呢,要求一组子协程等待主协达到某个状态时才继续运行。这个时候就需要用到 Cond 了。
45.es倒排索引。
46.任务消费消息队列顺序1,2,3,4,5的保证。
消息队列可以保证消息的顺序,如rabbitmq保证一个队列的消息是顺序的,kafka按分区顺序存储。
保证执行顺序还需要依赖状态管理或者任务依赖,
如任务1执行完之后再将任务2放入。
47.mysql写入流程。
写入数据时会先写入缓冲区,同时写入到redolog、undolog和binlog。写入时会对唯一索引或主键索引加锁,后续将脏页刷入磁盘。
48.早上大老远的跑过去,面试官一直忙着玩手机,根本没听你说话。
之后就是直接在纸上写代码,是真的牛逼,刚才实现了下,150多行。自己品吧。公司叫 36*。
怀疑被刷kpi了。
题意大概是实现两个超大数的减法,数字保存在字符串中。返回一个字符串,过滤无效输入。
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strAdd("-8836", "7"))
}
// 两个非常长的字符串相减
func strAdd(s1, s2 string) string {
//考虑正负
//str1 str2
// 负 正 -(str1 + str2)
// 正 负 (str1 + str2)
// 负 负 (str1 - str2) 无符号大-无符号小,最后判断结果正负
// 正 正 (str1 - str2) 大-小,最后判断结果正负
var strBuild1, strBuild2 strings.Builder
var s1Flag, s2Flag bool
var first = true
//预处理
for i, v := range s1 {
if i == 0 {
if v == '-' {
s1Flag = true
continue
}
}
if v < 48 || v > 57 {
return ""
}
if v == '0' && first {
continue
}
strBuild1.WriteString(string(v))
first = false
}
first = false
for i, v := range s2 {
if i == 0 {
if v == '-' {
s2Flag = true
continue
}
}
if v < 48 || v > 57 {
return ""
}
if v == '0' && first {
continue
}
strBuild2.WriteString(string(v))
first = false
}
max := strBuild1.String()
min := strBuild2.String()
resultFlag := strCompare(max, min)
if !resultFlag {
max, min = min, max
}
//此处需要注意,min的长度一直在变。
lenMin := len(min)
for i := 0; i < len(max)-lenMin; i++ {
min = "0" + min
}
//for i := 0; i < len(max)-len(min); i++ {
// min = "0" + min
//}
if (s1Flag && s2Flag) || (!s1Flag && !s2Flag) {
result := ""
tempFlag := false
for i := len(max) - 1; i >= 0; i-- {
var temp uint8
t1 := max[i]
if tempFlag {
t1 -= 1
}
if t1 >= min[i] {
temp = t1 - min[i]
tempFlag = false
} else {
temp = t1 + 10 - min[i]
tempFlag = true
}
result = string(temp+48) + result
}
if !s1Flag {
if resultFlag {
return result
} else {
return "-" + result
}
}
if s1Flag {
if resultFlag {
return "-" + result
} else {
return result
}
}
} else {
result := ""
tempFlag := false
for i := len(max) - 1; i >= 0; i-- {
t1 := max[i]
temp := t1 + min[i] - 48
if tempFlag {
temp += 1
}
if temp > 57 {
tempFlag = true
temp -= 10
} else {
tempFlag = false
}
result = string(temp) + result
}
//最后的进位
if tempFlag {
result = "1" + result
}
if s1Flag {
return "-" + result
} else {
return result
}
}
return ""
}
func strCompare(s1, s2 string) bool {
if len(s1) > len(s2) {
return true
}
if len(s2) > len(s1) {
return false
}
for i := 0; i < len(s1); i++ {
if s1[i] > s2[i] {
return true
}
}
return false
}
49.设计更新插入时间复杂度都是O(1)的,lur淘汰策略的内存缓存。
map和链表都存储节点指针。
50.一百级台阶,青蛙每次可以选择跳一节台阶或者两阶台阶,跳到100阶一共多少种跳法。