吉法师的博客

不知道能否追到喜欢的人呀,今年努力下吧~ 2022.1.4

Node.js后端面试整理

一、Node.js语言部分

I.语言基础

1.闭包的原理

函数就是一个闭包,js的特点是可以在函数内部创建另一个函数。js可以在函数内用外部的变量,所以自然而然产生的闭包概念。

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

在内部的那个函数可以引用外部的变量,但同时这个变量也无法被销毁,有消耗内存的风险。

块级作用域

{}也是划分作用域的概念,和闭包略有不同。

2.Js的内置对象

Object、Array、Boolean、Number、String、Buffer等等。

3.Nodejs gc

引用计数法

假设有一个对象A,任何对象对A进行引用,那么对象A的引用计数器+1,当引用失效时,对象A的引用计数器-1,当对象A的引用计数器为0时,就说明对象A没用被引用,那么就可以进行回收。

优点:
  • 实时性高 不需要等内存不足再回首。

  • 垃圾回收的时候不需要挂起应用。

  • 区域更新对象,不需要扫描全部对象

缺点:
  • 每次对象被引用都要更新

  • 浪费cpu资源,因为内存足够的时候还在统计

  • 无法解决循环引用

比如两个对象互相引用 都赋值成null,那就永远回收不了了。

标记清除法

标记清除法是把gc分为两个阶段,标记和清除。

  • 标记:从根节点开始标记引用的对象

  • 清除:没被标记的就是垃圾对象 可以清除。

结构有点像多叉树。

从根节点开始遍历,找到所有可达的对象,其他的都是可以清除的。

优点

解决了循环引用的问题

缺点

标记和清除都需要遍历所有对象,gc的时候需要暂停程序。对交互性高的程序体验很差。

内存碎片多,被清理的对象在内存的各个角落。

标记压缩算法

标记压缩算法是在标记清除法的基础上进行了优化,标记阶段是一样的,在清理阶段不是直接清理标记对象,而是将存活对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决碎片化问题。

优缺点

解决了内存碎片化的问题,但是多了压缩的一步,对象移动内存位置的步骤,对效率有影响。

4. 原型

在js中,对象都有__proto__属性,一般这个是被称为隐式的原型,该隐式原型指向构造该对象的构造函数的原型。

函数比较特殊,它除了和其他对象一样有__proto__属性,还有自己特有的属性—-prototype,这个属性是一个指针,指向一个包含所有实例共享的属性和方法的对象,称之为原型对象。原型对象也有一个constructor属性,该属性指回该函数。

II.libuv原理

libuv底层是用了生产者-消费者的模型,libuv在整个生命周期中,每一次循环都执行每个阶段(phase)维护的任务队列。逐个执行节点里的回调,在回调中,不断生产新的任务,从而不断驱动libuv。

Nodejs的事件循环原理

事件循环底层使用libuv。

Nodejs的特点

单线程,它不会为每个请求分配一个线程,而是用主线程处理所有请求,对输入输出进行异步处理,避开了创建销毁线程,和线程切换的开销和复杂性。

核心原理

Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。

当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。

无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过 线程池 来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程。

在事件队列中,如果前面的 CPU 计算任务没有完成,后面的任务就会被阻塞,出现响应缓慢的情况,当 Node.js 被CPU 密集型任务占用,导致其他任务被阻塞时,却还有 CPU 内核处于闲置状态,造成资源浪费。

使用要点

尽量不在服务器中使用需要同步等待的CPU密集型任务,如fs的同步读文件,因为这会阻塞事件队列的轮询。

每个 CPU 密集任务只在它被调度到的时候才会得到执行。

Node.js 有两种类型的线程:一个事件循环线程和 k 个工作线程。 事件循环负责 JavaScript 回调和非阻塞 I/O,工作线程执行与 C++ 代码对应的、完成异步请求的任务,包括阻塞 I/O 和 CPU 密集型工作。

这两种类型的线程一次都只能处理一个活动。 如果任意一个回调或任务需要很长时间,则运行它的线程将被 阻塞。 如果你的应用程序发起阻塞的回调或任务,在好的情况下这可能只会导致吞吐量下降(客户端/秒),而在最坏情况下可能会导致完全拒绝服务。

III.v8

v8解决的问题是快速解析和执行JavaScript脚本。

1.内存限制

32位是0.7gb,64位是1.4gb

2.和Nodejs的关系

v8把js代码翻译成机器码,直接运行。

3.内存结构

内存区主要能够分为如下几类:栈区、堆区、常量区、函数定义区、函数缓存区。

堆区

JavaScript的变量名是用来保存内存中某块内存区的地址的,而栈区就是用来保存变量名和内存地址的键值对的。

let a = {}

对于该语句,V8会在堆区中开辟一块内存,而后在栈区添加一个键值对,键名是咱们声明的变量名a,键值是堆区中开辟出的内存的地址。

在堆区中存在一个特殊的预置对象null,它在堆区中有固定的内存地址,而且是惟一的。也就是说全部被赋值为null的变量指向的都是这同一块内存地址(所以被赋值为null的变量也是有内存地址的)。

常量区

  • 全部的值都是不可变的
  • 全部相同的常量值在常量区都是唯一的。

函数定义区

//函数声明
function f(){
  ...
}
//函数引用
var f = function(){
 ...
}

其实没什么卵用的知识。

//能够正常调用,由于引擎会提早扫描代码,将该函数存储到函数定义区
f();
function f(){}

//报错,由于虽然g也进行了变量提高,但此时g的值是undefined,不能调用
g();
var g = function(){}

函数缓存区

所谓函数缓存区,就是函数运行所用的内存区。当V8引擎须要执行一个函数时,它就会在函数缓存区开辟一块内存,保存该函数运行所须要存储的状态和变量。

一般情况都会回收内存,用了闭包就有可能不回收(上面有提到)

理解 == 和 ===

var a = 1;
var b = 1;
a === b;   //值为true,同一个常量在常量区只会生成一个,所以二者获得地址是同样的

var a = {};
var b = {};
a === b;    //值为false,引擎会分别为a和b开辟内存,所以二者的地址并不相同

null === null;  //值为true,由于堆区只有一个null
[] === [];  //值为false,原理与{}相同

在比较字符串的时候,数字会先转成字符串,然后去比较地址。

4.V8的垃圾回收机制

V8的垃圾回收机制采用的是标记清除法,这也是如今JavaScript引擎通用的一种回收机制。

新生代空间小一点,老生代空间大一点。

新生代

新生代区域一分为二,每个16M,一个使用,一个空闲。

开始垃圾回收的时候,会检查FROM区域中的存活对象,如果还活着,拷贝到TO空间,所有存活对象拷贝完后,清空(释放)FROM区域 然后FROM和To区域互换。

新生代的空间小,存活对象少。

当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较差的对象会被移动到老生代,这个移动过程被称为晋升或升级。

  • 经历过5次以上的回收还存在

  • TO的空间使用占比超过25%,或者超大对象

老生代

老生代垃圾回收策略分为两种:

mark-sweep 标记清除

标记活着的对象,虽然清楚在标记阶段没有标记的对象,只清理死亡对象

会出现的问题:清除后内存不连续,碎片内存无法分配

mark-compact 标记整理

标记死亡后会对对象进行整理,活着的左移,移动完成后清理掉边界外的内存(死亡的对象)

老生代空间大,大部分都是活着的对象,GC耗时比较长

GC的时候程序无法进行响应

二、计算机网络

I.网络架构

七层网络模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

II.三次握手

粗略的目的

  • 第一次客户端给服务器发送,服务器能确认客户端的发送、服务器的接收能力。

  • 第二次服务器给客户端发送,客户端能确认服务器的发送、客户端的接收能力。同时根据第一次握手,能知道自己的发送、服务器的接收都正常。

为什么需要三次握手

  • 因为服务器不知道自己的发送,客户端的接收是否正常,所以需要第三次握手,得到客户端的应答才能做最后的确认。

SYN和ACK

SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

位码

位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)

III.四次挥手

  • 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送

  • 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号

  • 服务器-关闭与客户端的连接,发送一个FIN给客户端

  • 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1

IV.各种协议

1.TCP,UDP 协议的区别

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等。

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

2.TCP 协议如何保证可靠传输

简单来说,数据会分包、TCP有检验和:判断数据有缺失会直接丢掉,而且不确认这次收到了、流量控制、拥塞处理、ARQ协议。

3.DNS

域名系统,能够获取域名对应的ip地址。

V.http协议

1.当输入www.google.com时,页面发生了哪些事情

  1. 域名解析检查顺序为:浏览器自身DNS缓存—》OS自身的DNS缓存–》读取host文件–》本地域名服务器–》权限域名服务器–》根域名服务器。如果有且没有过期,则结束本次域名解析。域名解析成功之后,进行后续操作。

  2. 三次握手

  3. 建立连接,发起http请求。

  4. 服务器响应,浏览器得到内容。

  5. 浏览器解析html代码,并且请求其中的资源

  6. 浏览器对页面渲染,呈现给用户。

2.什么是Http协议无状态协议?怎么解决Http协议无状态协议?

http是无状态的,也就是后续处理无法处理之前的信息。

处理办法有Cookie,Section会话保存,甚至可以用数据库和缓存处理。

3.http协议组成

请求报文包含三部分:

  • 请求行:包含请求方法、URI、HTTP版本信息

  • 请求首部字段

  • 请求内容实体

响应报文包含三部分:

  • 状态行:包含HTTP版本、状态码、状态码的原因短语

  • 响应首部字段

  • 响应内容实体

空行

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

请求正文

请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。

返回值

在接收和解释请求消息后,服务器返回一个HTTP响应消息。

HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文

请求行

请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,GET /index.html HTTP/1.1。

GET方法:在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源,

POST方法要求被请求服务器接受附在请求后面的数据,常用于提交表单。

个人不推荐用GET,全部用POST比较方便。

请求头部

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

  • User-Agent:产生请求的浏览器类型。

  • Accept:客户端可识别的内容类型列表。

  • Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

4.状态码

  • 1xx:指示信息–表示请求已接收,继续处理

  • 2xx:成功–表示请求已被成功接收、理解、接受

  • 3xx:重定向–要完成请求必须进行更进一步的操作

  • 4xx:客户端错误–请求有语法错误或请求无法实现

  • 5xx:服务器端错误–服务器未能实现合法的请求

5.http的版本 1.0与1.1

在http1.0中,当建立连接后,客户端发送一个请求,服务器端返回一个信息后就关闭连接,当浏览器下次请求的时候又要建立连接,显然这种不断建立连接的方式,会造成很多问题。

在http1.1中,引入了持续连接的概念,通过这种连接,浏览器可以建立一个连接之后,发送请求并得到返回信息,然后继续发送请求再次等到返回信息,也就是说客户端可以连续发送多个请求,而不用等待每一个响应的到来。

6.GET和POST的区别

GET重点是请求数据,POST的重点是提交数据。GET传输数据以URL的方式,POST通过http的post机制,把数据封装在了请求实体中,所以是不可见的,安全性也更高一点。

GET只支持ASCII字符,有可能出现乱码,POST不会。

VI.https和http

HTTPS协议 = HTTP协议 + SSL/TLS协议,在HTTPS数据传输的过程中,需要用SSL/TLS对数据进行加密和解密,需要用HTTP对加密后的数据进行传输,由此可以看出HTTPS是由HTTP和SSL/TLS一起合作完成的。

传输的过程

服务器端的公钥和私钥,用来进行非对称加密。

客户端生成的随机密钥,用来进行对称加密。

Https先用非对称加密,去加密对称加密的密钥,后面的传输都用对称加密算法,因为可以提升性能。

传输步骤

一个HTTPS请求实际上包含了两次HTTP传输,可以细分为8步。

  • 1.客户端向服务器发起HTTPS请求,连接到服务器的443端口

  • 2.服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。

  • 3.服务器将自己的公钥发送给客户端。

  • 4.客户端收到服务器端的证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。

  • 5.客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。

  • 6.服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。

  • 7.然后服务器将加密后的密文发送给客户端。

  • 8.客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。

三、数据结构和算法

排序算法的稳定性是什么,有什么用?

稳定性,指两个相同元素在排序后的相对顺序。

稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。

I.常见排序算法

时间复杂度中,^符号代表幂次方。

1.冒泡排序

冒泡排序是两层循环,将数组两个元素挨个比较,进行交换。

第一层循环是n,第二层则是n-1,所以时间复杂度为O(n^2)

稳定,因为相同的元素选择不交换即可。

2.选择排序

每次都寻找最大的元素,交换的相应的位置。

和冒泡排序时间复杂度一样,因为循环结构相似,O(n^2)。

不稳定,一般会颠倒位置。

3.插入排序

未排序的元素从已排序的元素中选择位置插入。

和冒泡排序时间复杂度一样,因为循环结构相似,O(n^2)。

稳定,后来的碰到相同元素就往后插入。

4.希尔排序

希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

private static void sort(int[] array) {
        int n = array.length;
        int h = 1;
        while (h<n/3) { //动态定义间隔序列
              h = 3*h +1;
                 }
         while (h >= 1) {
            for (int i = h; i < n; i++) {
                for (int j = i; j >= h && (array[j] < array[j - h]); j -= h) {
                    int temp = array[j];
                    array[j] = array[j - h];
                    array[j-h]= temp;
                }
            }
            h /=3;
        }
    }

两次循环,有除以3的操作,所以是O(n^1.3)

不稳定,因为数据被打乱了。

5.快速排序

分治法,选中一个元素,把大的和小的各放一边,然后对每一块数据都按此方法做处理。

一次循环决定要处理几次,另外一个是递归操作,所以是O(n*logn)

递归,显然不稳定。

6.归并排序

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。它使用了递归分治的思想;相当于:左半边用尽,则取右半边元素;右半边用尽,则取左半边元素;右半边的当前元素小于左半边的当前元素,则取右半边元素;右半边的当前元素大于左半边的当前元素,则取左半边的元素。

和快排的时间复杂度一样,因为都用了递归,O(n*logn)

稳定,合并序列的时候相同的往后排。

7.堆排序

堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

因为是二叉树,也用到了递归的概念,和快速排序类似,O(n*logn)

不稳定,构建了二叉树。

8.计数排序

找出待排序元素最大最小值,然后开辟数据存储,计算每个元素出现的次数,最后一次排序完毕。

时间复杂度O(n+k)

K很大的时候就不能忽略了,这是O(n+k)的含义。

稳定,倒着输出,搞一个数组的栈即可。看实现方式,如果是只记录出现的次数,最后自己组数组,则不稳定,但如果是看到有存在的元素,将元素取出来,那就是稳定的。

9.桶排序

将数据分到不同的桶里面排序,然后每个再分别排序。

时间复杂度O(n+k)

不稳定,不同的桶遇到相同的值不好处理。

10.基数排序

按每一位进行排序,先从低位开始排序,每一轮遍历,最后排序到最后一位。

时间复杂度O(n+k) 时间复杂度和位数有关系。

稳定。

II.常见数据结构应用

1.链表LRU缓存淘汰

我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。

当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。

  1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。

  2. 如果此数据没有在缓存链表中,又可以分为两种情况:如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。

这样就实现了一个LRU算法。

2.栈在括号匹配中的作用

当遇到左括号的时候,压入栈,遇到右括号的时候出栈。空栈遇到右括号的话,则表示这个括号对已经不匹配了。

只要最后看栈中的元素是否为空,就能知道括号是否匹配了。

栈也可以实现浏览器的前进后退,前进的时候在后退栈压入数据(同时可能需要出栈),后退的时候出栈,入前进栈。前进时前进栈出栈,入后退栈。

3.检查拼写错误,用什么数据结构实现?

哈希表,不解释。

4.完全二叉树,满二叉树,堆的区别

完全二叉树是一棵树没有光秃秃的树枝,树叶长满了。满二叉树是除了最后一层,其他的树叶都长满了。堆是完全二叉树,且满足每一个节点都大于子节点的值,或小于,即大顶堆和小顶堆的概念。

5.第k大,即排行榜问题

可以用堆,链表的LRU算法感觉也很方便。

6.有向图

微信的好友关系,知乎的关注关系。

III.哈希算法

这里仅介绍下如何解决冲突,哈希算法的概念不多赘述:

1.开放地址法

我们可以对哈希值取mod,或者以其他规则,挨个在内存中探测,若发现某个地址为空,则把数据存入。

2.多次哈希

换一种哈希算法,不可能每次都一样的值,但这样会有效率问题。

3.拉链法

相同哈希值的拉链表,挨个查找。

4.溢出区

将哈希表分为公共和溢出区域,把冲突的哈希值放在另一个地方。

四、数据库

I.SQL、表、索引知识

1.SQL语句组成部分

数据定义:Create Table,Alter Table,Drop Table, Craete/Drop Index等

数据操纵:Select ,insert,update,delete,

数据控制:grant,revoke

数据查询:select

2.MySQL的锁

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

3.索引

索引就像是目录,为了能增加我们的查询速度而被设计了出来。

B+树

B+树索引能轻易存储上百万的数据,且只需要查询三次索引,非叶子节点不存储数据,但存储指向数据的位置。

真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

且B+树有数据块,能够轻易进行范围查找

哈希索引

单次查询极快,但不适合做范围查询。

使用索引的技巧

  • 最左前缀原则:如果name是索引,email不是,则查询得先查name再匹配email。

  • 选择区分度高,可能会被命中的列作为索引。

  • B+树用自增索引很适合,因为增删改查不会大幅度改变B+树的结构。

  • 索引值不能参与计算,否则开销极大

索引不匹配的问题

  • 使用了like

  • 使用了函数

  • 使用了or语句

  • 类型不一致,数字和引号,特别要注意如果类型是字符串,则传入数字也得用单引号

  • 普通索引!=运算不走索引,但主键还是会走索引

  • order by如果有索引,select字段没有索引,则不能走索引

  • 最左前缀原则,违反了不匹配

慢查询优化的基本步骤

  1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
  3. explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的sql语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照建索引的几大原则
  7. 观察结果,不符合预期继续从0分析

char和varchar的区别

char是固定长度的,存储的时候会用空格填充,varchar会变长

索引除了优点,还需要知道它可能存在的缺点

创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。

4.B+树的结构

一个m阶的B+树具有如下几个特征:

  • 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

形象地理解(为了不在博客里贴图):每个树枝存了一堆索引,每个索引都记录了对应的子树的最大值,如根节点存了8和15,而我们查找的是3,那么很容易发现是需要从左子树开始查找。随后左子树如果是三叉,分别是2、5、8,那么我们会知道3在中间的那棵树,随后遍历即可。

b+树的叶子节点是有序链表,所以很容易进行范围查询。

II.引擎

1.Myisam

不支持事务,但是每次查询都是原子的;

支持表级锁,即每次操作是对整个表加锁;

存储表的总行数;

一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;

采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。

2.Innodb

支持ACID的事务,支持事务的四种隔离级别;

支持行级锁及外键约束:因此可以支持写并发;

不存储总行数;

一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;

主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

3.InnoDB特殊的锁

  • 行锁

  • 间隙锁,锁定一个范围,但不包括记录本身,为了防止一个事务两次读,出现幻读。

  • next-key lock,1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

III.事务与回滚

1.事务的隔离级别

意外情况

脏读:一个事务读了另一个事务尚未提交的修改数据

非重复读:同一个事务,在两个时刻读取某一行的结果不同。

幻读:同一个事务中,返回的结果集被改动。

隔离级别

read uncommitted:读未提交,三种故障都会发生。

read commited:读已提交,除了脏读都有可能发生,也是oracle数据库的默认隔离级别。

REPEATABLE READ:可重复读,MySQL的默认事务隔离级别,只会允许幻读。通过next-key lock锁定一定范围的数据,阻止他人插入。

SERIALIZABLE:串行,效率最低,但不会有任何意外情况。

2.事务的特性

  • 原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。

  • 一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态

  • 隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,

  • 持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。

3.事务日志

innodb事务日志包括redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。

undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志。

1.redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

2.undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

redo log

redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

在概念上,innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。

在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:

  • 如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
  • 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。

上述两项变量的设置保证了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。

日志刷盘的规则

log buffer中未刷到磁盘的日志称为脏日志(dirty log)。

默认情况下事务每次提交的时候都会刷事务日志到磁盘中,这是因为变量 innodb_flush_log_at_trx_commit 的值为1。但是innodb不仅仅只会在有commit动作后才会刷日志到磁盘,这只是innodb存储引擎刷日志的规则之一。

刷日志到磁盘有以下几种规则:

1.发出commit动作时。已经说明过,commit发出后是否刷日志由变量 innodb_flush_log_at_trx_commit 控制。

2.每秒刷一次。这个刷日志的频率由变量 innodb_flush_log_at_timeout 值决定,默认是1秒。要注意,这个刷日志频率和commit动作无关。

3.当log buffer中已经使用的内存超过一半时。

4.当有checkpoint时,checkpoint在一定程度上代表了刷到磁盘时日志所处的LSN位置。

binlog

binlog即二进制日志,它记录了所有的 DDL 和 DML 语句(除了数据查询语句select、show等),以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。

工作场景:主从复制、数据恢复。

参数

对支持事务的引擎如InnoDB而言,必须要提交了事务才会记录binlog。binlog 什么时候刷新到磁盘跟参数 sync_binlog 相关。

如果设置为0,则表示MySQL不控制binlog的刷新,由文件系统去控制它缓存的刷新;

如果设置为不为0的值,则表示每 sync_binlog 次事务,MySQL调用文件系统的刷新操作刷新binlog到磁盘中。

设为1是最安全的,在系统故障时最多丢失一个事务的更新,但是会对性能有所影响。

如果 sync_binlog=0 或 sync_binlog大于1,当发生电源故障或操作系统崩溃时,可能有一部分已提交但其binlog未被同步到磁盘的事务会被丢失,恢复程序将无法恢复这部分事务。

牺牲一部分安全性,但是能提高性能

五、设计模式

I.各种设计模式的集合

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

II.设计模式的原则

1.开放封闭原则

尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化

2.里氏代换原则

概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。

优点:增加程序的健壮性,即使增加了子类,原有的子类还可以继续运行,互不影响。

3.依赖倒转原则

依赖倒置原则的核心思想是面向接口编程.

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,

这个是开放封闭原则的基础,具体内容是:对接口编程,依赖于抽象而不依赖于具体。

4.接口隔离原则

支付类的接口和订单类的接口,需要把这俩个类别的接口变成俩个隔离的接口。

降低依赖,降低耦合,可维护性会更高。

5.单一职责原则

原则思想:一个方法只负责一件事情。

描述:单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。 这是常识,几乎所有程序员都会遵循这个原则。

6.迪米特法则,最少知道原则

原则思想:一个对象应当对其他对象有尽可能少地了解,简称类间解耦。

大概意思就是一个类尽量减少自己对其他对象的依赖,原则是低耦合,高内聚,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。

III.各种设计模式的运用

1.单例类

保证一个类只有一个实例,并且提供一个访问该全局访问点。

饿汉式:先创建好等着被调用。

懒汉式:要用了再创建实例。

具体运用

  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
  • 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
  • Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
  • windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。

优点

  • 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  • 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。
  • 避免对共享资源的多重占用。

缺点

  • 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 可扩展性稍微差了点
  • 违背了单一职责原则

2.工厂模式

它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。实现了创建者和调用者分离,工厂模式分为简单工厂、工厂方法、抽象工厂模式。

工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。

Spring的Ioc控制反转,创建Bean的时候就是工厂模式。

工厂模式分类

简单工厂 :用来生产同一等级结构中的任意产品。(不支持拓展增加产品) 工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品)
抽象工厂 :用来生产不同产品族的全部产品。(不支持拓展增加产品;支持增加产品族)

应用

同一个类实例化出不同的产品

3.代理模式

通过代理控制对象的访问,可以在这个对象调用方法之前、调用方法之后去处理/添加新的功能。(也就是AO的P微实现)

代理在原有代码乃至原业务流程都不修改的情况下,直接在业务流程中切入新代码,增加新功能,这也和Spring的(面向切面编程)很相似

应用场景

Spring AOP、日志打印、异常处理、事务控制、权限控制等。

4.建造者模式

将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的方式进行创建。

使用场景:

  • 需要生成的对象具有复杂的内部结构。

  • 需要生成的对象内部属性本身相互依赖。

5.原型模式

原型设计模式简单来说就是克隆

原型表明了有一个样板实例,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可使程序运行更高效。

JavaScript大量使用了原型模式

6.观察者模式

先讲什么是行为性模型,行为型模式关注的是系统中对象之间的相互交互,解决系统在运行时对象之间的相互通信和协作,进一步明确对象的职责。

观察者模式,是一种行为性模型,又叫发布-订阅模式,他定义对象之间一种一对多的依赖关系,使得当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。

观察者模式应用场景

  • 关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。
  • Vue的双向绑定,Qt的信号槽,都有用到观察者模式

7.MVC模式

Model是业务模式,View是用户界面,Controller是控制器,三者分开单独管理。

一般的项目中会大量使用这样的设计模式

六、Linux

1.系统初始化的原理

Linux下有3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd(PID = 2)

idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换。

1号进程由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程。

Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。

2号进程它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程

2.进程间通信

Linux进程通信大致有管道、消息队列、共享内存、信号量等等。管道和消息队列很常用。

3.复习常用命令,了解系统维护等等知识。

七、Redis

八、操作系统

1.进程的通信方式

管道

管道(pipe)及命名管道(named pipe): 管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

信号

信号(signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;

消息队列

消息队列: 消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;

共享内存

共享内存: 可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等;

信号量

信号量: 主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段;

套接字

套接字: 这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

几种方式的比较:

  • 管道:速度慢、容量有限
  • 消息队列:容量收到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
  • 信号量:不能传递复杂信息,只能用来同步。
  • 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全。

2.操作系统死锁

在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。

造成死锁的条件

互斥:至少有一个资源必须属于非共享模式,即一次只能被一个进程使用;若其他申请使用该资源,那么申请进程必须等到该资源被释放为止;

占有并等待:一个进程必须占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有;

非抢占:进程不能被抢占,即资源只能被进程在完成任务后自愿释放

循环等待:若干进程之间形成一种头尾相接的环形等待资源关系

解除死锁可以简单选择一个牺牲品,强制回收资源来解除。

3.进程的状态

就绪:获得除处理资源外的所有资源,等待分配资源

运行:占用CPU资源运行,这类进程数量小于CPU核心数

阻塞:进程处于等待状态,条件满足前无法运行。

4.进程调度

  • FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU

  • SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度

  • 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化

  • 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。

  • 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。

  • 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。

5.进程和线程的区别

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

有需要再补充


Share