Appearance
面试题
Golang
初级特性面试题
- = 和 := 有什么区别? “=”是赋值,“:=”是声明变量并赋值,系统自动推断类型,不需要var关键字;
- 为什么传参使用切片而不使用数组? 数组是值类型,切片是引用类型,引用类型作为参数,可以避免深拷贝,效率比较高,引用传递。
- Golang不分配内存的指针类型能用吗?
- 不能,因为没有为指针变量分配内存空间,数据没地方存储。
- 每个指针类型变量,必须要先分配内存空间,否则使用未分配内存的指针类型变量会报错panic: runtime error
- var num *int;*num=200 这是没分配,var num *int;num = new(int);*num=200 这是正常
- Golang 里是怎么比较相等与否的三种情况?
- 1、对比变量的类型(type)和值(data),如果两个都相等则变量相等。data 存储的是结构体的地址,变量就不相等;
- 2、当一个 interface 的 type 和 data 都处于 unset 状态的时候,那么该 interface 的值就为 nil;
- 3、interface 与 非 interface 比较,会将 非interface 转换成 interface ,然后再按照 两个 interface 比较 的规则进行比较;
- Go语言如何实现包管理?
- 使用的是 Go Modules。是Go语言从1.11版本开始提供的新特性,可以方便地管理Go语言的包依赖。
- 初始化Go Modules:go mod init projectName
- 在项目中安装依赖的包:go get -u 包名
- 这样,Go Modules就会自动帮你管理包的版本,并且记录在项目的go.mod文件中。
- Go语言的优点是什么?
- Go语言是一种静态编译型、开源、多平台编程语言,广泛应用于服务端开发
- 简洁高效:Go语言语法简洁,代码容易理解,编译速度快,运行效率高。
- 高并发:Go语言的特有的CSP(Communicating Sequential Processes)模型使得它拥有高效的并发处理能力。
- 垃圾回收:Go语言拥有自动垃圾回收功能,可以极大减少内存泄漏的风险。
- Go语言正则表达式怎么使用?Go语言提供了regexp包,用于处理正则表达式,通过regexp可以完成字符串匹配、替换等任务。
- Go语言的并发模型是什么?
- 并发模型是基于 Goroutine 和 Channel 的。
- Goroutine 是 Go 语言提供的轻量级线程实现,它比线程更加轻便,启动和结束更加快速。
- Channel 是一种通信机制,用于在 Goroutine 之间传递数据,并发地安全地执行协同任务。
- Go语言读写文件的方式有那些?
- 有多种方式实现文件读写操作,包括 bufio 包、ioutil 包、os 包等。每种方法都有它们自己的特点,可以根据需要选择最适合的方式。
- 例如,如果需要读取整个文件,可以使用 ioutil 包操作比较方便。如果需要从文件读取大量数据并执行频繁的读写操作,推荐bufio 包。
- Go语言如何实现错误处理?
- 通过error接口来实现错误处理,通常会在函数中返回error类型的值,如果返回的error值不是nil,则表示函数发生了错误。
- 在代码中,通过if语句检测错误值,并进行适当的处理。
- 错误处理机制主要通过返回“error类型”来实现,有以下几个优点:
- 可读性高:Go语言“采用返回值”的方式进行错误处理,而“不是依靠异常机制”,这使得代码的错误处理逻辑更加清晰明了。
- 显式错误处理:在Go语言中,错误必须显式地处理,这提醒程序员可能出现的错误,并在编码时进行考虑。
- 编译器检查:Go语言的错误处理机制会在编译期间检查错误,从而可以发现程序中的问题。
- Go 引用类型与指针,有什么不同?
- 指针类型也可以理解为是一种引用类型,都可以避免深拷贝,提高变量赋值效率,但是在使用上指针类型有专用的指针操作符,引用类型使用上跟普通变量类似。
- slice,map,chan,interface等都是引用类型 特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
- Go 中哪些是可寻址,哪些是不可寻址的?
- Go语言中,常量、字面量、函数、map中的元素都是不可寻址的,否则会直接报错。
- Golang中可直接使用 & 操作符取地址的对象,就是可寻址的(Addressable),程序运行不会报错,说明这个变量是可寻址的。
- Go有类型常量和无类型常量的区别?
- 常量定义可以设置类型,也可以不设置,区别主要在于常量赋值的时候,无类型的常量会被隐式的转化成对应的类型,
- 有类型常量,不就会进行转换,在赋值的时候,类型检查就不会通过,会直接报错。
- 无类型常量const RELEASE = 3,可以赋值给 var int16/var y int32 = RELEASE的变量;
- Go 是值传递,还是引用传递、指针传递?
- Go语言中都是值传递。
- 在Go中函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。
- 如果拷贝的内容是值类型的,那么在函数中就无法修改原始数据;
- 如果拷贝的内容是指针(或者可以理解为引用类型 map、chan 等),那么就可以在函数中修改原始数据。
- 当你将切片作为实参传给函数时,函数是会拷贝一份实参的结构和数据,生成另一个切片,实参切片和形参切片,不仅是长度(len)、容量(cap)相等,连指向底层数组的指针都是一样的。
中级特性面试题
goroutine(协程) 存在的意义是什么?
- 是一种比线程使用成本更低的并发机制,主要体现在内存消耗、协程切换开销都比线程小。
- goroutine 的存在是为了换个方式解决操作系统线程的一些弊端
- 1、创建和切换成本太高,操作系统线程的创建和切换都需要进入内核,而进入内核所消耗的性能代价比较高,开销较大;
- 2、内存使用太高:
- 一方面,避免极端情况下操作系统线程栈的溢出,内核在创建操作系统线程时默认会为其分配一个较大的栈内存(虚拟地址空间),系统线程远远用不了这么多内存,这导致了浪费;
- 另一方面,栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。
- 用户态的goroutine则轻量得多:
- 创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换;
- goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大,同时,如果栈太大了过于浪费它还能自动收缩;
Go语言的协程是什么?它有什么优点?
- Go语言的协程是一种轻量级的线程,具有比线程更短的创建时间、更高的利用效率、更简洁的代码等优点。
Golang局部变量分配在栈上还是堆上?
- 一般来说,局部变量是分配在栈内存空间上,函数返回后就释放对应的栈内存空间,
- 但是有些特殊的局部变量则是分配在"堆内存"上,例如:指针类型、引用类型,这种局部变量,虽然在函数里声明定义,但是在函数外还会持续的使用。
- 根据内存管理(分配和回收)方式的不同,可以将内存分为 堆内存 和 栈内存。
- 堆内存:由内存分配器和垃圾收集器负责回收
- 栈内存:由编译器自动进行分配和释放
- 一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
- 每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
- 堆内存,由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要GC(垃圾回收)的介入参与,如果有大量的 GC 操作,将会吏程序性能下降得历害。
Golang 的默认栈大小是多少?最大值多少?
- 默认栈大小是2K,最大值是1G。
- Go 语言使用用户态线程 Goroutine 作为执行上下文。
- 在 v1.3 版本引入连续栈之后,Goroutine 的初始栈大小降低到了 2KB,进一步减少了 Goroutine 占用的内存空间。
介绍下Go数组和切片的关系?
- Go语言中数组的长度是固定的,在实际应用中非常不方便,一般将切片当成动态数组用,动态数组指的是数组的长度可以动态调整,切片是对数组的一个引用。
Go语言中的指针是什么?
- 指针是一种引用类型,指向一个内存地址,通过指针可以访问该内存地址存储的值。
- 指针的使用,主要由两个运算符完成
- “*”定义指针类型或者读取指针变量的值
- “&”读取变量地址
Go怎么做单元测试?
- Go语言提供了一个内置的单元测试库(testing),可以完成单元测试任务,
- 单元测试的主要过程:
- 为目标函数编写单元测试函数,单元测试函数名必须以Test开头命名,
- 单元测试代码文件名必须以 _test.go 结尾,
- 最后使用go test命令执行单元测试代码。
Go语言中的defer有什么作用?
- defer关键字的作用是实现延迟处理,入栈时就会把函数参数拷贝一份,在return之后执行,一般在defer捕获并recover来处理error;
- 最后当前函数执行完毕后,再将栈内的函数异常弹出执行,也就是defer修饰的函数是在当前函数执行完毕之后才会执行。
- defer延时执行机制通常同于关闭文件、网络连接、释放资源等。
Go语言怎么实现面向对象编程?
- Go语言不支持类和继承,但是提供了面向对象编程的一些特性,例如:借助结构体、函数(方法)和接口特性,可以实现一些面向对象编程特性(OOP)。
Go怎么打包和发布程序?使用go build命令对源代码进行编译,生成可执行文件,然后根据不同的部署方式,将可执行文件部署到服务器上即可。
Golang 有异常类型吗
- 在 Go 没有异常类型,只有错误类型(Error),我们可以使用Error处理各种异常。
- Go语言中主要有两类错误,一类是可预见的错误error,不会导致程序退出,一类是不可预见的错误panic,会导致程序退出。
Go语言中什么叫字面量和组合字面量?
- Go语言中字面量,指的是直接写在代码中的值,例如: s:= "tizi365.com" 这里tizi365.com就是字符串类型的字面量,
- 组合字面量指的是结构体、数组、map类型定义过程初始化数据使用的字面量。
Go 多值返回有什么用?
- Golang函数支持返回多个值,在一定程度上可以简化代码;
- go 错误处理,通常也是通过函数的最后一个值作为error信息返回。
高级特性面试题
Go slice 扩容机制简单介绍下?
- 如果slice的容量小于1024,则新的扩容会根据原先容量的2、3、4、5倍容量递增,直到满足新的容量需求,
- 如果slice的容量大于或者等于1024,则新的扩容将扩大大于或者等于原来1.25倍,即以0.25增加。
- 所需内存 = 预估容量 * 元素类型大小
Golang 什么是逃逸分析?
- Go中说变量a逃逸,通常指的是变量a逃逸到了堆内存中。函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而释放,因此只能分配在堆上。
- Go 编译器用于判断某个变量需要分配在栈上,还是堆上机制,就称之为逃逸分析(escape analysis),逃逸分析由编译器完成,作用于编译阶段。
- 函数的返回值是引用类型,就会发生变量逃逸到了堆上;
- 一个goroutine对应一个栈,栈是调用栈(call stack)的简称。
- 一个栈通常又包含了许多栈帧(stack frame),它描述的是函数之间的调用关系,每一帧对应一次尚未返回的函数调用,它本身也是以栈形式存放数据。
- Go程序在运行时只会存在一个堆,程序在运行期间可以主动从堆上申请内存,这些内存通过Go的内存分配器分配,并由垃圾收集器回收。
- 会发生逃逸的的场景:变量类型不确定、变量所占内存较大、变量大小不确定、栈空间不足
Golang 谈一下你对协程调度策略的理解?
- 因为CPU资源是有限的,Go协程调度解决什么时候该执行那些协程,常见的调度策略有抢占式调度、协作式调度、基于信号的抢占式调度等。
- 抢占式调度
- 依靠外部抢占的方式去中断当前任务,让其放弃当前资源交给其他正在等待的任务,这显得比较被动和强迫。
- 优点:任务执行时间可控、不会出现不合理分配的情况,保证公平性。
- 缺点:抢占操作会导致一些额外开销,调度程序需要保存任务的上下文。由于无法感知任务的执行情况,对于单个任务的执行效率无法做到最优。
- 在go1.14版本后开始引入基于信号的抢占式调度,是一种异步抢占机制。
- 协作式调度
- 以多个任务之间以协作的方式切换执行,每个任务执行一会,任务执行到某个点时会自己让出当前资源交给其他正在等待的任务,这显得比较主动和自愿。
- 优点:任务可以自己控制放弃资源的时间、由于任务保持自己的生命周期,调度程序不必注意每个任务的状态。
- 缺点:执行时间不可控,如果遇到流氓任务一直不放弃资源会导致其他任务得不到资源被饿死、新任务执行实时性差;
Go语言中并发安全问题指的是什么?如何避免?
- Golang中的并发安全问题指的是多个协程(goroutine)并行执行时,不会因为竞争访问共享资源(如内存、共享变量)而导致数据不一致、状态不确定等问题。
- 当多个goroutine同时读写同一个共享变量或资源时,如果没有有效的同步机制,则会出现并发安全问题。
- 解决并发安全问题的主要思路就是加锁、避开协程竞争和使用原子操作函数。
- 互斥锁(Mutex):互斥锁是最基础的并发安全控制方法,使用锁保证同一时间只有一个goroutine访问共享资源,简单说就是加锁。
- 原子操作(Atomic operations):Go语言内置了一些原子操作(atomic包),支持加法和递增等操作,使用原子操作可以保证操作的原子性。
- channel:多个协程之间通过channel机制传递数据,避开竞争读写共享变量。
- 延迟函数(Defer):延迟函数可以在协程结束时释放锁(配合加锁机制使用),从而保证协程的安全。
介绍下Go的栈空间管理?
- go语言选择栈的栈空间管理的方式是,一开始给一个比较小的空间,随着需要自动增长。当goroutine不需要那么大的空间时,栈空间也要自动缩小。
- Go程序运行的时候为每个goroutine创建并管理相应的栈空间,为每个goroutine分配的栈空间不能太大,goroutine开多时会浪费大量空间,也不能太小,会导致栈溢出。
- 连续栈(Contiguous Stacks),当栈空间不够时,直接new一个2倍大的栈空间,并将原先栈空间中的数据拷贝到新的栈空间中,而后销毁旧栈。
- 假设当前栈空间即将用尽,并且需要在for循环中执行一个比较消耗空间的函数。当该函数执行时,栈空间发生了扩容,变成原先2倍大小,函数执行完成一次后,
- 栈空间的使用量缩小回执行前的大小,但是栈空间的使用量并没有小于栈大小的1/4,不会触发栈收缩,所以在整个for循环执行过程中,不会反复触发栈空间的收缩扩容。
简单介绍下GO的GMP 模型?
- Go协程调度器采用的是GMP模型,描述如何给成千上万个Goroutine分配CPU资源,让他们执行任务;
- Goroutine是Golang支持高并发的重要保障,将这些Goroutine分配、负载、调度到处理器上采用的是G-M-P模型;
- GMP三个字母含义如下:
- G:Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大。
- M:Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常
- P:Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。
- GMP队列,在整个Go调度器的生命周期中,存在着两个重要的队列:
- 全局队列(Global Queue):全局只有一个
- 本地队列(Local Queue):每个 P 都会维护一个本地队列
- GMP调度流程大致如下:
- 1、线程M想运行任务就需得获取 P,即与P关联。
- 2、然从 P 的本地队列(LRQ)获取 G
- 3、若LRQ中没有可运行的G,M 会尝试从全局队列(GRQ)拿一批G放到P的本地队列,
- 4、若全局队列也未找到可运行的G时候,M会随机从其他 P 的本地队列偷一半放到自己 P 的本地队列。
- 5、拿到可运行的G之后,M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
能简述下Golang的GC原理吗?
- Golang中的垃圾回收主要应用三色标记法,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW(stop the world),
- STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,Golang进行了多次的迭代优化来解决这个问题,1.5版本以后采用三色并发标记法处理垃圾回收。
- 它的逻辑就是,准备三种颜色,分别对三种对象进行标记:
- 黑色:检测到有被引用,并且已经遍历完它所有直接引用的对象或者属性
- 白色:还没检测到有引用的对象(检测开始前,所有对象都是白色,检测结束后,没有被引用的对象都是白色,会被清查掉)
- 灰色:检测到有被引用,但是他的属性还没有被遍历完,等遍历完后也会变成黑色
- 新算法的出现,就是解决旧算法存在STW 的暂停挂起导致的程序卡顿;
- 总结来说,就是当在标记的时候出现 :一个白色对象被黑色对象引用,同时该白色对象又被某个灰色(或者上级有灰色对象)对象取消引用的情况,就会标记不准确。
- 解决方法是,使用:
- 插入屏障:在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
- 删除屏障:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
Go 中哪些动作会触发 runtime 调度?
- 当协程在进行IO等待、sleep、等待锁、等待channel或者手动触发 runtime.Gosched 都会触发协程调度器进行调度工作。
Go语言的闭包是什么?它有什么用处?
- 其实就是一个匿名函数,它可以访问闭包函数外部作用域的变量。
- 作用:
- 封装状态:闭包可以保存其外部作用域的状态,以便在以后使用。
- 支持高阶函数:闭包可以作为参数或返回值传递给其他函数,从而实现高阶函数。
Go语言的接口(interface)和其他编程语言有什么不同,如何实现多态?
- Go语言的接口(interface)是一种特殊的类型,它只定义了一组方法的签名,但没有实现,
- 与其他开发语言的不同之处在于,Go语言的接口不需要显式地实现,而是隐式地实现。
Go语言的反射机制是什么?它有什么用处?
- 用于获取程序运行时对象的类型和值,可以用于动态加载、检查和执行代码等。在很多场景下,反射机制可以使程序更加灵活和通用。
- 应用:
- 通过反射机制读取结构体信息,reflect.TypeOf、ValueOf、NumField、Field,一层层的;
- 通过反射机制调用函数,funcVal := reflect.ValueOf(func1); result := funcVal.Call(params);
介绍下Go 语言中的深拷贝和浅拷贝?
- 在变量赋值、参数传递、函数返回值过程中产生变量拷贝,
- 在Go 语言中 值类型 赋值都是 深拷贝 ,引用类型 一般都是 浅拷贝,拷贝结束还是互相影响,slice 拷贝是浅拷贝。
- 浅拷贝效率比较高,深拷贝,如果变量数据比较大的话,复制数据需要消耗的时间越多。
Golang 中的 rune 和 byte 有什么区别?
- 在 Go 语言中支持两个字符类型,就是byte数组和rune类型,主要区别就是能够表达的字符范围更大,
- byte (实际上是 uint8 的别名),代表UTF-8字符串的 单个 字节的值,用来储存ASCII码,表示一个ASCII码字符;
- rune(实际上是int32),代表单个 Unicode字符,常用来处理unicode或utf-8字符,就是rune的使用范围更大。
- byte 和 rune ,虽然都能表示一个字符,但 byte 只能表示 ASCII 码表中的一个字符(ASCII 码表总共有 256 个字符),数量远远不如 rune 多。
Go map 的值不可寻址,那如何修改值的属性?
- map的value类型使用指针类型,即可直接通过map修改value的属性。
- golang里面的map,当通过key获取到value时,这个value是不可寻址的,因为map 会进行动态扩容,
- 当进行扩展后,map的value就会进行内存迁移,其地址发生变化,所以无法对这个value进行寻址。
- 如果user[uid].UserName="polly",user[uid]获取的值不是引用类型,直接赋值就会报“无法修改map指向的结构体属性错误”;
map进行动态扩容和slice的动态扩容有什么区别?
- Map 的动态扩容:
- 扩容会在以下几种情况中触发,插入或更新元素、删除元素,扩容hash表的时候每次都增大2倍;
- map使用hash表来实现,插入一个元素到map之前,Go会判断当前的map是否需要扩容,以及是否正在进行扩容操作。
- 这是因为,如果map中的元素过多,迁移操作将会耗费大量的时间和内存。为了优化这一过程,Go采用了分批迁移的策略。
- 在 map 中插入新元素时,如果已有的桶(bucket)已满,就会触发动态扩容。
- 扩容时,Golang 会创建一个新的底层数组,并将原来的键值对重新哈希到新的桶中。
- 扩容操作会耗费一定的时间和内存,因为需要重新哈希和复制键值对。
- map的填充因子是6.5,如果负载因子没有超标,但是使用的溢出桶较多,也会触发扩容。但是是等量扩容
- Slice 的动态扩容:
- 扩容时,Golang 会创建一个新的底层数组,并将原有的元素复制到新的数组中。
- 扩容操作会耗费一定的时间和内存,因为需要创建新数组和复制元素。
- Map 的扩容是通过重新哈希键来实现,而 Slice 的扩容是通过创建新数组并复制元素来实现。
- 在性能方面,Map 的动态扩容可能会比 Slice 稍微慢一些,因为 map 需要重新哈希键值对。
- 在编写代码时,应尽量避免频繁的动态扩容操作,在预知数据量较大的情况下,可以通过提前分配足够容量的 map 或 slice 来减少扩容次数。
- Map 的动态扩容:
new和make的区别?
- 二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配
- 对于引用类型的变量,我们不光要声明它,还要为它分配内容空间,否则我们的值放在哪里去呢?
- 对于值类型的声明不需要,是因为已经默认帮我们分配好了,int类型的零值是0,string类型的零值是"",引用类型的零值是nil。
- new只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。
- make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建,而且它返回的类型就是这三个类型本身,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
- 其实new不常用,我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如:u:=user{}
mysql
B树和B+树的区别
- 索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数,B+树是在B树上优化的。
- B树
- 提高了磁盘IO性能,但数据分散在各个节点,并没有解决元素遍历的效率低下的问题;
- 在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作,只要遍历叶子节点就可以实现整棵树的遍历。
- B+树
- 索引的所有数据均存储在叶子节点,而且数据是使用指针顺序连接排列的,B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。
- 在 InnoDB 中,数据页之间通过双向链表连接,以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。
- B+树的阶数
- 是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。
- 一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。
- 增删文件(节点)时,效率更高,因为B+树的叶子节点包含所有关键字,提高效率;
- 适用场景:
- B+树常用于数据库索引,B树常用于文件索引;
聚簇索引和非聚簇索引
- 当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。
- innodb中,在聚簇索引之上创建的索引称之为辅助索引;InnoDB 中页的默认大小是 16KB。
- 非聚簇索引:将数据存储于索引分开结构,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,
- 然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因;
64位的操作系统用几个字节表示物理地址?
- 系统位数的不同代表着CPU一次处理数据的位数不同,对于32位系统,一次可以处理4个字节数据,而64位系统的CPU一次可以处理8个字节的数据。
- 32位系统,CPU的寻址范围是2^32,所以内存地址编号就对应有2^32个,那么地址应该是4个字节代表的。那么32位系统对应的内存大小是 4g个字节。
- 64位系统,CPU的寻址范围是2^64,所以内存地址编号就对应有2^64个,那么地址应该是8个字节代表的。
- 16kb/8B = 16 * 2^10b / 8B = 2 * 1024B = 2048,B+树一个结点可以存2048个地址;
MySQL和PgSQL的区别
- 数据类型:MySQL数据类型相对较少,而PgSQL提供了广泛的数据类型支持,并引入了一些扩展的数据类型,如array、json、布尔型、IP地址等)。
- 数据类型转化:MySQL、Oracle等都是默认对数据类型进行了隐式的转换,字符串类型和数字可以进行自动的隐式转换,但是PG确没有这么处理;
- 扩展性:MySQL不支持拓展性,而PgSQL是高度可扩展的;
- ACID兼容性:PostgreSQL是完全ACID兼容的数据库,而MySQL只在特定的存储引擎(如InnoDB)中支持ACID
- 存储引擎:MySQL支持多个存储引擎,包括MyISAM和InnoDB等。每种存储引擎都有自己的特点和优缺点。PostgreSQL仅支持单个存储引擎。
- SQL标准兼容性:PostgreSQL更加符合SQL标准,而MySQL在某些方面采用了自己的实现方式。
- 复杂的事务处理:PostgreSQL支持复杂的事务管理,适合需要严格ACID属性的金融等关键业务应用;
- 高可用性和可伸缩性:PostgreSQL提供高可用性和可伸缩性,适合于需要7*24小时运行的系统。
- 应用场景
- PostgreSQL因其强大的功能和性能,特别适用于处理大量复杂数据的应用场景,如数据仓库、科学计算、金融、地理信息系统等
- MySQL则更适用于简单的应用场景,如电子商务、博客、网站等。在这些场景中;
grpc
grpc框架
- gRPC 是一个高性能、跨平台、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。
- 带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特;
- 在服务端实现这个方法,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根,这个存根就是长得像服务端一样的方法(但是没有具体实现),客户端通过这个存根调用服务端的方法。
- gRPC 默认使用 protocol buffers,这是一套开源成熟的结构数据的序列化机制,当然也可以使用其他数据格式如 JSON,不过通常都使用protocol buffers这种灵活、高效的数据格式;
- 使用gprc
- 首先需要定义服务, 指定其可以被远程调用的方法及其参数和返回类型。
- 通过protobuf定义服务;
- gRPC 允许你定义四类服务方法,以及客户端和服务端的交互方式。
- 单向RPC:
- 即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
- 服务端流(stream)式 RPC:
- 即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
- 通俗的讲就是客户端请求一次,服务端就可以源源不断的给客户端发送消息。
- 客户端流式 RPC
- 即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
- 通俗的讲就是请求一次,客户端就可以源源不断的往服务端发送消息。
- 双向流式 RPC
- 即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,
- 例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
- 类似tcp通信,客户端和服务端可以互相发消息。
- 单向RPC:
ProtoBuf
- 是一种数据交换的、可扩展的序列化结构数据的方法,它可用于(数据)通信协议。
- 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
- json\xml都是基于文本格式,protobuf是二进制格式。
- 可以通过 ProtoBuf 定义数据结构,然后通过 ProtoBuf 工具生成各种语言版本的数据结构类库,用于操作 ProtoBuf 协议数据;
- 创建 .proto 文件,定义数据结构
- 定义数据结构,message 你可以想象成java的class,go语言中的struct,可以定义属性和方法;
- proto文件中,字段后面的序号,不能重复,定义了就不能修改,可以理解成字段的唯一ID。
- 安装ProtoBuf编译器,用protoc命令将.proto文件,编译成指定语言类库并使用;
- 如果你把service和message关键词当成class,是不是跟类定义很像!
- protobuf消息定义
- 语法结构,跟我们平时接触的各种语言的类定义,非常相似。
- 支持多种数据类型,例如:
- string、int32、double、float等等
- 还有枚举(enum)类型,枚举类型首成员必须为0,成员不应有相同的值;
- 在protobuf消息中定义数组类型,是通过在字段前面增加repeated关键词实现,标记当前字段是一个数组。
- 只要使用repeated标记类型定义,就表示数组类型。
- protocol buffers支持map类型定义。
- 在消息定义中,每个字段后面都有一个唯一的数字,这个就是标识号。
- 这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变,每个消息内唯一即可,不同的消息定义可以拥有相同的标识号。
- [1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号;
- 也可以为消息定义包foo.bar,类似与命名空间;
- protobuf 消息嵌套
- 和各种语言开发中类的定义是可以互相嵌套的,也可以使用其他类作为自己的成员属性类型。
- 在protobuf中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。
- 消息类型中引用其他消息类型;
- 消息嵌套中引用其他消息嵌套;
- import导入其他proto文件定义的消息,在版本之后syntax = "proto3";
- 将消息编译成各种语言版本的类库 protoc [OPTION] PROTO_FILES,javascript、go、python等“--php_out=OUT_DIR”指定代码生成目录;
Http 1.1和2
- 持久连接(HTTP/1.1支持)
- 双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特;
RabbitMQ
有队列名和路由两种匹配方式,队列名是通过队列一对N个消费者,路由是多加了一层交换机(fanout、Direct)来对应队列一对N个消费者;
RabbitMQ基本概念有哪些?
- Broker: 表示消息队列服务器
- Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
- Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
- Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
- Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
- VHost: vhost 可以理解为虚拟 broker ,包含:一批交换机,消息队列和相关的对象。
- Producer: 消息生产者,就是投递消息的程序
- Consumer: 消息消费者,就是接受消息的程序
- Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
RabbitMQ有哪些使用场景?
- 异步任务:主业务流程无需同步等待其他系统的处理结果,从而达到系统快速响应的目的。
- 服务间异步通信:多个服务之间或者系统之间,通过消息互相通信,就大家平时跟朋友互发短信差不多。
- 应用解耦:基于消息订阅机制实现业务扩展,例如:用户下单之后,产生一条订单消息,然后仓库模块订阅订单消息发货、积分模块可以订阅订单消息增加积分等等。
- 顺序消费:业务层面需要排队处理的场景,例如:活动报名,名额有限,先到先得。
- 定时任务:通过消息队列的延迟消息机制,可以实现定时执行任务的效果,例如:订单15分钟内未支付,则关闭订单,15分钟后消息才会投递给消费者处理。
- 请求削峰:可以起到调节上下游系统之间处理流量的能力存在差异的作用,维持在一个稳定运转的状态。
为什么使用MQ?MQ的优点有那些?
- 主要是,解耦、异步、削峰这三个方面;
- 解耦:一个系统调用了多个系统,不需要直接同步调用接口的,直接用 MQ 给它异步化解耦。
- 异步:A系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,A系统连续发送3条消息到MQ队列中就返回响应给用户;
- 削峰:减少高峰时期对服务器压力。
系统引入RabbitMQ有什么缺点?
- 会带来下面三个方面的影响,总体上引入RabbitMQ消息队列有好处,也带来了一些问题,需要权衡利弊;
- 系统可用性降低:RabbitMQ自己本身不稳定系统也会受影响,所以系统可用性会降低,总体上引入的服务组件越多,要维护的东西越多,不稳定的因素越多;
- 系统复杂度提高:加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等;
- 一致性问题:A 系统处理完了直接返回成功了,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
消息如何路由(有几种交换机类型)?
- 消息如何路由、消息如何投递跟交换机(exchange)类型有关,不同的交换机类型消息路由策略不一样。
- 常用的交换机如下,不同交换机的消息路由策略::
- Direct类型:如果路由键完全匹配,消息就被投递到相应的队列;
- Topic 类型:跟direct类似,区别是路由键支持通配符;
- Fanout 类型:如果交换器收到消息,将会广播到所有绑定的队列上;
RabbitMQ有几种工作模式?
- 根据交换机类型和消费者数量的不同,RabbitMQ有下面几种工作模式:
- 简单队列:一个消息生产者,一个消息消费者;
- Work队列:一个生产者,多个消费者;
- 发布订阅模式:
- 一个生产者发送的消息会被多个消费者获取,因为一条消息会被多个消费者分别消费处理,所以也叫广播模式、一对多模式;
- 有交换机(fanout Exchange)参与,交换机(Exchange)负责将消息转发至绑定交换机的所有队列。
- 可以定义多个队列,分别绑定同一个交换机,每个队列可以有一个或者多个消费者。
- 路由模式
- 路由模式大体上跟发布订阅模式一样,区别在于发布订阅模式将消息转发给所有绑定的队列,而路由模式将消息转发给那个队列是根据路由匹配情况决定的。
- 有交换机参与,交换机类型为direct,参与者有,P代表生产者, X代表交换机,红色Q1、Q2代表队列,C1、C2 代表消费者;
- direct交换机转发消息逻辑:将消息中的Routing key与该Exchange关联的所有Binding中的Routing key进行比较,如果相等,则发送到该Binding对应的Queue中。
- 主题模式(Topic)
- 跟路由模式类似,区别在于主题模式的路由匹配支持通配符模糊匹配,而路由模式仅支持完全匹配。
- 有交换机参与,交换机类型为Topic,参与者有,P代表生产者, X代表交换机,红色Q1、Q2代表队列,C1、C2 代表消费者;
- topic交换机转发消息逻辑:将消息中的Routing key与该Exchange关联的所有Binding中的Routing key进行模糊匹配,如果匹配,则发送到绑定的Queue中。
如何解决消息的顺序问题?
- RabbitMQ的消息顺序问题,需要分三个环节看待,发送消息的顺序、队列中消息的顺序、消费消息的顺序。
- 发送消息的顺序:如果遇到业务一定要发送消息也确保顺序,那意味着,只能全局加锁一个个的操作,一个个的发消息,不能并发发送消息。
- 队列中消息的顺序:在同一个队列中,消息是顺序的,先进先出原则,这个由Rabbitmq保证,不同队列中的消息顺序,是没有保证的;
- 消费消息的顺序:在多个消费者消费同一个消息队列的场景,通常是无法保证消息顺序的。
- 解决消费顺序的问题,通常就是一个队列只有一个消费者,这样就可以一个个消息按顺序处理,缺点就是并发能力下降了,无法并发消费消息,这是个取舍问题。
如何处理重复消息?(即消息幂等性保证)
- 处理RabbitMQ重复消息的问题,就是处理消息的业务逻辑保持幂等性,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。
- 这个问题需要灵活作答,因为消费的场景有很多,有数据库、有缓存、有第三方接口。
- 总体上根据不同的业务增加去重逻辑,通常就是根据业务设置一个唯一标识,通过检测这个唯一标识是否已经处理过来处理去重问题。
- 例如:
- 比如针对数据库,插入数据,可以通过唯一键,重复插入会报错,又或者插入之前先检测一下是否存在,针对更新操作,可以根据数据的状态判断是否已经处理过。
- 再比如redis缓存,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
- 再比如第三方接口,接口需要有去重能力。
如何确保消息正确地发送至RabbitMQ?
- RabbitMQ支持Confirm模式,确认消息有没有安全的投递给RabbitMQ。
- 大致原理如下:
- 将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。
- 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。
- 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。
- 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
如何确保消息接收方消费了消息?
- 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
- RabbitMQ消费者消息确认(ACK)有两种机制,
- 自动确认:就是消费者收到消息RabbitMQ就删除消息;
- 手动确认:需要我们在代码层面主动调用Ack方法通知RabbitMQ,消息已经处理。
- 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;
- 有几种特殊情况:
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
- 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息;
如何确保消息不丢失?
- 主要通过持久化机制,确保消息不丢,RabbitMQ持久化机制分为队列持久化、消息持久化、交换器持久化。
- 消息持久化
- RabbitMQ 的消息默认存放在内存上面,如果不特别声明,消息不会持久化保存到硬盘上面,如果节点重启或者意外crash掉,消息就会丢失。
- 要想做到消息持久化,必须满足以下三个条件:
- Exchange 设置持久化
- Queue 设置持久化
- Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息;
- 消息ACK机制
- 默认情况消费者收到消息,MQ就会从队列中删除消息,如果消费者没处理成功,消息就丢了,
- 可以使用手动ACK机制,处理完成手动调用MQ的ACK方法通知MQ删除消息。
- RabbitMQ集群模式:使用集群模式部署RabbitMQ,实现消息的高可用,避免单个MQ节点挂了,消息就没了。
- 消息补偿:有时候可能是因为消息过期(TTL)、或者消费者异常导致消息丢了,这个时候需要从业务数据角度,写个脚本重新生成消息,投递到消息队列中。
什么是延迟队列?
- 延迟队列,可以用来做定时任务。
- RabbitMQ延迟队列就是存储延迟消息的队列,延迟消息指的就是消息投递到队列后,消费者不能立刻消费,需要等待一段时间,消费者才能消费消息。
- RabbitMQ原生不支持延迟消息,目前主要通过死信交换机 + 消息TTL方案或者rabbitmq-delayed-message-exchange插件实现。
什么是死信队列?
- DLX,全称为 Dead-Letter-Exchange,死信交换机,死信邮箱。
- 当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX 的队列就称之为死信队列。
- 导致死信的原因:
- 消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
- 消息TTL过期。
- 队列满了,无法再添加。
- 什么是优先级队列?
- 优先级高的消息具备优先被消费的特权。
- RabbitMQ优先级队列注意点:
- 只有当消费者不足,不能及时进行消费的情况下,优先级队列才会生效。消息如果瞬间消费完了,那优先级就没意义;
- RabbitMQ3.5版本以后才支持优先级队列。
- 使用层面,可以通过x-max-priority参数来设置队列最大优先级,官方推荐1-10之间,然后发送消息的时候设置消息优先级即可。
如何解决消息积压?
- 如何解决RabbitMQ消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?数百万消息持续积压几个小时,说说怎么解决?
- 消息堆积解决策略1
- 消费者临时扩容,例如:原先是10个消费者,扩容10倍,100个消费者,目的是加快消息消费速度。
- 扩容数量根据实际情况确定。
- 消息堆积解决策略2
- 修复消费者问题,消费者本身的问题,主要体现在两个方面: 业务异常和消息处理速度慢。
- 如果是消费者自己异常了,导致无法正常消费消息,只要修复异常问题即可。
- 如果是消费者处理速度慢,可以分析下业务代码有没有进一步提升性能的空间,有就优化,没有就走策略1扩容方案。
- 消息堆积解决策略3
- 如果消息堆积的太多,短时间内消费不完(需要几个小时,甚至更长时间),可以做个取舍,反正前面的客户已经得罪了,新的客户不能得罪,我们可以确保新的消息可以正常消费,老的消息慢慢处理。
- 可以新开一个队列,让新的消息投递到这个队列,新开一批消费者,处理新的消息,老的队列里面堆积的消息,让一批消费者慢慢跑。
- 消息堆积解决策略4
- 如果堆积的消息不重要,直接干掉(删除)队列,创建新的队列,处理新的消息就行,不能在一棵树上吊死。
- 消息堆积解决策略5
- 如果消息设置了TTL,这种情况,消息可能因为已经到期,被丢弃了,丢了多少我们不知道,为确保业务消息都被正常消费,这里首先要决绝的是怎么找回丢失的消息,
- 主要思路是根据业务数据,重新投递消息到MQ中,例如:根据订单记录,如果订单未处理,重新投递消息到消息队列。