服务端开发知识点总结(2020)

数据结构

  • 数组、单链表、双链表、循环链表、散列表

相关LC算法题和常见增删改查操作

散列表解决哈希冲突,如拉链法和线性探测法,二次探测法

  • B树、B+树,字典树Trie,平衡二叉树、红黑树、哈夫曼树、堆

相关LC算法题,树的遍历,以及元素的增删改查操作

① B树

多路平衡查找树
在这里插入图片描述
结构:

排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则;

子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉);

关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2);

所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子;

查询流程:

如上图我要从上图中找到E字母,查找流程如下

  1. 获取根节点的关键字进行比较,当前根节点关键字为M,E<M(26个字母顺序),所以找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);

  2. 拿到关键字D和G,D<E<G 所以直接找到D和G中间的节点;

  3. 拿到E和F,因为E=E 所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点,即找到的是null节点,则返回null);

插入流程:

定义一个5阶树(平衡5路查找树;),现在我们要把3、8、31、11、23、29、50、28 这些数字构建出一个5阶树出来;

遵循规则:

(1)节点拆分规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须<=5-1(这里关键字数>4就要进行节点拆分);

(2)排序规则:满足节点本身比左边节点大,比右边节点小的排序规则;

先插入 3、8、31、11
在这里插入图片描述
再插入23、29
在这里插入图片描述
再插入50、28
在这里插入图片描述
应用:

B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度;

② B树

B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找

结构:

B+跟B树不同,B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;

B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;

B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。

非叶子节点的子节点数=关键字数

应用:

1、B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;

2、B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;

3、B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。

4、B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描

B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

上述几种查找树的不同点是他们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的;

③ 红黑树

红黑树是2-3树的一种二叉树实现,2-3树通过将4-节点转化为3个2-节点的形式,可以保证2-3树的平衡性;红黑树通过使用节点的颜色来表示2-节点/3-节点,红色节点表示该节点将与其父节点连接形成一个3-节点,注意这里要求红色节点为其父节点的左节点;黑色节点即为普通节点

红黑树的定义:

即为满足下列条件的BST
红节点均为左节点
不能有两个连续的红节点
完美黑平衡,即任意空节点到根节点路径的黑节点个数相同

实现红黑树:

只需要在增删节点后返回给调用函数时进行下顺序列旋转操作即可保持平衡
在这里插入图片描述

  1. 如果右子节点是红色的而左子节点是黑色的,进行左旋转
  2. 如果左子节点是红色的,且左子节点的左子节点也是红色的,则进行右旋转
  3. 如果左右子节点均为红色,则进行颜色转换
④ 堆

要牢记堆的上浮和下沉操作

上浮(模拟最大堆)

func swim(nums []int, k int) {
	for k>0 {
		index:=(k-1)/2
		if nums[index]<nums[k] {
			nums[index], nums[k]=nums[k], nums[index]
			k=index
		} else {
			break
		}
	}
}

下沉

func sink(nums []int, k int, N int) {
	for 2*k+1 < N {
		index := 2*k + 1
		if 2*k+2 < N && nums[2*k+2] > nums[2*k+1] {
			index = 2*k + 2
		}
		if nums[index] > nums[k] {
			nums[index], nums[k] = nums[k], nums[index]
			k = index
		} else {
			break
		}
	}
}

  • 有向图,无向图,带权图

图的遍历(dfs, bfs)
连通分量个数:每一轮都会标记已遍历的点,当发现遍历起点未遍历,则计数器加一
最小生成树
Prim算法流程:
选取起始点,维护一个已在最小生成树内的顶点集合,对该起始点的邻接节点,将不在顶点集合的点与该点构成的边加入最小堆,然后选取最小堆中的与当前顶点集合距离最小的点,忽略已在顶点集合的边,然后加入顶点集合中;直至最小堆长度为0。
Kruskal算法使用并查集将两棵树不断合并直到只剩下一棵树

最短路径
单源点有权重,Dijkstra算法
算法流程:首先将其他点与起点之间的距离均设置为正无穷大,同时维护一个顶点集合,然后对于起点的邻接节点,如果其到起点的距离能够被放松且不在顶点集合中,则加入最小堆中;随后每次从最小堆中抽出一个顶点,满足该顶点到起始点的距离最小,忽略已在顶点集合的边,加入顶点集合同时设置最小距离,直至最小堆长度为0

单源点/多源点无权重,BFS即可

判断有向无环图中是否有环
算法流程:首先基于DFS的流程,在每一个连通分量的DFS遍历图中,先维护一个集合,该集合区别于已遍历的集合,在每一轮DFS需要置空,存储了每一轮已遍历节点,当发现在一轮DFS中碰到了重复节点,则表示存在环

拓扑排序
对于有向无环图,其拓扑排序即为该图的逆后序遍历

二分图问题
同样使用DFS,在每一个连通分量中,如果是相邻边则使用不同的颜色着色,如果相邻节点在已遍历节点集合中,则判断是否颜色相同,若相同则无法划分为二分图

算法

排序算法

堆排序

func HeapSort(nums []int) {
	for i := len(nums)/2 - 1; i >= 0; i-- {
		sink(nums, i, len(nums))
	}

	for i := len(nums) - 1; i > 0; i-- {
		nums[0], nums[i] = nums[i], nums[0]
		sink(nums, 0, i)
	}
}

func sink(nums []int, k int, N int) {
	for 2*k+1 < N {
		index := 2*k + 1
		if 2*k+2 < N && nums[2*k+2] > nums[2*k+1] {
			index = 2*k + 2
		}
		if nums[index] > nums[k] {
			nums[index], nums[k] = nums[k], nums[index]
			k = index
		} else {
			break
		}
	}
}

快速排序

func QuickSort(nums []int) {
	if len(nums) <= 1 {
		return
	}
	lo := 0
	hi := len(nums) - 1
	tmp := nums[lo]
	for lo < hi {
		for lo < hi && nums[hi] >= tmp {
			hi--
		}
		nums[lo] = nums[hi]
		for lo < hi && nums[lo] <= tmp {
			lo++
		}
		nums[hi] = nums[lo]
	}
	nums[lo] = tmp
	QuickSort(nums[:lo])
	QuickSort(nums[lo+1:])
}

查找算法

  • 二分搜索,常用于小数据量集的内存查找;注意有左右边界的变种形式
  • 二分搜索树查找,特点是有序查找
  • B树/B+树查找,用于数据库索引
  • Hash查找
  • BloomFilter,布隆过滤器,常用于在大数据集中判断某元素是否存在

算法流程:
布隆过滤器是一种用于高效地插入和查询的数据结构,与哈希表相比,其可以更少的占用空间实现大规模数据的插入和查询;但是缺点在于其判断存在这个操作是概率性的

插入
在这里插入图片描述
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7

然而随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在

哈希函数个数和布隆过滤器长度的选择

过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

  • BitMap

Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省

  1. 如何表示一个数
    每一位表示一个数,0表示不存在,1表示存在,这正符合二进制;下示表示{1, 2, 4, 6}
    在这里插入图片描述
    使用int(32位),原来一个数就要占用32位,现在可以用这个int来表示32个数,如0~31…
    tmp[0]:可以表示0~31
    tmp[1]:可以表示32~63
    tmp[2]:可以表示64~95
    给定任意整数M,那么M/32就得到下标,M%32就知道它在此下标的哪个位置
  2. 添加一个数
    如添加5,首先,5/32=0,5%32=5,也是说它应该在tmp[0]的第5个位置;
    将1左移代表该数字的那一位,然后与原数进行按位或操作
    在这里插入图片描述
  3. 删除一个数
    如删除6,到达6这个数字所代表的位,然后按位取反,最后与原数按位与
    在这里插入图片描述
  4. 快速查找
    判断一个数存不存在就是判断该数所在的位是0还是1
  5. 快速排序
    ① 假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。
    ② 要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,然后将对应位置为1。
    ③ 最后,遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。
    ④ 缺点在于无法对重复数据进行排序
  6. 快速去重
    20亿个整数中找出不重复的整数的个数,内存不足以容纳这20亿个整数
    ① 一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间2G左右。
    ② 接下来的任务就是把这20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为11,表示已经有一个了,即出现多次;如果为11,则对应的状态位保持不变,仍表示出现多次。
    ③ 最后,统计状态位为01的个数,就得到了不重复的数字个数,时间复杂度为O(n)。

常用算法思路

字符串匹配

  • BF算法
    暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,即回退,依次比较下去,直到得出最后的匹配结果;时间复杂度 O ( M ∗ N ) O(M*N) O(MN)

  • BM算法
    BM算法借助“坏字符规则”和“好后缀规则”,在每一轮比较时,让模式串尽可能多移动几位,减少无谓的比较。
    BM算法的移动规则是:
    BM算法是每次向右移动模式串的距离是 MAX(shift(好后缀),shift(坏字符)),即按照好后缀算法和坏字符算法计算得到的最大值。
    BM算法与KMP思想类似,但是更好了利用了预处理数组,使得更有效率,但是模式串的预处理数组也变得相对更加复杂,所以当面临较小场景时,并不一定适合选用BM算法
    BM算法的时间复杂度在 O ( m / n ) − O ( m ∗ n ) O(m/n) - O(m*n) O(m/n)O(mn)

  • Sunday算法
    Sunday算法是从前往后扫描模式串的,关注的是主串中参与匹配的最末字符(并非正在匹配的)的下一位
    Case1:当遇到不匹配的字符时,如果关注的字符没有在模式串中出现则直接跳过;即移动位数 = 子串长度 + 1;
    在这里插入图片描述
    Case2: 当遇到不匹配的字符时,如果关注的字符在模式串中也存在时,其移动位数 = 模式串中该字符最右出现的位置到尾部的距离 + 1
    在这里插入图片描述
    缺点:
    主串:baaaabaaaabaaaabaaaa
    子串:aaaaa
    没错,这个时候,效率瞬间变成了 O ( m ∗ n ) O(m*n) O(mn)

    Sunday算法的移动是取决于子串的,这一点跟BM算法没什么区别,当这个子串重复很多的时候,就会非常糟糕了

    时间复杂度
    Sunday O ( m / n ) − O ( m ∗ n ) O(m/n) - O(m*n) O(m/n)O(mn)

  • KMP算法
    首先需要构建next数组
    在这里插入图片描述
    next数组的含义
    next数组的下标:指的是当前位置为坏字符
    next数组的值:指的是下一个模式串中应该比较的位置

比如下标为3,表示T为坏字符,那么对于前缀GTG代表已经匹配上了,所以接下来相当于使用GTG的最大相同前后缀G,模式串匹配了前缀部分,主串匹配了后缀部分,即模式串下一个比较1位置

流程:

  1. 对模式串预处理,生成next数组
  2. 进入主循环,遍历主串
    2.1. 比较主串和模式串的字符
    2.2. 如果发现坏字符,查询next数组,得到匹配前缀所对应的最长可匹配前缀子串,移动模式串到对应位置
    2.3.如果当前字符匹配,继续循环

时间复杂度:
构建next数组为O(M);遍历主串为O(N)
所以为O(M+N)

// 计算next数组
public static int[] getNexts(String pattern) {
    int[] next = new int[pattern.length()];
    int j = 0;
    for (int i=2; i<pattern.length(); i++) {
        while (j != 0 && pattern.charAt(j) != pattern.charAt(i-1)) {
            //从next[i+1]的求解回溯到 next[j]
            j = next[j];
        }
        if (pattern.charAt(j) == pattern.charAt(i-1)) {
            j++;
        }
        next[i] = j;
    }
    return next;
}
// KMP匹配
public static int kmp(String str, String pattern) {
    int[] next = getNexts(pattern);
    int j = 0;
    //主循环,遍历主串字符
    for (int i = 0; i < str.length(); i++) {
        while (j != 0 && str.charAt(i) != pattern.charAt(j)) {
            //遇到坏字符时,查询next数组并改变模式串的起点
            j = next[j];
        }
        if (str.charAt(i) == pattern.charAt(j)) {
            j++;
        }
        if (j == pattern.length()) {
            //匹配成功,返回下标
            return i - pattern.length() + 1;
        }
    }
    return -1;
}

  • Tire算法

场景算法

  • 一个很大文件,如何对其排序
    在内存不够的情况下,使用分治策略,先将这个大文件分成若干个小文件,再把这些小文件分别读入内存后进行内部排序,随后我们利用堆合并k个有序数组的思想,只要维护一个最小堆,就能按升序对所有数进行排序
  • 大量的URL,如何进行去重
  1. 可以使用布隆过滤器,对大量数据进行去重判断
  2. 可以构造一个合适的哈希函数,将所有的URL散列到若干个小文件中,并存到外存中,此时由于相同的URL会位于同一个文件中,又因为哈希冲突,所以在每个文件去除重复的URL后,所有小文件合并即为去重后的文件

计算机网络

  • 五层网络协议体系结构,各层的作用以及常用的协议
  • 报文在各层附加头部的形式变化:报文→用户数据报/分段→IP数据报→帧→比特流
  • 在这里插入图片描述
  • 路由器和交换机分别在哪一层
    路由器需要进行IP寻址,因此在网络层;交换机需要进行MAC寻址,因此在数据链路层
  • 网络层ARP协议的工作原理
  • IP地址五种分类,每种分类的IP地址范围,掩码的两种表示方式,如何根据掩码求网络ID和主机ID,以及子网ID
  • TCP和UDP的主要特点以及二者之间的区别
    TCP报文头
    在这里插入图片描述
    UDP报文头
    在这里插入图片描述
    HTTP请求头
    在这里插入图片描述
  • 请求报头通知服务器关于客户端请求的信息,典型的请求头有:
    Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机
    User-Agent:发送请求的浏览器类型、操作系统等信息
    Accept:客户端可识别的内容类型列表,用于指定客户端接收那些类型的信息
    Accept-Encoding:客户端可识别的数据编码
    Accept-Language:表示浏览器所支持的语言类型
    Connection:允许客户端和服务器指定与请求/响应连接有关的选项,例如这是为Keep-Alive则表示保持连接。
    Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式。
    HTTP响应头
    在这里插入图片描述
  • 用于服务器传递自身信息的响应,常见的响应报头:
    Location:用于重定向接受者到一个新的位置,常用在更换域名的时候
    Server:包含可服务器用来处理请求的系统信息,与User-Agent请求报头是相对应的
  • TCP和UDP对应的常见应用层协议
  • TCP三次握手过程
  • 在这里插入图片描述

最初客户端和服务端都处于 CLOSED(关闭) 状态。本例中 A(Client) 主动打开连接,B(Server) 被动打开连接。

一开始,B 的 TCP 服务器进程首先创建传输控制块TCB,准备接受客户端进程的连接请求。然后服务端进程就处于 LISTEN(监听) 状态,等待客户端的连接请求。如有,立即作出响应。

第一次握手:A 的 TCP 客户端进程也是首先创建传输控制块 TCB。然后,在打算建立 TCP 连接时,向 B 发出连接请求报文段,这时首部中的同步位 SYN=1,同时选择一个初始序号 seq = x。TCP 规定,SYN 报文段(即 SYN = 1 的报文段)不能携带数据,但要消耗掉一个序号。这时,TCP 客户进程进入 SYN-SENT(同步已发送)状态。

第二次握手:B 收到连接请求报文后,如果同意建立连接,则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都置 1,确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时 TCP 服务端进程进入 SYN-RCVD(同步收到)状态。

第三次握手:TCP 客户进程收到 B 的确认后,还要向 B 给出确认。确认报文段的 ACK 置 1,确认号 ack = y + 1,而自己的序号 seq = x + 1。这时 ACK 报文段可以携带数据。但如果不携带数据则不消耗序号,这种情况下,下一个数据报文段的序号仍是 seq = x + 1。这时,TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)状态。

  • 为什么两次握手不可以(防止已失效的连接又传送到了B)
  • 为什么不需要四次握手(经过三次握手之后已经可以确认客户端和服务端之间的通信状态)
  • SYN泛洪攻击
    A(攻击者)发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当这个服务器返回ACK以后,A不再进行确认,那这个连接就处在了一个挂起的状态,也就是半连接的意思,那么服务器收不到再确认的一个消息,还会重复发送ACK给A。这样一来就会更加浪费服务器的资源。A就对服务器发送非法大量的这种TCP连接,由于每一个都没法完成握手的机制,所以它就会消耗服务器的内存最后可能导致服务器死机,就无法正常工作了。更进一步说,如果这些半连接的握手请求是恶意程序发出,并且持续不断,那么就会导致服务端较长时间内丧失服务功能——这样就形成了DoS攻击。这种攻击方式就称为SYN泛洪攻击。

    那么我们如何去防范这种SYN攻击呢?

    其实最常用的一个手段就是优化主机系统设置。比如降低SYN timeout时间,使得主机尽快释放半连接的占用或者采用SYN cookie设置,如果短时间内收到了某个IP的重复SYN请求,我们就认为受到了攻击。我们合理的采用防火墙设置等外部网络也可以进行拦截。
  • TCP四次挥手过程
  • 在这里插入图片描述

第一次挥手:A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 置 1,其序号 seq = u(等于前面已传送过的数据的最后一个字节的序号加 1),这时 A 进入 FIN-WAIT-1(终止等待1)状态,等待 B 的确认。请注意:TCP 规定,FIN 报文段即使不携带数据,也将消耗掉一个序号。

第二次挥手:B 收到连接释放报文段后立即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v(等于 B 前面已经传送过的数据的最后一个字节的序号加1),然后 B 就进入 CLOSE-WAIT(关闭等待)状态。TCP 服务端进程这时应通知高层应用进程,因而从 A 到 B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭(half-close)状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一段时间。A 收到来自 B 的确认后,就进入 FIN-WAIT-2(终止等待2)状态,等待 B 发出的连接释放报文段。

第三次挥手:若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段必须使 FIN = 1。假定 B 的序号为 w(在半关闭状态,B 可能又发送了一些数据)。B 还必须重复上次已发送过的确认号 ack = u + 1。这时 B 就进入 LAST-ACK(最后确认)状态,等待 A 的确认。

第四次挥手:A 在收到 B 的连接释放报文后,必须对此发出确认。在确认报文段中把 ACK 置 1,确认号 ack = w + 1,而自己的序号 seq = u + 1(前面发送的 FIN 报文段要消耗一个序号)。然后进入 TIME-WAIT(时间等待) 状态。请注意,现在 TCP 连接还没有释放掉。必须经过时间等待计时器设置的时间 2MSL(MSL:最长报文段寿命)后,A 才能进入到 CLOSED 状态,然后撤销传输控制块,结束这次 TCP 连接。当然如果 B 一收到 A 的确认就进入 CLOSED 状态,然后撤销传输控制块。所以在释放连接时,B 结束 TCP 连接的时间要早于 A。

  • 为什么TIME-WAIT状态要等待2MSL
  • TCP协议如何保证可靠传输
    数据包校验、对数据包重排序、应答机制、超时重传、滑动窗口
  • TCP滑动窗口流量控制机制
  • TCP拥塞控制机制
    拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致于过载。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是限制发送端发送数据的速率,以便使接收端来得及接收。
    为了进行拥塞控制,TCP 发送方要维持一个拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。
  • TCP拥塞控制算法
    TCP 的拥塞控制采用了四种算法,即:慢开始、拥塞避免、快重传和快恢复。
  • TCP的粘包以及如何产生粘包
  • HTTP的状态码
  • forward和redirect的区别
  • HTTP的方法有哪些
  • GET和POST方法的区别
  • 在浏览器中输入 URL 地址到显示主页的过程?
  • DNS的递归查询和迭代查询
  • HTTP长连接和短连接的理解
  • HTTP1.0/1.1/1.2的主要变化
    HTTP1.1 的主要变化:
  1. HTTP1.0 经过多年发展,在 1.1 提出了改进。首先是提出了长连接,HTTP 可以在一次 TCP 连接中不断发送请求。

  2. 然后 HTTP1.1 支持只发送 header 而不发送 body。原因是先用 header 判断能否成功,再发数据,节约带宽,事实上,post 请求默认就是这样做的。

  3. HTTP1.1 的 host 字段。由于虚拟主机可以支持多个域名,所以一般将域名解析后得到 host。
    HTTP2.0 的主要变化:

  4. HTTP2.0 支持多路复用,同一个连接可以并发处理多个请求,方法是把 HTTP数据包拆为多个帧,并发有序的发送,根据序号在另一端进行重组,而不需要一个个 HTTP请求顺序到达;

  5. HTTP2.0 支持服务端推送,就是服务端在 HTTP 请求到达后,除了返回数据之外,还推送了额外的内容给客户端;

  6. HTTP2.0 压缩了请求头,同时基本单位是二进制帧流,这样的数据占用空间更少;

  7. HTTP2.0 适用于 HTTPS 场景,因为其在 HTTP和 TCP 中间加了一层 SSL 层。

  • HTTPS的工作过程
  1. 客户端发送自己支持的加密规则给服务器,代表告诉服务器要进行连接了;

  2. 服务器从中选出一套加密算法和 hash 算法以及自己的身份信息(地址等)以证书的形式发送给浏览器,证书中包含服务器信息,加密公钥,证书的办法机构;

  3. 客户端收到网站的证书之后要做下面的事情:

    3.1 验证证书的合法性;
    3.2 如果验证通过证书,浏览器会生成一串随机数,并用证书中的公钥进行加密;
    3.3 用约定好的 hash 算法计算握手消息,然后用生成的密钥进行加密,然后一起发送给服务器。

  4. 服务器接收到客户端传送来的信息,要做下面的事情:

    4.1 用私钥解析出密码,用密码解析握手消息,验证 hash 值是否和浏览器发来的一致;
    4.2 使用密钥加密消息;

  5. 如果计算法 hash 值一致,握手成功。

  • select, poll, epoll区别
  1. select
    时间复杂度O(n)
    它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。单个进程所能打开的最大连接数由FD_SETSIZE宏定义
  2. poll
    时间复杂度O(n)
    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
  3. epoll
    时间复杂度O(1)
    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)),Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

    select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
    epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
  • HTTP和HTTPS的区别
  • 数字签名
  • 数字证书
  • 对称加密和非对称加密
  • 常用命令
    ifconfig命令,用于显示或设置网络设备;常用功能包括显示网络设备,启动/关闭网卡,配置/删除IPv6地址,配置MAC地址,设置IP地址/掩码
    netstat命令,用于显示系统的网络状态,以及查找端口
  • Socket
    Socket层图示
    在这里插入图片描述
    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
    Socket概述
    在这里插入图片描述
    先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

    Socket()函数:就相当于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
    Bind()函数:通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
    作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求,TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    网络中进程的通信方式
    网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    Socket中的三次握手
    在这里插入图片描述
    从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    Socket中的四次挥手
    在这里插入图片描述
  • WebSocket
    能够在单个TCP连接上进行全双工通信的协议
    在这里插入图片描述
  • Session和Cookie
    由于HTTP协议原则上是无状态的协议,每次交换数据需要建立新的连接,所以无法在连接上进行跟踪,会话指用户登录网站后的一系列动作,比如浏览商品添加到购物车并购买。会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。
  1. Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端会把Cookie保存起来。
    当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
    会话Cookie/持久Cookie,Cookie是不可跨域名使用的
  2. Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
  3. ① cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
    ② session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
    ③ 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
    ④ 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

操作系统

  • 并发和并行

  • 操作系统的基本功能

  • 系统调用
    如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成
    在这里插入图片描述

  • 中断分类(外中断、异常、陷入)
    在这里插入图片描述

  • 堆和栈

  • 进程和线程及其区别
    进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

    线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。

    由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

    进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。

  • 进程状态的切换
    在这里插入图片描述

  • 进程调度算法

  1. 先来先服务(FCFS),短作业等待过长
  2. 短作业优先(SJF),长作业饿死
  3. 最短剩余时间优先(SRTN),按估计最短剩余时间调度
  4. 时间片轮转调度
  5. 优先级调度,为了防止低优先级的进程得不到调度,会随着时间增加等待进程的优先级
  6. 多级反馈序列,优先级调度和时间片轮转调度的结合
  • 首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程;
  • 对于同一个队列中的各个进程,按照FCFS分配时间片调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。
  • 在最后一个队列QN中的各个进程,按照时间片轮转分配时间片调度
  • 在低优先级的队列中的进程在运行时,又有新到达的作业,此时须立即把正在运行的进程放回当前队列的队尾,然后把处理机分给高优先级进程。换而言之,任何时刻,只有当第1~i-1队列全部为空时,才会去执行第i队列的进程(抢占式)。

    示例:
    假设系统中有3个反馈队列Q1,Q2,Q3,时间片分别为2,4,8;设有3个作业J1,J2,J3分别在时间 0 ,1,3时刻到达。而它们所需要的CPU时间分别是3,2,1个时间片。
    ① 时刻0 J1到达。于是进入到队列1 , 运行1个时间片 , 时间片还未到,此时J2到达。
    ② 时刻1 J2到达。 由于同一队列采用先来先服务,于是J2等待。 J1在运行了1个时间片后,已经完成了在Q1中的2个时间片的限制,于是J1置于Q2等待被调度。当前处理机分配给J2。
    ③ 时刻2 J1进入Q2等待调度,J2获得CPU开始运行。
    ④ 时刻3 J3到达,由于同一队列采用先来先服务,故J3在Q1等待调度,J1也在Q2等待调度。
    ⑤ 时刻4 J2处理完成,由于J3,J1都在等待调度,但是J3所在的队列比J1所在的队列的优先级要高,于是J3被调度,J1继续在Q2等待。
    ⑥ 时刻5 J3经过1个时间片,完成。
    ⑦ 时刻6 由于Q1已经空闲,于是开始调度Q2中的作业,则J1得到处理器开始运行。 J1再经过一个时间片,完成了任务。于是整个调度过程结束。
  • 进程通信方式
  1. 管道(Pipe):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
  2. 命名管道(Named Pipe):匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  4. 消息队列(Message Queue):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。消息队列克服了信号承载信息量少的缺点
  5. 共享内存(Shared Memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
  6. 信号量(Semaphore):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
  7. 套接字(Socket):此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
  • 线程间通信方式
  1. synchronized同步
    synchronized同步本质上就是 “共享内存” 式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
  2. while轮询的方式
    while轮询的方式就是,ThreadA 不断地改变条件,ThreadB 不停地通过 while 语句检测这个条件 (list.size()==5) 是否成立 ,从而实现了线程间的通信。但是这种方式会浪费 CPU 资源。这个有点浪费资源。
  3. wait/notify机制
    当条件未满足时,ThreadA 调用 wait() 放弃 CPU,并进入阻塞状态。(不像 while 轮询那样占用 CPU)
    当条件满足时,ThreadB 调用 notify() 通知线程 A,所谓通知线程 A,就是唤醒线程 A,并让它进入可运行状态。
  • 互斥锁,自旋锁,读写锁,乐观锁,悲观锁
  1. 互斥锁:当有一个线程要访问共享资源(临界资源)之前会对线程访问的这段代码(临界区)进行加锁。如果在加锁之后没释放锁之前其他线程要对临界资源进行访问,则这些线程会被阻塞睡眠(不占用任何cpu资源),直到解锁,如果解锁时有一个或者多个线程阻塞,那么这些锁上的线程就会变成就绪状态,然后第一个变为就绪状态的线程就会获取资源的使用权,并且再次加锁,其他线程继续阻塞等待。
  2. 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。同互斥锁不同的是在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁,互斥锁阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁。这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率。适用于锁的持有时间比较短。
  3. 读写锁:也叫做共享互斥锁,读模式共享,写模式互斥。有点像数据库负载均衡的读写分离模式。它有三种模式:读加锁状态,写加锁状态和不加锁状态。简单来说就是只有一个线程可以占有写模式的读写锁,但是可以有多个线程占用读模式的读写锁。
  4. 乐观锁:这其实是一种思想,当线程去拿数据的时候,认为别的线程不会修改数据,就不上锁,但是在更新数据的时候会去判断其他线程是否修改了数据。通过版本来判断,如果数据被修改了就拒绝更新,之所以叫乐观锁是因为并没有加锁。
  5. 悲观锁:当线程去哪数据的时候,总以为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。这两种锁一般用于数据库,当一个数据库的读操作远远大于写的操作次数时,使用乐观锁会加大数据库的吞吐量。
  • 孤儿进程和僵尸进程
    僵尸进程产生及处理
    例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵尸进程,倘若用 ps 命令查看的话,就会看到很多状态为 Z 的进程。 严格地来说,僵尸进程并不是问题的根源,最有危害的是产生出大量僵尸进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵尸进程时,答案就是把产生大量僵尸进程的那个元凶枪毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号啦)。kill了真正元凶进程之后,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程就能瞑目而去了。
  • 上下文切换
  • 什么是死锁
    资源 A 和资源 B,都是不可剥夺资源,现在进程 C 已经申请了资源 A,进程 D 也申请了资源 B,进程 C 接下来的操作需要用到资源 B,而进程 D 恰好也在申请资源A,进程 C、D 都得不到接下来的资源,那么就引发了死锁。
  • 引发死锁的必要条件
  • 预防死锁发生,破坏必要条件
  • 避免发生死锁,银行家算法
    安全状态:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。

    单个资源的银行家算法
    在这里插入图片描述
    多个资源的银行家算法
    在这里插入图片描述
    左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源
    检查一个状态是否安全的算法如下:
  1. 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
  2. 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
  3. 重复以上两步,直到所有进程都标记为终止,则状态时安全的。

    但是,如果不存在任何一个状态是安全的,那么就判断会发生死锁。(回溯法)
  • 虚拟内存
    为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令

    由此可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。
    在这里插入图片描述
  • 程序局部性原理
    程序局部性原理,是指程序执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。
  • 分页系统
    内存管理单元(MMU):管理着地址空间和物理内存的转换。
    页表(Page table):页(地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。
  • 页面置换算法
    在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
  • 分段系统
  • 段页式
    程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。
  • 分段系统与分页系统
  1. 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。
  2. 地址空间的维度:分页是一维地址空间,分段是二维的。
  3. 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
  4. 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

数据库

MySQL

  • 数据库三大范式
  • MySQL中的存储引擎种类
  • MyISAM与Innodb的区别以及二者索引的区别
  1. InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  2. InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  3. MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  4. InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
  5. 覆盖索引和回表查询
  • 存储引擎的选择
  • 索引的优缺点
  • 使用索引的场景;WHERE/ORDER BY/JOIN
  • 索引有哪几种类型
  • 索引的数据结构;BTREE算法和HASH算法的不同
  • 创建索引的原则
  • 百万级别的数据如何进行删除
  • 最左前缀匹配原则
  • 数据库为什么选用B+树不选用B树
  • 事务的ACID特性
  • 什么是脏读、幻读、不可重复读
  • 事务的隔离级别
  • 隔离级别与锁的关系
  • 按锁的粒度分数据库的锁有哪些
  • 按锁的类别分数据库的锁有哪些
  • 什么是死锁,怎么解决
    死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
    常见的解决死锁的方法
  1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
  2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
  3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
  • 数据库的乐观锁和悲观锁
    悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
    乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
    两种锁的使用场景
    从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
    但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
  • 什么是视图?为什么要使用视图?
  • 视图的特点
  • 什么是游标?
    游标是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果
  • 什么是存储过程?有哪些优缺点?
  • 什么是触发器?使用场景?
  • SQL语句分为哪几类
  1. 数据定义语言DDL(Data Definition Language)CREATE,DROP,ALTER
    主要为以上操作 即对逻辑结构等有操作的,其中包括表结构,视图和索引。
  2. 数据查询语言DQL(Data Query Language)SELECT
    这个较为好理解 即查询操作,以select关键字。各种简单查询,连接查询等 都属于DQL。
  3. 数据操纵语言DML(Data Manipulation Language)INSERT,UPDATE,DELETE
    主要为以上操作 即对数据进行操作的,对应上面所说的查询操作 DQL与DML共同构建了多数初级程序员常用的增删改查操作。而查询是较为特殊的一种 被划分到DQL中。
  4. 数据控制功能DCL(Data Control Language)GRANT,REVOKE,COMMIT,ROLLBACK
    主要为以上操作 即对数据库安全性完整性等有操作的,可以简单的理解为权限控制等。
  • 超键、候选键、主键、外键
  • SQL中有哪几种约束
  • 六种关联查询
  • 子查询以及子查询的使用场景
  1. 子查询是单行单列的情况:结果集是一个值,父查询使用:=、 <、 > 等运算符
  2. 子查询是多行单列的情况:结果集类似于一个数组,父查询使用:in 运算符
  3. 子查询是多行多列的情况:结果集类似于一张虚拟表,不能用于where条件,用于select子句中做为子表
-- 1) 查询出2011年以后入职的员工信息
-- 2) 查询所有的部门信息,与上面的虚拟表中的信息比对,找出所有部门ID相等的员工。
select * from dept d,  (select * from employee where join_date > '2011-1-1') e where e.dept_id =  d.id;    

-- 使用表连接:
select d.*, e.* from  dept d inner join employee e on d.id = e.dept_id where e.join_date >  '2011-1-1'  
  • mysql中int(10)和char(10)以及varchar(10)的区别
  • drop、delete、truncate的区别
  • 如何定位及优化SQL语句的性能问题?创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因?
  • 使用explain查看执行计划中包含的信息
    在这里插入图片描述
  1. id 由一组数字组成。表示一个查询中各个子查询的执行顺序,id值越大优先级越高,越先被执行

  2. select_type 每个子查询的查询类型

  3. table 查询的数据表

  4. type(非常重要,可以看到有没有走索引)

    ALL 扫描全表数据
    index 遍历索引
    range 索引范围查找
    index_subquery 在子查询中使用 ref
    unique_subquery 在子查询中使用 eq_ref
    ref_or_null 对Null进行索引的优化的 ref
    fulltext 使用全文索引
    ref 使用非唯一索引查找数据
    eq_ref 在join查询中使用PRIMARY KEYorUNIQUE NOT NULL索引关联.

  5. possible_keys 可能使用的索引

  6. key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL

  7. key_length 索引长度

  8. extra 的信息非常丰富

    Using index 使用覆盖索引
    Using where 使用了用where子句来过滤结果集
    Using filesort 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化。
    Using temporary 使用了临时表 sql优化的目标可以参考阿里开发手册

  9. 优化目标

    SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts最好。
    说明:
    1) consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
    2) ref 指的是使用普通的索引(normal index)。
    3) range 对索引进行范围检索。
    反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。

  • 大数据表中数据查询,如何优化性能
  1. 优化schema,sql语句,添加索引;务必禁止不带任何限制数据范围条件的查询语句
  2. 增加缓存,对于常用的查询结果放在缓存里
  3. 主从复制,读写分离
    在这里插入图片描述
  4. 垂直拆分,简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表,将不同的数据表分到不同的数据库中,解决表与表之间的io竞争;但是主键会出现冗余,需要额外考虑join操作
  5. 水平拆分,保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。水品拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。(垂直拆分和水平拆分)
  • 解决超大分页
    解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可
  • MySQL的分页
    用于将数据以多页展示出来,使用分页的目的是为了提高用户的体验
    LIMIT语句用于限制分页的条目数
  • 慢查询日志
    使用slow_query_log,用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。
  • 如何优化慢查询
  1. 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
  2. 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
  3. 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行垂直或者水平的分表。
  • 如何优化查询过程中的数据访问
  1. 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列
  2. 避免犯如下SQL语句错误

    查询不需要的数据。解决办法:使用limit解决
    多表关联返回全部列。解决办法:指定列名
    总是返回全部列。解决办法:避免使用SELECT *
    重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存

  3. 是否在扫描额外的记录

    使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化:
    使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。
    重写SQL语句,让优化器可以以更优的方式执行查询

  4. 数据库分表
  • WHERE子句的优化
    对于此类考题,先说明如何定位低效SQL语句,然后根据SQL语句可能低效的原因做排查,先从索引着手,如果索引没有问题,考虑以上几个方面,数据访问的问题,长难查询句的问题还是一些特定类型优化的问题,逐一回答。
  1. 建立索引
  2. 避免全表扫描;null值查询、!=操作符、or查询条件、in和not int、like “%a%”
  • 分库分表后面临的问题
    如何合并结果,示跨分片的排序
    在这里插入图片描述
  • MySQL主从复制
    主从复制:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行(重做);从而使得从数据库的数据与主数据库保持一致。
  • 主从复制的作用
    主数据库出现问题,可以切换到从数据库。
    可以进行数据库层面的读写分离,实现负载均衡。
    可以在从数据库上进行日常备份。
  • 主从复制的基本流程
    主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;
    从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中;
    从:sql执行线程——执行relay log中的语句;
    在这里插入图片描述
  • 数据库备份和损坏修复

Redis

  • 什么是Redis
    Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库
  • Redis 与其他 key-value 缓存产品的不同点
  • Redis的优势
  1. 性能极高,因为数据存在内存中
  2. 丰富的数据类型
  3. 原子性;Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务
  4. 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除
  • Redis支持的五种数据类型
    string(字符串),hash(哈希),list(列表),set(集合)及 Zset(有序集合)
  • Memcache 与 Redis 的区别都有哪些
  • Redis 是单进程单线程的?
  1. Redis采用的是基于内存的单进程单线程模型的KV数据库
  2. Redis快的主要原因是:

    完全基于内存
    数据结构简单,对数据操作也简单
    使用多路 I/O 复用模型

  3. 多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)
  4. 单进程单线程好处

    代码更清晰,处理逻辑更简单
    不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
    不存在多进程或者多线程导致的切换而消耗CPU

  5. 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善
  • Redis的持久化机制
    Redis提供两种持久化机制 RDB 和 AOF 机制
  1. RDB(Redis DataBase)持久化方式:
    是指用数据集快照的方式(半持久化模式)记录 redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

    优点:

    1. 只有一个文件 dump.rdb,方便持久化。
    2. 容灾性好,一个文件可以保存到安全的磁盘。
    3. 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis的高性能
    4. 相对于数据集大时,比 AOF 的启动效率更高。

    缺点:
    数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。

  2. AOF(Append-only file)持久化方式
    是指所有的命令行记录以 redis 命令请求协议的格式(完全持久化)存储保存为 aof 文件

    • 按照一定时间间隔,将缓存的操作命令记录到文件中
    • 有一定的时间差,数据可能丢失比较多
  • Redis过期键的删除策略
  1. 定时删除:在设置键的过期时间的同时,创建一个定时器 timer,让定时器在键的过期时间来临时,立即执行对键的删除操作。
  2. 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  3. 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
  • Redis的淘汰策略
    将Redis用做缓存时,如果内存空间用满,就会自动驱逐老的数据。
    volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
    volatile-ttl:从已设置过期时间的数据集中挑选将要过期,即剩余时间(time to live,TTL)短的数据淘汰
    volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
    allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
    allkeys-random:从数据集中任意选择数据淘汰
    no-enviction(驱逐):禁止驱逐数据
    volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略
  • 为什么 Redis 需要把所有数据放到内存中?
    Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。在内存越来越便宜的今天,redis 将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值
  • MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据?
    结合Redis的淘汰策略
  • Redis 集群的主从复制模型是怎样的?
    为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制品

设计模式

  • 单例模式
    一个类只有一个实例;必须自己创建;给其他调用者提供同一实例
    图解单例模式
    第一版;私有构造函数+静态变量+懒汉式
public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

第二版;双重检测判空+同步锁+volatile防止JVM指令重排

public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
   //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {      //双重检测机制
        synchronized (Singleton.class){  //同步锁
            if (instance == null) {     //双重检测机制
                instance = new Singleton();
               }
            }
         }
        return instance;
    }
}

常用后端技术

  • 消息队列MQ
    使用消息队列的主要作用
  1. 通过异步处理提高系统性能
    在这里插入图片描述
    在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。
    但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。
    消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。
  2. 降低系统耦合性
    在这里插入图片描述
    消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。
    另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。
  • 常用的消息队列kafka
    一个消息系统负责将数据从一个应用传递到另外一个应用,分布式消息传递基于可靠的消息队列,在客户端应用和消息系统之间异步传递消息。有两种主要的消息传递模式:点对点传递模式发布-订阅模式。大部分的消息系统选用发布-订阅模式。Kafka就是一种发布-订阅模式。
  1. 点对点消息传递模式
    在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多个消费者消费队列中的数据。但是一条消息只能被消费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。该模式即使有多个消费者同时消费数据,也能保证数据处理的顺序。
    生产者发送一条消息到queue,只有一个消费者能收到。
    在这里插入图片描述
  2. 发布-订阅消息传递模式
    在发布-订阅消息系统中,消息被持久化到一个topic中。与点对点消息系统不同的是,消费者可以订阅一个或多个topic,消费者可以消费该topic中所有的数据,同一条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系统中,消息的生产者称为发布者,消费者称为订阅者。
    发布者发送到topic的消息,只有订阅了topic的订阅者才会收到消息
    在这里插入图片描述
  • MongoDB
    MongoDB 是一个基于分布式文件存储的数据库,介于关系数据库和非关系数据库之间

参考

平衡二叉树、B树、B+树、B*树 理解其中一种你就都明白了
详解布隆过滤器的原理、使用场景和注意事项
字符串匹配 - Sunday算法
Socket原理讲解
Redis为什么单进程单线程也那么快
Bitmap简介
session和cookie的区别
自已动手做高性能消息队列
Kafka学习之路 (一)Kafka的简介
各类锁(互斥锁,自旋锁,读写锁,乐观锁,悲观锁,死锁)
自旋锁和互斥锁区别
深入理解select、poll和epoll及区别

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值