Go

make和new区别

1
2
3
new: 分配内存清零并返回指针, 如果编译器发现 new 出来的内存在函数结束后就没有使用
且申请内存空间不是很大,那么 new 申请的内存空间还是会被分配在栈
make: 用于slice,map,和channel的初始化并返回对象

内存逃逸

  1. 如果函数外部没有引用,则优先放到栈中
  2. 如果函数外部存在引用,则必定放到堆中
1
2
3
4
5
6
指针逃逸:
- 函数返回指针
- interface{} 动态类型逃逸
- 栈空间不足
- 闭包
- 在切片上存储指针或带指针的值的时候, 对应的变量会逃逸

range 的 Bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v := []int{1,2,3,4,5}
for _, v2 := range v {
defer func () {
fmt.Printf("v2: %v\n", v2)
}()
}
// v1.22.0 版本以前会输出5,5,5,5,5
// v1.22.0 版本以后输出5,4,3,2,1

v := []int{1,2,3,4,5}
for _, v2 := range v {
fmt.Printf("v2: %v\n", v2)
}
// v1.22.0 版本以前会输出5,5,5,5,5
// v1.22.0 版本以后输出1,2,3,4,5

defer关键字

1
2
3
4
栈顺序先进后出
return 之后的语句先执行,defer 后的语句后执行
defer 最大的功能是 panic 后依然有效
defer 出现 panic 会覆盖掉前一个 panic 继续执行下一个 defer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func test() int { //无名返回
i := 9
defer func() {
i++
}()
return i
// 最终返回 9
// 函数的返回值没有被提前声名,其值来自于其他变量的赋值
// 而defer中修改的也是其他变量,而非返回值本身,因此函数退出时返回值并没有被改变。
}

func test() (i int) { //有名返回i
i = 9
defer func() {
i++
}()
return i
// 最终返回 10
// 函数的返回值被提前声名,也就意味着defer中是可以调用到真实返回值的
// 因此defer在return赋值返回值 i 之后,再一次地修改了 i 的值
// 最终函数退出后的返回值才会是defer修改过的值。
}

数组和切片

1
2
数组: 长度固定, 数组作为函数参数时,函数操作的是数组的一个副本,不会影响原始数组
切片: 长度可变, 当切片作为函数参数时,函数操作的是切片的引用,会影响原始切片
1
切片扩容: append后len大于cap会触发扩容, cap小于1024翻倍,超过1024后每次扩容1.25倍

Map

1
主要为bmap,每个bmap最多装8个key,当超过8个key会创建一个溢出桶指向新的bmap
1
扩容: 元素个数大于bmap*6.5 或者 溢出桶的数量过多

Sync Map

1
2
3
4
5
6
7
8
9
10
主要是空间换时间的概念,通过read和dirty两个map来实现
read读操作不加锁,读取不到数据后会对read加锁再读一次,然后再去dirty读取
1.当read miss次数过多会将原本read删除然后dirty提升为read
2.使用内置range函数当read和dirty不一致时也会触发dirty提升机制
3.删除元素read有直接删除,没有则去dirty执行删除
4.新增修改
- 在read中查找key,找到了则通过原子操作,尝试更新value
- key在read中存在,但是被标记为已删除,则kv加入dirty中,并更新value值
- key在read中不存在,但在dirty中存在,则直接在dirty中更新value
- key在read和dirty中都不存在,则直接在dirty中加入kv

Channel

1
主要由一个循环链表加上读写下标, 加上两个等待队列(双向链表)

GMP

线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程
因为它是发生在操作系统的用户态的,不需要进入内核态进行系统调用,操作系统的上下文切换会带来很大的开销,切goroutine和线程一样,共享堆,不共享栈。

1
2
线程: 由1个用户态和1个内核态组成, 内存占用高, 线程调度消耗大
协程: 通过调度器将N个用户态和M个内核态组成, 占用内存更小(几kb可扩容), 调度更灵活

G: Goroutine,它携带上下文运行的信息,是需要允许的任务
M: Machine,即一个真正的系统线程
P: Processor处理器,负责把Goroutine调度到M上

1
2
3
4
1.P在程序开始的时候就会创建,根据参数GOMAXPROCS(默认为cpu核数)
2.每次新建一个G时,都会尝试去唤醒其它的M,我们称它为M2,M2同样也会找一个P2去依附,但此时,P2本地没有可执行的G,那它这时候的策略就是去全局队列里面去偷n个G.
3.如果全局队列里面再没有G的话, 就去其他P的本地队列里面去偷一半的数量过来,这就是work-stealing机制。
4.如果其他P本地队列里面还是没有G的话,系统线程M就会进入自旋状态而不是销毁,因为我们希望我当有新的G创建时,能立刻有M运行它。

GC

1
2
3
4
5
白色对象 - 潜在的垃圾,表示还未搜索到的对象,其内存可能会被垃圾收集器回收

黑色对象 - 活跃的对象,表示搜索完成的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象

灰色对象 - 活跃的对象,表示正在搜索还未搜索完的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象
1
2
3
4
5
1.初始时所有对象都是白色的
2.从gc root对象出发,扫描所有可达对象标记为灰色,放入待处理队列
3.从队列取出一个灰色对象并标记为黑色,将其引用对象标记为灰色,放入队列
4.重复上一步骤,直到灰色对象队列为空
5.此时剩下的所有白色对象都是垃圾对象

强三色不等式: 黑色不能直接指向白色
弱三色不等式: 黑色可以指向白色,但是需要白色间接被灰色指向

删除屏障: B对象失去A对象的引用时,如果B对象是个白色对象,那么它会变成灰色对象,这一点是为了满足弱三色不变式
插入屏障: 实现强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障将白色对象置为灰色

三色标级+混合屏障: gc开始时所有栈标记为黑色, 以满足弱三色不等式

Gin

动态路由

通过字典树实现

中间件原理

http请求来到时先经过中间件,主要由一个函数切片通过index下标访问

Redis

基本数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string
- 底层: 动态字符串,最大为512M
- 应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
list
- 底层: 双向链表
- 应用场景:消息队列
hash
- 底层: 哈希表
- 应用场景:缓存对象、购物车等
set
- 底层: 哈希表
- 应用场景:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等
zset
- 底层: 跳表
- 应用场景:排序场景,比如排行榜、电话和姓名排序等。

为什么快

1
2
3
1.操作都是在内存中操作,再加上Redis自身的数据结构优化
2.采用单线程防止了多线程之间的竞争,避免线程切换带来的时间开销
3.采用IO多路复用即select/epoll机制,实现一个线程来处理多个IO

持久化

1
2
3
4
AOF日志: 命令追加方式写入文件, 性能差,体积大,恢复速度慢,保证数据完整性

RDB快照: 某一时刻内存数据的快照保存, 保存频率高影响性能,频率低数据丢失
混合持久化: 集成了 AOF 和 RBD, 前半部分为RDB格式的全量数据,后半部分为AOF的增量数据

集群

1
2
3
4
5
6
7
8
9
10
1.主从复制
主服务器负责读写,从服务器负责只读
由于数据同步是异步的所以存在数据不一致的问题

2.哨兵模式
在主从的基础上增加了一个哨兵节点, 哨兵持续与服务器心跳交互
通过投票算法: 以配置文件的优先级 复制偏移量 runid大小进行判断

3.切片集群模式
类似bitmap的方式进行存储分配

过期删除与内存淘汰

1
2
惰性删除策略: key过期不做操作,当对key进行查询才会判断过期并删除返回null
定期删除策略: 每隔一段时间抽取一定量的key检查是否过期,如果过期率大于25%重复开头操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1、不进行数据淘汰的策略(默认策略)
当运行内存超过最大设置内存时,不淘汰任何数据直接返回错误

2、进行数据淘汰的策略

在设置了过期时间的数据中进行淘汰:
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:
allkeys-random:随机淘汰任意键值;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

缓存雪崩、击穿、穿透

1
2
3
4
5
6
7
8
缓存雪崩: 大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机
- 解决方案: 互斥锁, 均匀设置过期时间

缓存击穿: 某个热点数据过期
- 解决方案: 互斥锁, 热点数据不设置过期时间

缓存穿透: 大量请求既不在缓存中,也不在数据库中的数据
- 解决方案: 缓存空值或者默认值, 布隆过滤器(类似bitmap)

Mysql

索引

1
2
3
4
5
6
7
8
9
10
11
12
B+树: 
- 主键索引(聚簇索引):
根节点按顺序存放索引, 叶子节点双向链表并存放数据

- 二级索引:
根节点按顺序存放索引, 叶子节点双向链表只存放索引和主键

- 联合索引(复合索引): 最左匹配原则
- 根节点按最左侧字段顺序存放多个索引, 叶子节点双向链表并存放多个索引和主键
- 联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配
- 也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
- 注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配

什么时候需要 / 不需要创建索引

索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:

1
2
3
1.需要占用物理空间,数量越大,占用空间越大;
2.创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;
3.会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。

需要索引

1
2
3
1.字段有唯一性限制的,比如商品编码
2.经常经常where条件查询的字段
3.经常用于group by和order by的字段

不需要索引

1
2
3
4
1.极少作为查询条件的字段
2.大量重复数据
3.数据少
4.经常维护修改的数据

索引失效

1
2
3
4
1.左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
2.查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
3.使用联合索引没有按照最左匹配原则会导致失效
4.where语句中or存在没有添加索引的字段。

事务

1
特性: 原子性,一致性,隔离性,持久性
1
2
3
4
5
6
7
8
9
10
11
12
13
读未提交: 指一个事务还没提交时,它做的变更就能被其他事务看到

读已提交: 指一个事务提交之后,它做的变更才能被其他事务看到
- 解决脏读:
一个事务还没提交的修改数据被读取到

可重复读(默认): 指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的
- 解决不可重复读: 一个事务第一次读取到的数据,在第二次读取之前被另一个事务提交修改
导致两次读取的数据不一样

串行化: 会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
- 解决幻读: 一个事务第一次查询结果5条数据,在第二次读取之前另一个事务添加并提交了一条新数据
导致第二次查询结果为6条数据

网络

http,https和http2.0

http1.1: 新增tcp长连接, 增加缓存处理, 断点续传
http2.0: header压缩, 多个request共用一个连接(多路复用), 二进制格式传输, 服务器推送
https: ca证书加密,端口修改