- 基础面试题
- 1 GO
- 1.1 如何防止goroutin泄露
- 1.2 Go语言的锁
- 1.3 Go的IO
- 1.4 new和make的区别
- 1.5 go中init函数
- 1.6 golang中引用类型和值类型及内存分配
- 1.7 接受者和方法参数何时使用指针类型,何时使用值类型,区别?
- 1.8 值接受者和指针接受者互相调用问题
- 1.9 Go的调度
- 1.10 go中的slice扩容规则
- 1.11 go中的map扩容规则
- 1.12 Go的反射包怎么找到对应的方法
- 1.13 Go的channel(有缓冲和无缓冲)
- 1.14 Gin的context和Go的context
- 1.15 go的垃圾回收
- 1.16 golang中开协程的限制,底层调度?8C8G的服务器最多开多少协程?
- 1.17 Go调度前身(进程,线程,协程)
- 2 Redis
- 3 Mysql
- 4 网络
- 5 Linux
- 6 并发
- 7 算法
- 8 Web
- 1 GO
基础面试题
1 GO
1.1 如何防止goroutin泄露
其实无论是死循环、channel 阻塞、锁等待,只要是会造成阻塞的写法都可能产生泄露。因而,如何防止 goroutine 泄露就变成了如何防止发生阻塞。为进一步防止泄露,有些实现中会加入超时处理,主动释放处理时间太长的 goroutine。
1.2 Go语言的锁
Go语言存在两种锁,排他锁和共享锁。
var (
lock sync.Mutex // 排他锁又叫互斥锁
rwlock sync.RWMutex // 读写锁又叫共享锁
)
1.3 Go的IO
1、常规读写
- os.Mkdir(name string, perm FileMode) error // 仅创建一层
- os.MkdirAll(path string, perm FileMode) error // 创建多层
- os.Create(name string) (file *File, err error) // 存在则覆盖
- os.Open(name string) (file *File, err error) // 只读方式打开文件
- os.OpenFile(name string, flag int, perm FileMode) (file *File, err error) // parm控制权限,例如0066、0777
- file.Close() error // 关闭文件,断开程序与文件的连接
- os.Remove(name string) error // 删除文件一层
- os.RemoveAll(path string) error // 级联删除
2、带缓冲读写bufio
io.Reader和io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
1.4 new和make的区别
new(T) 和 make(T,args) 是 Go 语言内建函数,用来分配内存,但适用的类型不同。
new(T) 会为 T 类型的新值分配已置零的内存空间,并返回地址(指针),即类型为 *T的值。换句话说就是,返回一个指针,该指针指向新分配的、类型为 T 的零值。适用于值类型,如数组、结构体等。
make(T,args) 返回初始化之后的 T 类型的值,这个值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。make() 只适用于 slice、map 和 channel.
1.5 go中init函数
一个包中,可以包含多个 init 函数;
程序编译时,先执行依赖包的 init 函数,再执行 main 包内的 init 函数;可以用_来忽略导入包,但是执行该包的init函数
main包也可以包含不止一个Init函数,且init函数不能被其他函数显示调用,否则编译错误
1.6 golang中引用类型和值类型及内存分配
1.值类型:变量直接存储值,内存通常在栈中分配。
值类型:基本数据类型int、float、bool、string以及数组和struct
2.引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在 堆上分配。通过GC回收。
引用类型:指针、slice、map、chan等都是引用类型。
1.7 接受者和方法参数何时使用指针类型,何时使用值类型,区别?
方法接受推荐使用指针类型。
- 推荐在实例方法上使用指针(前提是这个类型不是一个自定义的 map、slice 等引用类型)
- 当结构体较大的时候使用指针会更高效。一般结构体超过五个字段,用指针
- 如果要修改结构内部的数据或状态必须使用指针
- 当结构类型包含 sync.Mutex 或者同步这种字段时,必须使用指针以避免成员拷贝
- 如果你不知道该不该使用指针,使用指针!
方法参数该使用什么类型?
- map、slice 等类型不需要使用指针(自带 buf)
- 指针可以避免内存拷贝,结构大的时候不要使用值类型
- 值类型和指针类型在方法内部都会产生一份拷贝,指向不同
- 小数据类型如 bool、int 等没必要使用指针传递
- 初始化一个新类型时(像 NewEngine() *Engine)使用指针
- 变量的生命周期越长则使用指针,否则使用值类型
1.8 值接受者和指针接受者互相调用问题
1、值类型可以调用值接受者方法,也能调用指针接收者方法,当值类型调用指针接受者方法是,编译器底层会先取地址&,再调用
2、指针类型也已调用指针接受者方法,也可以调用值接收方法,当指针类型调用值接受者方法是,编译器底层会先解引用*,再调用
特殊情况需要注意:
1、当值类型不能被寻址的时候,该值不能调用对应的指针接受者方法
2、当指针接受者方法是为了实现某个接口的时候,那么只有指针类型的实体实现了接口。用值类型,不算实现接口。如果是值接收者,实体类型的值和指针都可以实现对应的接口;
1.9 Go的调度
G: 协程,对应的数据结构为runtime.G。全局变量allgs记录着所有的g
P: 数据结构为runtime.P。拥有本地runq变量,和全局变量sched,sched对应的结构是runtime.schedt代表调度器,P会和一个M绑定,M可以直接从P这里获取待执行的G
M: 工作线程,对应的数据结构为runtime.M。全局变量allm记录着所有的m
如果P的本地队列已满,那么等待执行的G就会被放到全局队列中,M会先从关联P所持有的本地runq中获取待执行的G,如果没有的话再去全局队列中领取一些G来执行,如果全局队列中也没有多余的G,那就去别的P那里领取一些G。
1.10 go中的slice扩容规则
规则1:需要增长到的容量cap是原始容量的两倍还多,则扩容到cap
规则2.1:需要增长的容量小于原容量2倍,但是原用量小于1024,则2倍扩容
规则2.2:需要增长的容量小于原容量2倍,但是原容量大于1024,则1.25倍扩容
扩容后需要分配的内存,并不是扩容后的容量乘以数据类型字节。而是根据扩容后的容量去内存管理模块申请最匹配且覆盖的容量,根据这个容量乘以数据类型,才是最终需要分配的内存空间
1.11 go中的map扩容规则
- go语言map的默认负载因子是6.5
规则1:count / (2 ^ B) > 6.5 时,翻倍扩容;
规则2:负载因子没超标,但是溢出桶使用较多,触发等量扩容。所谓等量扩容,就是创建和旧桶数目一样多的新桶,把旧桶中的值迁移到新桶中。
注意:等量扩容有什么用,如果负载因子没超,但是用了很多的溢出桶。那么只能说明存在很多的删除的键值对。扩容后更加紧凑,减少了溢出桶的使用
超过负载因子,翻倍扩容;溢出桶较多,等量扩容
1.12 Go的反射包怎么找到对应的方法
反射主要两个函数:
func TypeOf(i interface{}) Type // 用来提取一个接口中值的类型信息。
func ValueOf(i interface{}) Value
返回的Type是接口类型,Type类型包含很多个方法,可以调用:
type Type interface {
// 所有的类型都可以调用下面这些函数。下面简单列举,函数比较多
// 此类型的变量对齐后所占用的字节数
Align() int
// 如果是 struct 的字段,对齐后占用的字节数
FieldAlign() int
// 返回类型方法集里的第 `i` (传入的参数)个方法
Method(int) Method
// 通过名称获取方法
MethodByName(string) (Method, bool)
// 获取类型方法集里导出的方法个数
NumMethod() int
// 类型名称
Name() string
// 返回类型所在的路径,如:encoding/base64
PkgPath() string
// 返回类型的大小,和 unsafe.Sizeof 功能类似
Size() uintptr
// 返回类型的字符串表示形式
String() string
// 返回类型的类型值
Kind() Kind
// 类型是否实现了接口 u
Implements(u Type) bool
// 是否可以赋值给 u
AssignableTo(u Type) bool
// 是否可以类型转换成 u
ConvertibleTo(u Type) bool
// 类型是否可以比较
Comparable() bool
// ......
}
Value是结构体,它包含类型结构体指针、真实数据的地址、标志位。Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:
// 设置切片的 len 字段,如果类型不是切片,就会panic
func (v Value) SetLen(n int)
// 设置切片的 cap 字段
func (v Value) SetCap(n int)
// 设置字典的 kv
func (v Value) SetMapIndex(key, val Value)
// 返回切片、字符串、数组的索引 i 处的值
func (v Value) Index(i int) Value
// 根据名称获取结构体的内部字段值
func (v Value) FieldByName(name string) Value
// ……
Value 字段还有很多其他的方法。例如:
// 用来获取 int 类型的值
func (v Value) Int() int64
// 用来获取结构体字段(成员)数量
func (v Value) NumField() int
// 尝试向通道发送数据(不会阻塞)
func (v Value) TrySend(x reflect.Value) bool
// 通过参数列表 in 调用 v 值所代表的函数(或方法
func (v Value) Call(in []Value) (r []Value)
// 调用变参长度可变的函数
func (v Value) CallSlice(in []Value) []Value
// 等等....
1.13 Go的channel(有缓冲和无缓冲)
1、ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.
2、ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.
3、channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
4、channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞
1.14 Gin的context和Go的context
Context是由Golang官方开发的并发控制包,一方面可以用于当请求超时或者取消时候,相关的goroutine马上退出释放资源,另一方面Context本身含义就是上下文,其可以在多个goroutine或者多个处理函数之间传递共享的信息。
创建一个新的context,必须基于一个父context,新的context又可以作为其他context的父context。所有context在一起构造一个context树。
1、Context用途:
- Context一大用处就是超时控制。
- Context另外一个用途就是传递上下文信息。
2、Context接口一共包含四个方法:
- Deadline:返回绑定该context任务的执行超时时间,若未设置,则ok等于false
- Done:返回一个只读通道,当绑定该context的任务执行完成并调用cancel方法或者任务执行超时时候,该通道会被关闭
- Err:返回一个错误,如果Done返回的通道未关闭则返回nil,如果context如果被取消,返回Canceled错误,如果超时则会返回DeadlineExceeded错误
- Value:根据key返回,存储在context中k-v数据
3、使用Context注意事项:
- 不要将Context作为结构体的一个字段存储,相反而应该显示传递Context给每一个需要它的函数,Context应该作为函数的第一个参数,并命名为ctx
- 不要传递一个nil Context给一个函数,即使该函数能够接受它。如果你不确定使用哪一个Context,那你就传递context.TODO
- context是并发安全的,相同的Context能够传递给运行在不同goroutine的函数
Gin的Context是Go中Context接口的一个实现。
1.15 go的垃圾回收
常用垃圾回收原则有引用计数和标记清扫。Go采用的是标记清扫。
Go语言中的垃圾回收采用标记清扫算法,支持主体并发增量式回收。使用插入和删除两种写屏障的混合写屏障,并发是用户程序和垃圾回收可以并发执行,增量式回收保证一次垃圾回收的STW分摊到多次。
标记清扫-三色标记:
要识别存活对象,可以把栈,数据段上的数据对象作为根root,基于他们进一步追踪,把能追踪到的数据都进行标记。剩下的追踪不到的就是垃圾了。
- 垃圾回收开始时,所有数据都为白色,然后把直接追踪到的root节点标记为灰色,灰色代表基于当前节点展开的追踪还未完成。
- 当基于某个root节点的追踪任务完成后,便会把该root节点标记为黑色,黑色表示它是存活数据,而且无需基于它再次追踪了。
- 基于黑色节点找到的所有节点都被标记为灰色,表示还要基于它们进一步展开追踪
。当没有灰色节点时,意味着标记工作可以结束了。此时有用数据都为黑色,无用数据都为白色,接下来回收这些白色对象的内存地址即可
1.16 golang中开协程的限制,底层调度?8C8G的服务器最多开多少协程?
1、计算机资源肯定是有限的,所以goroutine肯定也是有限制的,单纯的goroutine,一开始每个占用4K内存,所以这里会受到内存使用量的限制,还有goroutine是通过系统线程来执行的,golang默认最大的线程数是10000个。可以通过https://gowalker.org/runtime/debug#SetMaxThreads来修改。
2、要注意线程和goroutine不是一一对应关系,理论上内存足够大,而且goroutine不是计算密集型的话,可以开启无限个goroutine。
注意:如果限制了内存和cpu,例如8C8G,那么协程开辟是有限的,当开辟到一定数量,占据用户态的cpu和内存,以及文件句柄过多的话,会导致主协程崩溃退出。所以科学的方式需要使用chan和sync.waitGroup{}对Goroutine限流,也就是worker的原理。参考1.17
1.17 Go调度前身(进程,线程,协程)
首先,进程不开线程,默认是一个线程,每个线程对应进程中的一个执行单元,cpu按照执行单元来分配时间片。CPU的视野是只能看见内核的,它不知晓谁是进程和谁是线程,谁和谁是一家人。时间片轮询平均调度分配。进程拥有更多的执行单元就有了资源供给的优势。
其次,进程开很多线程也是不合理的,cpu在内核态切换一个执行单元的时候,会有一个时间成本和性能开销。
综上,我们不能够大量的开线程辟,因为线程执行流程越多,cpu在切换的时间成本越大。很多编程语言就想了办法,既然我们不能左右和优化cpu切换线程的开销,那么,我们能否让cpu内核态不切换执行单元,而是在用户态切换执行流程呢?很显然,我们是没权限修改操作系统内核机制的,那么只能在用户态再来一个伪执行单元,那么就是协程了。
协程切换比线程切换快主要有两点:
(1)协程切换完全在用户空间进行线程切换涉及特权模式切换,需要在内核空间完成;
(2)协程切换相比线程切换做的事情更少,线程需要有内核和用户态的切换,系统调用过程。
协程切换非常简单,就是把当前协程的CPU寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的CPU寄存器上就ok了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十ns 这个量级。
线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统调度模块完成线程调度(taskstruct),而且除了和协程相同基本的 CPU 上下文,还有线程私有的栈和寄存器等,说白了就是上下文比协程多一些,其实简单比较下 task_strcut 和 任何一个协程库的 coroutine 的 struct 结构体大小就能明显区分出来。
进程占用多少内存:4G
线程占用多少内存:一般是4~64Mb不等, 多数维持约10M上下
协程占用多少内存:2~4kb
go的协程切换成本如此小,占用也那么小,是否可以无限开辟呢?
我们迅速的开辟goroutine(不控制并发的 goroutine 数量 )会在短时间内占据操作系统的资源(CPU、内存、文件描述符等)。导致CPU 使用率浮动上涨,Memory 占用不断上涨。主进程崩溃(被杀掉了)。
这些资源实际上是所有用户态程序共享的资源,所以大批的goroutine最终引发的灾难不仅仅是自身,还会关联其他运行的程序。所以在编写逻辑业务的时候,限制goroutine是我们必须要重视的问题。
所以需要限制goroutine的创建速度,常见的可行的方法是chan+sync.waitGroup{}
package main
import (
"fmt"
"math"
"sync"
"runtime"
)
var wg = sync.WaitGroup{}
func busi(ch chan bool, i int) {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
<-ch
wg.Done()
}
func main() {
//模拟用户需求go业务的数量
task_cnt := math.MaxInt64
ch := make(chan bool, 3)
for i := 0; i < task_cnt; i++ {
wg.Add(1)
ch <- true
go busi(ch, i)
}
wg.Wait()
}
2 Redis
2.1 redis中缓存穿透和缓存雪崩的概念?
缓存穿透:就是查询一个数据库不存在的key,先查缓存,缓存没有再查数据库,数据库也没有也就不会缓存。那么不停的查这个不存在的key就属于恶意攻击,存在缓存击穿的概念
缓存雪崩:就是一批商品的缓存时间是一样的,如果遇到双十一,一批商品同时缓存失效,查询都会落到DB的头像。所以设置缓存尽量分散缓存过期时间,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源
缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
2.2 redis持久化?两种持久化的方式?
RDB(Redis DataBase)和AOF(Append Only File)。
RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。
全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。
2.3 redis怎么保证数据不丢
1、持久化保证了即使redis服务重启也不会丢失数据,因为redis服务重启后会将硬盘上持久化的数据恢复到内存中
2、通过redis的主从复制机制就可以避免单点故障,例如某台机器宕机或者某台机器硬盘损坏
2.4 Redis是单线程的,但Redis为什么这么快?
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
2.5 为什么Redis设计成单线程的?
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
2.6 RDB和AOF的优缺点
RDB持久化
优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
AOF持久化
与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。
2.7 Redis分布式锁实现
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!setnx和expire合成一条指令实现了原子操作一个key,并设置过期时间
SET key value [EX seconds] [PX millisecounds] [NX|XX]
EX seconds:设置键的过期时间为second秒
PX millisecounds:设置键的过期时间为millisecounds 毫秒
NX:只在键不存在的时候,才对键进行设置操作
XX:只在键已经存在的时候,才对键进行设置操作
SET操作成功后,返回的是OK,失败返回NIL
eg: set abc 123 ex 10 nx
2.8 redis常见性能问题和解决方案
Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
尽量避免在压力很大的主库上增加从库
2.9 布隆过滤器
bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。
2.10 多节点 Redis 分布式锁:Redlock 算法
获取当前时间(start)。
依次向 N 个 Redis节点请求锁。请求锁的方式与从单节点 Redis获取锁的方式一致。为了保证在某个 Redis节点不可用时该算法能够继续运行,获取锁的操作都需要设置超时时间,需要保证该超时时间远小于锁的有效时间。这样才能保证客户端在向某个 Redis节点获取锁失败之后,可以立刻尝试下一个节点。
计算获取锁的过程总共消耗多长时间(consumeTime = end - start)。如果客户端从大多数 Redis节点(>= N/2 + 1) 成功获取锁,并且获取锁总时长没有超过锁的有效时间,这种情况下,客户端会认为获取锁成功,否则,获取锁失败。
如果最终获取锁成功,锁的有效时间应该重新设置为锁最初的有效时间减去 consumeTime。
如果最终获取锁失败,客户端应该立刻向所有 Redis节点发起释放锁的请求。
3 Mysql
3.1 Mysql事务ACID特性
- 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
- 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
- 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
- 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
3.2 事务的隔离级别
- 脏读:读取未提交数据、常发生在转账与取款操作中
- 不可重复读:前后多次读取,数据内容不一致。
- 幻读:前后多次读取,数据总量不一致
读未提交:在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。
读已提交:在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。
可重复读:在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。mysql的默认事务隔离级别
可串行化:在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted(未提交读) | T | T | T |
read committed(提交读) | F | T | T |
repeatable read(可重复读) | F | F | T |
serializable (可串行化) | F | F | F |
3.3 InnoDb是表锁还是行锁,为什么
只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁
3.4 Mysql索引的底层数据结构是什么?为什么要使用B+树?B+树为什么要比B树稳定?
Mysql底层数据结构是B+树,B+树的查找效率更高,B+树中间层级的节点不保存数据,只保存索引,所以整体层数回更少,范围查询上B+树优势更大,原因是B+树数据节点都在最下层,且节点与节点之间有引用指向
- 单一节点存储更多的元素,使得查询的IO次数更少;
- 所有查询都要查找到叶子节点,查询性能稳定;
- 所有叶子节点形成有序链表,便于范围查询。
3.5 Mysql的执行计划?执行计划中,哪些语句可以看出来该语句走了全表扫描?
其中最重要的字段为:id、type、key、rows、Extra
- id : id相同:执行顺序由上至下 ;id不同:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
- select_type:查询的类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询
- type :该属性表示访问类型,有很多种访问类型。最常见的其中包括以下几种: ALL(全表扫描), index(索引扫描),range(范围扫描),ref (非唯一索引扫描),eq_ref(唯一索引扫描,),(const)常数引用, 访问速度依次由慢到快。一般来说,好的sql查询至少达到range级别,最好能达到ref
3.6 如何优化一条慢sql
1、查看SQL是否涉及多表的联表或者子查询,如果有,看是否能进行业务拆分,相关字段冗余或者合并成临时表(业务和算法的优化)
2、涉及联表的查询,是否能进行分表查询,单表查询之后的结果进行字段整合
3、如果以上两种都不能操作,非要联表查询,那么考虑对相对应的查询条件做索引。加快查询速度
4、针对数量大的表进行历史表分离(如交易流水表)
5、数据库主从分离,读写分离,降低读写针对同一表同时的压力,
至于主从同步,MySQL有自带的binlog实现主从同步
6、explain分析SQL语句,查看执行计划,分析索引是否用上,分
析扫描行数等等
7、查看MySQL执行日志,看看是否有其他方面的问题
个人理解:从根本上来说,查询慢是占用MySQL内存比较多,那么可以从这方面去酌手考虑
4 网络
4.1 time-wait和close-wait
1、主动关闭连接的一方 - 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态
2、被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接
3、TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;
4、在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!
TIME_WAIT可以通过调参解决,CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。
4.2 TCP三次握手
4.3TCP的拥塞控制?拥塞控制做什么用的,通过哪种方式控制网络的传输速度?
1、慢启动阶段思路是不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小,在没有出现丢包时每收到一个 ACK 就将拥塞窗口大小加一(单位是 MSS,最大单个报文段长度),每轮次发送窗口增加一倍,呈指数增长,若出现丢包,则将拥塞窗口减半,进入拥塞避免阶段;
2、当窗口达到慢启动阈值或出现丢包时,进入拥塞避免阶段,窗口每轮次加一,呈线性增长;当收到对一个报文的三个重复的 ACK 时,认为这个报文的下一个报文丢失了,进入快重传阶段,要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认;
5 Linux
5.1 孤儿进程,僵尸进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程控制块(PCB)仍然保存在系统中。这种进程称之为僵死进程
危害:
僵尸进程危害,大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,除了init进程会忙一些,并没有什么危害
5.2 死锁条件,如何避免
死锁产生的四个必要条件:
1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
4.循环等待:发生死锁时,线程进入死循环,永久阻塞。
避免死锁的方法为破坏后三个必要条件:
1.破坏“请求和保持”条件:想办法,让进程不要那么贪心,自己已经有了资源就不要去竞争那些不可抢占的资源。比如,让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
2.破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
3.破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出
6 并发
6.1 三个协程ABC同时启动,不停循环打印ABC。如何实现?
- 方法1:协程通知与切换方式
func main() {
chA, chB, chC := make(chan string), make(chan string), make(chan string)
go func() {
for i := 1; ; {
select {
case <-chA:
fmt.Println("[A]:", "A")
chB <- "B"
i += 3
}
}
}()
go func() {
for i := 2; ; {
select {
case <-chB:
fmt.Println("[B]:", "B")
chC <- "C"
i += 3
}
}
}()
go func() {
for i := 3; ; {
if i == 3 {
chA <- "A"
}
select {
case <-chC:
fmt.Println("[C]:", "C")
chA <- "A"
i += 3
}
}
}()
for !false {
fmt.Print()
}
}
- 方法2:协程写入,主协程打印的方式
func main() {
chA, chB, chC := make(chan string), make(chan string), make(chan string)
go func() {
for i := 1; ; {
var A = "A"
chA <- A
i += 3
}
}()
go func() {
for i := 2; ; {
var B = "B"
chB <- B
i += 3
}
}()
go func() {
for i := 3; ; {
var C = "C"
chC <- C
i += 3
}
}()
//打印1000次
for i := 0; i < 1000; i++ {
// 这里用的是for,不是select,所以通道是阻塞且顺序执行的
dataA := <-chA
fmt.Println("A: ", dataA)
dataB := <-chB
fmt.Println("B: ", dataB)
dataC := <-chC
fmt.Println("C: ", dataC)
}
}
7 算法
7.1 LRU算法
LRU 是 Least Recently Used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大概率将会再次被使用。而最近很少被使用的数据,很大概率下一次不再用到。当缓存容量满的时候,优先淘汰最近很少使用的数据。
思路:
准备两张表,一个哈希表,一个双向链表。假设A,3存入表中,则map中key还是原始的key,key=A,value加工一下,包括A和3.
对于双向链表从尾部加,从头部出。如果需要将某个元素从拿出,则此时将需要拿出的元素拿出,放到链表的最后,此时,该元素优先级最高。
7.1.1 手写一个LRU算法
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
7.2 TopK问题。内存2G, 文件10G,按行读取文件,求任意两行互为逆序的字符串,保存起来
Hash大法好!
8 Web
8.1 Cookie和Session
- Cookie
Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。
由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie具有不可跨域名性。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。
- Session
Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。
- 对比
1、cookie数据存放在客户的浏览器上,session数据放在服务器上.
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
4、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
8.2 http,https
https是基于安全套接字的http协议,也可以理解为是http+ssl/tls(数字证书)的组合
8.3 单点登录,tcp粘包
单点登录,门户鉴权后,分发token,用户请求在cookie中带上token信息,就可以访问门户下的所有子系统了。
tcp粘包:
发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。那么这样一来,接收端就必须使用高效科学的拆包机制来分辨这些数据。
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
解决方法:
1、发送方:对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
2、接收方:接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
3、应用层:
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。