bc's club

This is Bc's club

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start#

Create a new post#

1
$ hexo new "My New Post"

More info: Writing

Run server#

1
$ hexo server

More info: Server

Generate static files#

1
$ hexo generate

More info: Generating

Deploy to remote sites#

1
$ hexo deploy

More info: Deployment

Golang

一、并发#

1 GMP模型——goroutine调度#

1.1 GMP优点#

GMP模型可以在用户空间实现任务的切换,上下文切换成本更小,可以达到使用较少的线程数量实现较大并发的能力

1.2 GMP含义#

G是goroutine,是golang协程,是用户态轻量级线程。
M是machine,是内核级线程,是实质上实现业务逻辑的载体。
P是processor本地队列

1.3 M必须拥有P,才能执行G中的代码,P负责G的调度。#

1.4 如何新增G#

M1新增G会被保存在M1所绑定的本地队列P1中,P1队列超过256个满了以后,会把新增的G放进全局队列中

1.5 M如何从P中获取G#

从本地队列P中获取G(无锁)
如果本地队列为空,从全局队列中获取G(加锁)
如果全局队列也为空,再去另一个本地队列P中偷一半的G

1.6 当M1在执行G1的时候被阻塞了,如何继续执行#

当M1在执行G1的时候阻塞了,M1与绑定的本地队列P1解绑,接着M2绑定P1,然后执行P1的下一个协程G2。

1.7 G的生命周期#

  1. 创建G:go func()可以创建协程G
  2. 保存G:创建的G优先保存到本地队列P,如果本地队列P满了,则会放到全局队列P中
  3. M获取G:M1首先从本地队列P1获取G,如果P1为空,则从全局队列中获取G,如果全局队列也为空,则从另一个本地队列偷一半的G
  4. M调度和执行G:M1调用G.func()函数执行协程G,如果M1在执行G的过程被阻塞了,则本地队列P1与M1解绑。其他M绑定P1,继续执行P1的其他G。

1.8 GM和GMP的区别#

  1. 减少大量的全局队列锁竞争。M 绑定本地队列 P 后,直接在 P 中获取、添加和执行 G(无锁操作)。
  2. 尽量在同 1 个 M 中创建和执行 G。因为 G 的信息在创建时保存到 M 中,所以在后续执行的过程中不需要转移 G 的信息,即不涉及线程上下文切换。

1.9 M和P的数量#

  1. P的个数在程序启动时决定,默认情况下等同于CPU的核数。
  2. P的数量一般大于M的数量
  3. M创建的条件
    没有足够的M来绑定P。
    比如所有的M此时都阻塞了,但是 P 中还有很多就绪任务,就要去寻找空闲 M,没找到空闲的 M 就会去创建新的 M。

2 Golang协程切换时机#

  1. 会阻塞的系统调用,比如文件io,网络io
  2. time系列定时操作
  3. 协程执行完成
  4. 管道读写阻塞
  5. 垃圾回收之后
  6. 主动调用

3 如何在Golang中对性能优化#

  1. 使用 goroutine 和 channel 实现并发
  2. 使用 sync 包中的锁,但是尽量减少锁的使用,可以优先考虑读写锁。
  3. 使用原子操作

4 Golang中线程同步的方法#

  1. channel:用于在goroutine之间传递数据和同步操作。
  2. WaitGroup:用于等待一组 goroutine 执行完成后再进行下一步操作。
  3. Mutex 和 RWMutex:用于保护共享资源,避免多个 goroutine 同时访问。
  4. Atomic:用于对共享资源进行原子操作。

二、内存#

1 GC垃圾回收:三色标记法和混合写屏障#

1.1 三色标记法#

  1. 在最开始,所有对象的颜色设置成白色
  2. 从根节点开始遍历所有对象,将遍历到的对象放进灰色集合
  3. 遍历灰色集合,将灰色对象引用的对象变为灰色,遍历之后将本对象标记为黑色
  4. 重复第三步,直到灰色中无任何对象
  5. 回收所有白色对象。

1.2 混合写屏障#

  1. GC开始将栈上的对象全部扫描并标记为黑色,之后不再进行第二次重复扫描
  2. GC期间,任何在栈上创建的新对象均为黑色
  3. 被删除的对象标记为灰色
  4. 被添加的对象标记为灰色

1.8版本后为了不造成stop the world,提高回收精度混合写屏障满足弱三色不变式,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,不需要STW。因为栈在扫描后始终是黑色的,也不需要进行re-scan操作。

强三色不变式#

不存在黑色对象引用到白色对象的指针。

弱三色不变式#

黑色对象可以引用白色对象,但前提是白色对象存在其他灰色对象对它的引用,或链路上游存在灰色对象。

1.3 GC触发时机#

  • 主动触发
    调用runtime.GC,阻塞式地等待当前 GC 运行完毕
  • 被动触发
    • 距上一次GC的最长时间,默认两分钟
    • 分配的堆大小达到阈值

golang内存模型#

只有使用原子库、互斥锁、channel,才能保证在不同的Goroutines间安全地共享数据。

三、数据结构和关键字#

1 mutex#

mutex很像操作系统中的PV操作,通过信号量来处理线程中同步与互斥的问题。
S代表剩余资源数。
P代表申请资源,S原子减一,如果减一后S小于0则将自己阻塞起来。
V表示释放资源,S原子++,如果S++后S<=0,表示等待队列上有等待线程,需要将第一个等待的线程唤醒。

  1. mutex是一个结构体,提供lock()和unlock()方法。
  2. state代表互斥锁的状态,例如是否被锁定。内部实现分成四部分
  3. sema表示信号量,等待信号量的协程会阻塞,解锁的协程释放信号量从而唤醒等待信号量的协程

2 channel#

  • channel是一个用于通信的管道,遵循先进先出。
  • 需要用make来初始化channel,可以选择是否有缓冲,无缓冲是同步的。

channel的底层实现#

  1. 缓冲区是个循环队列,保存队列当前的大小,和队列最大大小
  2. 发送者队列和接收者队列,用于存储等待写入或读取数据的 goroutine 的信息;
  3. 互斥锁

如果往一个关闭的channel读、写会怎么样#

  1. 如果写的话,会直接panic。所以永远不要在读端关闭channel,多个写端可以通过context来解决。
  2. 如果里边有数据,会拿到数据
  3. 如果里边没有数据,会读到零值,可以通过判断ok为false来解决。

使用channel的注意事项#

  1. channel关闭后的读写问题
  2. 无缓冲的channel是同步的,避免阻塞
  3. 有缓冲的channel的容量如果满了,发送方会阻塞

对map的理解,map有哪些注意事项#

  1. map是引用类型,将map赋值、传递的时候,用的是引用,而不是内容。
  2. map不是线程安全的,可以用互斥锁或者sync.Map
  3. map的底层实现使用哈希表来存储元素,迭代顺序不一定
  4. 对map切片会指向原内存空间,可能出现并发或者内存泄漏问题。通过申请新的内存空间做拷贝来解决
  5. 当元素数量达到容量2/3触发自动扩容,容量翻倍。创建新哈希表进行计算、拷贝等,释放旧哈希表内存。
  6. 需要使用ok来判断

当向 map 中存储一个 kv 时,
通过 k 的 hash 值与 buckets 长度取余,定位到 key 在哪一个bucket中,
hash 值的高8位存储在 bucket 的 tophash[i] 中,用来快速判断 key是否存在。
当一个 bucket 满时,通过 overflow 指针链接到下一个 bucket。

对slice的理解#

slice和数组的区别#

  1. 数组(array)的长度是固定的,切片(slice)是可变的
  2. 数组是值类型,切片是引用类型。拷贝数组会复制整个数组的内容,复制切片只能复制指针
  3. 数组是连续的内存块,切片是通过指针和长度来实现的。

slice扩容规则是什么#

  1. 如果当前切片长度小于1024,新容量为原来的2倍
  2. 当前切片长度大于等于1024,新容量为原来的1.25倍
  3. 如果一次扩容后仍无法容纳新增元素,会继续扩容(加法和对齐)
  4. 扩容时会将原来的元素复制到新的内存空间,旧的被释放。

context#

1 context的作用#

  • 在gorountine之间传递上下文信息。一般用来:超时控制,并发控制。
  • context的树形结构可以在不同层级的goroutine之间有效的传递信号。

2 context树的结构#

  • 树的根是一个空的context(context.Background()或者context.TODO())
  • 需要某个节点是子操作,只需要在声明ctx的时候把父ctx传进去
  • 每个节点代表一个新创建的context,可能包含值、取消信号或截止时间
  • 树的边代表从父context到子context的继承关系

3 取消操作#

  • 当需要取消一个操作和所有子操作的时候,通过调用树中某个节点的cancel()函数实现。
  • 触发沿树向下传递的取消信号,使所有子节点都能收到取消信号。

4 context树的构建#

  • context.WithValue,包含键值对,在整个请求范围内传递数据
  • context.WithCancle、context.WithDeadline、context.WithTimeout,附加取消信号或截止时间,信号用于在请求被取消或超时时通知子goroutine

new和make的区别#

  • new用于基本数据类型和结构体类型的内存分配,置为初始值
  • make适用于引用类型的内存分配,如切片、map和channel等

其他#

1 如何在Golang中对性能优化#

  1. 使用 Golang 的内置工具进行性能分析
  2. 减少内存分配。尽可能地重用变量和对象。
  3. 减少垃圾收集,减少内存分配也有助于减少垃圾收集。
  4. 使用并发处理。
  5. 避免使用过多的锁,可以使用读写锁来减少锁的使用
  6. 使用适当的数据结构
  7. 使用编译器优化。

golang中常见的引发panic的情况#

  1. 往被关闭的channel写数据
  2. 关闭已经被关闭的channel
  3. 类型断言失败,可以通过判断是否断言成功来避免
  4. 数组下标越界
  5. 空指针调用
  6. 除数是0

MySQL

一、索引#

1 B树和B+树#

B树(B-树)特点#

B树和B+树是平衡多路查找树(b是balance(平衡))

  1. 每个根结点有多个叶子节点
  2. 高度平衡,每个根节点高度一致
  3. 高度小,查找速度较快
  4. 所有节点遵循左小右大

B+树和B树的区别#

  1. 所有结果只存储在叶子节点,所有根节点不存储数据。
  2. 每个叶子节点都有指向下一个节点的指针。

B+树特点:

  1. 查询速度稳定,存储在叶子节点,查找次数相同
  2. 遍历更快
  3. 通过叶子节点存储指针,能满足空间局部性原理,如果存储器上某个位置被访问,那么它附近的位置也会被访问

2 各种类型的索引#

2.1 聚簇索引和非聚簇索引#

按照底层存储方式角度划分

  1. 聚簇索引(聚集索引)
    索引结构和数据一起存放的索引,InnoDB的主键索引就属于聚簇索引(字典里的拼音)
    优点:

    • 查询速度快。相当于直接定位到了数据

    缺点:

    • 需要数据有序
    • 更新代价大。更新数据,需要更新索引,需要更新索引里的数据。
  2. 非聚簇索引(非聚集索引)
    索引顺序和物理存储顺序不同(字典里的偏旁)
    优点:

    • 更新代价较小。叶子节点不存放数据

    缺点:

    • 需要数据有序
    • 可能会需要二次查询,回表。(查的内容就在索引里就不需要回表,满足覆盖索引的条件)

2.2 主键索引和二级索引#

  1. 主键索引
    加速查询,列值唯一(不可以为NULL),一张表只有一个主键
  2. 唯一索引
    加速查询,列值唯一(可以有NULL),一张表可以有多个
  3. 普通索引
    只能加速查询,允许值重复和有NULL,一张表可以有多个
  4. 前缀索引
    只适用于字符串类型,对文本的前几个字符创建索引,比普通索引建立的数据更小

2.3 覆盖索引和联合索引#

覆盖索引

  • 索引覆盖了查询内容
  • 比如:对列a、b做索引,只查a或者b不需要回表。

联合索引(组合索引、复合索引)

  • 使用表中的多个字段创建索引

3 联合索引的最左前缀匹配原则和失效条件#

  • 定义:按照最左优先的方式进行索引的匹配。
  • 示例:比如对a,b,c三列做索引,a、ab、abc可以使用索引,b、c、bc无法使用索引。
  • 结构:
    先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。
    所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
  • 联合索引失效条件:
    • 不满足最左前缀原则
    • 在列上做操作:计算、函数、类型转换
    • 使用不等于、大于、小于,后边的会失效(a>100 and b=1,此时b的索引失效,但a用到了)
    • 使用LIKE的时候,以%开头会导致索引失效
    • 字符串不加单引号

4 使用索引的规范#

MySQL索引规范#

  1. 在常用的查询中使用索引
  2. 尽量使用覆盖索引,即在索引中包含查询所需的所有列,以避免回表操作。
  3. 遵循最左前缀原则

什么情况不建议使用索引#

  1. where、group by、order by用不到的字段不加索引
  2. 大量重复数据不建索引,例如性别
  3. 谨慎为经常更新的表创建过多索引
  4. 不建议使用无序的值作为索引
  5. 数据量小的表最好不要使用索引,少于1000个

二、特性#

InnoDB#

InnoDB的默认级别是可重复读。
InnoDB的MVCC和next-key lock 可以避免幻读产生,已经可以完全保证事务的隔离性要求,达到可串行化的效果,并且不会有可串行化的更多的锁的性能损失。

  • 事物的原子性是通过undo log来保证。
  • 事务的隔离性是通过读写锁 + MVCC机制来实现的。
  • 事务的持久性是通过redo log来实现的。

MVCC#

MVCC是多版本并发控制,是InnoDB在可重复读隔离级别下事务的实现方式。
一般情况下读读不需要锁,读写、写写都需要锁。用了MVCC后,在读写时不需要加锁,但可能读到历史数据。
MVCC实现基于:隐式字段、undo log、read view。

undo log#

  • 生成时间
    • 事务开始之前
  • 作用及内容
    • 保存的是当前事物上一版本的数据,用于事物回滚数据、MVCC
  • 使用的原因
    • 事务执行过程中可能遇到各种错误,比如服务器本身的错误等。
    • 程序在执行过程中通过ROLLBACK取消当前事务的执行。
    • 可能已经执行一半就结束,但已经修改了很多数据,为了事务的原子性,需要把修改的数据给还原回来。
  • insert undo log
    • 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    • 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被统一清除。
  • 对同一行加锁时,undolog是链表,新的会放在旧的前边。

redo log#

  • 生成时间
    • 事物开始之后(由于事物两阶段提交的原因,redolog会在事物执行过程中产生)
  • 作用及内容
    • 保存的是内存中修改的数据,用于数据库宕机后数据的恢复
  • 刷盘策略
    • 先写入redo log buffer 中,然后再按照一定频率刷新到redo log file

两阶段提交协议#

  • 准备阶段
    • 协调者向参与者发起指令、参与者评估自己的状态。如果参与者评估指令可以完成,则会写undolog。然后锁定资源,执行操作,但是并不提交。
  • 提交阶段
    • 如果每个参与者明确返回准备成功,则协调者参与者发起提交指令,参与者提交资源变更的事物,释放锁定的资源。
    • 如果任何一个参与者明确返回准备失败,则协调者向参与者发起终止命令,参与者取消已变更的事物,执行undolog,释放锁定的资源

三、架构、引擎#

1 基础架构#

MySQL如何执行一条SQL#

  1. 客户端发起请求
  2. 连接器(验证用户身份,给予权限)
  3. 查询缓存(存在缓存则直接返回,不存在则执行后续操作)
  4. 分析器(对SQL进行词法分析和语法分析操作)
  5. 优化器(主要对执行的sql优化选择最优的执行方案)
  6. 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口)
  7. 去存储引擎获取数据返回(如果开启查询缓存则会缓存查询结果)

2 存储引擎#

存储引擎基于表,而不是数据库(比如同一个库下,a表是InnoDB,b表是MyISAM)。
默认InnoDB。

2.1 MyISAM和InnoDB的区别#

  1. MyISAM只支持表级锁,InnoDB还支持行级锁,默认为行级锁
  2. MyISAM不支持事物,InnoDB支持事物,默认可重复读。这个级别下解决幻读,是基于MVCC和Next-Key LOCK实现的。
  3. MyISAM不支持外键,InnoDB支持使用外键。但是一般情况下使用外键概念必须在应用层解决。
  4. InnoDB的redo log支持崩溃后的恢复,MyISAM不支持
  5. InnoDB支持MVCC,减少加锁操作,提高性能。

四、基本原理#

事物的特性(ACID)#

  • 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有被执行过一样。
  • 一致性:执行事务前后,数据库的完整性没有被破坏,写入的内容必须完全符合所有的预设规则。例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
  • 持久性: 一个事务被提交之后,对数据的修改就是永久的,即便系统故障也不会丢失。

脏读、幻读、不可重复读#

  • 脏读:读取到了未提交的事务数据。
  • 不可重复读:在同一事务中,两次查询同一个记录得到的结果不一致。
  • 幻读:在同一事务中,两次查询同一范围,后一次查询看到了前一次查询没有看到的行。
  • 脏读
    例如:变量为50,事物A要修改为100,A还未提交,事物B已经读取到了100。但此时发生回滚,数据库里的变量还是50而不是100,事物B读取到的和数据库真实的不一致。
  • 丢失修改
    例如:事物A和事物B都对变量修改,期望将结果+1的修改。t1时刻事物A获取变量值是50,t2时刻事物B也获取变量值是50。但事物A还未执行修改完毕,数据库最后的结果是51而不是52,事物A的修改丢失了。
  • 不可重复读
    例如:事物A对变量只读,事物B对变量修改。t1时刻事物A读取到变量结果是50,t2时刻事物B将结果修改为100,t3时刻事物A发现变量的结果发生改变。
  • 幻读
    1. select 某记录是否存在——不存在。
    2. 准备插入此记录
    3. 但执行 insert 时发现此记录已存在,无法插入 此时就发生了幻读
  • 不可重复读和幻读的区别:幻读是查询到的个数的区别,不可重复读是内容的区别。两者解决方案不一致,加的锁不一样。
    • 不可重复读:UPDATE和DELETE,幻读:INSERT。

事物隔离级别#

  • 未提交读:事务中发生了修改,即使没有提交,其他事务也是可见的。
    • 可能会导致脏读、幻读或不可重复读。
  • 提交读:可以避免未提交读发生的情况,只有提交后的才能被看到。
    • 可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • 可重复读:对一个记录读取多次的结果是相同的,除非数据是被本身事务自己所修改。
    • 可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • 串行化:最高的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。
    • 该级别可以防止脏读、不可重复读以及幻读。

InnoDB的默认级别是可重复读。
InnoDB的MVCC和next-key lock 可以避免幻读产生,已经可以完全保证事务的隔离性要求,达到可串行化的效果,并且不会有可串行化的更多的锁的性能损失。

五、锁#

  • 行级锁、表级锁:行级锁开销大,冲突少,会死锁。表级锁开销小,冲突大,不会死锁
  • 共享锁、排他锁
  • 乐观锁、悲观锁
  • next-key lock
    • 间隙锁+行锁,能解决幻读的问题
  • gap lock间隙锁

六、使用#

SQL执行的慢的原因和解决方法#

该SQL偶尔执行慢#

  1. 在刷新脏页,redo log写满了需要直接写入磁盘
  2. 执行的时候遇到了锁

该SQL一直执行慢#

  1. 没有用上索引,加索引、查看是否是对字段进行运算、函数,导致未用上索引。
  2. 数据库自己选错了索引,可以用index某列强制走索引。

计算机网络

一、传输层#

TCP#

1 TCP三次握手#

  • 第一次握手:客户端发送带有SYN的数据到服务端,客户端进入SYN_SEND状态。如果成功的话,此时服务端确认客户端发送正常,自己接收正常。
  • 第二次握手:服务端发送带有SYN和ACK的数据到客户端,服务端进入SYN_RECV状态。如果成功的话,此时客户端确认自己发送接收正常,服务端发送接收正常。
  • 第三次握手:客户端发送带有ACK的数据到服务端,客户端和服务端都进入ESTABLISHED状态。如果成功的话,此时服务端确认自己的发送正常,客户端接收正常。

第2次握手传回了ACK,为什么还要传回SYN?

  • SYN是同步序列编号,是 TCP/IP 建立连接时使用的握手信号。
  • SYN是为了建立并确认从服务端到客户端的通信。ACK只是告知已正确接收到数据。

2 TCP四次挥手#

  • 第一次挥手:客户端发送一个FIN的数据到服务端,同时关闭客户端到服务端的发送。客户端进入FIN-WAIT-1状态。客户端告诉服务端,自己没什么要发送的了。
  • 第二次挥手:服务端收到后,发送一个ACK到客户端。服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。服务端告诉客户端,自己知道你没什么要发送的了。但此时客户端不知道服务端是否已经把需要发送的都发送完毕。
  • 第三次挥手:当服务端将需要发送的发送完毕之后,服务端发送一个FIN到客户端,服务端进入LAST-ACK状态。服务端告诉客户端,自己也没什么要发送的了。
  • 第四次挥手:客户端发送ACK,进入TIME-WAIT状态。服务端接收到之后,进入CLOSE状态。客户端在等待2MSL(报文段最长寿命)没收到回复后,认为服务端已正常关闭,客户端关闭连接。

如果第二次挥手时服务器的ACK没有送达客户端。

  • 如果客户端没有收到ACK确认,会重新发送FIN请求。

为什么第四次挥手客户端需要等待 2*MSL(报文段最长寿命)时间后才进入 CLOSED 状态。

  • 如果服务端因为某些原因而没有收到ACK的话,服务端就会重发FIN,如果客户端在 2倍MSL 的时间内又收到了 FIN,客户端会重新发送ACK并再次等待 2MSL,防止服务端因为没有收到 ACK 而不断重发 FIN。
  • MSL是一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,Client 都没有再次收到 FIN,那么 Client 推断 ACK 已经被成功接收,则结束 TCP 连接。

3 TCP如何保证传输的可靠性#

javaguide.cn/cs-basics/n…

  1. 基于数据块进行传输
  2. 对失序数据包重新排序以及去重
  3. 校验和
  4. 超时重传
  5. 流量控制
  6. 拥塞控制

4 TCP如何实现流量控制#

javaguide.cn/cs-basics/n…
TCP利用滑动窗口实现流量控制。流量控制是通过控制发送方发送速率,保证接收方来得及接收。

5 TCP 的拥塞控制是怎么实现的#

  1. 慢开始
    • 初始化拥塞窗口cwnd为1。
    • 每当收到一个ACK,cwnd大小加一。
    • 每当过了一个往返延迟时间RTT。cwnd大小直接翻倍,乘2,以指数让升。
  2. 拥塞避免算法
    • 当cwnd的值到了慢启动阈值时,每当过了一个往返延迟时间RTT,cwnd大小加一。
    • 可以将cwnd缓慢的增加,调整到网络的最佳值。
  3. 快速重传
    • 当发送端接收到3个以上的重复ACK,认为网络拥塞发生,触发快速重传和快速恢复。
    • 发送端会快速重传丢失的数据包,无需等待定时器超时。
  4. 快速恢复
    • 当拥塞发生时,也会立即触发快速恢复。
    • cwnd和慢启动阈值都设置为当前cwnd的一半,立即进入拥塞避免算法,可以避免慢启动过程。

为什么需要拥塞控制

  • 在网络出现拥堵时,如果继续发送大量的数据包,可能会导致数据包、时延、丢失,这时 TCP 就会重传数据,但是⼀重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包。

6 TCP包乱序、丢包怎么办#

www.cnblogs.com/kxdblog/p/4…

  • TCP通过SEQ和ACK的值保证顺序。
  • 如果接收端收到乱序包,接收端会缓存下来,然后立即发送,上一个ACK,不必让发送方等到超时重传,而是立即重传。
  • 接收到重发的之后,直接发送按顺序的最大一个的ACK,缓存的数据不需要重发。

7 粘包和拆包#

  • 什么是
    • 粘包:两个或者多个以上的包粘在一起
    • 拆包:解决粘包问题
  • 为什么会出现
    1. 发送方发送不及时,多个包在发送端粘在一起
    2. 接收方接收不及时,包在接收端堆叠
  • 怎么解决
    • 带包头就可以解决,包头定长,以特定标志开头,带着负载长度。

UDP#

1 TCP和UDP的区别#

  1. TCP需要连接,UDP不需要连接。
  2. TCP提供可靠的传输服务。
  3. TCP有状态,记录自己发送消息的状态,比如消息是否发送了、是否被接收了等。UDP无状态。
  4. TCP效率比UDP低,因为多了建立连接,确认,重传等机制。
  5. TCP首部开销比UDP大
  6. TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多

二、应用层#

HTTP#

1 post和get区别#

  1. get用于获取信息,是幂等的,且可缓存
  2. post用于修改服务器上的数据,非幂等,不可缓存
  3. GET把参数包含在URL中,POST通过request body传递参数
  4. GET请求在URL中传送的参数是有长度限制
  5. post比get安全

2 http和https#

  1. http用明文进行数据传输,https用ssl协议对数据进行加密。HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
  2. http用80端口,https用443端口。
  3. https协议需要到ca申请证书。

SSL/TLS 的核心要素是非对称加密。非对称加密采用两个密钥——一个公钥,一个私钥。在通信时,私钥仅由解密者保存,公钥由任何一个想与解密者通信的发送者(加密者)所知。

3 HTTP 各个版本的区别#

1.0

  • 新增了POST,可以传输多媒体资源

1.1

  • 新增长连接,支持断点传输

2.0

  • 多路复用,支持服务器推送

DNS#

DNS作用

  • DNS是一个数据库,域名解析指的是通过主机名获得IP地址

DNS是应用层协议,传输层使用的是UDP。
DNS的解析流程

  • 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存

三、其他#

1 浏览器从输入网址URL到页面展示的过程#

  1. DNS解析
  2. TCP连接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析渲染页面
  6. 连接结束

2 URL和URI的区别#

  • URI是统一资源标志符,可以唯一标识一个资源。
  • URL是统一资源定位符,可以提供该资源的路径。URL是一种特殊的URI,不仅标识了一个唯一的资源,还提供了找到它的路径

3 短连接和长连接的区别#

  1. 连接方式不同:短连接每次请求响应建立一次连接,完成数据传输后断开连接,长连接建立连接后保持连接状态,多次传输共用同一个连接
  2. 传输效率不同:短连接频繁建立断开连接,产生额外的开销。长连接效率高
  3. 应用场景不同:短连接适合频率低、请求响应小的场景,例如http请求、RPC调用。长连接适用TCP连接、WebSocker等。

4 七层模型#

  • 应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
  • TCP和UDP是传输层
  • IP是网络层
  • ARP是数据链路层

5 time wait过多怎么办#

  1. 修改time wait连接状态上限值
  2. 启动快速回收机制
  3. 开启复用机制
  4. 修改短连接为长连接方式
  5. 由客户端主动断开连接

Redis

一、基础#

1 什么是Redis#

  1. redis是一个将数据存储在内存中的数据库,读写速度非常快,存储的是键值对
  2. redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用
  3. redis不仅支持简单的kv类型的数据,还提供list,set,zset,hash等数据结构的存储
  4. redis支持数据的备份,即master-slave模式的数据备份

2 Redis为什么这么快#

  1. redis完全基于内存
  2. redis有高效的事件处理模型,主要是单线程事件循环和IO多路复用
    • 单线程可以避免了上下文切换和竞争,不需要锁
    • 是多路IO模型,非阻塞IO(阻塞IO会一直占用CPU,多路复用IO单个线程就可以同时处理多个IO请求)
  3. redis内置了多种优化过后的数据结构实现,性能非常高

3 为什么要用Redis#

  1. 高性能
    Redis和传统数据库存在磁盘中相比,直接使用内存,速度非常快
  2. 高并发
    MySQL能达到一万QPS,
    Redis能达到十万QPS到三十万QPS

3.1 redis适合的场景#

  1. 缓存:减轻MySQL查询压力,提高性能
  2. 排行榜:利用redis的SortSet(有序集合)实现
  3. 计数器:利用Redis的自增操作,可以统计点赞数、页面访问数
  4. 限速器:
  5. 好友关系:利用集合的一些命令,求交集、并集、差集,解决共同好友、共同爱好问题
  6. 消息队列:用list完成异步解藕
  7. session共享:客户登录任意一台机器都可以获得session信息

3.2 redis常见的功能#

  1. 支持数据缓存
  2. 支持分布式锁
  3. 支持数据持久化
  4. 支持事务(不满足原子性和持久性)
  5. 支持消息队列(异步处理,应用解耦,流量削峰和消息通讯),一般没人用

二、内存#

1 设置过期时间#

  1. 设置过期时间减少内存消耗,绝大部分数据不需要一直保存
  2. 传统的数据库判断数据是否过期性能非常差
  3. 使用expire设置过期时间,字符串使用setex设置过期时间,persist移除一个键的过期时间,ttl查看还有多久过期

2 Redis如何判断数据是否过期#

过期字典

  1. 过期字典保存了数据库中所有键的过期时间
  2. 过期字典的key是一个指针
  3. value是long long类型,保存了该键过期时间

3 过期数据的删除策略#

  1. 定时删除
    • 在设置键的过期时间的同时,创建一个定时器,通过定时器执行对键的删除操作
    • 内存友好,对CPU时间不友好(执行删除占用时间)
  2. 惰性删除
    • 每次从键空间获取键时,都检查是否过期,过期就删除
    • CPU时间友好,内存不友好
  3. 定期删除
    • 每隔一段时间,就对数据库进行一次检查,删除其中的过期键
    • 通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响

4 内存淘汰机制#

  1. volatile-lru:从已设置过期时间的数据中最少使用淘汰
  2. volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据中随机淘汰
  4. allkeys-lru:从全部数据中最少使用淘汰
  5. allkeys-random:从全部数据中任意淘汰
  6. 禁止驱逐数据

5 内存碎片#

  1. redis存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
  2. 当redis中某个数据删除时,通常不会轻易释放内存给操作系统。
  3. info memory查到内存碎片率大于1.5需要清理碎片
  4. 碎片清理时,设置内存碎片清理所占用CPU时间的比例

三、性能优化#

1 性能瓶颈#

  1. 网络
  2. 内存

2 内存优化#

  • 降低key和value的大小
  • 维护共享整数对象池
  • 选择合适的底层存储结构

2.1 如何解决大Key的问题#

  1. 定义:一般来讲string的value大于10KB的就是大key,其他类型field超过一万个
  2. 影响:
    1. 单线程,耗时增加会阻塞其他请求
    2. 大key导致分片内存不平衡,CPU使用率不平衡
  3. 查看:
    1. 看单分片监控,是否和平均一致
    2. bigkeys命令
  4. 解决:主要是业务角度
    1. 删除机制
    2. 做拆分,大key变小key
    3. 是否key设计不合理,比如用的不是随机值而是固定值

2.2 设置ziplist和hashtable#

  1. 区别
    • ziplist是双向链表,占用空间比hashtable小,查询耗时比hashtable长
    • hashtable是散列表,占用空间比ziplist大,查询耗时比hashtable短
  2. 使用ziplist的条件
    • 又value字符串长度小于64(线上是8192)
    • 又field-value对的数量小于512个
  3. 查看底层存储的方法
    OBJECT ENCODIN

3 网络优化#

3.1 网络优化方法#

核心:使用批量操作

  1. 原生命令:hmget、hmset
    • 问题1:无法保证所有的 key 都在同一个hash slot,仍需要多次网络传输(总体来说还是优化)
    • 问题3:命令只能一种,都是set或者get这种
    • 优点:可以保证原子操作
  2. pipeline:
    • 问题一:需要控制一次批量操作的元素个数
    • 问题二:也无法保证所有的 key 都在同一个hash slot
    • 问题三:非原子操作
    • 优点:可以使用多种命令
  3. lua:
    • 问题:redis-cluster下无法保证原子性(也是哈希槽的问题)
    • 优点一:支持简单逻辑处理
    • 优点二:非redis-cluster可以保证原子性
    • 并且一段lua执行过程中,不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

四、线程模型#

1 单线程模型#

Redis 基于 Reactor 模式开发了自己的网络事件处理器,被称作文件事件处理器

  • 文件事件处理器使用I/O多路复用,来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生。这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,实现了高性能的网络通信模型。
I/O 多路复用技术能让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。

单线程优点:

  1. 容易编程,且易于维护
  2. Redis瓶颈不在CPU,而在内存和网络
  3. 多线程可能会存在死锁、线程上下文切换问题,影响性能

2 新版本多线程#

  • Redis4.0,在对一些大键值对的删除操作的命令时,会异步删除。
  • Redis6.0,针对提高网络 IO 读写性能,使用多线程。
    执行命令仍然是单线程顺序执行。

五、高并发#

1 高并发场景下使用缓存需要注意哪些问题#

  1. 缓存一致性问题
    需要保证缓存中的数据与数据库中的一致,需要选择业务适合的缓存过期和更新策略。一般在数据发生更改的时,主动更新缓存中的数据。
  2. 缓存击穿问题
    在某个高访问量的key过期时,大量的请求会直接落到数据库中。解决方法是加载缓存时采用互斥锁,保证只有一条请求落到数据库,其他请求先自旋,然后查询缓存,最后再申请锁。
  3. 缓存穿透问题
    一些没有value的key被频繁查找,可以通过在缓存中缓存空对象来缓解。
  4. 缓存雪崩问题
    大量的缓存同时失效或过期。解决方式:限流、降级、熔断、多级缓存。比如在设置过期时间时,合理增加随机性。

2 高并发下使用规范#

2.1 键值设计#

  1. key设计
    • 有随机性,不要用固定值做key(防止哈希槽冲突)
    • 以业务名为前缀,用冒号分隔
    • 缩短key的长度
    • 不包含特殊字符
  2. value设计
    • 业务阻止大key(string类型10kb以内,其他元素小于5000)
    • 选择合适的数据类型(比如使用hash拆分)
    • 设置过期时间

2.2 命令使用#

  • 尽量使用批量操作降低网络耗时(pipeline或者hgetall)
  • 使用批量操作注意个数
  • 禁止使用keys、flushall、flushdb,用rename禁用
  • 尽量避免使用redis的事物(不支持回滚、集群版key必须在同一个slot里)

2.3 使用规范#

  • 避免多个应用使用同一个Redis实例
  • 使用连接池,控制连接数,提高效率
  • 添加熔断机制
  • 选择合适的内存淘汰策略(allkeys-lru),设置过期时间

六、持久化#

  • 防止系统故障
  • 重启机器之后数据还在
  • 备份到其他位置

1 RDB#

快照

1.1 什么是RDB#

  • 快照持久化是默认的持久化方式
  • redis通过创建快照,来获得存储在内存里面的数据,在某个时间点上的副本。

1.2 RDB 创建快照时会阻塞主线程吗?#

  • bgsave是默认命令,会fork出一个子进程,子进程创建快照,不会阻塞主线程
  • save是同步保存操作,会阻塞主线程

2 AOF#

只追加文件

2.1 什么是AOF#

  • 开启AOF持久化后,每执行一条更改数据的命令,会将该命令写入到内存缓存,根据配置决定何时将其同步到硬盘中的AOF文件
  • 与RDB相比,AOF持久化的实时性更好

2.2 AOF是如何实现的#

aof会在执行完命令之后才记录日志。

优点:

  • 这样可以避免额外的检查开销
  • 命令执行完之后再记录,不会阻塞当前的命令执行

缺点:

  • 执行完命令还没有AOF就宕机,会导致对应的修改丢失
  • 会阻塞后续其他命令的执行(AOF记录日志是在Redis主线程中进行的)

2.3 AOF重写#

  1. 含义和条件
    当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
  2. 实现方式
    这个功能是通过读取当前的数据库状态来实现的
  3. 可能发生的问题和解决方式
    • 问题1:创建新AOF时会出现进行大量写操作,主线程被长时间阻塞,无法处理其他命令
    • 问题1的解决方案:将AOF放到子线程执行
    • 问题2:在后台AOF重写时,服务器会对当前数据进行修改,会导致数据不一致
    • 问题2的解决方案:使用AOF重写缓存。完成AOF文件重写后,将AOF重写缓存中的内容全部写入到新的AOF文件中,之后,改名覆盖旧的AOF文件

3 如何选择RDB和AOF#

3.1 RDB优点#

  1. RDB比AOF更小,适合做灾备
  2. 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。

3.2 AOF优点#

  1. AOF实时性比RDB更好
  2. RDB在产生的时候会对CPU和资源影响比较大
  3. AOF易于理解,可以轻松导出进行分析

4 混合持久化#

  • 结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
  • 默认关闭,需要通过配置项开启

七、集群/哨兵/主从#

八、应用#

1 分布式锁#

  • 用lua脚本,先判断setnx是否结果为1,获取到了锁,再立即执行expire设置一个合理的超时时间,保证原子性。
  • 同时在此线程开启守护线程,给锁续期。
  • 能确保真的拿到锁,也可以避免线程异常结束,锁未被释放的情况。

2 购物车#

购物车信息一般用Hash存储,因为购物车中的商品频繁修改和变动

  • 用户id为 key
  • 商品id为field,商品数量为value 普通维护
  • 用户添加商品就是往 Hash 里面增加新的 field 与 value;
  • 查询购物车信息就是遍历对应的 Hash;
  • 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
  • 删除商品就是删除 Hash 中对应的 field;
  • 清空购物车直接删除对应的 key 即可。

3 排行榜#

sorted set经常被用在排行榜上。
常见的命令有:ZRANGE (从小到大排序) 、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。

4 抽奖系统#

使用set

  • SPOP key count : 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
  • SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。

5 活跃用户#

使用bitmap
使用日期(精确到天)作为 key,然后用户ID为offset,如果当日活跃过就设置为 1。
….

6 页面访问量#

  1. PFADD将访问指定页面的每个用户 ID 添加到 HyperLogLog 中。
  2. PFCOUNT统计指定页面的 UV。

九、数据结构#

1 有哪些数据结构#

五种基础数据结构

  1. String:字符串类型,可以存储字符串、整数和浮点数。
  2. List:有序列表类型,可以存储多个元素,每个元素可以是字符串或者数字。底层是双向链表
  3. Set:集合类型,可以存储多个元素,每个元素必须是唯一的。底层是哈希表
  4. Hash:哈希类型,可以存储多个字段和对应的值,每个字段和值都是字符串类型。key field value
  5. Zset:有序集合类型,可以存储多个元素,每个元素可以关联一个分数,用于排序。底层实现是跳跃表和哈希表

三种特殊数据结构

  1. HyperLogLogs(基数统计)

    用概率统计数组中不重复的元素个数。用非常少的内存,存储非常多的数据,误差大概0.81%。

    • PFADD将value存进key中
    • PFCOUNT返回该key的近似基数
  2. Bitmap (位存储)

  3. Geospatial (地理位置)。

2 String#

3 Hash#

3.1 Hashtable和ziplist#

4 ZSet#

4.1 跳跃表#

  • 通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
  • 跳跃表在链表的基础上增加了多级索引以提升查找的效率,是一个空间换时间的方案。
  • 当节点本身比较大或者元素数量比较多的时候,空间的缺点可以忽略。
  • Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

5 List#

6 Set#

十、事物#