1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 基础知识------我所知道的 应该知道的

基础知识------我所知道的 应该知道的

时间:2021-07-01 08:40:47

相关推荐

基础知识------我所知道的 应该知道的

-3-24 更新:

//03/21/illustrated-tales-of-go-runtime-scheduler/

这里解释了一下M阻塞时会发生什么,我的理解:

1 G存在写文件等操作,真正把M阻塞,这个时候M不会把G推回到队列里,但M和P必然分离,允许其他M结合P。同时会申请P数量个的自旋线程来执行调度,因为M会一直处于阻塞状态。

2 G存在一些IO操作,直接会被M推到队列里,这时候M继续在P上做别的G。

当G阻塞结束后:

运行时会尝试获取之前绑定的那个P,然后继续执行。运行时尝试在P空闲列表中获取一个P并恢复执行。运行时将goroutine放在全局队列中,并将关联的M放回M空闲列表。

关于redis分布式锁,看到一篇不错的文章,分享一下:

/demingblog/p/9542124.html

1.0 版本 : -12-1:

1 hashmap的原理 源码解析

(这里有个很大的疑惑, 1.7版本和1.8版本的源码存在一些出入,包括初始容量、阈值、负载因子实现方式都有改变,让人很是疑惑。)

选了一个讲的很详细的视频:/video/av68945084?from=search&seid=14077771566901559411

总结

容量:默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16),这么做是因为

1散列函数的实现方式有位异或运算,这么做可以让key尽量平均的分布在数组中,减少碰撞。

2 提高计算索引下标的效率。

3 数组扩容时,会产生大量的rehash(位运算代替取模运算) 在java7和之前中!,8之后就不需要做了。

(alibaba 手册建议:设置初始值为 (存储元素个数/负载因子)+1)

负载因子default=0.75: 为什么是0.75 ? : 其值影响什么?

1如何>=1 意味着hash碰撞会变大,导致链表长度会变大,则效率肯定变低。但是空间利用率显然增高。

2 load 过小的话,意味着hash碰撞会变少,则效率变高,但空间利用率变低.

3 0.75源自于牛顿二项式(每一个位置要么碰撞要么不碰撞)极限值的推导 log2=0.698,

阈值-threshold = 负载因子*容量

散列函数: hashcode 异或 其高位数字,目的:提高hashcode的随机性。

插入方式:先散列函数求出hashcode(),然后判断该位置上存的是否为红黑树。如果是直接插入,否则是链表,那么往链表中插入(如果链表的长度>=阈值默认8把链表转换为红黑树。这是因为红黑树旋转是有成本的,链表在长度短的时候更优秀) 如果链表中包含这个key,替换其值。 为什么阈值是8?

1 概率统计公式 --- 泊松分布: 链表长度为n的概率遵循泊松分布(指数对称分布),在load因子=0.75时,可以发现当n=8的时候概率无限接近0(亿分之六)。所以红黑树几乎不会产生,(除非数据量非常庞大)

2 并不是任何时候都马上转红黑树,数组容量<某个阈值64的时候会先选择扩容。

3 其实当判断链表循环达到8次时,此时链表长度已经=9个元素了,因为第0次循环的时候已经有一个元素。

计算索引下标: 关于index_FOR 中存在一个& Length -1 的操作 在 length= 2的幂指数情况下 = %length

扩容:先判断原来的容量是否>0 。新容量=it*2(不能大于最大容量2^30). 否则判断原map是否有阈值,有的话新容量=原来的阈值。 ELSE : 新map容量= 默认初始容量,新map阈值=负载因子*容量。然后for循环,一个位置一个位置进行迁移到新的数组中。(单个元素直接移, 红黑树进行拆分?,链表结构进行循环,每个元素都重新分组)

线程不安全:扩容时死环, 当两个或者多个线程同时扩容时,对数组同一个index上的链表进行迁移时,第一个线程迁完后、第二个线程迁移时,链表会成环,源码如此,所以hashmap并不支持并发使用。具体过程和链表拆分循环有关:/video/av64481290?p=4这个问题在1.8中已经的到解决:

解决方法: 在迁移数据的时候定义两组指针: high+low 高低位指针: 定义当前节点hash值(注意hashcode在相同index下是一定相同的,但hash值显然不一定相同,因为hashcode= hash & length-1 就是取模。所以同一个index下的链表中的每一个元素&oldlength值是不同的)& oldlength =0的时候(这里也说明为什么长度一定是2的幂指数,oldlength = 10..0,所以&的结果只可能为0 或者oldlength)如果值为0 则是放到低位,如果不为0则放到高位。然后把高低位的尾部指针的next 都断掉,然后把低位放在新数组原位置,把高位放在index+oldlength。而且整个过程没有进行rehash,节省时间,而且把一段长度为n的链表拆分成了两段短的链表,提高了利用率和时间效率。

golang中的map: 线程不安全

源码分析:/articles/19219?fr=sidebar

/fengshenyun/article/details/100582679

大体差不多。不同之处在于:

散列函数用hash值后8位定义index, 用高8位定义bucket位置,其实就是tophash,在for循环的时候如果tophash不相等就直接跳过提升效率。

数据存储为 数组buckets+ bucket链

buckets(array of 2^B Buckets. may be nil if count==0.)

----bucket1--------bucket2--------------

每一个bucket包含8个item,三层结构:

tophash1[0], 1, 2,.......8

key1 key2 key3 .......8

value1 value2 v3 ......8

nextbucket->

负载因子默认为6.5

扩容方式:

根据需扩容的原因不同(overLoadFactor/tooManyOverflowBuckets),分为两类容量规则方向,为等量扩容(不改变原有大小)或双倍扩容 , 双倍扩容和java类似。 等量扩容新申请的扩容空间(newbuckets/newoverflow)都是预分配,等真正使用的时候才会初始化、通过buckets/oldbuckets 的设计 ,做一个类似lazy的操作,不会一次性全部迁移完成,而是分摊在每一次操作中。

线程不安全处理方式:

1 加锁,

2 官方处理方式 sync.mapsync.Map的原理介绍:sync.Map里头有两个map一个是专门用于读的read map,另一个是才是提供读写的dirty map;优先读read map,若不存在则加锁穿透读dirty map,同时记录一个未从read map读到的计数,当计数到达一定值,就将read map用dirty map进行覆盖。

3 优化: 而sync.Map其实也是相当于表级锁,只不过多读写分了两个map,本质还是一样的。优化方向:就是把锁的粒度尽可能降低来提高运行速度。思路:对一个大map进行hash,其内部是n个小map,根据key来来hash确定在具体的哪个小map中,这样加锁的粒度就变成1/n了。 (类似于ConcurrentHashMap)

题外话: 关于slice

slice的扩容, if newL > 2*oldL = newL, else if oldL <1024, newL=oldL *2。 else NewL=oldL*1.25.这里请学习这片博客:https://juejin.im/post/5ca4239ef265da30807fea48。当slice扩容之后超过原slice的容量cap,会复制一个切片出来,此时两个slice彼此独立。如果没超过,则只是一个引用。一改多改。再了解下slice的实现原理:/weixin_30920597/article/details/98369065。 实际上slice是数组的引用而已,不过多了一个len和一个cap. len代表现在使用的数组长度(右指针-左指针),cap代表容量(左指针到最右边的长度)

d := []int{1,2,3,4,5,6,7,8,9,10}d = d[3:5]

此时len=2,cap=7.

s := make([]int, 1, 3)fmt.Printf("%p\n", &s)fmt.Printf("%p\n", s)

像这样,第一个&s输出的就是slice的指针,第二个s输出的就是slice中数组的指针

slice和数组的使用场景究竟有何不同? :总结一下:在1000~10000000长度情况下,数组的性能是优于slice的

slice和数组有些差别,特别是应用层上,特性差别很大,那什么时间使用数组,什么时间使用切片呢。

之前做了性能测试,在1000以内性能几乎一致,只有10000~1000000时才会出现数组性能好于slice,由于数组在编译时确定长度,也就是再编写程序时必须确认长度,所有往常不会用到更大的数组,大多数都在1000以内的长度。我认为如果在编写程序是就已经确定数据长度,建议用数组,而且局部使用的位置建议用数组(避免传递产生值拷贝),比如一天24小时,一小时60分钟,ip是4个byte这种情况是可以用时数组的。

为什么推荐用数组,只要能在编写程序是确定数据长度我都会用数组,因为其类型会帮助阅读理解程序dayHour := [24]Data一眼就知道是按小时切分数据存储的,如要传递数组时可以考虑传递数组的指针,当然会带来一些操作不方便,往常我使用数组都是不需要传递给其它函数的,可能会在struct里面保存数组,然后传递struct的指针,或者用unsafe来反解析数组指针到新的数组,也不会产生数据拷贝,并且只增加一句转换语句。slice会比数组多存储三个int的属性,而且指针引用会增加GC扫描的成本,每次传递都会对这三个属性进行拷贝,如果可以也可以考虑传递slice的指针,指针只有一个int的大小。

再拓展一个点: make 和 new的区别 todo

2ConcurrentHashMap

关于ConcurrentHashMap 和其分段锁:很详细的源码分析:/zerotomax/p/8687425.html,不写java的话,了解实现方式和优化方向即可。

2 红黑树的一些性质:

一种平衡的二叉搜索树, 节点要么为红要么为黑。根节点和叶子节点一定为黑,红节点的儿子一定为黑色,任意节点到任意叶子节点的路径上的黑色节点数目一定相同。 有点类似之前做过的splay,有插入删除旋转等操作。可以拓展下B+树(见mysql部分)

3 golang 的runtime

1 runtime 做了什么工作:

调度协程、内存分配、GC回收内置map、string、slice等的实现、一些关键字的实现例如go=newproc , new=newobject等操作系统和cpu相关操作的封装。

2 runtime中的有栈协程

每一个go routine 在会被编译成type g strcut,系统会分配一段内存栈给他存储id、status、上下文内容、func地址等。当栈的内存不够的时候1.4之前会用“分段栈”的方法扩容,之后用“连续栈”来扩容,当然空间闲置不用也会缩容(方式都是申请一块新的内存栈来用:更详细的内容:/articles/21411?fr=sidebar)在协程中申请新的内存时 请往下阅读内存管理合作式抢占 -抢占式调度 : 在1.2版本才引入初级的调度方式,runtime开了一条后台线程,运行一个sysmon函数。这个函数会周期性地做epoll操作,同时它还会检测每个P是否运行了较长时间。当P的运行时间过长就会对P进行切换。P的状态为Prunning,并且它已经运行了超过10ms,也会进行一个标记,通知其在合适时机(?)切换,而且这个切换还是在每个函数入口时进行比较。。。。 (所以不调用其他函数就能一直占着cpu直到被剥夺)

3 GMP调度模型 :(调度的本质就是 P 将 G 合理的分配给某个 M 的过程。) 结合go routine看!

G: go routine,每一个gr都是一个G是一个有栈协程。 每一个G被创建都会进入运行队列(会被调度),等待被执行。 当G阻塞了时,会保存上下文,重新进入队列。M就是底层执行的线程,会和p结合去执行G。P:process,默认=机器cpu核数量。 每一个p有自己的运行队列,也会优先执行自己队列中的G,当空闲时也会执行全局队列G和其他p的G(这里存在一个负载均衡的调度策略)。 p有自己的mcache。 那么如果把P理解为cpu对吗?p实际上是一个处理器、局部调度器, 由P来调度G在M上的运行。其中P的状态有Pidle, Prunning, Psyscall, Pgcstop, Pdead,代表:闲置的、运行的、当前P中的被运行的那个G正在进行系统调用、runtime正在进行GC(runtime会在gc时试图把全局P列表中的P都处于此种状态)、当前P已经不再被使用(在调用runtime.GOMAXPROCS减少P的数量时,多余的P就处于此状态)M和P分离之后,去哪儿了? 这个地方的解释,我更倾向于M和P分离后,G会进入P的队列,M会进入Idle队列去执行别的,当G被runtime唤醒时 再循环执行(只是这里有个问题是G是一直没有和M分离,还是在阻塞的时候就和M分离了?会)最后总结------详细的调度过程:参考/sunsky303/p/9705727.html。

4 网络操作:

封装了epoll等模型的fd操作?,一系列操作能block的时候立马切换且ready的时候会立马操作,达到网络操作不阻塞又是同步执行效果。 这部分理解不了,费力,大概我理解为js中大部分操作都会命名函数、等待函数回调(回想Django中的h5和后端的通信回调),golang封装了这些东西,就可以通过协程切换达到不阻塞又是同步的效果

5 runtime的内存管理

粒度为span,最小为8kb,span会组成slot分配给对应的内存栈。主协程会有全局的内存管理块“mheap”(只有一个) 和 mcental(有67*2个),每一个P上面会有一个mcache[134],mcache中有一个mspan的内存管理数组。当协程申请内存时, 会从mcache中的mspan中申请(无锁),如果没有从全局的mcentral(有锁),如果还没有会从mheap中申请。 mheap不会从堆上申请而是自己维护了一块内存 mtreap ,如果这个都没有就会直接向操作系统申请。申请到的内存会放进mcache中的span。具体的规则: 如果>32K 直接从mheap中申请,如果<16k且无指针,则用tiny分配器自动分配。有指针或者16~32之间则走上述的流程。 总结: 多层次的cache分配为了加快效率,毕竟p中的mcache是本地的内存。内存归还:monitor协程会定时判断 会把5分钟内完全没有对象分配的某块span归还给操作系统(只是告诉操作系统这一块的物理内存我不用了,但是虚拟内存还是存在着的, 这就是go的虚拟内存地址不会下降,但是物理内存是归还了的)协程栈是什么? 和内存之间的关系又是什么。 每个协程会有一段协程栈存储上下文信息等,那么创建的临时变量会放在协程栈中,那为什么说会向P的mcache申请内存呢? :首先栈本质上就是一段内存,协程有自己的栈,创建的变量,上下文都保存在栈中,当内存不够需要扩容的时候才会申请内存(此时才会走上述的申请流程)一篇还不错的博客:/studyhard232/article/details/89395455,这个解决了我对多级cache的流程、目的的疑惑,还有tiny分配器的一些原理简介,可以多次回头看看!

6 runtime的GC

三色标记法: 灰-黑-白 概念很简单,但实际操作并不简单。具体操作还得研究写屏障:/developer/article/1500335GC 触发: 1 申请内存时,当前已分配内存/上一次GC结束时存活对象内存 > 某个阈值。2 goTiggerTimer每过两分钟会自动检测一边GC的执行,没有就执行一边。3 强制触发。栈分配+逃逸分析;/u010853261/article/details/102846449。 只要被函数当指针应用且返回了,必然逃逸(毕竟函数栈都销毁了),被当interface用了也会逃逸,比如fmt.println(x) ,x就会逃逸到heap上。被引用(或者放进channel中)的指针会逃逸到堆上,不然栈销毁之后就取不到值了。但如果指针没有被作用域之外应用的话,就不会逃逸:

func PrintStr() {str := new(string)*str = "hello world"}func main() {PrintStr()}

当某个函数的形参为interface时,编译器无法确认其类型,这个时候会逃逸到堆上。因此,在调用频繁的地方慎用 interface。当申请的内存过大,比如申请一个10000容量的slice或者map都会分配到堆上。或者扩容的时候超过了某个临界点,就会重新分配在堆上。、

4 go routine 详解

资料1:/u010853261/article/details/84790392

资料2:/sunsky303/p/9705727.html

资料3:/articles/20991

-3-24 找到了一个图解版本,很nice的讲解:

//03/21/illustrated-tales-of-go-runtime-scheduler/

G

type g struct {stack stack // 描述了真实的栈内存,包括上下界m *m// 当前的mschedgobuf // goroutine切换时,用于保存g的上下文paramunsafe.Pointer // 用于传递参数,睡眠时其他goroutine可以设置param,唤醒时该goroutine可以获取goid int64 // goroutine的IDwaitsinceint64 // g被阻塞的大体时间lockedm muintptr // G被锁定只在这个m上运行}

goroutine切换的时候不同于线程有OS来负责这部分数据,而是由一个gobuf对象来保存,这样能够更加轻量级,

M 具体只写了和调度模式相关的参数

type m struct {g0*g// 带有调度栈的goroutinecurg*g // 当前运行的goroutinep puintptr // 关联p和执行的go代码spinningbool // m是否out of workblocked bool // m是否被阻塞inwbbool // m是否在执行写屏蔽incgo bool // m在执行cgo吗lockedg *g // 锁定g在当前m上执行,而不会切换到其他mcreatestack [32]uintptr // thread创建的栈}

结构体M中有两个G是需要关注一下的,一个是curg,代表结构体M当前绑定的结构体G。另一个是g0,是带有调度栈的goroutine,这是一个比较特殊的goroutine。普通的goroutine的栈是在堆上分配的可增长的栈,而g0的栈是M对应的线程的栈。所有调度相关的代码,会先切换到该goroutine的栈中再执行。也就是说线程的栈也是用的g实现,而不是使用的OS的

P

type p struct {lock mutexidint32statusuint32 // 状态,可以为pidle/prunning/...link puintptrschedtick uint32// 每调度一次加1syscalltick uint32// 每一次系统调用加1sysmontick sysmontick m muintptr // 回链到关联的mmcache*mcacheracectxuintptrgoidcache uint64 // goroutine的ID的缓存goidcacheend uint64// 可运行的goroutine的队列runqhead uint32runqtail uint32runq[256]guintptrrunnext guintptr // 下一个运行的g// Available G's (status == Gdead)gfree *g}

每一个P都必须关联一个M才能使其中的G得以运行。p的几种状态(status): Pidel:当前P未和任何M关联、Prunning:当前P已经和某个M关联,M在执行某个G、Psyscall:当前P中的被运行的那个G正在进行系统调用、Pgcstop:runtime正在进行GC(runtime会在gc时试图把全局P列表中的P都处于此种状态)、Pdead:当前P已经不再被使用(在调用runtime.GOMAXPROCS减少P的数量时,多余的P就处于此状态。

P被释放主要有两种情况。1 主动释放:最典型的例子是,当执行G任务时有系统调用,当发生系统调用时M会处于Block状态。调度器会设置一个超时时间,当超时时会将P释放。2被动释放:如果发生系统调用,有一个专门监控程序,进行扫描当前处于阻塞的P/M组合。当超过系统程序设置的超时时间,会自动将P资源抢走。去执行队列的其它G任务。、、

schedt

type schedt struct {goidgen uint64lastpoll uint64lock mutexmidle muintptr // idle状态的mnmidle int32 // idle状态的m个数nmidlelocked int32 // lockde状态的m个数mcount int32 // 创建的m的总数maxmcount int32 // m允许的最大个数ngsys uint32 // 系统中goroutine的数目,会自动更新pidlepuintptr // idle的pnpidleuint32nmspinning uint32 // 全局的可运行的g队列runqhead guintptrrunqtail guintptrrunqsize int32// dead的G的全局缓存gflock mutexgfreeStack *ggfreeNoStack *gngfree int32// sudog的缓存中心sudoglock mutexsudogcache *sudog}

其中: go

通常一个 P 可以与多个 M 对应,但同一时刻,这个 P 只能和其中一个 M 发生绑定关系(源码中的m指针指向一个m);M 被创建之后需要自行在 P 的 free list 中找到 P 进行绑定,没有绑定 P 的 M,会进入阻塞态。

整体流程:当G被创建,会选进入P的本地队列、如果本地队列满了进schedt的全局队列;当这个G等到队首的时候,然后会被M从队列中弹出来绑定(此时的M早就和P结合好了),当M执行这个goroutine时候如果发生了syscall或则其余阻塞(/articles/20991)操作,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P,继续去运行新的G,也就是说P会和M解绑,但此时的M还和原G绑定(这个绑定应该理解为M可以被多个G绑定,但G只能和一个M绑定),当原G结束了阻塞会唤醒原M,此时原M会窃取一个P来执行M,如果窃取到了然后将G放到P的local queue,如果窃取不到就会把G放进全局队列。这时候M应该就进入全局idle列表、G也进行runable状态。各自等待! 重复过程

关于 schedt ----全局调度者。

golang 的并发模型:

csp模型: ------- 起源: 1978年就发表了csp理论论文 =。= 牛逼 但是在go里面其实就是 process(go routine) + channel。 并发哲学:不要通过共享内存来实现通信,而要通过通信来实现共享内存!select + chan 语句:

ch1 := make(chan int, 1)ch2 := make(chan int, 1)...select {case e1 := <-ch1:fmt.Printf("1th case is selected. e1=%v.\n", e1)case e2 := <-ch2:fmt.Printf("2th case is selected. e2=%v.\n", e2)default:fmt.Printf("No data!")}// ch1 和ch2 都有数据的话,此时会随机选一个执行。ch3 := make(chan int, 100)...select {case ch3 <- 1:fmt.Printf("Sent %d\n", 1)case ch3 <- 2:fmt.Printf("Send %d\n", 2)default:fmt.Println("Full channel!")}// 这个case 的满足条件是channel 有缓冲区间,能容纳

一般都是 在select 外层加一个for 来避免channel的死锁等问题。 select 编译器会优化的,比如一个select{},编译器直接翻译为block。 有时候也会翻译成if else (如果只有一个switch 和 default)。如果有多个swtich ,那么会随机触发一个。

-4-21更新:select case default 一定要加chan来使用,如果所有case的通信都没法完成的话,会调用default。

/go/go-select-statement.html

go channel 源码

资料:/video/av64926593、/a/1190000018875154?utm_source=tag-newest、

首先其目的是为了保持协程并行的数据一致性

先了解一下hchan的结构。 buf-即缓冲区,实际上是个数组实现(环形队列:/p/67d2874eb2e0)。创建一个不带缓冲区的channel也会有这个指针(nil)。

发送数据的时候首先会加锁,把channel锁上、然后把要发送的数据放入(拷贝)buf数组中、同时会更新sendindex +1。解锁。

如果发送时缓冲区不够了的话。会阻塞,此时会把发送的G1(goroutine),封装成sudog保存到sendq队列中。调用gopard停止G1。最后解锁。

如果此时有一个接受来了,拿走了buf中一个数据,此时会唤醒sudog1,然后把数据给放入buf中。

接受数据也是同理,先锁、然后拿(拷贝)数据、解锁,接受也会更新receiveindex + 1.

如果buf中有数据,直接从buf中COPY数据

否则从sendq中copy数据,

如果接收时,啥都没有,此时阻塞把G1封装成sudog放入接受队列receiveq尾部,调用gopard停掉G,等待被唤醒解锁

此时G没有进入调度队列,如果此时出现了一个发送方G2,此时发送数据不会再进入队列,G1会出队,然后数据直接从G2到G1中。 其实这个时候就是nobuffer的情况。

关闭channel: 先加锁,标记closed=1,会先把队列里面的G都拿出来拷贝一份数据(这部分怎么实现的?),然后尽快的把所有的G都放回P的调度队列,最后解锁。(两个队列中应该最多只有一个队列会有sudog)

关闭之后不允许发送,但此时可能还有buf的值,所以依然支持从chan中读到值。(buf空的话一直读0值)

一个死锁的分析博客:/hotdust/article/details/78283039

往未初始化的chan中(nil chan)发送、接受数据都是导致goroutine 挂掉。往已经关闭的chan中send会panic,从已经关闭的chan中recive数据会?

5 网络协议篇

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

1 分层; 可以理解为两层: 程序+操作系统。 = 应用层 (http、dns等等) +(传输层 tcp、网络层 ip、数据链路层 网络)

对于一个http报文,经过下面三层会被加上tcp首部、ip首部、以太网首部。(比如上图中的报文通过tcp、ip和以太网/令牌环)。互联网目的就是在应用层(程序)中隐藏所有物理细节。

2 应用层和传输层是使用端到端协议,网络层则是逐跳协议。IP层是不可靠的,就像快递公司有可能把你的包裹弄丢掉。但是TCP是可靠的。tcp=淘宝,tcp超时重传(买家没收到货),收到了则确认收获。

前人经验1 :/justloveyou_/article/details/78303617超级详细, 高屋建瓴!

几个不懂得问题:

1 当建立起应用层----应用层(http握手)或者传输层到-----传输层的协议(tcp建立连接)时,就不会走下面的网络层、数据链路层的流程了吗? 自己的理解: 还是会走下面的基础网络层,这个建立的所谓的连接,只是让彼此认识,保证IP协议下数据交互的可靠性。实际的数据通道还是下层的通道。

2http协议和tcp协议的关系到底是怎样的呢? 上下层协议关系,http决定要搬运什么数据,以及封装好了tcp各个位置的报文该怎么写(你可以直接用tcp连接,如果不嫌麻烦的话)。而tcp决定这个数据从哪儿搬运,怎么搬运,保证搬运不出错等(IP)。

3 TCP协议无非就是三次握手 + 保证IP协议数据传输无误+四次挥手, 那UDP协议呢? 似乎还不明白,例如udp对应的应用层DNS协议,去解析物理地址整个过程似乎和UDP协议毫无关系吧。 理解:不需要和udp的概念扯上什么关系,只要他的报文是用udp协议传递的,那么他就是基于udp的协议。 而且如果不用udp的话,难道用tcp吗?对于dns协议要做的事情来说,tcp太慢了,显然udp更合适。

4 http 和 https 有什么区别:

1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。关于加密。客户端会把自己支持的加密等信息都发过来,服务端会并生成私钥公钥,把公钥发给客户端。

6 操作系统相关:

1 同样是这个人的博客:/justloveyou_/article/details/78304294, 这个还行。

2 常用的linux命令

3 linux 脚本教官

4 linux socket编程。

5 底层对并发锁的另一种实现方式? 原子性操作?

7 关于各种锁的详解:

互斥锁: 也就是最常见的锁了,比如锁住redis某个key。这里之前有过一个错误:通过get 和set 命令实现,这里如果在高并发的情况下,事实证明过无效。 正确的姿势应该是:通过setnx 和 expire命令实现:用redis单线程的特性setnx获得一个key,并设置过期时间= 获得锁。其他线程setnx返回=0,,设置过期时间防止此线程crash就死锁了。结束之后记得释放key。上述代码看上去是不是没问题,但存在一种极端情况: 当setnx之后crash了,还没来得及expire=死锁了,所以记得用set+传参的姿势把nx和过期时间一起传进去才行。是不是完美了?那假设A线程设置了过期时间30s,但你程序卡了31s,锁被自动释放了,支持B线程获得了锁,此时A会去把B的锁给干掉了,这就乱套了。 第一部的解决方案可以使先把key的value = 线程id, 这样就不会删掉别人的锁。但还是会存在两个线程获得锁,此时我们可以设置一个守护线程不停地为这个key续命=。=1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。互斥锁用redis实现的另一种姿势:watch+事务,能让多个线程拿到资源锁,但只会有一个写成功,其他都是由于原子性而放弃修改。但显然性能不如第一种好。读写锁 资料:/hesper/p/10738987.html。 注意写锁的优先级更高,比如这两种场景:线程A持有读锁, 然后线程B请求写锁, 然后线程C请求读锁

B阻塞, C阻塞 --> 写的优先级高

A解锁, B线程加写锁成功, C继续阻塞

B解锁, C加读锁成功线程A持有写锁, 然后线程B请求读锁, 然后线程C请求写锁

B, C阻塞

A解锁, C加写锁, B阻塞

C解锁, B加锁成功

所以为什么写锁优先级更高的原因就是避免: 读 + 写+ 读读读读读读读读读 这种情况的发生,一个写把后面的读全block住。

python 中并没有支持读写锁,而在golang中有RWMutex这个东西,包括Lock() 、Unlock()、RLock() 、 RUnlock()。 底层源码解析:todo

互斥锁原理详解:

博客:/yefengzhichen/article/details/89428983、//10/23/golang-mutex/

我感觉很难看懂,不妨先看看aqs 原理:/liqiangchn/p/11960944.html

+ CAS(其实就是乐观锁): 无锁、无竞争、无协程切换、但是有自旋。

再回头看看前两个博客试试。

粗略的总结一下: 首先协程会尝试直接获得锁, 获得不到的话会尝试自旋模式获得锁,如果也获得不到,就会通过修改信号量的方式来进入队列等待(这里的队列饥饿状态不明)。 解锁的话可以看看aqs的队列解锁。悲观锁与乐观锁:是mysql的一个锁机制。 悲观锁=在操作之前就认为其他线程肯定回来动数据,所以先锁上再说(行锁,表锁等等)。如果写的操作很多,那么就很适合悲观锁,否则会不停的被重试。 乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(可以使用版本号机制和CAS算法实现)如果被修改过,那么放弃修改, 否则更新数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁的版本号机制: 每次更新都存一个版本号。当前最后更新的version与操作员第一次的版本号是否相等”来判断是否有权利更新,更新之后version++。乐观锁的CAS算法:1 需要读写的内存值 V。 2 进行比较的值 A。 3 拟写入的新值 B 。 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。乐观锁的缺点:1 ABA 问题(用引用+标记解决) 2 循环时间长开销大3 只能保证一个共享变量的原子操作

具体优化参考:/qq_34337272/article/details/81072874

一个前辈的实践:/wt645631686/p/7987149.html

分布式锁:资料:/p/a1ebab8ce78a在分布式环境下,一个对象A, 在每一台机器每一个进程每一个线程里都会被创建和使用,但是我们想要A只能同时被一台机器的一个线程调用。那么这就是分布式锁的意义。在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行高可用的获取锁与释放锁高性能的获取锁与释放锁具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)具备锁失效机制,防止死锁具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁实现方式:1 显然redis可以用setnx+expire实现。(具体实现和细节可参照上文)2Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。3Memcached:利用 Memcached 的add命令。此命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。和redis类似。

8 设计模式:

好资料:

https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/simple_factory.html

/senghoo/golang-design-pattern

两个资料已经足够了解所有。

问题:

简单工厂模式

9 go 的杂项:

继承:资料1:/articles/12175 sync.atomic 资料:/p/228c119a7d0esync包(/articles/11038?fr=sidebar): 包含了once、互斥锁、读写锁、WaitGroup、conda、pool、等sync.once : 只执行一次,源码可自行查看。G的复用:/cpu-idle-cannot-recover-after-peak-load/

9 redis底层实现原理

redis 服务器一次只会执行一个命令,其他命令会在队列中等待,因为redis是单线程的。(可以认为redis是socket服务)Redis的命令不区分大小写,但是key 严格区分大小写!!!string 类型: 注意mset会覆盖所有的key,而且其是原子性操作(所有key会在同一时间被覆盖)。而且可以使用msetnx来避免覆盖那些已经有值存在的key。setex设置值和过期时间是原子操作,但set就不是。底层实现采用:embstr当长度>39 会使用SDS(简单动态字符串

struct sdshdr{//记录buf数组中已使用字节的数量//等于 SDS 保存字符串的长度int len;//记录 buf 数组中未使用字节的数量int free;//字节数组,用于保存字符串char buf[];}

/yinbiao/p/10740212.html这里讲了为什么要用sds、以及很多的源码,因为对扩容有兴趣所以只看了扩容的函数:sdsMakeRoomFor

// s 最少需要的长度newlen = (len + addlen);// 根据新长度,为 s 分配新空间所需的大小if (newlen < SDS_MAX_PREALLOC)// 如果新长度小于 SDS_MAX_PREALLOC 最大预先分配长度// 那么为它分配两倍于所需长度的空间 空间预分配策略newlen *= 2;else// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOCnewlen += SDS_MAX_PREALLOC;// T = O(N)

todo:把其他函数都看一遍。链表:redis构建了双向链表

typedef struct listNode{//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点的值void *value; }listNode

注意value的值是指针,也就意味着链表结构可以存储任何值。

typedef struct list{//表头节点listNode *head;//表尾节点listNode *tail;//链表所包含的节点数量unsigned long len;//节点值复制函数void (*free) (void *ptr);//节点值释放函数void (*free) (void *ptr);//节点值对比函数int (*match) (void *ptr,void *key);}list;

同时,也维护了一些函数和length(也就是说O(1)的时间可以获得长度)

hash:数组+链表+渐进式扩容 和java的hashmap原理几乎一样。

跳表: 多层有序链表,搜索就是从高层到底层找。删除就是删掉点和线即可。插入抛硬币,索引层数=硬币正面出现次数。

intset:整数集合。

①升级当我们新增的元素类型比原集合元素类型的长度要大时(比如int32->int64),需要对整数集合进行升级,才能将新元素放入整数集合中。具体步骤:

1、根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。2、将底层数组现有的所有元素都转成与新元素相同类型的元素,并将转换后的元素放到正确的位置,放置过程中,维持整个元素顺序都是有序的。3、将新元素添加到整数集合中(保证有序)。升级能极大地节省内存。

②、降级:整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

ziplist:是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值

上面都是基本数据类型: 这里统一讲各个功能实现:/ysocean/p/9102811.html

string 底层:int 编码:保存的是可以用 long 类型表示的整数值、raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)、embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

embstr与raw都使用redisObject和sds保存数据区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的)而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

list: 底层是链表结构或者ziplist:

当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

1、列表保存元素个数小于512个

2、每个元素长度小于64字节 不能满足这两个条件的时候使用 linkedlist(双端链表) 编码。上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置。

hash:哈希对象的编码可以是 ziplist 或者 hashtable。当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾。 条件和11 一样。

set:集合对象的编码可以是 intset 或者 hashtable。intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。

hashtable 编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。条件和11一样

内存回收:每个redis对象都是由

typedef struct redisObject{//类型unsigned type:4;//编码unsigned encoding:4;//指向底层数据结构的指针void *ptr;//引用计数int refcount;//记录最后一次被程序访问的时间unsigned lru:22;}robj

我们上面的说的set、zset、list实现都是指的编码。每个对象都是创建一个redisOb。Redis自己构建了一个内存回收机制,通过在 redisObject 结构中的 refcount 属性实现。这个属性会随着对象的使用状态而不断变化:创建一个新对象,属性 refcount 初始化为1。对象被一个新程序使用,属性 refcount 加 1。对象不再被一个程序使用,属性 refcount 减 1。当对象的引用计数值变为 0 时,对象所占用的内存就会被释放。这里会产生一个循环引用的问题:abc彼此引用,初次之外就没其他用处了。

这里会涉及到内存共享:refcount 属性除了能实现内存回收以外,还能用于内存共享。

比如通过如下命令 set k1 100,创建一个键为 k1,值为100的字符串对象,接着通过如下命令 set k2 100 ,创建一个键为 k2,值为100 的字符串对象,那么 Redis 是如何做的呢?1、将数据库键的值指针指向一个现有值的对象;2、将被共享的值对象引用refcount 加 1。 note: 共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。

回收策略: maxmemory-policy :当内存使用达到最大值时,redis使用的清楚策略。有以下几种可以选择:

1)volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )

2)allkeys-lru 利用LRU算法移除任何key

3)volatile-random 移除设置过过期时间的随机key

4)allkeys-random 移除随机ke

5)volatile-ttl 移除即将过期的key(minor TTL)

6)noeviction noeviction 不移除任何key,只是返回一个写错误 ,默认选项

持久化:/ysocean/p/9114268.html

1 配置中有save字段。 save a b :表示a秒内如果至少有 b个 key 的值变化,则保存。

2 手动触发:

save:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷,为了解决此问题,Redis提供了第二种方式。

bgsave:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。类似binlog

基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

stop-writes-on-bgsave-error :默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了

为什么单线程还那么快呢: 一篇完善的博文/chenyao1994/article/details/79491337

怎么理解redis的多路复用机制:反正不是很懂。。。怎么理解redis集群中主从同步的机制:/p/f0e042b95249redis 存热搜关键字:/post/95780.html。 简单理解就是每一个关键字(北),建立key 存储其搜索意图(北京、北方等)。然后在搜索意图中每个意图都有score代表搜索次数。 当然对于一个搜索意图abcd,会对其每一个前缀都建立key。LRU算法原理:/Dhouse/p/8615481.html

10 关于mysql(主要是innodb引擎)

资料:/fengyumeng/p/9852735.html

/video/av71677395?from=search&seid=16077524383523723564

理解: 本质上是个存储东西的应用,但是包含了很多存储引擎等为我们完成了很多其他事情:比如where、行锁、事务等等局部性原理:cpu访问磁盘时,访问的内容趋于一块较小的连续的区域。而底层数据(innodb)是分页、区、段等,一次访问可能只拿一个字节的内容,但cpu会把一页的内容全部取出来。一行数据逻辑存储形式(Dynamic行和Compress行): 变长字段长度列表+ null标志位+记录头信息+每一列的数据。。。一行数据可能会行溢出(一行数据大小>一页数据最大值16k),这个就会往下一页存(前一页记录这一页地址)。(但实际上因为索引的关系,每一行只会在一页中占据很小的空间(只存其他页地址),真正的数据都放在其他页,后面会讲)聚表=innodb,这个会根据索引(例如主键)来排好序、 Myisam=堆表,什么顺序插入就是什么顺序。为什么呢? 在innodb里面,每一行数据利用上文说过的记录头信息里面的字段:next_record,(在插入的时候就排好序)。但是这始终是一个链表结构,太慢了。innodb 利用页里面的(PageDirector) 页目录,做一个类似跳表的索引(一层)(这个结构也会溢出至下一页。)页多了之后, 其实也就是链表了,那么也会将每一页的最小值提出来,在做一层跳表的索引(第二层)。这个东西抽象出来就是B+树!。

(最上面一行就是第二层索引, 第二行就是第一层索引)可以看到每一个数据(除了1和7)都冗余了一份,=主键的数据其实都冗余了一份,因为第1~7行的数据都包含主键数据! 而且一个叶子节点可以存多个数据(且数据之间有顺序)。如果没有主键、innodb会找唯一索引来建树。 如果唯一索引也没有,innodb会自动生成一个rowId作为隐藏主键!B+树 查 WHERE >=2 等范围查询显然比平衡树要快很多,毕竟底层是链表。 详细原因:B+树叶子非节点存主键+指针,叶子节点存数据。当B+树高度=2时,大概能存2w行数据,=3时大概能存2000W行数据。

基本上好的主键(int8),一页里面16k可以存主键+指针(8+6字节) = 1170对键值对 = 1170个页子节点。 一个叶子节点也是一页16k,假设一行数据占1Kb那么一个节点可以存 1170*16行数据。 在一行数据1kb的情况下,高度为2 可以存1170*16=1w8+ 的数据。B+树=3 : 1170*1170*16从上可得主键最好是自增(避免要维持顺序浪费时间)、且主键最好比较小(越小每一页能存更多的行、且冗余少,否则树的高度会变高也会浪费时间。)当建表时mysql会创建一个页,当插入的数据占满了此页时,mysql会复制第一页,再开辟第二页,再把之前的第一页改成目录页(类似树的根节点),这么操作的可以让起始页一直都不变!然后可以把起始页放进内存,加速查找!除了主键索引其他索引是怎么做的呢? 会将每一行索引的字段 组成一个索引值。然后用索引值来进行建B+树,但是叶子节点并不是存每一行的数据,而只是存主键值(因为这样在更新的时候就不用来更新这树了,而且也不会有过多的数据冗余),唯一索引只是多了一层约束,其他原理和这个是一样的。一条语句可以用explain 关键字查看其运用的索引情况! 本质上有两种方式:1 通过辅助索引找到主键索引再去找数据,2 全表扫描(走不到索引),但并不是全表扫描一定就差,如果1找出了所有的主键,那么显然此时2优于1如果有多个索引可以用,这时优化器会优化出一个最好的索引来用。(比如上述情况只要主键索引>全表*阈值就是选择2)最左前缀原则:联合索引(A+B+C),where A = ? 是可以走索引的,B和C不是索引的开头,显然就没办法走索引了。接上 where A like %*** 无法走索引,但是 A like ***%这个就可以走了。索引匹配范围值:where A>1 and A<2000,依然可以走索引,找两下,取中间的值。where A>1 and C>1 :这个A肯定可以走索引,找到数据之后 在根据C(此时没索引)。到底走没走根据查询优化器选择到底走不走(有可能直接就全表扫描了)排序(select * Orde By a b c) 此时abc和联合索引(a+b+c)不谋而合,直接用就完了,但order by b显然就用不到索引了,除非能加一个where A 的条件才可以 (其实也是最左前缀原则)Group By 基本和Order By 一样事务,必须保证原子性、隔离性、一致性(原子性和隔离性的结果)、持久性(有记录可查)。隔离性:隔离级别: 1 脏读:在一个事务中可以读到其他事务未提交的修改。(破坏了隔离性) 2 已读提交:在一个事务中可以读取到其他事务已经提交的修改。(但其实在另一个事务中可能读到2个不同的数据)3 可重复读:直到事务结束前,都可以反复读取到事务刚开始时看到的数据。解决了前两个问题,但存在幻影行的问题(只是标准库,mysql已经解决了这个问题),其他事务插入的一行(这是mysql默认的级别)4最高的隔离级别,它强制事务串行执行,避免了前面说的幻读现象,简单来说,它会在读取的每一行数据上都加锁,所以可能会导致大量的超时和锁争用问题。mysql是如何解决幻读问题:/u013067756/article/details/90722490。总结一下就是快照读、和当前读。快照读:每一个修改会有一个版本号,当前事务只会取小于当前版本号的。 当前读:将当前行和间隙行锁住。关于锁TODO:集群主从同步:/cowbin/article/details/89737250

利用binlog,有三种模式: 异步复制、半同步复制、中间件(实现) 用一个缓存包在binlog上面,当读操作来的时候回先读缓存。如果进行分库分表:/butterfly100/p/9034281.html

两阶段提交协议:/ggibenben1314/article/details/48812501

事务补偿:

10 关于kafka的了解

kafka 吞吐量大的原理?

1Zero Copy :跳过“用户缓冲区”的拷贝,建立一个磁盘空间和内存的直接映射,数据不再复制到“用户态缓冲区” (mmap )

2topic下分区,增加并发能力。

3 批量发送

4数据压缩,把所有的消息都变成一个的文件。通过mmap提高I/O速度,写入数据的时候它是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出

11 go 的指针:

资料:/p/10b8870a9e8e

主要因为不了解unsafe.pointer 和 uintpr

12 go 的 sync 包

1 sync.WaitGroup :/u013474436/article/details/88749749。 内部实现就是一个计数器,add(N)=+n,done=-1,wait会一直阻塞知道计数器为0。计数器为负数会panic。 源码:/jiangz222/p/10348763.html

12 寄存器:

CPU计算时,先预先把要用的数据从硬盘读到内存,然后再把即将要用的数据读到寄存器。最理想的情况就是CPU所有的数据都能从寄存器里读到,这样读写速度就快,如果寄存器里没有要用的数据,就要从内存甚至硬盘里面读。等于操作系统也是个多级缓存机制,寄存器是最快一级的local-cache。

关于常用的登录加密等:

资料:/p/79b578026abd

我们的实际上的加密是怎么做的?

其他:

三元组:/view/184.html

一个死亡问题:

你做的过最好的东西是什么? 他好在哪里? 不好在哪里? 如何改进?

:面试官的回答: 这个东西不是挺简单的吗? 这个改进不是很容易想到吗?

我: 。。。(当场自闭

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。