# goRedis **Repository Path**: jnshao/goRedis ## Basic Information - **Project Name**: goRedis - **Description**: go-redis - **Primary Language**: Go - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-02-09 - **Last Updated**: 2024-07-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: TCP, Redis, aof, 分布式 ## README ## 编译过程 - go build -n main.go 不实际编译查看编译过程 - 查看中间码生成: windows下powershell:$env:GOSSAFUNC="main" linux下shell:export GOSSAFUNC="main" go build main.go - 查看plan9汇编生成、runtime调用方法: **go build -gcflags -S main.go** ~~~tex 1.中间码(ssa,平台无关)->plan9汇编(平台有关)->机器码(.a,平台相关)->可执行文件(.exe) 2.runtime是永远跟随用户代码一起编译的,不需要引用runtime包,自动引入和程序一起编译 3.调用compile.exe(windows系统下)对应用程序编译,编译出来的产物是一堆.a文件(机器码) 4.调用Link.exe(windows系统下),将这些.a机器码链接成统一的可执行文件main.exe(windows系统下.exe文件) ~~~ ## 运行过程 - 程序入口:runtime/rt0_XXX.s(汇编),用户代码入口:main.main() - 母协程(第一个协程,汇编代码):g0,主协程(第二个协程,go代码):runtime.main - 用户main.main()是在主协程中运行 ~~~tex 1.g0是不归调度器管理的协程,g0是为了调度协程而产生的协程,担负后面启动其他协程和调度器的工作,初始化调度器和创建主协程及运行业务协程(线程循环中运行一个g0协程) 2.启动协程的关键字go底层就是调用newproc方法,newproc是用来启动新协程 3.主协程放入调度器等待调度,mstart初始化调度器的m,主协程就会真正的执行起来,主协程定义了main_main方法并执行 4.编译的时候第二阶段,链接器Link.exe会把用户写的main.main()链接到main_main ~~~ ## 包管理 - 使用GO Module管理: ~~~shell #(1)设置环境变量 $ go env -w GO111MODULE=on $ go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy,https://goproxy.io,direct #(2)初始化go mod $ go mod init #(3)同步依赖 $ go mod tidy ~~~ - 外部包引用:go get - 导入本地依赖包两种方法: ​ (1)go.mod中使用replace <包名> => <本地包相对路径> ​ (2)go mod vendor 一次性缓存到本地 ​ go build -mod vendor - 管理内部私有包: ~~~tex (1)go env -w GOPRIVATE=gitee.com,*.imooc.com,不走go proxy (2)git init (3)git remote add origin (4)go.mod module修改为仓库路径名,去掉git协议部分 (5)git add . (6)git commit -m"init" (7)git push origin master -f (8)go mod tidy ~~~ - go.sum:主要是依赖包包校验,保存hash值,避免被篡改 GOPRIVATE和vendor依赖包不会包校验 ## 数据结构 - 内存中占用的字节数: unsafe.Sizeof() - 指针和部分数据如int的字节长度是跟随**系统字长**的(32/64位) ##### 空结构体 - 空结构体有地址没长度,指针全部指向同一个地址zerobase(所有长度为0字节值的地址,单独变量使用而非字段,字段会跟随其他兄弟成员) - 空结构体是为了节约内存的,结合map和channel使用 ##### string - 字符串底层表示:runtime.stringStruct结构体 - stringStruct两个字段:str为字节数组指针,len为字节数组长度 - 使用unsafe.Sizeof()获取所有字符串占用内存长度都是8+8=16 - 字符串utf-8编码,访问字符串不能直接使用下标,使用range遍历 - 字符串切分,要转rune数组在切片再转string:string([]rune(s)[:3]) ##### slice - 数组的引用,切片底层表示:runtime.slice结构体 - slice三个字段:array为底层数组指针,len为切片长度,cap数组容量(底层数组长度) - 使用unsafe.Sizeof()获取所有slice占用内存长度都是8+8+8=24 - 切片创建 ~~~tex 1.先创建底层数组,在创建slice结构体 2.使用make创建,调用方法:runtime.makeslice() ~~~ - 切片追加append ~~~tex 1.不扩容时,只调整len 2.扩容时,调用方法:runtime.growslice(),底层数组已改变 1)如果期望容量大于当前容量的两倍就会使用期望容量 2)如果当前切片的长度小于1024,将容量翻倍 3)如果当前切片的长度大于1024,每次增加25% 3.切片扩容时,并发不安全,底层数组发生变化,记得加锁 ~~~ ##### map - 底层使用HashMap 拉链法实现 ~~~tex 1.开放寻址法:槽存放数据,冲突后横向往后寻址; 2.拉链法:槽不直接放数据(存放链表地址,可以没有槽直接指向桶),每个槽挂链表(桶,存放相同hash值的k/v),冲突后纵向寻址 ~~~ - map底层表示:runtime.hmap结构体,直接指向buckets(桶,数据结构bmap) ~~~tex bmap: 1.一个bucket容量存储8个键值对,数组长度是8个 2.tophash:hash数组,记录的是8个key+hash0(种子) hash值的高8位,为了快速遍历 3.keys:key数组 8个 4.elems:value数组 8个 5.overflaow:溢出指针,数组满了,指向下一个溢出bucket ~~~ - 使用make创建,调用方法:runtime.makemap() ~~~tex 1.创建2^B个正常桶和一些溢出桶 2.算出查找桶号,是根据B值以及key+hash0(种子)算出的hash二进制值 后B值位来确定桶号,比如B=3,则取hash后三位的值来确定桶号(比如后三位010=2,确定bucket2) ~~~ - map渐进式扩容,插入数据要考虑扩容:runtime.mapassign() ~~~tex 扩容类型: 1.装载因子超6.5(平均每个槽6.5个key)或溢出桶超出普通桶,触发扩容 2.等量扩容:不增加普通桶,溢出桶太多,数据很稀疏(以前的数据被删了),需要整理 3.翻倍扩容:增加普通桶(槽)的数量 扩容hashGrow(): 1.更新flags指示扩容状态,更新B,oldbuckets,newbuckets,更新 extra记录溢出桶信息 2.渐进式数据驱逐,扩容执行growWork方法,evacuate方法把老桶的数据驱逐到新桶中 1)翻倍扩容:B+1,hash多一个高位,操作新增修改map桶数据时,会同时将对应该旧桶数据迁移到新桶中,此时把旧bucket2(10)平均分配到新bucket2(010)和bucket6(110),读取数据不会扩容只判断读新桶还是旧桶,渐进式扩容,操作的桶数据迁移 2)等量扩容:把老桶和溢出桶的数据整理方法新桶中,桶号不变 3.所有旧桶驱逐完成后,回收oldbuckets ~~~ - map不允许并发读写,扩容时驱逐桶数据会产生并发问题,并发使用sync.Map,分离了扩容问题,不会引发扩容(查/改)使用read map,可能引发扩容(增)加锁使用dirty map ##### 普通接口 - 接口值的底层表示:runtime.iface结构体 - iface的tab字段记录接口实际结构体类型信息(用于**类型断言**进行弱转换)和实现方法,data字段记录数据的地址 ~~~tex 两种类型断言: 1.t,ok=:c.(Truck) 2.c.(type)使用switch case判断 ~~~ - 结构体实体实现接口(值接受者实现),会自动多生成一个用结构体指针实现接口的方法;仅仅结构体指针实现接口(指针接受者实现),是不会自动生成一个结构体实体实现接口; 所以指针接收者的实现只能以指针方式转换类型和使用;值接收者都可 ##### 空接口interface{} - 底层不是普通接口,空接口值的底层表示:runtime.eface结构体 - 可以承载任何类型数据,用于任意类型传参 - eface既没有类型也没有数据(空接口)接口值则为nil,空接口值不一定为nil ##### nil - 在builtin包中定义,nil是个Type的变量,是6中类型(指针,方法,接口,map,slice,channel)的零值(初始值) - 每种类型的nil是不同的,不能比较 - 空结构体的指针和值都不是nil,有地址,nil不能是结构体值 - 空接口零值是nil,一旦有了类型信息就不是nil ## 内存对齐 - 提高内存操作效率 - 对齐系数:unsafe.Alignof(),变量的内存地址必须被对齐系数整除,基础类型的对齐系数和长度大小是一致的 - 结构体内部对齐:结构体成员偏移量,是自身大小与其对齐系数较小值的倍数 - 结构体·长度填充:增加结构体的长度对齐系统字长,结构体长度是最大成员长度与系统长度较小值的整数倍,空结构体成员顺序在最后会导致结构体长度填充一个系统字长 - 结构体对齐系数:成员的最大对齐系数,结构体第一个值地址分配地址必须被对齐系数整除 ## 协程 - 进程用来分配内存空间 - 线程用来占用CPU时间,系统分配CPU资源的单位 - 协程精细化利用协程,实现超高并发(一个线程并发运行多个协程程序而不需要线程的来回切换调度,线程做不多超高并发) ##### 协程底层 - 协程本质:runtime.g结构体 - stack:堆栈地址,协程栈区空间,lo低地址,hi高地址 - gobuf:目前程序运行现场,sp指向协程当前运行栈指针,pc程序计数器(协程当前运行代码行数) - atomicstatus:协程状态 - goid:协程id ##### 线程底层描述 - 线程描述:runtime.m结构体 - g0:g0协程母协程,操作调度器 - curg:目前线程运行的g - mOS:操作系统线程信息 - 线程循环: ~~~tex g0 stack:schedule()->execute()->gogo()-> g stack: ->业务方法->goexit()->g0 stack ~~~ ##### G-M-P调度模型 - P(处理器)本地协程队列:runtime.p结构体,M和P一对一,减少全局协程队列锁并发的情况 - 任务窃取:本地和全局都没有会窃取其他P的协程,增强线程利用率 - 新建协程优先放入P的runnext(插队执行),若本地队列满了,放入全局队列 - 协程饥饿:耗时协程阻塞时,**线程循环中协程顺序执行**会遇到饥饿问题,本地和全局循环取协程解决,防止全局队列饥饿每61次线程循环从全局队列抓取一个协程,当前协程休眠放入sudog,不休眠放会本地队列 ~~~tex 触发协程切换时机: 1.主动挂起(休眠):runtime.gopark(),可以直接跳到线程循环的开始,重新调度协程,如time.Sleep() 2.系统调用完成时:程序遇到系统调用时,会运行runtime.exitsyscall(),可以直接跳到线程循环的开始,重新调度 任务没有主动挂起和系统调用:基于协作的抢占式调度 1.每次函数跳转会运行:runtime.morestack(),本意检查协程栈空间 2.下钩子,检查协程是否被抢占(系统判断,通过运行时间标记),被抢占重新调度 没有函数调用:基于信号的抢占式调度 1.使用操作系统底层原理,通过GC监听系统向目标线程发SIGURG信号,立即跳到doSigPreempt(),重新调度 ~~~ ##### 协程太多问题处理 - 协程太多会给程序带来性能和稳定性的问题,会panic ~~~tex 1.优化业务逻辑,减少程序执行时间 2.利用channel缓存区控制创建协程的数量 3.协程池(tunny),不太推荐 4.调整系统资源 ~~~ ## 锁 ##### 锁基础 - atomic操作:硬件层面的加锁机制,只能处理简单的操作,并发高时数据竞争激烈**协程阻塞** - sema锁(uint32的值):信号量锁,对应一个semaRoot结构体有个平衡二叉树数据结构保存协程(sudog结构体),可以进行**当前协程的挂起休眠和唤起** ~~~tex 1.协程获取锁:uint32原子操作减一,获取成功,协程开始执行,若uint32==0当前协程休眠(挂起)gopark,进入堆树等待 2.协程释放锁:uint32原子操作加一,释放成功,协程执行完成,若uint32==0从堆树中取出一个协程,唤醒 ~~~ - sema经常被用作**休眠队列**(初始值uint32==0,获取锁时,将当前协程休眠到队列中,释放锁时唤醒) ##### 互斥锁sync.Mutex - 字段state(前29位做等待协程数WaiterShift,后三位分别是Starving、Woken、Locked)、sema - 正常模式:自旋加锁+sema休眠等待,但存在锁饥饿(前面的协程一直抢不到锁) - 饥饿模式: - 当前协程等待锁的时间超过1ms,切换到饥饿模式Starving置1 - 新来的协程不自旋,直接sema休眠,被唤醒的协程直接获取锁 - 休眠队列没有协程时回到正常模式 - 使用经验 - 减少锁的使用时间 - 善用defer确保锁释放 ##### 读写锁sync.RWMutex ~~~go type RWMutex struct { w Mutex // 写锁,用来写协程之间互斥等待 writerSem uint32 // sema锁,用作写协程队列等待读锁释放 readerSem uint32 // sema锁,用作读协程队列等待写锁释放 readerCount int32 // 记录读协程个数,相当于是读锁 readerWait int32 // 记录写协程之前的读协程个数,还有几个读协程才能加写锁 } ~~~ - 写锁互斥锁,读锁共享锁可以并发执行,读/写锁释放后才能加写/读锁操作即读(并发)的时候不允许写,写(同步)的时候不允许读 - 使用经验 - 适合读多写少,减少锁冲突 - 写少读多,使用互斥锁 ##### 等待组sync.WaitGroup - 实现一组协程等待另一组协程 - 等待的协程(需要等待组中的协程全部运行完才能运行的协程)陷入sema并记录个数(waiter) - 被等待的协程(等待组添加的协程)计数(counter)归零时,唤醒所有sema中的协程 ##### sync.Once - 一个方法只允许一个协程运行一次(无论是否执行成功,defer都会设置标志位),用来一些初始化的操作 - 使用标志+mutex实现了并发冲突的优化,类似多线程单例模式双重锁定 - go once.Done(方法) ##### 锁注意事项 - **锁拷贝**:可能会导致死锁问题,**禁止锁拷贝(值传递)**,要新建锁,**使用vet工具检查**锁拷贝:go vet main.go - RACE竞争检测:go build -race main.go在运行执行文件,发现隐含的**数据竞争问题**(可能加锁的建议或者bug提醒) - go-deadlock项目:可以**死锁检测**,用法和sync包中一样 ## Channel - 使用通信的方式共享内存,提高性能(休眠等待而不是轮询)和解耦易维护,channel是go一等公民 - 底层数据结构:runtime.hchan,包括缓存区(环形队列),发送等待队列(协程链表 sudog结构体),接收等待队列(协程链表 sudog结构体),互斥锁Mutex(锁使用时间很短,加数据那一下所以性能高),closed(0开启1关闭) - 发送c <- x:转化为runtime.chansend1()最后调用chansend() ~~~tex 1.接收队列有协程,优先直接拷贝数据给接收协程并唤醒goready() 2.没有则放入缓存 3.如无缓存或缓存满了,发送协程将自己包装sudog进入发送队列,休眠并解锁 ~~~ - 接收:i <- c转化为runtime.chanrecv1(),i,ok <- c转化为runtime.chanrecv2(),最后都会调用chanrecv() ~~~tex 1.发送队列有协程,优先从缓存区取数据并唤醒接收队列的协程 2.没有缓存则直接拷贝数据给等待的G中并唤醒 3.如缓存无数据或发送队列,接收协程将自己包装sudog进入接收队列,休眠并解锁 ~~~ - 非阻塞channel:使用select ~~~tex 1.select没有default且case中channel都不能来立即执行,会把自己select协程注册到里面所有的channel中(发送进入发送队列,接收进入接收队列),休眠等待 2.使用定时器timer配合select可以实现超时、定时特性 ~~~ ## TCP网络编程 - 操作系统提供了Socket作为TCP通信的抽象 - IO模型:同时操作Socket的方案 ~~~tex 1.阻塞模型(一个线程负责一个socket),线程开销大,内核态切换开销大 2.非阻塞模型,开发难度大,不会陷入内核态(socket阻塞),需要自旋轮询 3.多路复用:socket事件池,由系统监听注册到事件池中的socket可读/可写/断开事件,业务再处理相应socker请求,性能好业务编写麻烦,多路复用器:epoll(linux)、IOCP(windows)、kqueue(Mac) ~~~ - GO采用协程阻塞模型(休眠)+多路复用(g0协程的gc监听事件、触发唤醒,netpoller适配器)的IO模型 ## 内存模型与垃圾回收 - go协程栈位于go堆内存上 - 协程栈记录协程的执行现场,还负责记录局部变量,传递参数和返回值 - go方法使用参数拷贝(值)传递 - 协程栈空间不足 ~~~tex 1. 局部变量太大,逃逸到堆中:指针逃逸(返回值变量)、参数空接口逃逸(需要用到反射的时候会发生逃逸)、大变量逃逸 2. 栈帧太多(方法调用深),栈扩容,runtime.morestack(),1.13后使用连续栈,地址连续,拷贝旧的重新开辟新栈 ~~~ - 堆内存结构 ~~~tex 1.操作系统给进程提供虚拟内存空间(linux给每个进程分配256TB虚拟内存,go中runtime.mheap结构体描述整个堆内存(虚拟内存),由heapArena(内存块)组成) 2.heapArena:go中虚拟内存单元(内存块),一次申请一个内存块(heapArena=64M),最多可以申请2^22个内存单元(=256TB) 3.go heapArena内存分配:分级分配,将内存块按需开辟切分不同级别runtime.mspan(一组相同的内存格,共划分了67(0-67)个级别),防止内存碎片化 4.中心索引runtime.mcentral:相同级别的mspan目录(不同heapArena中的相同级别mspan,方便写,内存分配),每级分为需要GC扫描和不需要GC扫描的,共136个mcentral,使用互斥锁保护 5.mcache记录了分配给各个P的本地mspan ~~~ - 分配堆内存:runtime.mallocgc() ~~~tex 1.对象分级:微对象、小对象、大对象(>32K),微小对象使用mcache,mcache中的mspan填满后,与mcentral交换新的空的mspan 2.mcentral不足时,在heapArena开辟新的mspan 3.大对象直接在heapArena开辟新的mspan(级别0) ~~~ - 垃圾回收:标记-清除法 ~~~tex 1.标记:GC Root节点进行广度优先搜索(DFS),可达性分析,标记被引用的对象,没有没引用的就是无用对象 2.并发标记-三色标记法:起初白色无用对象,灰色有用未扫描,黑色有用已扫描,删除屏障,对指针释放的白色对象置灰;插入屏障,对对象新指向白色对象置灰 ~~~ - 优化GC效率 ~~~tex GC触发: 1.系统定时触发:2分钟触发,谨慎调整 2.用户显示触发:runtime.GC方法,不推荐调用 3.申请内存触发::runtime.mallocgc(),不能改 优化方案:减少堆上产生垃圾 1.内存池化:如环形队列 2.减少逃逸:如返回指针 3.使用空结构体 GC分析工具: 1.go tool pprof 2.go tool trace 3.go build -gcflags="-m" 4.环境变量:GODEBUG="gctrace=1" ~~~ ## 其它特性 - cgo:临时使用go语言调用c方法的技术,但并不能提高go性能 ~~~go package main /* int sum(int a, int b) { return a+b; } */ import "C" func main() { println(C.sum(1, 1)) } ~~~ - defer ~~~tex 两种思路三种实现: 1.记录defer信息,函数退出时调用:堆上分配、栈上分配 2.将defer代码直接编译进函数尾:开放编码法 ~~~ - panic + defer + recover ~~~tex 1.panic会终止当前协程,带崩整个Go程序 2.panic在退出协程之前会执行当前协程所有已注册的defer,不会执行其他协程的defer 3.defer中执行recover,可以拯救panic的协程,不会影响其他协程的运行 ~~~ - 反射 - reflect.TypeOf() - reflect.ValueOf()