C++ 知识点

文章目录

🍻 语言对比

🥂 C++ 的特点?

  • C++ 在 C 语言的基础上引入了新特性,并兼容 C 语言。
  • C++ 是一种面向对象的编程语言,具有继承、封装、多态三大特性。
  • C++ 安全性好,引入了 const 常量,cast 类型转换,智能指针等。
  • C++ 可复用性好,引入了模板的概念,并在此基础上实现了 STL。
  • C++ 是不断发展的语言,在后续版本中增加了很多特性,比如 nullptr,auto,lambda,右值引用等。

🥂 C++、C 的区别?

  • C++ 在 C 语言的基础上引入了新特性,并兼容 C 语言。
  • C++ 是一种面向对象的编程语言,具有继承、封装、多态三大特性;而 C 语言是一种面向过程的编程语言。
  • C++ 安全性好,引入了 const 常量,cast 类型转换,智能指针等;而 C 语言中指针使用,类型转换可能存在潜在的风险。
  • C++ 可复用性好,引入了模板的概念,并在此基础上实现了 STL;而 C 语言可复用性不好。

🥂 C++、C 中 struct 的区别?

  • C++ 中 struct 允许有函数存在;而 C 语言中不行。
  • C++ 中 struct 对成员的访问权限可以为 private,protected 和 public;而 C 语言只可以为 public。
  • C++ 中结构体是可以继承的;而 C 语言不行。
  • C++ 中使用结构体时可以省略 struct 关键字;而 C 语言不行。

🥂 C++ 如何使用 C 代码?

  • 在 C++ 中使用 extern “C” 关键字,可以让编译器以 C 语言的规则对相应代码进行编译。
  • 比如 C++ 中支持函数重载,会涉及到命名倾轧技术,编译过程中函数名和参数类型会生成新的函数名;而 C 语言不支持函数重载,编译时函数名不变。

🥂 C++、C 静态变量的初始化时间?

  • C++ 中全局和局部静态变量只有在首次用到时才进行初始化;而 C 语言中全局和静态变量在预编译时进行初始化。

🍻 其他

🥂 “” 与 <> 的区别?

  • ”“ 的头文件是自定义头文件;而 <> 的头文件是系统头文件。
  • ”“ 的头文件查找路径是当前文件目录 -> 编译器设置的头文件目录 -> 系统变量;而 <> 的头文件查找路径是编译器设置的头文件目录 -> 系统变量。

🥂 C++ 从代码到可执行文件的过程?

  • 首先会经过预编译阶段,它会将 #define 删除,展开宏定义,处理预编译指令和条件预编译指令。删除注释,并添加行号与文件名标识。
  • 编译阶段:进行语法分析,词法分析,语义分析,并进行一定的代码优化,生成汇编代码。
  • 汇编阶段:将汇编代码转换成机器可以执行的二进制指令,生成目标文件。
  • 链接阶段:将不同源文件的目标文件进行链接,生成一个可以执行的程序。其中链接可以分为动态链接和静态链接。静态链接是链接时就把静态库添加到可执行文件中,而动态链接是程序执行的过程中调用动态库中的内容。

🥂 static 关键字的作用?

  • static 可以定义全局静态变量和局部静态变量,变量的生命周期在程序开始时就存在,直到程序结束。
  • static 可以对全局变量进行隐藏,使得全局变量只能在本文件中被访问,而对其他文件是不可见的。
  • static 也可以对函数进行隐藏,使得函数只能在本文件中被调用,不能被其他文件所调用。
  • static 修饰的类成员变量或函数,可以被所有实例所共享。

🥂 class、struct 的区别?

  • class 是对一个对象数据的封装;而 struct 用于描述一个数据的集合。
  • class 默认的访问权限是 private;而 struct 默认的访问权限是 public。
  • class 默认的继承权限是 private;而 strcut 默认的继承权限是 public。
  • class 关键字可以用于定义模板参数,也就是 class 替换 typename;而 struct 关键字却不能。

------------------------------

🥂 C++ 类型转换有哪些?+1

🔸 隐式类型转换:由编译器自动完成的类型转换,涉及到数据类型的自动提升或降低。
🔸 static_cast:用于父类和子类之间指针引用类型的转换。向上转换相对较为安全,但向下转换由于缺乏动态类型检查,存在潜在的不安全风险。也可以用于基本数据类型之间的转换,例如将 int 转为 char 等。
🔸 dynamic_cast:主要用于类之间指针引用类型的转换,只能用于类的指针、引用或 void*类型。向上转换相对较为安全。在向下转换时,由于有类型检查,仍然是相对安全的,如果检查出错误则会返回空指针。
🔸 const_cast:将常量的指针或引用转换为非常量的指针或引用,并指向原来的对象。
🔸 reinterpret_cast:能够实现一个类型的指针转换为毫无关联的另外一个类型的指针。这个操作不会对转换双方的类型进行检查,因此是不安全的。还可以实现将一个指针类型转换为整数,然后再将这个整数转换为指针。

🥂 类中成员变量的初始化顺序?+1

🔸 首先,父类的全局变量和静态变量会被初始化,子类的全局变量和静态变量会被初始化。
🔸 接着,父类中的成员变量会被初始化,子类中的成员变量会被初始化。
🔸 如果采用的是列表初始化,初始化顺序与变量声明顺序有关。如果采用的是构造函数初始化,初始化顺序与变量在构造函数中的位置有关。

🥂 父类的私有成员子类如何访问?+1

🔸 可以通过父类的公有成员函数来间接访问。
🔸将子类声明为父类的友元,子类就能够访问父类的私有成员。
🔸 将继承权限设置为私有,也可以实现子类访问父类私有成员。

🥂 final、override 关键字的作用?+1

🔸 final 关键字用于修饰基类中的虚函数,以明确表示该函数不能在派生类中再次被重写。
🔸 override 关键字用于显式声明一个函数是基类中的虚函数,并且有意对该函数进行重写。

🥂 智能指针的种类?+1

🔸 智能指针本质上是一个类,里面封装了一个指向动态分配内存的普通指针,并且负责该内存的释放,避免内存泄漏。当智能指针所指对象的生命周期结束后,会自动调用析构函数来释放动态分配的资源。

🔸 shared_ptr:shared_ptr 允许多个指针指向同一个对象,当增加一个 shared_ptr 指向对象时,所有的 shared_ptr 内部的引用计数都会加 1,当减少一个 shared_ptr 指向对象时,所有的 shared_ptr 内部的引用计数都会减 1。因此当智能指针内部的引用计数为 0 时,代表内存未被使用,则会自动调用析构函数释放动态分配的资源。
🔸 unique_ptr:unique_ptr 独享所指向对象的所有资源,因此最多只允许一个 unique_ptr 指向对象。当进行资源转移时,原来的 unique_ptr 将会被置空,因此 unique_ptr 不支持普通的拷贝操作,因为这会导致两个 unique_ptr 指向同一资源,造成内存多次释放。
🔸 weak_ptr:weak_ptr 主要是为了解决 shared_ptr 循环引用的问题,导致内存无法释放。因为 weak_ptr 可以指向 shared_ptr 所指的对象,但是却不会增加引用计数。当在 shared_ptr 所指的两个对象内部采用 weak_ptr 时,外部 shared_ptr 的引用计数不会受到 weak_ptr 的影响。当外部 shared 智能指针被释放时,引用计数会降低为 0,内存成功释放。
🔸 auto_ptr:auto_ptr 主要是为了解决有异常抛出时,无法正常释放内存的问题,在 C++11 中已经被废弃。

🥂 开发中用到了哪些智能指针?+1

🔸 在服务器中,使用 shared_ptr 可以指向线程池的内部资源,例如 mtxcondtasks等。这种设计有助于实现多线程共享同一组资源,避免了不必要的资源复制开销。
🔸 在服务器中,使用 unique_ptr 可以指向定时器和线程池。这意味着一个 webserver 类对象只会拥有一个独占的线程池和一个独占的定时器对象。当所指的线程池或定时器对象超出其作用域时,std::unique_ptr 会自动释放动态分配的内存,避免内存泄漏。

🥂 vector 的原理?+1

🔸 vector 是一种序列化的容器,对数据的操作和数组类似,但是它们对空间运用的灵活性不同。
🔸 数组占用的是静态空间,不可随意改变大小,如果内存不够就手动分配更大的空间,并将原数组数据进行拷贝移动,释放原空间。但是 vector 在空间不足时,可以自动扩展空间,容纳新的元素,不过其中也涉及到内存分配,数据移动,释放原空间的操作。vector 的动态扩容大小在 Linux 下通常是原长度的两倍。

🥂 sizeof(vector) 的输出?+1

🔸 输出的结果是 vector 类型的大小,根据 vector 的实现方式大小也有所不同。通常其中包含内存开始指针,内存结束指针,已分配内存指针,共24字节。

🥂 C++ 的内存分区 ?+1

🔸 C++的内存分区从高到低分别是栈区、堆区、全局数据区、常量区和代码区。

🔸 栈区:主要用于存放函数中的局部变量和函数参数。这些变量的生命周期与函数的调用有关。栈的内存分配由操作系统自动管理,分配和释放速度较快,但空间有限。
🔸 堆区:用于存放动态分配的内存。动态内存的生命周期由程序员手动管理。堆的内存分配和释放相对较慢,但可以提供较大的可用空间。
🔸 全局数据区:存放全局变量和静态变量。未初始化的全局变量和静态变量在这里进行默认初始化。
🔸 常量区:存放常量数据,包括字符串常量等。这些数据是不允许修改的。
🔸 代码区:存放程序的二进制代码。

🥂 linux 的内存分区 ?+1

🔸 linux 的内存分区从高到低分别是栈区、匿名映射区、堆区、BSS 段和数据段、代码段。

🔸 栈区:主要用于存放函数中的局部变量和函数参数。
🔸 文件和匿名映射区:是动态链接库中存储代码段、数据段、BSS段以及通过 mmap 进行内存映射的区域。
🔸 堆区:用于存放动态分配的内存。
🔸 BSS 段和数据段:未初始化的全局变量和静态变量放在 BSS 段,已经初始化的全局变量和静态变量放在数据段。
🔸 代码段:存放程序的二进制代码。

🥂 C++11 的新特性?+1

🔸 引入了 nullptr 代替了 NULL,auto 类型推导,cast 类型转换,基于范围的 for 循环等。
🔸 引入了 lambda 表达式,可以用来替代独立的函数或函数对象。
🔸 引入了智能指针,可以自动释放动态内存,避免内存忘记释放而导致的内存泄漏。
🔸 引入了右值引用和 move 语义,可以在资源转移时避免不必要的拷贝操作。
🔸 引入了列表初始化,使得自定义类型的初始化的代码更加简洁。

🍻 NULL 和 nullptr 的区别 ?

🔸 NULL 是通过宏定义实现的常量,可以兼容 C;而 nullptr 则是 C++11 的新增关键字。
🔸 在 C 中 NULL 的定义为 (void*) 0,而在 C++ 中 NULL 代表数字 0,这样会导致 NULL 无法与数字 0 区分,因此 C++ 通过引入 nullptr 来区分数字和指针类型。

🍻 lambda 的作用 ?

🔸 lambda 可以编写内嵌的匿名函数,用来替换独立的函数或函数对象,增加代码的可读性。
🔸 定义一个 lambda 时,编辑器会自动生成一个匿名类,称为闭包。程序运行时,lambda 会创建一个闭包实例,它可以通过传值或引用的方式获取当前作用域内的变量,也就是前面的 [],也可以通过参数列表来获取变量,也就是后面的 ()
🔸 lambda 的语法定义为: [捕捉列表] (参数列表) -> 返回类型 {函数体}。其中捕获列表和函数体是必须有的,其他的部分可以省略。

🍻 智能指针的循环引用 ?

🔸 循环引用是指两个 shared_ptr 指向两个对象,同时这两个对象内部也有 shared_ptr 互相指向对方,使得每个 shared_ptr 的引用计数都为 2。如果释放掉外部的 shared_ptr,那么内部的 shared_ptr 仍然存在,导致引用计数为 1,内存无法释放造成内存泄漏。
🔸 循环引用可以使用 weak_ptr 弱引用来解决,因为弱引用可以指向 shared_ptr 所指的对象,但是却不会增加引用计数。当在两个对象的内部采用弱引用时,shared_ptr 的引用计数为 1。当外部 shared_ptr 被释放时引用计数为 0,内存成功释放。

🍻 智能指针的缺点 ?

🔸 智能指针需要额外的空间取存储引用计数和相关信息,占用空间更大。
🔸 智能指针在进行计数,内存管理等操作时,会引入额外的开销。
🔸 智能指针可能会出现循环引用的问题,导致内存无法释放,造成内存泄漏。
🔸 智能指针不能正确处理循环的数据结构,比如环形链表。

🍻 move 的作用?

🔸 move 是一种高效的资源转移机制,相当于转移了资源的所有权,这样可以让待销毁对象的资源原地转移给其他对象,避免了不必要的资源拷贝操作,提高了程序的性能。
🔸 move 本身不能移动任何东西,它的唯一作用是将一个左值引用强制转化为右值引用,因为编辑器只能对右值引用的对象调用移动构造函数或拷贝构造函数,实现移动语义。

🔸 当 CPU 访问数据时,并不是逐字节的访问,而是以字长为单位进行访问。比如在 32 位系统中,字长为 4 字节,此时同样是访问 8 字节的数据,以字长为单位读取仅需要两次即可,而逐字节访问需要 8 次,因此使用内存对齐时可以提高 CPU 访问内存的效率。

🔹 结构体中的成员,会按照声明的顺序进行存储,而且第一个成员的地址就是结构体的地址。
🔹 结构体中的成员,一定会存放在整数倍自身大小的偏移量上,比如 int 只会方在 4 为倍数的偏移量上。
🔹 结构体的总大小为最大对齐数的整数倍,而最大对齐数与结构体最大的成员大小有关。
🔹 可以通过 alignof 查看当前对齐数,alignas 指定对齐数,或通过 pragma back 指定对齐数。

🍻 内存泄漏是什么?如何避免?+1

🔸 内存泄漏通常是指堆中内存的泄漏,当使用 new,malloc 分配堆中内存时,使用完毕后未使用 delete,free 进行释放,导致这块内存就无法被再次使用。

🔸 采用记数法,当使用 new 或 malloc 时,让计数器加一,当是用 delete 或 free 时,让计数器减一,程序执行完毕后检查计数器,如果不为 0 则存在内存泄漏。
🔸 析构函数需要声明为虚函数,防止父类指针指向子类对象时,编辑器实施静态绑定只会调用父类的析构函数,造成子类对象的析构不完全,导致内存泄漏。
🔸 使用智能指针,自动释放动态内存,避免内存忘记释放而导致的内存泄漏。

🍻 C++ 程序执行的过程 ?

🔸 C++ 程序执行会经历四个过程,分别是预编译、编译、汇编、链接。

🔹 预编译阶段会删除所有的注释,展开宏定义,处理条件预编译指令和预编译指令,添加行号和文件名标识,生成编译文件。
🔹 编译阶段会进行一系列的语法分析,词法分析,语义分析和代码优化,生成汇编文件。
🔹 汇编阶段主要是将汇编代码转变成机器可以执行的指令,生成目标文件。
🔹 链接阶段将不同源文件产生的目标文件进行链接,生成一个可执行的文件。其中链接可以分为静态链接和动态链接。

🍻 new 的实现 ?

🔸 首先,new 会根据传入的参数计算需要分配的内存大小,然后在堆中调用 operator new 尝试分配这段大小的内存,如果分配成功,则会调用相应的构造函数初始化对象,并返回指向该分配内存的指针。
🔸 如果 new 分配内存的操作失败,会抛出一个 bad_alloc 异常,并返回一个空指针。

🍻 new 和 malloc 的区别 ?

🔸 new 是 C++ 的关键字,需要编译器支持;而 malloc 是库函数,需要头文件支持。
🔸 new 申请动态内存时不需要指定大小,编译器会自动计算;而 malloc 申请动态内存时,需要手动计算大小。
🔸 new 分配内存成功时,会返回一个指向对象的指针;而 malloc 会返回一个 void* 类型的指针,需要强制类型转换为其他类型。
🔸 new 分配内存失败时,会抛出异常;而 malloc 则会返回 NULL。
🔸 new 分配内存时,会调用 operator new 申请足够多的内存,然后调用对象的构造函数初始化成员变量;而 malloc 只能申请动态,无法进行对象的构造工作。

🍻 new 和 allocator 的区别 ?

🔸 new 可以进行动态内存的分配,并将内存分配与对象构造步骤结合在一起。
🔸 allocator 可以将内存分配,对象构造这两步分开,先只进行内存分配,等待需要的时候再进行对象构造。

🍻 malloc 申请的内存可以用 delete 释放吗 ?

🔸 可以,但是不建议这么用。因为 malloc,free 主要是为了兼容 C 语言而保留的,new,delete 可以完全替代其功能。使用 delete 相当用就是调用 free 函数来释放内存。

🍻 new 申请的内存可以用 free 释放吗 ?

🔸 不可以,因为 new 分配的地址和其调用 malloc 分配的地址并不相同,系统预留了空间去存储析构函数的调用次数等信息,使用 free 并不能完全释放 new 分配的空间。

🍻 malloc,calloc 和 realloc 的区别 ?

🔸 malloc 会在堆中分配一块连续的内存区域,长度需要自己指定,默认初始化的值是随机的。
🔸 calloc 功能和 malloc 类似,但是它可以默认给所分配的内存初始化为 0。
🔸 realloc 可以给动态分配的内存空间进行扩大和缩小。

🍻 构造函数和析构函数的作用?

🔸 构造函数主要用于在创建对象时,进行一系列的初始化操作,比如参数赋值。
🔸 析构函数主要用于在撤销对象前,进行一系列的清理操作,比如内存释放。

🍻 空类的大小是多少 ?

🔸 空类的大小不为 0,通常为 1,因为编辑器要求每个对象实例都有着自己独一无二地址,因此会为空类自动分配一个字节的大小,来确保他们的地址不一样。
🔸 空类中增加一个普通函数时,大小仍为 1,因为对象在调用成员函数时,编辑器可以通过 this 指针来获取成员函数的地址,this 指针是不占用类的大小的。
🔸 空类中增加一个虚函数时,大小为 8,因为存在虚函数时,对象中会创建虚表指针指向虚表,这个指针大小为 8 字节。

🍻 空类的缺省函数有哪些?

🔸 构造函数:创建对象时,进行对象的初始化操作。
🔸 析构函数:对象生命周期结束时,进行一些清理工作,如释放动态分配的内存。
🔸 拷贝构造函数:用一个类型的引用来初始化另一个同类型的对象,进行浅拷贝。
🔸 赋值运算符重载:可以把一个类型的对象用等号赋值给另一个同类型的对象。

🍻 浅拷贝和深拷贝的区别?

🔸 浅拷贝只是拷贝了一个指针,没有开辟新的内存空间,拷贝的指针和原指针指向的是同一块地址。如果原指针所指向的内存释放,新指针访问内存就会报错。
🔸 深拷贝它拷贝了一个指针,而且开辟了一块新的内存空间来存放拷贝的值。即使原指针指向的内存被释放,也不会影响到到当前值。

🍻 指针数组和数组指针的区别 ?

🔸 指针数组:本质上是一个数组,不过数组中的每个元素都是一个指针,指向不同的对象。形式为 int* p[10],元素类型为 int*
🔸 数组指针:本质上是一个指针,不过该指针指向一个数组。形式为 int (*p)[10],元素类型为 int[]

🍻 指针常量和常量指针的区别 ?

🔸 指针常量:本质上是一个常量,而这个常量的值是一个指针,即指针的指向是不可变的,但指向对象的值是可以变的。形式为 int* const p,const 修饰的是指针 p,因此指针的指向不能变。
🔸 常量指针:本质上是一个指针,指向的对象是一个常量,因此指针的指向是可以变的,但是所指对象的值不可以变。形式为 int const* p,const 修饰的是指针指向的值 * p,因此指针指向的值不能变。

🍻 迭代器和指针的区别 ?

🔸 迭代器可以提供对容器中元素的访问,比如说链表,数组等;指针是一种特定类型的变量,直接指向所指对象的内存地址。
🔸 迭代器主要是通过 begin,end 函数来获取,通过 ++,-- 来进行遍历;指针是通过 & 符号获取地址,通常使用 ++,-- 来进行遍历。
🔸 迭代器与容器之间是相互独立的,同一种迭代器接口可以应用于不同类型的容器;但是指针的类型与所指对象的类型是直接相关的。
🔸 迭代器可以提供更安全的访问,因为在编译时会进行类型检测,防止类型不匹配。而指针没有类型检查,可能会发生类型不匹配,也容易发生越界。

🍻 this 指针的作用?

🔸 this 指针是供类使用的指针,指向对象的首地址。
🔸 this 指针只能在成员函数中使用,而在全局函数,静态成员函数中都不能使用。
🔸 this 指针不占用类的大小,但它的作用域是在类的内部,它作为非静态成员函数的隐式形参,对类的成员进行访问。

🌸 友元函数 友元类 是什么?

🔶 友元函数是一个定义在类外的普通函数,它不属于任何类,但是可以访问其他类中的私有成员。但是需要在被访问的类中声明它为可访问的友元函数。
🔶 友元类是一个所有成员都为友元函数的类,友元类可以访问其他类中的私有成员。但是同样需要在被访问的类中声明它为可访问的友元类。
🔶 友元的关系不能被继承,是单向的,也不具有传递性。

🌸 i++ / ++i 的区别?

🔶 它们两者都是 C++ 中的自增运算符。
🔷 i++ 是后置自增运算符,它会先返回变量当前的值,再将变量的值加 1。而 ++i 是后置自增运算符,他会先将变量的值加 1,再返回该变量的值。
🔷 i++ 期间会产生一个临时的对象,导致效率变低。而 ++i 期间不会产生临时的对象。
🔷 i++ 返回的是一个具体的值。而 ++i 返回的是一个引用。

🌺 内联函数是什么?

🔶 内联函数是以代码复制为代价,省去了调用函数时的开销,从而提升程序的运行效率。但是如果执行代码的事件,比调用函数的事件更多的话,那么内联函数的收益就很小。
🔶 当函数体中的代码比较长的时候,使用内联函数会导致内存消耗变高。当函数体内出现循环时,那么执行循环代码的事件比调用函数的事件高很多,没必要使用内联函数。

🌸 extern C 的作用?

🔶 extern C 可以在 C++ 代码中调用 C 的代码,相当于告诉编辑器,这部分是 C 写的,因此要按照 C 的规则进行编译。
🔶 extern C 主要用于 C++ 代码调用 C 代码,多人协同开发但语言不同的情况。

🌸 const 的作用?

🔶 const 修饰普通变量时,可以防止变量被修改。
🔶 const 修饰指针时,可以指定指针本身为 const,不能改变指向,也可以指定所指数据为 const,所指数据不可修改。
🔶 const 修饰函数形参时,表明函数内部不可以修改它的值。
🔶 const 修饰的成员函数,表示为常函数,只可以访问成员变量,而不能修改成员变量。

🌸 全局变量 / 局部变量 的区别?

🔶 全局变量的生命周期伴随着主程序的创建和销毁。而局部变量的生命周期只在函数作用域,循环体内部之中。
🔶 全局变量可以被程序的各个地方访问到。而局部变量只可以在局部作用域中被访问。
🔶 全局变量存储在内存中的全局数据区,程序运行时就被初始化。而局部变量存储在堆栈之中。

🌸 静态成员 / 普通成员 的区别?

🔶 静态成员从类被加载到卸载,一直存在。而普通成员仅存在于对象的生命周期里。
🔶 静态成员是所有类共享的。而普通成员是每个对象独有的。
🔶 静态成员存储在静态全局区中。而普通成员存储在堆或栈中。
🔶 静态成员在类外被初始化。而普通成员在类内被初始化。

🌸 unordered_set / set / unordered_map / map 的区别?应用场景?

🔶 map 是一种键值对的数据结构,支持自动排序,底层基于红黑树实现,查询复杂度为 O(logN),但是占用空间比较大,需要存放父子节点,染色等信息。
🔶 unordered_map 不支持排序,底层是通过哈希表来实现,通过哈希函数计算元素的位置,因此查询复杂度为 O(1)。
🔶 set 和 map 类似,支持自动排序,底层基于红黑树实现,它是一种只有键的数据结构。
🔶 unordered_set 和 unordered_map 类似,不支持排序,基于哈希表实现,是一种只有键的数据结构。
🔷 map、set 主要用于需要元素有序的场景,unordered_map、unordered_set 主要用于需要快速查询的场景。

🌸 模板 是什么?

🔶 模板是一种对类型进行参数化的工具,通常分为函数模板和类模板。函数模板主要针对函数中不同类型的参数,类模板主要针对类中不同类型的成员属性或成员函数,使用模板主要是可以编写与类型无关的代码。

🌸 模板类 的作用?

🔶 模板类的作用主要是为了实现泛型编程,即编写一个通用的类或函数,适用于不同的数据类型。因此可以避免编写重复的代码,提高代码的复用性和可维护性。

🌸 sort 函数是基于什么实现?

🔶 sort 中采用的是一种 IntroSort 内省式的混合排序算法。
🔶 首先判断排序的元素个数是否大于阈值,通常这个阈值为 16,如果元素个数小于阈值则直接使用插入排序。因为当序列中大多元素有序时,采用插入排序会更快。
🔶 如果元素个数大于阈值时,判断序列递归的深度会不会超过 2log(n),如果递归深度没有超过 2log(n) 时就使用快速排序。
🔶 如果递归深度超过 2*log(n) 时,那么快排的复杂度就退化了,这时候就采用堆排序,因为堆排序的复杂度是 nlog(n),比较稳定。

🌸 push_back / emplace_back 的区别?

🔶 push_back 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中。
🔶 而 emplace_back 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

🌸 resize / reverse 的区别?

🔶 resize 可以改变 vector 的大小,使其包含 n 个元素,如果 n 大于当前大小,新的元素会添加到 vector 末尾,如果 n 小于当前大小,那么末尾元素会被删除。
🔶 reserve 不会改变 vector 的大小,但是可以预分配足够的内存,确保在未来可以容纳 n 个元素,避免添加新元素时频繁进行内存分配。

🍻 构造函数为啥不能为虚函数 ?

🔸 虚函数的调用需要用到虚表,而虚表需要用到虚表指针,虚表和虚表指针都是在构造函数中创建的。如果构造函数是虚函数,那么此时还没有虚表,就无法访问身为虚函数的构造函数,因此构成矛盾。

🍻 析构函数为啥需要为虚函数 ?

🔸 如果析构函数为虚函数,则编辑器实施动态绑定。当父类指针指向子类的对象时,删除父类的指针时,会根据指针所指具体类型调用子类对象的析构函数,再自动调用父类的析构函数,整个子类的对象被完全释放。
🔸 如果析构函数不为虚函数,则编辑器实施静态绑定。当父类指针指向子类的对象时,删除父类指针时,只会调用父类的析构函数而不会调用子类的析构函数,造成子类析构不完全,导致内存泄漏。

🍻 纯虚函数和虚函数的区别 ?

🔸 纯虚函数主要用在抽象类中,没有具体实现的函数体,而只是对外提供一个接口;虚函数主要是为了实现动态多态,目的是通过父类指针指向不同类型的对象时,可以自动调用相应对象的同名函数。
🔸 纯虚函数对象不能够实例化的,子类对抽象类继承后,才能对子类对象进行实例化;虚函数对象随时都可以实例化。
🔸 纯虚函数和虚函数都是有虚表指针的,指向自己的虚表,不过纯虚函数因为没有具体实现的函数体,因此虚表中的值为空;而虚函数中虚表的值为函数地址。

🍻 父类虚表存放位置 ?

🔸 虚表是一个全局共享的元素,通过类中的虚表指针可以访问虚表。
🔸 虚表中存放的是指向成员函数的指针,而且虚表的大小是在编译时就已经确定,不涉及内存空间的分配,所以不会在堆中。
🔸 因此虚函数表它类似于静态成员变量,全局共享,大小固定,存放在全局数据区中。

🍻 虚表指针的初始化时间 ?

🔸 虚表指针是在对类进行实例化的时候产生的,会在构造函数中被初始化,并存放在实例对象的首部位置。

🍻 重载,重写和隐藏的区别 ?

🔸 重载:是指在同一个定义域中,有多个函数同名,但是函数参数列表的类型、数量、顺序有所不同。函数重载主要是通过命名倾轧(qingya)技术来区分同名函数,这个命名倾轧技术只与参数列表的类型、数量、顺序顺序有关,与返回值并没什么关系。
🔸 重写:是指在子类中定义与父类中的同名,参数列表,返回值均相同的虚函数,从而对父类函数的功能进行修改或扩展。当父类指针指向子类对象时,可以根据指针所指的具体类型来调用相应的函数。
🔸 隐藏:是指再子类中定义与父类中的同名,参数列表相同的非虚函数,从而对父类中的函数实现进行完全覆盖。

🍻 组合和继承的区别 ?

🔸 组合是 has-a 的关系,即整体与部分的关系,当前对象可以把包含对象当作自己的成员变量;而继承是 is-a 的关系,即特殊和一般的关系,子类可以重写父类的方法并对父类的功能进行扩展。
🔸 组合中当前对象只能通过所包含的对象调用其方法,所包含对象内部细节对外是不可见的;而继承中父类的内部细节是对子类可见的。
🔸 组合中当前对象与包含对象是低耦合的关系,修改包含对象代码不会影响当前对象。而继承中父类和子类对象是高耦合的关系,父类修改后子类需要相应的修改。

🌺 C++ / Python 的区别?

🔶 C++ 是编译语言,需要编译后在特定平台运行。而 Python 是一种脚本语言,是解释执行的。
🔶 C++ 是通过花括号来区分不同代码块。而 Python 是通过缩进来区分不同的代码块的。
🔶 C++ 需要提前定义变量的类型。而 Python 中不需要。
🔶 Python 中的库函数比 C++ 中的多,调用起来也更加的方便。

🌸 C++ / C 的区别?

🔶 C++ 通过 new / delete 来管理动态内存。而 C 通过 malloc / free 来管理内存。
🔶 C++ 中封装更多使用的是类,而 C 中封装使用的是结构体。
🔶 C++ 除了值和指针以外,还引入了引用。
🔶 C++ 中可以实现函数的重载,也就是函数名相同,参数列表不同。
🔶 C++ 新增了很多关键字,比如 bool dynamic_cast namespace 等。
🔶 C++ 中使用 iostream 类替代了 C 中的 stdio 函数库。

🍺 QT 基础

🍺 计算机网络

🍻 HTTP / HTTPS

🌸 在浏览器上输入一个 URL 会涉及到的内容?

🔶 首先浏览器会对 URL 进行解析,生成发送给服务器的请求报文。
🔶 查询服务器域名对应的 IP 地址,浏览器会先查询本地缓存,本地缓存没有的话,就从通过 DNS 服务器进行查询。
🔶 查询到服务器的 IP 地址后,向服务器请求连接,进行 TCP 的三次握手,连接后始终保持通信。
🔶 期间浏览器可以向服务器发送 GET,POST 等请求,比如获取、修改数据库中的信息等。
🔶 服务器会对浏览器的请求进行处理,并生成响应报文发送给浏览器。
🔶 浏览器获取响应报文,并进行内容的解析,渲染网页呈现在屏幕上。

🌸 HTTP / HTTPS 的区别?

🔶 安全性:HTTP 是超文本传输协议,报文是明文传输的容易被窃取,因此不安全;HTTPS 在 HTTP 和 TCP 之间加入了 SSL / TLS 协议,报文可以加密进行传输,相对更安全。
🔶 连接过程:HTTP 建立连接比较简单,仅需要 TCP 三次握手;HTTPS 不仅需要 TCP 三次握手,还需要经历 SSL/TLS 的握手。
🔶 端口号:端口号不同,HTTP 的默认端口是 80;HTTPS 的默认端口为 443。
🔶 数字证书:HTTP 不需要数字证书认证;而 HTTPS 需要服务端去申请数字证书认证,来确保服务端的身份是可信的。

🌸 HTTP 1.0 / 1.1 / 2 / 3 的区别?

🔶 HTTP 1.0 是一种无状态的应用层协议,浏览器每次请求都需要与服务器建立一个 TCP 连接,处理完请求之后立即断开连接,不记录过去的状态。
🔶 HTTP 1.1 支持长连接,在一个 TCP 连接期间可以同时处理多个 HTTP 请求和响应,减少了网络延迟。
🔶 HTTP 2 是基于二进制流的,数据可以分解为独立的帧交错发送,提高网络的传输效率。
🔶 HTTP3 是最新的版本,它使用了 QUIC 协议来提高网络的传输效率。

🌸 HTTP 状态码有哪些?

🔶 1xx:信息状态码,表示当前请求正在被处理,比如 100 代表一切正常。
🔶 2xx:成功状态码,表示当前请求已经被成功处理,比如 200 代表成功。
🔶 3xx:重定向状态码,表示当前请求需要附加的操作才能完成,比如 301 代表永久重定向,访问的资源已经被转移到其他位置。
🔶 4xx:客户端错误码,代表客户端发送的请求中存在错误,比如 404 代表访问资源不存在。
🔶 5xx:服务端错误码,代表服务端处理请求时出现错误,比如 503 代表服务器繁忙,无法处理请求。

🌸 HTTP 报文 的主要字段?

🔶 请求报文中首先是请求行,包含了请求方法,URL,协议版本。
🔶 接下来的多行的请求头信息,比如 Accept 接收类型,Connection 连接类型等。
🔶 最后是请求体的具体内容,请求头和请求体之间用空格区分开。
🔷 响应报文中首先是响应行,包含了协议版本,状态码,状态描述。
🔷 接下来是多行的响应头信息,主要包含 Content-length 响应长度,Content-type 响应格式等。
🔷 最后是响应体的具体内容,响应头和响应体之间同样用空格区分开。

🌸 HTTP 如何维持 长连接?

🔶 可以在服务端设置一个保活定时器,定时器在工作时会向客户端发送探测报文,如果客户端收到探测报文并返回 ACK,则代表 HTTP 连接还维持,否则就重新申请连接。
🔶 可以在服务端设置 keep-alive 参数来保持长连接,它可以指定长连接超时的时间,如果在超时时间内没有发生任何数据传输,就会断开连接。如果期间发生了数据传输,则对超时时间进行重置。

🌸 SSL/ TLS 的握手过程?

🔶 HTTPS 采用混合加密机制,首先采用非对称密钥来加密用于传输的对称密钥,确保对称密钥传输的安全性。之后使用对称密钥来加密传输的数据,确保数据通信的效率。
🔷 首先客户端会生成一个随机数,给出协议版本号和自己支持的加密算法。
🔷 服务端确认好双方的加密算法后,会生成一个随机数,给出自己的数字证书。
🔷 客户端确认数字证书有效后,会生成一个新的随机数,并使用数字证书中的公钥加密这个新随机数。
🔷 服务端通过自己的私钥,对客户端发送来的新随机数进行解密。
🔷 最后客户端和服务端使用之前所生成的三个随机数,生成对话密钥,用来加密之后的整个对话过程。

🌸 GET / POST 的区别?

🔶 GET 是获取数据;而 POST 是修改数据。
🔶 GET 是把请求的数据放在 URL 上,通过 URL 传输数据,容易被截取不安全;而 POST 的请求数据是放在 HTTP 包体中的,相对而言安全些。
🔶 GET 提交的数据量最大为 2K;而 POST 提交的数据量没有限制。
🔶 GET 请求会被浏览器主动缓存;而 POST 请求则不会。
🔶 GET 操作是幂等的;而 POST 操作是不幂等的。

🌸 GET / POST 安全吗?

🔶 POST 与 GET 相比就是数据在地址栏上不可见,相对安全些。
🔶 但是它俩本质上都不安全,因为 HTTP 在网络上是明文传输的,只要对网络节点上的包进行抓取,就能够数据报文的内容。

🌸 HTTP 的请求方法有哪些?

🔶 GET:从指定的资源中请求数据。
🔶 HEAD:和 GET 类似,不过没有响应体,仅查看响应头中的数据。
🔶 POST:将数据发送到服务器,并交给服务器进行处理,可能会创建或修改部分的资源。
🔶 PUT:将数据发送到服务器并放在 URL 指定的位置,如果目标文件不存在,则添加对应资源,如果存在则替换对应资源。
🔶 DELETE:用于删除 URL 所指定的资源,如果目标存在则删除。
🔶 PATCH:是对已有的资源进行部分更新。
🔶 TRACE:是对服务器收到的请求进行回显,用于测试诊断。
🔶 OPTIONS:用来查看服务器的性能。
🔶 CONNECT:是将连接改为管道方式的代理服务器。

🌸 HTTP 的缺点有哪些?

🔶 信息是通过明文传输的,因此可能会被截获。
🔶 不能够验证服务器的身份,服务器身份可能会进行伪装。

🌸 长连接 / 短连接 的区别?

🔶 短连接就是浏览器每进行一次请求,就需要和服务器建立一次 TCP 连接,处理完请求后断开连接,HTTP 1.0 默认使用短连接。
🔶 长连接是指浏览器只需要和服务器建立一次 TCP 连接,就可以进行多次请求,直到一段时间内没有数据传输才断开连接,HTTP 1.1 默认使用长连接。

🎃 计算机网络 🎃

🌸 TCP、UDP

🍁 TCP 的三次握手过程?

  • 最开始,客户端和服务端都处于 CLOSE 状态,其中服务端主动监听连接,处于 LISTEN 状态。
  • 客户端向服务端发送 SYN 连接报文,进入 SYN_SEND 状态。
  • 服务端收到后会回复一个 SYN+ACK 报文,表示已经收到消息,进入 SYN_RECV 状态。
  • 最后当客户端收到 SYN+ACK 后,发送最后的一个 ACK 报文进入 ESTABLISH 状态,服务端收到 ACK 后也进入 ESTABLISH 状态。

🍁 TCP 的四次挥手过程?XXX

  • 最开始,客户端和服务端都处于 ESTABLISH 状态,其中客户端想要断开连接。
  • 客户端向服务端发送 FIN 断开报文,随后进入 FIN_WAIT_1 状态。
  • 服务端收到后会回复一个 ACK 报文,表示已经收到消息,随后进入 CLASE_WAIT 状态。客户端收到的 ACK 后,进入 FIN_WAIT_2 状态。
  • 服务端将需要处理的数据处理完毕,发送 FIN 报文给客户端,进入 LAST_ACK 状态。
  • 客户端收到 ACK 后发送最后的 ACK 给服务端,并进入 TIME_WAIT 状态,等待 2MSL 时间,如果在此期间没有收到来自服务端的 RST 报文,则进入 CLOSE 状态。而服务端在收到 ACK 后就立即进入 CLOSE 状态。

🌸 TCP 如何保证可靠传输?

🔶 超时重传:当接收端收到发送方的报文后,会发送一个响应报文给发送方,如果发送方一段时间内没有收到响应报文,就会触发超时重传机制。
🔶 数据校验:TCP 头部有数据校验和,用来校验报文是否受到损坏。
🔶 分片排序:TCP 会按照最大传输单元 MTU 合理的进行分片发送数据,接受端会对未按序到达的数据进行缓存,之后将数据重新排序后发送给应用层,确保数据的有序性。
🔶 窗口控制:当接收方来不及处理发送的数据时,会使用滑动窗口提醒发送方降低发送速率。当网络拥塞时,通过拥塞窗口减少发送方的发送速率,避免丢包。

🌸 TCP / UDP 的区别?

🔶 建立连接:TCP 是面向连接的协议,传输前需要先建立连接;而 UDP 可以不建立连接直接传输。
🔶 可靠性:TCP 具有可靠性,数据可以无差错,有序,完整的传输;而 UDP 是尽最大可能的交付,不具备可靠性。
🔶 服务类型:TCP 是一对一的点到点服务;而UDP 支持一对一,一对多,多对一的服务。
🔶 窗口控制:TCP 有流量控制,拥塞控制,可以调节方法送的发送速率;而 UDP 没有。
🔶 首部开销:TCP 首部长,开销较大,至少 20 字节;UDP 首部较小,只有固定 8 字节。
🔶 传输方式:TCP 是基于字节流的传输,无边界但是有序;UDP 是基于包的传输,有边界但是无序。
🔶 应用场景:TCP 是面向连接且可靠的,因此主要用于文件传输这种对数据完整性要求高的场景;而 UDP 是无连接不可靠但数据传输比较快的,可以用于音视频通话这种对实时性要求比较高的场景。

🌸 封包 / 拆包 是什么?

🔶 封包和拆包都是基于 TCP 的概念,因为 TCP 是无边界的二进制的流传输,所以需要对 TCP 进行封包和拆包,确保发送和接收的数据的不粘连。
🔷 封包:在发送数据包的时候,为每个 TCP 数据包加上一个包头,将数据报文分为包头和包体两个部分,其中包头中包含数据包的长度信息。
🔷 拆包:接收方在收到报文后,根据包头中数据的长度信息来截取包体中的数据。

🌸 流量控制使用什么数据结构实现的?

🔶 流量控制是通过滑动窗口来实现的,接收方确认报文的窗口字段可以用来控制发送方的发送速率。
🔶 如果窗口的值为 0,则代表发送方停止发送数据,但是发送方会定期的发送探测报文来查询当前窗口的大小。

🌸 UDP 如何实现可靠传输?

🔶 可以添加确认和重传机制,接受方在收到数据后会发送 ACK 给发送方,如果发送方没收到 ACK 则进行数据重传。
🔶 可以添加校验和,检查发送的数据包是否有损坏。
🔶 可以添加序列号机制,便于检测丢失的数据包。
🔶 可以添加滑动窗口,是发送方发送速度和接收方接收速度匹配,避免丢包。

🌸 TCP 报文 的主要字段?

🔶 首先是源端口和目的端口,也就是收发双方的端口号。
🔶 序列号,用来标识通信过程中发送的数据字节流。
🔶 确认号,是接收方期望下次所收到的数据序列号。
🔶 标志位,最常用的是 ACK 确认序序列;SYN 同步序列号;FIN 传输结束序列号;RST 重置连接序列号。
🔶 滑动窗口,用来进行流量控制。

🌸 POST 可以产生两个 TCP 数据包吗?

🔶 POST 发送 TCP 数据包的个数是根据浏览器框架决定的,有些是将 TCP header 和 data 分开发送,那么会产生两个数据包,有些不会分开发送,只会有一个数据包。

🌸 拥塞控制的原理是什么?

🔶 慢启动:TCP 在刚建立连接时,首先有个慢启动的过程,就是一点一点的提高发送数据包的数量。每当发送方收到一个 ACK,拥塞窗口就 +1。
🔶 假设初始化拥塞窗口为 1,表示可以传输一个 MSS 大小的数据。当收到一个 ACK 后,cwnd+1,此时一次可以传输两个 MSS 大小的数据。当再次收到两个 ACK 后,cwnd+2,此时一次可以传输四个 MSS 大小的数据。慢启动有一个门限,当 cwnd 小于门限时使用慢启动算法,大于门限时就会使用拥塞避免算法。
🔷 拥塞避免算法:每当收到一个 ACK,cwnd 会增加 1/cwnd。
🔷 比如慢启动的阈值为 8 时,初始会收到 8 个 ACK,这时 cwnd 会增加 1/8 * 8 的大小,也就是增加 1。接着一次可以传输 9 个 MSS 大小的数据,cwnd 呈现线性的增长。当线性增长出现拥塞后,会进入拥塞发生算法。
🔶 拥塞发生算法:产生拥塞时数据包会进行重传,分为超时重传和快速重传。
🔶 当发生超时重传时,阈值会设为当前拥塞窗口的一半,窗口大小设置为 1,重新进入慢启动阶段。当发生快速重传时,阈值和窗口大小都设为当前窗口值的一半,进入快速恢复算法。
🔷 快速恢复算法:如果还能收到 3 个重复的ACK,说明网络不是特别的糟。
首先将拥塞窗口 +3,重传丢失的数据包。如果再收到重复的 ACK,cwnd+1。如果收到新的数据包,表示重传的包接收成功,把 cwnd 设置为阈值大小,进入拥塞避免算法。

🌸 域名解析为什么用 UDP 而不是 TCP?

🔶 因为 UDP 更快,只需要一个请求应答,但是 TCP 需要三次握手四次挥手很慢。
🔶 浏览器向 DNS 服务器查询域名时,返回的内存不会超过 512 字节,用 UDP 传输完全足够。

🌸 流量控制的原理是什么?

🔶 流量窗口的原理是接收方在发送 ACK 确认报文时,TCP 窗口字段中会包含窗口大小的信息,也就是接收方可以接受的最大数据量,发送方根据这个值来控制自己的发送速度。
🔶 当窗口值为 0 时,TCP 会停止发送数据,但是不断的发送探测报文来检测接收方的情况。

🌸 TCP 粘包是什么?如何解决?

🔶 TCP 粘包是指发送方发送若干数据包时,在接受缓冲区中,前一个数据包的末尾紧贴后一个数据包的头部,作为同一个数据包进行接收。它主要是因为 TCP 基于二进制流传输,没有边界信息。
🔷 可以添加消息边界,比如在每个消息的开始和结尾添加特定的字符串。
🔷 可以发送固定长度的消息,接收方根据固定长度来确定消息边界。
🔷 可以为每个消息添加一个头部的表示消息长度的字段,接收方根据这个长度字段确定消息边界。

🌸 四次挥手为什么不能为三次?

🔶 当服务器收到客户端发送的 FIN 报文时,会先发送一个 ACK 表示自己已经收到信息,但此时服务端可能还有数据没有处理完,所以并不会马上关闭连接。只有当服务端的数据全部处理完毕后,才发送 FIN 报文,这两个报文不能合并发送,所以需要四次挥手。

🌸 三次握手中可以携带数据吗?

🔶 在第三次握手中可以携带数据,但是在第一二次握手中不可以。
🔶 因为如果在第一握手中携带数据的话,服务器很可能会受到攻击,比如发送大量 SYN 报文,并在报文中放入大量的数据,会让服务器浪费很多时间,内存去接受这些报文。
🔶 对于第三次握手的话,此时客户端已经处于建立连接状态了,并且也知道服务器的发送和接收能力正常,因此携带数据也没有关系。

🌸 四次挥手中 ACK 和 FIN 可以合并发送吗?

🔶 可以合并发送。
🔶 在四次挥手中,服务端收到了来自于客户端的 FIN 报文,会先发送 ACK 报文告知已经收到消息,之后在处理完需要处理的数据之后,才会发送 FIN 报文。
🔶 如果在服务端在收到客户端的 FIN 时没有数据需要处理,并且开启了 TCP 延迟确认机制,那么二三次握手是可以合并的,也就是 ACK 包和 FIN 包合并发送。

🌸 三次握手为什么不能为两次?

🔶 第一次握手主要是为了说明客户端有发送的能力,服务端有接收的能力。
🔶 第二次握手主要是为了说明服务端具有发送和接收的能力。
🔶 第三次握手主要是为了说明客户端具有发送和接收的能力。
🔶 因此需要三次握手才能确保客户端和服务端的发送接收能力都是正常的。
🔶 如果只进行两次握手,当客户端发送的 SYN 被网络阻塞,同时客户端宕机时,客户端重启后会重新发送一个新的 SYN。由于服务端没有中间状态给客户端来阻止历史连接,因此服务端在建立历史连接后进行无效的数据数据,造成资源浪费。

🌸 为什么 TIME_WAIT 需要等待 2MSL?

🔶 因为在第四次挥手时,客户端会给服务端发送一个 ACK 报文,如果 ACK 报文成功被服务器接收,那么双方都可以成功关闭。
🔶 但是 ACK 报文在传输的过程中丢失,那么就会导致服务端就无法成功关闭。
🔶 此时服务端可以发送一个 RST 报文通知客户端重新发送,设置 2MSL 是为了确保这个 RST 报文可以成功送达。

🍻 网络模型 / 协议

🌸 七层 / 五层模型 的主要协议有哪些?

🔶 应用层:HTTP 超文本传输协议,FTP 文件传输协议,SMTP 简单邮箱传输协议,DNS 域名解析服务,TFTP 简单文件传输协议。
🔶 传输层:TCP 传输控制协议,UDP 用户数据报协议。
🔶 网络层:IP 协议,RIP 路由信息协议。
🔶 数据链路层:ARP 地址解析协议,RARP 反向地址解析协议,PPP 点对点协议。
🔶 物理层:EIA422 数据传输协议。

🌸 OSI 七层模型及各层的功能?

🧡 应用层:最直观的理解就是人机交互界面,比如用户 A 将 你好 输入到聊天界面。
🔶 表示层:就是将数据进行翻译,加密,压缩等处理,便于计算机去操作。
🔶 会话层:找到数据的接收方,并和接收方用户建立会话关系。
🔶 传输层:用于在同一个软件中的两个不同的端口进行数据传输。此时传输的时数据段,是由 TCP 头部和上层数据组成的。
🔶 网络层:用于从自身的 IP 地址,找到接收方的 IP 地址,并选择最佳的路径进行传输。此时传输的是数据包,是由 IP 头部和数据段组成的。
🔶 数据链路层:在物理层提供服务的基础上,实现对通信实体之间的链路连接,需要用到网卡,交换机等设备。此时传输的是数据帧,是由 MAC 头部和数据包组成的。
🔶 物理层:是网络通信的数据传输介质,主要传输的是二进制比特流。

🌸 IP 数据报的报头字段有哪些?TTL 字段的作用?

🔶 IP 数据报的报头字段有版本号,服务类型,总长度,标识符,标志位,生存时间,协议和头部校验和等。
🔶 其中 TTL 是生存时间,通过 IP 数据报的发送者设置,在 IP 数据报的整个转发路径上,每经过一个路由器,都会修改这个 TTL 的字段将其减 1,然后继续转发出去。如果在到达目的地址之前 TTL 值为 0,就会被抛弃掉。

🌸 数据链路层的协议有哪些?

🔶 ARP 地址解析协议,RARP 反向地址解析协议,PPP 点对点协议。

🌸 TCP / UDP 对应的应用层协议有哪些?

🔶 TCP 对应的应用层协议有:FTP 文件传输协议,SMTP 简单邮件传输协议。
🔶 UDP 对应的应用层协议有:DNS 域名解析服务,TFTP 简单文件传输协议。

🌸 服务器缓存 的作用?

🔶 服务器缓存的作用主要是为了缓解服务器的压力。降低客户端获取资源时的延迟。
🔶 其中服务器缓存位于内存中,所以缓存的读取速度会很快,并且缓存服务器的地理位置可能也会比源服务区更近。

🌸 DNS 负载均衡是什么?

🔶 当一个网站拥有特别多的用户时,如果每次请求的资源都在同一个服务器上,那么这台服务器很可能会崩溃。
🔶 而采用 DNS 负载均衡可以为同一个主机名配置多个 IP 地址,在进行 DNS 查询的时候会返回不同的 IP 地址解析结果,将客户端引导到不同的服务器上去,从而达到负载均衡的目的。

🌸 路由器 / 交换机 的区别?

🔶 路由器工作在网络层,根据 IP 地址来寻址,可以处理 TCP / IP 协议;而交换机在链路层,根据物理地址来寻址。
🔶 路由器可以把一个 IP 分配给多个主机使用,对外只表现出一个 IP;而交换机可以把很多主机连接起来,这些主机对外有各自的 IP。

🌸 Cookie / Session 的区别?使用场景?

🔶 Cookie 是服务器发给到浏览器,并在本地所保存的一小块数据。如果浏览器之后访问服务器则会带上 Cookie,用于告知请求是否来自于同一浏览器。Session 是浏览器访问服务器时,在服务器内部所创建的一个对象数据,用于识别请求的身份。
🔷 两者都是 key-value 的结构,不过 Cookie 存放的是字符串,Session 可以存放任意类型数据,因此考虑数据复杂性时优先使用 Session。
🔷 Cookie 存放的数据大小为 4k。而 Session 存放的数据大小没有限制,但是不建议存放所有用户的信息,因为内存开销会很大。
🔷 Cookie 的生命周期是累积的,比如从创建开始计时,20 分钟后就会结束;而 Session 的生命周期是间隔的,比如 20 分钟内如果没有访问 Session,它的生命周期就会结束。

🍺 操作系统

🌸 进程、线程

🍁 进程间的通信方式有哪些?

* 管道:它允许一个进程和一个拥有公共祖先的进程间进行通信。

* 匿名管道:它允许一个进程和另外一个毫不相关的进程之间进行通信。但是管道的通信效率低,不适合频繁的数据交换。

* 消息队列:它是一种存放在内核中的消息链表,有写权限的进程可以向里面添加任务,有读权限的进程可以从中取出任务。消息队列它的消息体有大小限制,而且在通信的过程中会涉及从用户态到内核态之间的数据拷贝。

* 共享内存:把多个进程中的虚拟空间地址映射到相同的物理地址内存中去。这样当一个进程修改数据后,其他进程都可以看到对应的修改,避免了内核态到用户态的数据拷贝。但是会存在内存访问冲突的问题。

* 信息量:它本质上是一个整形的计数器,代表用户可以访问资源的数量。当进程访问资源时,进行 P 操作,计数器 +1,当进程离开资源时,进行 V 操作,计数器 -1。

* 信号:它是一种进程间的异步通信机制,可以在任意时刻发送信号给其他进程。信号可以分为硬件来源(比如键盘 ctrl C)和软件来源(kill 指令)。

* socket:它可以实现两个主机间不同进程的通信,也可以实现同一个主机上冈进程间的通信。

🌸 线程 / 进程 / 协程的区别?

🔶 定义:进程是资源分配的基本单位;线程是 CPU 调度的基本单位;协程是轻量级的线程,是线程内部调度的基本单位。
🔶 资源拥有:进程拥有 CPU 资源,内存资源,文件资源等;而线程和协程只拥有自己的寄存器,栈等资源。
🔶 切换效率:进程和线程的切换内容都保存在用户态,切换时会涉及到用户态到内核态的切换,但是因为线程需要的切换内容比较少,所以效率会更高些。协程的切换内容保存在用户态中,但是其手动切换是不涉及内核态的,因此切换效率更高。
🔶 切换时机:进程和线程的切换者是操作系统,切换时机由系统决定。而协程的切换者是用户,切换时机由用户来决定。

🌸 线程的通信方式有哪些?

🔶 线程之间资源共享,所以线程没有像进程通信中的数据交换机制,线程间通信的目的主要是用于线程同步。
🔷 锁机制:锁机制包含互斥锁,读写锁,条件变量等。互斥锁以排他的方式防止同共享数据同一时间被多个线程修改。读写锁允许多个线程同时读数据,但是同一时间只能有一个线程写数据。条件变量可以阻塞线程,直到某个特定的条件为真则让线程继续执行,通常和互斥锁配合使用。
🔷 wait / notify 等待:当一个线程调用同步锁的 wait 方式时,线程会放弃同步锁进入等待队列,直到其他线程进入同步锁调用 notify 唤醒该线程,此时俩线程间实现同步。
🔷 信号量:它是一个整形的计数器,主要用于实现进程间的互斥和同步。信号量的值代表资源的数量,控制信号量的方式有 P 操作和 V 操作,分别发生在进入共享资源,离开共享资源时。
🔷 信号:它是进程间的一种异步通信机制,可以在任何时候发送信号给某一进程。信号的来源有硬件来源(ctrl+c 终止进程)和软件来源(kill 命令)等。

🌸 进程 / 线程的切换状态有哪些?

🔶 进程间的切换状态有就绪状态,运行状态,阻塞状态。
🔶 就绪状态:进程已经准备好,但是还没有被分配到 CPU 上执行。
🔶 运行状态:进程被分配到 CPU 上执行。
🔶 阻塞状态:进程因为某些原因无法继续执行,比如等待 IO 操作的完成。
🔷 线程的切换状态有就绪状态,运行状态,阻塞状态,等待状态,终止状态。
🔷 就绪状态:线程已经准备好,但是还没有被分配到 CPU 上执行。
🔷 运行状态:线程被分配到 CPU 上执行。
🔷 阻塞状态:线程因为某些原因无法继续执行,比如等待 IO 操作的完成。
🔷 等待状态:线程在等待其他线程或系统资源完成操作,比如等待信号量,条件变量等。
🔷 终止状态:线程已经执行完毕,或者被终止执行。

🌸 阻塞 / 非阻塞的区别?

🔶 阻塞:是指当一个进程执行某个操作时,如果该操作没有完成,则会被挂起,等待结束阻塞时继续执行。
🔶 非阻塞:是指当一个进行执行某个操作时,如果该操作没有完成,也不会被挂起,而是立即返回一个状态码,并且继续执行。

🌸 并行 / 并发 是什么?

🔶 并发是指一个处理器同时处理多个任务,但实际上是由系统的不断切换实现的,一个时间点上只处理一个任务。
🔶 并行是指在多核处理器或者分布式系统上,多个处理器同时处理多个任务。

🌸 守护进程 / 僵尸进程 / 孤儿进程是什么?

🔶 守护进程是指在后台运行的,没有与控制终端相连的进程。它独立于控制终端,周期性的执行某些任务。比如说 Linux 中的web 服务器。
🔶 僵尸进程是指子进程已经退出,但是父进程还没有退出,那么子进程必须等待父进程捕获到子进程的信息后才算正真的退出,在此期间子进程为僵尸进程。
🔶 孤儿进程是指父进程已经退出,但是子进程还没有退出,那么这些子进程就是孤儿进程。这些进程将会被 init 进程所收养,由 init 进程对其进行信息收集工作。

🌸 进程调度算法有哪些?

🔶 先来先服务算法:遵循先来后到的原则,每次从就绪队列中选择最先进入队列的进程,然后一直运行,直到进程退出或者被阻塞,才会轮到下一个进程接着运行。
🔶 最短作业优先算法:优先选择运行时间较短的进程来运行,这样有利于提高系统的吞吐量。
🔶 高响应比优先算法:在每次进行进程调度时,先计算响应比优先级,然后让响应比优先级最高的进程优先运行。其中优先级计算公式为 (等待时间+服务时间) / 服务时间。
🔶 时间轮调度算法:这种算法最古老,最简单,最公平,每个进程被分配一个相等的时间片,所有进程轮流执行。
🔶 最高优先级调度算法:从就绪队列中选择最高优先级的进程先运行,其中优先级分有固定的静态优先级,和会随进程运行时间调整的动态优先级。
🔶 多级反馈队列调度算法:该算法设置了多个队列,每个队列有不同的优先级,其中优先级越高,分配的时间片越短。同一个队列中的进程按先来先服务算法分配时间片,如果当前队列中的进程在时间片内没有完成,则会把该进程放入下一队列的尾部。只有较高优先级的队列为空时,才会调度较低优先级的队列。这种算法可以为长任务提供更多的CPU时间,同时让优先级高的任务可以得到更快的响应。

🌸 进程上下文切换的资源有哪些?

🔶 进程上下文切换的资源有寄存器状态,堆栈,内存映射,文件描述符,处理器装填,调度信息等。
🔶 线程上下文切换的资源有寄存器状态,堆栈,程序计数器等。

🍻 锁

🥂 如何实现分布式锁?+1

🔸 实现基于 MySQL 的分布式锁
🔸 使用表的唯一约束性,当往数据表中成功插入一条数据时,代表获取到锁,将该数据从数据表中删除,代表释放锁。
🔸 因此需要创建一张锁表,其中属性包含锁名和客户端的唯一编码,通过唯一编码判断是否为锁的持有者,再执行释放或重入操作。

🔸 实现基于 Redis 的分布式锁
🔸 使用 Redis 的 set 命令及可选参数实现,通过 NX 参数设置当 key 不存在才执行set操作,EX 参数设置锁的过期时间,超时自动释放锁。
🔸 Redis 表中的 key 代表锁名,value 代表客户端的唯一编码,通过唯一编码判断是否为锁的持有者,再执行释放或重入操作。

🔸 实现基于 ZookKeeper 的分布式锁
🔸 Zookeeper 会在本地创建一个mylock目录。
🔸 线程 A 想获取锁,就在 mylock 目录下创建临时顺序节点。
🔸 线程 A 会获取 mylock 目录下的所有子节点,并判断自己的序号是不是最小,如果最小则获取锁。
🔸 此时线程 B 想获取锁,也会在 mylock 目录下创建临时顺序节点。
🔸 线程 B 会获取 mylock 目录下的所有子节点,但此时线程 A 序号最小,因此获取锁失败。
🔸 线程 B 监听次小序号的节点,如果监听的节点释放锁,那么线程B重新获取锁。

🌸 死锁是什么?产生条件?如何避免?

🔶 死锁:是指在并发编程中,当两个线程为了访问两个不同的共享资源时,会使用两个互斥锁,如果这两个互斥锁之间存在着资源的循环依赖,则会同时等待对方释放锁,导致死锁状态。
🔷 死锁的产生需要同时满足四个条件:
🔷 互斥条件:线程 A 已持有的资源,不能被线程 B 持有。
🔷 持有并等待条件:线程 A 已持有资源 1,想要资源 2,则需要等待其他线程释放资源 2,在此期间资源 1 不会被释放。
🔷 不可剥夺条件:线程 A 已持有资源 1,在使用完毕之前,不可以被其他线程剥夺。
🔷 环路等待条件:线程间获取资源的顺序构成了环路,比如线程 A 持有资源 1,想要资源 2,线程 B 持有资源 2,想要资源 1。
🔶 避免方法:采用资源有序分配法来破坏环路等待条件。当多个线程都需要部分相同资源时,应该保证获取资源的顺序是一致的,比如线程 A 和线程 B 都是先获取资源 1,再获取资源 2。

🌸 锁有哪些?

🔶 互斥锁:它是为了在任意时间内,仅有一个线程可以访问共享资源。比如当线程 A 加锁成功时,那么线程 B 再加锁就会失败并进入阻塞睡眠状态,并会释放 CPU 让给其他线程。等待锁被释放后,内核会唤醒线程 B 去加锁继续执行业务。
🔶 自旋锁:它也是为了在任意时间内,仅有一个线程可以访问共享资源。但是当线程获取自旋锁失败时,不会立即释放CPU,而是一直循环尝试获取锁,直到获取锁成功。因此自旋锁一般用于加锁时间很短的场景,减少了线程上下文切换的开销。
🔶 读写锁:它由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果需要修改共享资源则需要用写锁加锁,可以同时有多个线程进行读,但是最多只能有一个线程进行写。它主要用于可以明确区分读操作和写操作的场景,在读多写少的场景下可以发挥出优势。
🔶 条件变量:条件变量是一种同步机制,可能使线程进入阻塞状态,并在满足某个条件时解除阻塞继续执行业务。比如现在有一个生产线程和庆祝线程,希望在生产 100 个产品后进行庆祝。如果使用互斥锁的话,庆祝线程需要不断地加锁解锁轮询检查是否满足条件,非常的浪费资源。而使用条件变量时,当庆祝线程发现不满足条件时,直接进入阻塞状态。接着在生产线程中去判断是否满足条件,当满足条件时由生成线程发送信号去唤醒庆祝线程,避免了无效的加锁解锁操作。

🌸 公平锁 / 非公平锁是什么?

🔶 公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是优先获取锁。
🔶 非公平锁:每个线程获取锁的顺序是随机的,不会遵循先来后到的原则,所有的线程会竞争锁。
🔶 在实际的应用中,公平锁可以避免发生饥饿现象,但由于需要维护线程队列,因此效率相对较低。而非公平锁并不需要维护线程队列,效率更高,但可能会导致某些线程长时间无法获取锁。

🌸 哲学家进餐问题?

🔶 当每个哲学家都拿起左边的筷子时,那么都会等待隔壁的哲学家放下右边的筷子,进入死锁状态。
🔶 可以设置为必须同时拿起左右两双筷子,同时左右两边没人进餐时,才可以进餐。

🍻 内存管理

🌸 内存交换 / 覆盖的区别?

🔶 内存交换是在不同的进程之间进行,而内存覆盖则是在同一个进程中进行。

🌸 内核态 / 用户态的区别?

🔶 在用户态中,只能访问部分有权限的内存,CPU 占用的能力被剥夺,CPU 资源可以被其他线程获取。
🔶 在内核态中,可以访问内存中的所有数据,CPU 也可以将自己从一个程序切换到另一个程序。

🌸 分段 / 分页的区别?

🔶 分段是将物理内存分成固定大小的块,称为段,再将逻辑地址分成相同大小的段,把每个段映射到一个段表中。
🔶 分页是将物理内存分成固定大小的块,成为页框,再将逻辑地址分成大小相同的页,把每个页映射到一个页框中。
🔶 分段主要是将进程的代码,数据,栈等数据分配到不同的虚拟地址上,避免了不同段之间的内存访问冲突。分页可以将整个进程映射到一个连续的物理地址空间中,减少了内存碎片。

🌸 堆 / 栈的区别?

🔶 申请方式不同:栈是由系统自动分配的,堆是程序员手动分配的。
🔶 申请大小不同:栈是一片连续的内存区域,大小是操作系统预定义好的。堆是一片不连续的内存区域,大小可以灵活调整。
🔶 申请效率不同:栈由系统分配,速度快,且不会有碎片,堆由程序员分配,速度慢,而且会产生内存碎片。
🔶 生长方向不同:栈的内存分配是由高到低的,堆的内存分配是由低到高的。

🌸 为什么不全用栈?

🔶 栈存在大小限制,并且在编译时就确定。如果需要存储的数据超过了栈的大小,就会造成栈溢出错误。
🔶 栈上变量的生命周期属于函数,当函数执行结束后,栈上变量会跟着释放。而堆创建的变量可以自己决定何时销毁。

🌸 虚拟地址如何找到物理地址?

🔶 虚拟地址转化为物理地址是通过页表来实现的。
🔶 在分页和分段的机制下,CPU 会将虚拟地址转化为线性地址,再将线性地址转化为物理地址。

🌸 操作系统的作用?

🔶 可以进行处理机的管理,进行进程控制,进程同步,进程通信,死锁处理等。
🔶 可以进行存储器的管理,比如内存分区,内存映射,资源共享,内存扩充等。
🔶 可以进行文件的管理,比如目录管理,文件空间管理,文件读写管理等。
🔶 可以进行设备管理,比如完成用户的 I/O 请求,提高设备利用率等。

🌸 内存碎片 / 外部碎片是什么?

🔶 内部碎片是指分配给某些进程的内存区域,有些部分没用上,常见于固定分配的情况。
🔶 外部碎片是因为内存中的某些区域比较小,难以利用上,常见于动态内存分配中分段式分配的情况。

🌸 局部性原理是什么?

🔶 局部性原理分为时间局部性和空间局部性。
🔶 其中时间局部性是指如果执行了程序中的某条指令,那么不久后这条指令可能会被再次执行。如果每个数据被访问过,那么这个数据之后可能会被再次访问。
🔶 空间局部性是指一旦程序访问了某个内存单元,那么在不久后附近的内存单元很可能也会被访问。

🌸 异常 / 中断 的区别?

🔶 发送源:异常是由 CPU 产生的,而中断是由硬件设备产生的,不过他们都是发送到内核,由内核调用相关程序解决。
🔶 时钟同步:因为异常是由 CPU 产生的,所以具有时钟同步的特点,而中断不是时钟同步的,所以随时可以发送中断。

🌸 大端存储 / 小端存储是什么?

🔶 大端存储是指字节数的高字节存储在低地址中,小端存储是指字节数的低字节存储在低地址中。

🍻 Socket 编程

🌸 select 是什么?

🔶 select 将已经连接的 socket 放到一个文件描述符集合中,并将这个文件描述符集合拷贝到内核态中,让内核来检查是否有事件发生,通过遍历的方式进行检查。如果有事件发生,内核会将相关 socket 标记为可读或可写,接着将文件描述符集合拷贝回用户态中,然后用户态再次通过遍历的方式找到可读或可写的 socket,调用线程进行处理。
🔶 select 的特点是会经历两次文件描述符的遍历操作,两次文件描述符的拷贝操作,性能损耗比较大。而且文件描述符的个数也受到限制,默认上限为 1024。

🌸 poll 是什么?

🔶 poll 本质上和 select 没有什么区别,都是使用线性结构来存储已连接 socket 的文件描述符,但是 poll 使用的是链表来进行存储,因此对文件描述符的大小没有个数限制。

🌸 epoll 是什么?

🔶 epoll 会在内核中建立一个 epoll 对象,这个 epoll 对象内部通过红黑树来跟踪所有待检测的文件描述符,每当建立一个新的 socket 连接时,只用把新的文件描述符拷贝到内核态中。epoll 使用了事件驱动的机制,内核里维护了一个链表来记录就绪事件,如果有一个事件就绪,会通过回调函数让内核把这个事件加入就绪列表,并返回有事件发生的文件描述符的个数。epoll 虽然有连接数限制,但这限制数很大,可以进行上万的连接。

🌸 select,poll,epoll 的区别?

🔶 连接数不同:select 的最大连接数有限制,大小默认是 1024;poll 和 select 本质上没有区别,因为使用链表存放文件描述符信息,使得最大连接数没有限制;epoll 虽然有连接数限制,但这个数很大,可以进行上万的连接。
🔶 IO 效率不同:select 和 poll 检查文件描述符时都采用遍历的机制,效率很慢;epoll 是事件驱动的,当有一个事件就绪时,会通过回调函数让内核把这个事件加入就绪列表,因此 epoll 的效率与 socket 的数量无关,而仅与活跃的 socket 数量有关。
🔶 数据拷贝不同:select 和 poll 中会涉及到两次的已连接 socket 的文件描述符集合的拷贝;而 epoll 仅需要在内核中添加一个最新连接 socket 的文件描述符。

🌸 边缘触发,水平触发 的区别?

🔶 边缘触发是指当被监控的 socket 上有可读时间发生时,epoll 会立即通知应用进程去读取数据,但是之后不论应用进程有没有读取完数据,都不会再通知应用进程。
🔶 水平触发是指当被检控的 socket 上有可读事件发生时,epoll 会不断地通知应用程序去读取数据,直到内核缓冲区中的数据被读取完为止。
🔶 总的来说,边缘触发只在 socket 发生状态时才通知应用程序,而水平模式则会持续的通知应用程序,直到事件处理完毕。

🌸 Reactor 是什么?

🔶 Reactor 是非阻塞同步网络模式,其中分为 3 各部分,分别是反应器,接收器和处理器。
🔶 其中反应器通过 select 来监听文件描述符,当监听到事件后,通过 dispatch 进行分发。
🔶 如果监听到的是连接事件,那么将文件描述符交给接收器,接收器通过 accept 获取连接,并创建一个处理器处理后续事件。如果监听到的是读写事件,那么则交给处理器进行操作,其中有读取、业务处理、发送等流程。

🍺 MySQL

🍻 事务

🌸 事务的四大特性是什么?

🔶 原子性:一个事务中的操作,要么全部完成,要么全部不满成,不存在只完成部分的情况。如果中间发生错误会进行回滚操作。
🔶 一致性:事务开始前和结束后,数据库的完整性没有遭到破坏。
🔶 隔离性:当多个事务访问数据库时,多个事务之间相互隔离,互不干扰。
🔶 持久性:事务结束后,对数据库的修改应该是永久的,即使遇到故障也不会丢失。

🌸 事务的隔离级别有哪些?

🔶 读未提交:一个事务还没有提交时,它做出的修改可以被其他事务读到。可能会出现脏读、不可重复读、幻读等情况。
🔶 读提交:一个事务在提交后,它做出的修改才能被其他事务读到。可能会出现不可重复读,幻读等情况。
🔶 可重复读:当前执行的事务所看到的数据,都是事务启动时所看到的数据,因此事务进行多次相同的查询操作时,查询结果应该是相同的。可能会出现幻读情况。
🔶 可串行化读:当多个事务对同一个记录进行读写时,会加上读写锁,在写操作时,只有一个事务处理完毕后才会轮到下一个事务。

🌸 脏读,不可重复读,幻读是什么?

🔶 脏读:事务 A 读取到了事务 B 未提交并修改过的数据,那么当事务 B 发生错误并回滚时,事务 A 读取的数据和实际的数据不一致,发生脏读现象。
🔶 不可重复读:事务 A 读取了一个数据,但随后事务 B 对数据进行了修改,事务 A 再次读取发现数据不一样,发生了不可重复读现象。
🔶 幻读:事务 A 多次查询符合某个条件的记录数量,但同时事务 B 进行一些插入删除操作,使得事务 A 多次查询的结果不一致,发生幻读现象。

🌸 数据库如何保证一致性?

🔶 从数据库层面,数据的一致性主要是通过数据的原子性,隔离性和持久性来保证的,而这些特性的基础都是原子性。
🔶 从应用层面,通过判断数据库中的数据是否有效,还决定提高还是进行回滚来保证一致性。

🍻 索引

🌸 索引为什么使用B+树,而不是B树,红黑树,哈希表?

🔶 Mysql 中的数据是放在磁盘的,读取数据会有访问磁盘的操作,而访问磁盘的 IO 操作效率很低。
🔶 当用二分查找树时可以快速地查找到数据,但是极端情况下,比如依次插入递增的元素,那么查询数据的时间复杂度会从 O(logN) 退化为 O(N)。
🔶 自平衡二叉树是高度平衡的,确保复杂度为 O(logN),但是当数据量过大时,树的高度依然很高,磁盘 IO 次数也更多。
🔶 B 树和 B+ 树中,一个节点有多个子节点,因此树的高度会变低,磁盘 IO 次数也会降低,查询效率更高。
🔷 B+ 树中的叶子节点存放数据,非叶子节点仅存放索引,而 B树在所有节点都存放数据。在相同数据量下,B+ 数的非叶子节点可以存放更多的索引,因此高度比 B 树更低,查询效率也更高。
🔷 B+ 树中有着大量的冗余索引,这些冗余索引可以让 B+ 树在插入删除时的效率更高,树的结构也不会发生太复杂的变化;
🔷 B+ 树的叶子节点之间用链表连接了起来,有利于范围查询,而 B 树只能通过前序遍历来范围查询。
🔶 红黑树属于一种平衡二叉树,它没有实现绝对平衡而是选择实现局部平衡,使得查找,删除,插入的复杂度都为 O(log2N)。但它本质上还是二叉树,当数据量过大时高度还是会很高,磁盘 IO 次数会增加导致效率变低。
🔶 当进行连续数据查询时,红黑树也需要通过前序遍历来范围查询,效率不如 B+ 树。
🔷 采用哈希表的查询复杂度为 O(1),速度确实更快,但数据库中可能查询多条连续数据,由于 B+ 树的有序性,进行范围查询时会比 hash 表更快。
🔷 hash 表需要把数据全部加载到内存中,非常消耗内存,而采用 B+ 树可以按节点分段加载,内存消耗更少。

🌸 索引有哪些?

🔶 普通索引:可以加速查询,列值可以为空。
🔶 唯一索引:可以加速查询,并且列值唯一可以为空。
🔶 主键索引:可以加速查询,并且列值唯一不可以为空,一个表中只能有一个主键索引。
🔶 索引合并:使用多个单列索引,进行组合搜索。
🔶 组合索引:是由多列组成的一个索引,专门用于组合搜索,效率高于索引合并。
🔶 覆盖索引:使用多字段的组合索引,使得从非主键中就能查询到的记录,而不需要查询主键,避免了回表,可以减少树的搜索次数。
🔶 聚簇索引:它的表数据和索引是放在一起存储的,存放在叶子节点中,找到索引就相当于找到了数据。
🔶 非聚簇索引:它的表数据和索引分为两部分存储,它的叶子节点只存放对应的主键值,根据主键值二次查找数据。

🌸 索引的好处?

🔶 通过创建索引,可以确保数据表中每一行数据的唯一性。
🔶 索引可以大大的提升对数据的检索速度。
🔶 索引可以增加表与表之间的关联,增加数据的完整性。
🔶 索引可以显著减少查询中分组和排序的时间。

🌸 聚集索引,非聚集索引的区别?

🔶 聚集索引记录的数据在物理上是连续的;非聚集索引记录的数据在逻辑上是连续的,但是在物理上是不连续的。
🔶 聚集索引的叶子节点就是数据节点,其他节点是索引节点;非聚集索引的所有节点都是索引节点。
🔶 聚集索引的作用是通过主键可以直接查询到需要查找的数据;非聚集索引可以查找到记录对应的主键,根据主键再通过聚集索引查找到数据。
🔶 聚类索引一张表只能有一个;非聚类索引一张表可以有多个。

🌸 索引的注意事项?

🔶 不要在列上使用函数,运算符,否定操作符,这将会导致索引失效而进行全局扫描。
🔶 进行多个单列的索引合并不能提高数据的查询效率,应该使用多列组合的组合索引。
🔶 组合索引应该遵循最左前缀原则,即在查询中使用了组合索引的第一个字段,索引才会被使用。
🔶 如果索引中存在着所需要查询字段的值,那么就可以按照索引的查询结果直接返回数据,而不需要二次查找,可以提升查询的效率。
🔶 对于某一列如果有 NULL 值存在,则不会去使用索引。对于组合索引,只要某列出现 NULL 值,该列对于组合索引就是无效的。
🔶 当查询条件左右两端的数据类型不匹配时,会进行隐式类型转换,这可能会导致索引失效而全表扫描。
🔶 当使用 like 进行模糊查询时,查询只有右边有 % 的数据时支持索引,但是查询左右两边都有 % 的数据时就会导致索引失效进行全表扫描。

🌸 外键索引是什么?

🔶 外键索引的作用是为了维护表与表之间的关系,确保数据的一致性和完整性。
🔶比如说现在有学生表和课程表,学生可以选择多门课,那么可以在学生表中设置课程 id 为外键,确保学生所选的课都在课程表中从而维护数据的完整性和一致性。如果没有外键,那么学生可以选择不存在的课,破坏数据的完整性和一致性。

🌸 索引主要使用的两种数据结构?

🔶 对于哈希索引来说,它的底层数据结构是哈希表。因此在进行单条记录查询的时候,可以选择哈希索引,查询速率最快。
🔶 对于B树索引,它的底层数据结构是B+树,按照一定的算法将索引值存入到一个树型结构中,通过遍历来找到叶子节点中的值。

🌸 可以主动创建哈希索引吗?

🔶 可以,通过在数据库中使用 CREATE INDEX 就可以来创建哈希索引。

🌸 为什么使用自增ID作为主键?

🔶 使用自增 ID 作为主键时,每次插入新的记录,记录可以顺序添加到当前索引的后续位置,当一页写满时,会自动开辟新的一页。
🔶 而使用非自增 ID 时,每次插入的主键值相对随机,因此每次记录都会被插到索引页的中间某个位置,会涉及频繁的移动,分页操作,得不到紧凑的索引结构。后续需要通过 optimize table 来重建优化表,使得索引结构紧凑。

🌸 为什么不为表的每一列都建立一个索引?

🔶 因为对表中的数据进行修改时,索引也需要动态的维护,如果索引太多的话,维护的速度就会变慢。
🔶 而且索引是会占用存储空间的,如果索引太多,占用存储空间也更多。

🍻 锁

🌸 行级锁有哪些?

🔶 记录锁:仅仅锁定一条记录,可分为排他锁和共享锁。
🔶 间隙锁:锁定一个范围,但是不包括记录本身,主要是为了解决可重复读下的幻读现象。
🔶 临键锁:它是记录锁+间隙锁的作何,锁定一个范围,同时也锁定记录本身。

🌸 悲观锁,乐观锁是什么?

🔶 悲观锁:它认为多线程访问共享资源时,同时修改共享资源的概率比较高,为了避免发生冲突,会在访问共享资源前为资源上锁。像互斥锁,自旋锁,读写锁等都属于悲观锁。
🔶 乐观锁:它认为多线程访问共享资源时,同时修改共享资源的概率比较低,因此默认不加锁,在线程修改共享资源后,会检查有无发生冲突,如果没有发生冲突,则操作完成。如果发生了冲突,则放弃本次修改操作。乐观锁适用于冲突概率小,加锁成本高的场景,比如在线文档编辑。

🌸 排他锁,共享锁的区别?

🔶 排他锁也就是写锁,他允许加锁期间只有一个事务对数据进行读取和更新操作,而其他事务则需要进行等待,直到他释放排他锁。
🔶 共享锁也就是读锁,它允许加锁期间多个事务对数据进行读取操作,期间不能被被加上写锁,直到所有共享锁被释放。

🍻 备份 / 日志

🌸 MySQL的备份和容灾?

🔶 MySQL 的备份是将指创建一个指定时间的数据库副本到另一个位置,当发生数据丢失或者数据库崩溃时,可以利用副本文件进行恢复。
🔶 MySQL 的容灾是指在数据库主服务器故障崩溃时,为了确保数据库可以继续运行,通过备用服务器接管来提供服务,减少系统中断时间。
🔷 MySQL 备份可以分为完全备份,差异备份和增量备份。完全备份是指每次对数据库进行完整的备份,它的特点是占用磁盘大,恢复慢。差异备份是指备份上一次完全备份后发生变化的数据,与完全备份相比占用空间小,但是随着时间的推移,差异备份也会越来越大。增量备份是指备份上一次完全备份或者是差异备份后发生变化的数据,特点是占用空间较小。
🔷 数据库的备份还可以分为冷备份和热备份,其中冷备份是指只有当数据库停止运行,没有发生数据修改时才可以进行备份,而热备份可以在数据库运行的过程中进行备份。
🔶 MySQL 的容灾可以分为数据库复制,数据库镜像。数据库复制:是指将数据从一台服务器复制到另一台或多台服务器上,实现数据共享和负载均衡,是一种读写的备份方式,所有的服务器都可以读取和写入数据。数据库镜像:是指在一台服务器上创建一个与原始服务器完全相同的副本,是一种只读的备份方式,镜像服务器只能被用于读取数据,而不能用于写入数据。

🌸 binlog的作用?

🔶 binlog 是 MySQL 的一种二进制日志文件,用于记录数据库中的修改操作,以便在需要的时候进行数据恢复,数据复制等操作。
🔶 其中数据恢复是将数据回滚到之前的某个时间点,从而恢复数据。数据复制用于主服务器和从服务器之间,从而实现高可用性和负载均衡。

🌸 MySQL 如何保证持久性?

🔶 redo log:是重做日志,记录某个数据页中发生了什么修改,一旦事务提交,redo log 将会被持久化到磁盘。MySQL 会把数据库中的数据加载到内存,在内存中修改数据,再写会到磁盘上,如果此时发生故障内存数据丢失,那么仍可以根据 redo log 中的信息将数据恢复到最新的状态。

🌸 MySQL 日志文件有哪些?

🔶 undo log:是回滚日志,负责记录更新前的对应数据。当执行数据增删的操作时,再事务提交前,mysql 会隐式的开启事务来执行这个增删的操作。如果在此期间出现了崩溃,可以通过 undo log 来回滚到之前的数据。
🔶 redo log:是重做日志,记录某个数据页中发生了什么修改,一旦事务提交,redo log 将会被持久化到磁盘。可以根据 redo log 中的信息将数据恢复到最新的状态。
🔶 bin log:是二进制日志,在执行任意更新操作后,会生成一个 binlog,当事务提交之后会把所有的 binlog 写入一个 binlog 文件,其中只会记录修改了数据库内容的日志,不会记录查询操作。binlog 主要是用于数据库的备份场景。
🔶 relay log:是中继日志,主要用于主从服务中,存储来自于主服务器的二进制日志,从服务器根据这个日志可以确保与主服务器保持一致。

🌸 MySQL 主从同步方式有哪些?

🔶 基于二进制日志的复制,主数据库将自己修改的操作放在二进制日志中,从数据库根据日志中的修改操作,实现数据同步。
🔶 基于 GTID 的复制,GTID 是一个全局唯一的事务标识符,主数据库会将每个事务 GTID 记录在二进制日志中,从数据库根据二进制日志来解析其中的 GTID来获取对应的事务,实现数据同步。
🔶 基于半同步复制,主数据库在修改数据后,会等待至少一个从数据库确认并修改了这个数据,才会进行下一个修改操作,提高数据库的安全性。
🔶 基于并行复制,主数据库在进行修改操作后,会将多个操作并行的传输给从数据库,这样可以提高数据的同步速度。

🍻 其他

🌸 InnoDB,MylSAM的区别?

🔸 InnoDB 支持事务,具有 ACID 属性;而 MyISAM 不支持事务,适用于读多写少的场景。
🔸 InnoDB 支持行级锁,仅锁定实际需要的数据;而 MyISAM 只支持表级锁,需要对整张表锁定。
🔸 InnoDB 支持外键,建立表与表之间的联系,保障了数据的完整性;而 MyISAM 不支持外键。
🔸 InnoDB 在崩溃后可以自动恢复;MyISAM 崩溃后需要手动恢复,比较麻烦。
🔸 InnoDB 支持热备份,即在数据库运行的时候进行备份;MyISAM 只支持冷备份。

🌸 MVCC机制是什么?

🔶 MVCC 是一种多版本并发控制机制,它可以在数据库的读写操作中,将数据按照时间的顺序进行保存,并且在读取时,只读取已提交的版本,避免在并发访问中出现问题。
🔶 MVCC 它在每行记录后面会保存两个隐藏列,其中一列是行的创建时间,另一列是行的过期时间。不过保存的不是具体的时间,而是一个系统的版本号,每开启一个事务版本号都会递增。

🌸 Drop,Delete,Truncate的区别?

🔶 Delete:用于删除表中的全部或部分数据。执行后,用户需要 commit 或 rollback 来完成删除或撤销操作。
🔶 Drop:用于从数据库中删除表中的全部数据,且不能回滚。
🔶 Truncate:用于删除表中的所有数据,但是不会删除索引,本质上相当于删除表后有创建了个新表,这个操作也不能回滚。

🌸 非关系型数据库是什么?

🔶 非关系型数据库称为 NOSQL,采用键值对的形式来存储数据。
🔶 它的读写性能很高,易于扩展,可以分为内存型数据库 Redis 和文档型数据库 mongoDB。
🔶 适用非关系型数据库的场景为:日志系统,海量信息存储,多格式数据存储和对查询速度要求高的场景。

🌸 CHAR,VARCHAR的区别?

🔶 char 是长度不可变的,会用空格填充到指定长度的大小;varchar 的长度是可变的。
🔶 char 的存取速度比 varchar 更块。
🔶 char 的存储是对英文字符占 1 字节,而对中文字符占 2 字节;varchar 对中文还是英文字符都占 2 字节。

🌸 如何从海量数据中读取数据?

🔶 可以使用游标,逐行或逐批次的读取数据进行操作,而不用将数据全部加载到内存中。
🔶 可以采用分块查询,将海量数据划分为不同的一块,每次只读取一块数据到内存中,用完释放内存。
🔶 可以使用索引优化查询,减少读取的数据量,加快数据的查询效率。
🔶 可以采用分布式计算框架,将任务分发到多台计算机上并行运算,充分利用多台计算机的资源。

🌸 分库,分表是什么?

🔶 分库主要是为了解决并发量大的问题,通过增加数据库实例的方式来提供更多的数据库连接,提高系统的并发量。
🔶 分表主要是为了解决数据量大的问题,数据库的查询消息比较低,通过将数据拆分到多张表中,减少单表的数据量,来提升查询速度。

🌸 执行一条SQL的具体过程?

🔶 连接器来建立连接,管理连接,验证用户身份。
🔶 查询缓存,如果缓存命中则直接返回查询结果。
🔶 解析 SQL,解析器会对 SQL 进行一系列的语法分析,词法分析等。
🔶 优化器,会确定 SQL 语句的最优执行方案,选择相应索引。
🔶 执行器,使用引擎执行,并从存储引擎中获取执行的结果。

🌸 数据库高并发的解决方案?

🔶 在数据库和服务器之间加入缓存,将高频访问的数据放在缓存中,减少数据库的访问负担。
🔶 增加数据库的索引,提高数据的查询速度。
🔶 使用主从读写分离,主服务器负责写入,从服务器负责读取。
🔶 对数据库进行分库,通过增加数据库实例的方式提供更多的数据库连接,增加并发性。
🔶 采用分布式架构,分散数据库的计算压力。

🍺 Redis

🌸 Redis如何实现分布式锁?

🔶 使用 setnx + expire 实现。首先利用 setnxkey 值设为 value,当 key 不存在时加锁成功,而当 key 存在的时候什么也不做。因为分布式锁需要超时机制,因此使用 expire 来设置过期时间。但这种方法存在问题,因为 setnxexpire 是两步单独的操作,不具有原子性,如果执行第一条语句后发生异常,锁将无法过期。
🔶 可以采用 lua 脚本,将 setnxexpire 写在脚本中,由于 lua 脚本在 redis 中的实现是原子性的,因此加锁和设置过期会同时发生。
🔶 使用 set 及可选参数实现。比如参数 EX 代表设置过期时间,NX 代表 key 不存在时设置过期时间。其中设置的 value 必须具有唯一性,以便区分来自不同客户端的加锁操作。释放锁时,需要根据 value 值判断是不是自己的锁,再进行释放锁操作。

🌸 Redis的底层数据结构?

🔶 简单动态字符串 SDS:SDS 的结构中包含参数 len,用来记录字符串的长度,因此查询字符串长度的复杂度仅为 O(1),速度快。SDS 还是二进制安全的,它不像 C 字符串一样用 ‘\0’ 标识字符串末尾,而是使用 len 来记录,因此可以存放任意的二进制数据。SDS 中参数 alloc 用于计算字符串的剩余空间,在拼接字符串时如果空间不够会自动扩容,避免缓存区溢出,更加的安全。
🔶 链表:链表的每个节点中都有一个前向和后向指针,因此是双向链表。链表结构中的 head 和 tail 可以获取头尾元素节点。参数 len 可以用来获取链表长度,查询链表长度的复杂度仅为 0(1)。链表节点由于使用的是 void* 指针,因此可以存放不同类型的数据。它的缺点是内存不连续,数据查找效率不高,以及创建节点内存开销大。
🔶 压缩列表:压缩列表类似于数组,使用连续的内存块来存放数据。当前元素节点会记录前一节点的长度,因此可以实现双向遍历。压缩列表在存储数据时会根据数据的大小分配合适的内存,可以很好的节省了内存空间。但是当插入一个较大元素时,可能出现记录前一节点长度所需空间不够的情况,导致连锁更新使压缩列表的性能下降。
🔶 哈希表:哈希表是一种 key-value 的数据结构,可以使用 O(1) 的复杂度进行快速查询。然而,随着数据的不断增加,可能会发生哈希冲突。redis 采用链式哈希结构来解决哈希冲突,就是将拥有相同哈希值的数据用链表串起,确保数据仍可以被查询到。另一种解决方法就是 rehash,当发生哈希冲突时,新建一个哈希表并扩增哈希桶的大小为两倍,迁移旧哈希表中的数据到新哈希表中。
🔶 整数集合:整数集合是一块连续的内存空间,类似于数组。其中元素的存放类型取决于参数 encoding 的属性,比如 8,16,32 位的 int。此外当插入的新数据类型超过当前最大类型时,会进行类型的升级操作。
🔶 跳表:跳表在链表的基础上进行改进的,实现了一种多层的有序链表,可以快速定位数据。它的查询步骤是从头节点的顶层开始,查找第一个大于指定元素的节点,再退回上一节点,在下一层节点继续查找,使得查找的复杂度为 O(log n)。

🌸 Redis如何保证缓存与数据库的数据一致性?

🔶 首先使用缓存就可能会发生数据库数据和缓存数据不一致的现象。这个不一致可以分为:强一致性,即用户写入什么,读取的立刻就是什么。弱一致性:写入数据后,不保证数据立即一致,而是某个时间级别后能够一致。
🔶 不论是先更新缓存再更新数据库,还是先更新数据库再更新缓存,都会在并发请求时出现问题,因为更新缓存和更新数据库的时间间隔是不确定的。
🔶 旁路缓存模式:服务端会以数据库的结果为准。读请求时,先读取缓存,如果命中的话直接返回数据,未命中的话从数据库中获取数据,并添加到缓存中。写请求时,先更新数据库,再删除缓存。而先删除缓存,再更新数据库时,如果在读写并发的场景下,还是会出现数据不一致的问题,可以采用延迟双删策略,确保缓存一定会被删除。
🔶 读写穿透模式:服务端会以缓存的结果为准。读请求时,先读取缓存,如果命中的话直接返回数据,未命中的话从数据库中获取数据,并添加到缓存中。当进行写请求时,先检查缓存,缓存未存在则直接更新数据库,如果缓存存在,则先更新缓存,再更新数据库。读写穿透在旁路缓存的基础上提供了 cache服务,在旁路缓存中缓存未命中客户端自己负责写入数据到缓存,而读写穿透中是由 cache 服务自动将数据写入缓存的。
🔶 异步缓存写入模式:该模式和缓存穿透类似,都是通过 cache 服务负责缓存和数据库的读写。不同的是读写穿透同时更新数据库和缓存,而该模式是先更新缓存数据,之后通过异步的方式批量更新数据库。

🌸 Redis的作用?

🔶 Redis 是一种非关系型的数据库,Redis 中的数据是存储在内存中的,所以读写速度非常的快,具有高性能和高并发的特性。
🔶 高性能:如果用户第一次访问数据库中的某些数据,过程可能会很慢,因为是从硬件中读取的。如果将访问的数据存放在缓存中,那么下次访问时可以直接中内存中获取数据,速度会很快。
🔶 高并发:缓存能够承受的请求数远远大于数据库,所以可以考虑将数据库中的热点数据给转移到缓存中,这样很多请求都不会经过数据库,而是通过缓存来获取数据。

🌸 Redis的持久化机制有哪些?

🔶 Redis 有两种持久化方式,分别为 RDB 持久化和 AOF 持久化技术。
🔶 通过持久化机制,将内存中的数据同步到硬盘中,当 Redis 重启后,将持久化数据从硬盘数据加载到内存,达到恢复数据的目的。Redis 默认使用 RDB 持久化。
🔷 RDB:Redis 可以通过创建快照来获取某个时刻的内存数据副本,创建的快照是二进制文件。创建快照完成后对快照进行备份,发送给其他服务器,通过快照可以恢复缓存中的数据。
🔷 Redis 有两个命令来生成快照,分别是 save 和 bgsave。其中 save 会在主线程中生成文件,而 bgsave 会创建子线程来生成文件,避免主线程的阻塞。RDB 的备份是全量备份,如果备份频繁的话会影响 Redis 的性能,但不频繁可能会丢失较长时间的数据。
🔶 AOF:Redis 在执行一条写操作命令时,会把命令以追加到形式写入到磁盘中的日志里。当 Redis 重启的时候,读取并执行日志中的命令,就可以恢复内存数据。
🔶 写回策略:Redis 在执行完写操作后,会将命令写入缓冲区,系统调用 write() 函数将缓冲区命令写入 AOF 日志,由内核决定何时将日志写入到磁盘。其中参数为 always,可以让内核在每次执行写操作后将日志写入到磁盘,everysec,可以让内核每秒将日志写入到磁盘,参数为 no,意味着将日志写入磁盘的时机由内核决定。
🔶 重写机制:随着写操作的越来越多,AOF 日志文件的大小也会越来越大,这会导致性能的问题。于是 redis 给 AOF 文件大小设置阈值,当超过后会启用 AOF 重写机制。当出现了多条命令修改同一个 key 时,只会记录最后一条更新 key 的命令。然后将重写后的 AOF 文件覆盖掉现有的 AOF 文件,AOF 文件的大小也得到了压缩。
🔶 后台重写:当触发 AOF 重写机制时,如果 AOF 文件太大的话,主线程的重写是很耗时的,因此 AOF 重写可以由后台的子进程完成,在此期间主进程一直是非阻塞的。
🔷 Redis 也支持 RDB 和 AOF 的混合持久化。使用混合持久化时,AOF 重写会将内存数据以快照的形式全量写入 AOF 文件,之后将新增命令以 AOF 的形式增量写入到 AOF 文件,实现混合持久化。

🌸 热点数据,冷数据是什么?

🔶 热点数据就是经常被访问的数据,冷数据是指没有被访问过,或者访问次数很少的数据。
🔶 热点数据的缓存更有意义,因为冷数据肯定没有机会被访问,就已经过期了,不仅占用内存而且价值也不大。

🌸 单线程Redis为什么很快?

🔶 Redis 的全部操作都是在内存中的操作,IO 效率很好。
🔶 Redis 采用单线程,避免了频繁的上下文切换。
🔶 Redis 采用了非阻塞的 IO 多路复用机制,处理大量客户端的 Socket 请求。

🌸 Redis为什么使用跳表而不是红黑树?

🔶 因为调表的实现方式和原理更加的简单。
🔶 内存占用低,跳表中只需要存储数据值和层级索引,而红黑树需要额外的指针来维护红黑树的性质。
🔶 扩展性强,跳表只需要增加层级索引即可,而红黑树需要一系列的平衡操作,可能需要重构整个树结构。

🌸 Redis的基础数据结构?

🔶 string:string 是最基本的 key-value 结构,其中 key 为字符串,但是 value 可以为多种数据类型。string 是基于简单动态字符串 SDS 实现的。string 通常用在常规计数(粉丝数,订阅数)等场景。
🔶 list:list 为简单的字符串列表,按照插入顺序排序,可以从列表的头部或尾部添加元素。list 的底层结构由双向链表或压缩列表实现。lisit 通常用在消息队列(关注列表,好友列表)等场景。
🔶 hash:hash 是一个 key-value 集合,其中 key 为字符串,value 的形式为多个 field_i 和 value_i 组成的映射表。hash 的底层结构由压缩列表和哈希表实现。hash 通常用于存储对象信息(用户信息,商品信息)等场景。
🔶 set:set 是键值集合,元素存储顺序和插入顺序无关,且无法存储重复数据。set 是由哈希表或整数集合实现的。set 可以用于需要交集并集之类的场景(共同好友,共同关注)等场景。
🔶 zset:zset 是有序键值集合,相比于 set 多了个排序属性,因此可以实现元素有序排列。zset 是由压缩列表或跳表实现的。zset 可以用于排序(播放排行榜,电商排行榜)等场景。
🔶 bitmap:bitmap 是位图,由一串连续的二进制数组组成,通过设置 0/1 值可以表示某个元素的状态。bitmap 是基于 string 实现的。bitmap 可以用在二值状态统计(签到统计,登录状态)等场景。
🔶 hyperloglog:hyperloglog 是一种用于统计基数的集合类型,用于统计集合中非重复元素的个数。hyperloglog 可以用在百万级网页 UV 计数等场景。
🔶 geo:geo 主要用于存储地理位置信息,并对存储的信息进行操作。geo 基于 zset 类型实现。geo 可以用在查找附近商家,附近网约车等场景。

🌸 缓存雪崩,缓存穿透,缓存击穿的区别?

🔶 缓存雪崩:是指大量缓存数据在同一时间过期或者是 redis 宕机时,如果此时出现大量的用户请求,那么因为 redis 无法处理,所有的请求都直接访问数据库,造成数据库的压力剧增,造成数据库宕机或系统崩溃。
🔶 对于大量数据同时过期的情况,可以采用(1)给过期时间设置为随机数确保过期时间分布均匀;(2)设置后台进程进行缓存更新的操作,读取数据库数据到缓存。对于 redis 宕机的情况,可以采用(1)启用请求限流机制,只接收部分的请求访问数据库,其他请求拒绝其访问。(2)构建 redis 集群,如果主节点发生宕机,那么将从节点切换为新的主机点继续提供缓存服务。
🔶 缓存击穿:缓存击穿是缓存中的某个热点数据过期,此时大量的请求访问该热点数据,但从缓存中获取不到,于是大量请求访问数据库,数据库承受很大的压力。可以采用(1)不给热点数据设置过期时间,通过后台线程异步更新缓存.(2)在热点数据即将过期前,通过后台线程更新缓存并重制过期时间。
🔶 缓存穿透:缓存穿透是用户访问的数据,既不在缓存中,也不在数据库中,导致请求缓存时未命中,从数据库读取不到值无法构建缓存,当有大量请求时数据库承受很大的压力。可以采用(1)接收请求前判断请求参数是否合理,是否含有非法值。(2)当发现缓存穿透发生时,可以为查询数据的缓存设为一个空值返回,这样就不会继续查询数据库。(3)设置布隆过滤器快速判断数据是否存在,而避免通过数据库来查询数据是否存在,因此减轻数据库的压力。

🌸 Redis 的内存淘汰策略有哪些?

🔶 当 Redis 中的内存数据已经超过了 Redis 设置的最大内存后,会使用内存淘汰策略来删除符合条件的 key,保障 Redis 的运行效率。
🔶 noeviction:不淘汰任何数据,当有新的数据写入时会被禁止写入,只能进行一些查询和删除操作。
🔶 volatile-random:随即淘汰设置了过期时间的任何 key。
🔶 volatile-ttl:优先淘汰过期时间更早的值,。
🔶 volatile-lru / lfu:淘汰所有设置了过期时间的 key 中,最久没有使用的 key,最少使用次数最少的 key。
🔶 allkeys-random:随即所有 key 中的任意的 key。
🔶 allkeys-lru / lfu:淘汰所有 key 中,最久没有使用的 key,使用次数最少的 key。

🌸 Redis 的过期删除策略有哪些?

🔶 定时删除:在设置 key 的过期时间时,创建一个定时事件,时间到达时事件处理器自动执行 key 删除操作。优点是可以尽快的删除 key 释放内存;缺点是过期 key 较多时比较占用 CPU 时间。
🔶 惰性删除:不主动删除过期的 key,当从数据库中访问 key 时,检查 key 是否过期,过期的话就删除。优点是对 CPU 时间友好,缺点是比较占用内存。
🔶 定期删除:每隔一段时间从数据库中随机选取一定数量的 key 进行检查,删除其中过期的数据。它的特点是 CPU 时间和内存占用是上述方法的折中。
🔶 redis 删除策略:redis 采用惰性删除 + 定期删除的策略组合使用,Redis 每次访问 key 时都会检查 key 是否过期,每隔一段时间会随机抽选 key 检查是否过期,如果过期的 key 超过 1/4,则立即再次抽查。

🍺 OTHER

🌸 大量客户请求连接时,服务端可以如何处理?

🔶 多线程同步阻塞:每个客户端的连接到来时,都会创建一个新的线程来处理连接,实现并发处理。
🔶 IO 多路复用:使用一个线程同时监听多个端口,当有连接请求到来时,该线程会为连接请求分配一个空闲线程取处理。

🌺 一致性哈希 是什么?

🔶 一致性哈希是一种用于分布式操作系统中的数据分片算法,它主要的目的是为了在系统扩容或缩容时最小化数据的迁移量,减小系统的开销。
🔶 一致性哈希的原理是将哈希值域组成一个虚拟的圆环,然后将数据和节点信息映射到这个圆环上。在写入数据的时候,可以先计算数据的哈希值,根据哈希值找到它在圆环中的位置,然后顺时针找到下一个节点,存放数据到节点中。如果这个节点发生故障,则继续寻找下一个节点并存储数据。
🔶 在一致性哈希算法中,当一个节点需要变更时,只需要移动对应节点的少量数据就可以实现数据的迁移,减少了系统的开销。

🌺 gdb 的常见命令?

🔶 启动 gdb:gdb my_file,运行程序:run,退出 gdb:quit
🔶 单步执行:step,下一行执行:next,继续执行:continue
🔶 设置断点:break n,删除断点:delete n
🔶 查看函数调用堆栈:backtrace,查看变量的值:print param

🌸 JWT 是什么?

🔶 JWT 主要是用来在身份提供者和服务提供者之间传递被认证的用户信息,以便从服务器中获取资源。
🔶 JWT 主要由头部,荷载,签名组成。其中头部包含 JWT 的类型,加密算法等信息;荷载存放用户的相关信息;签名是由加密后的 header,payload 和 secret 组合而成的字符串,以确保安全性。

🥂 IO 多路复用是什么?

🔶 IO 多路复用是一种处理多个 IO 流的技术。它允许单个进程同时监听多个文件描述符,当一个或多个文件描述符准备读写时,可以立即响应。这种技术可以提高系统的并发能力和响应能力,减少系统资源的浪费。
🔶 在 linux 中,select、poll、epoll 都是 IO 多路复用的实现方式,它们都可以同时监听多个文件描述符,一旦某个文件描述符就绪,就能通知程序进行对应的读写操作。

🥂 Linux 中常用的命令有哪些?

🔶 文件:mv 移动文件,cp 拷贝文件,mkdir 创建目录,cd 切换目录,ls 显示目录中的文件。
🔶 权限:chmod 修改权限,chown 修改所有者,useradd 添加新用户。
🔶 资源:netstat 显示网络状况,ping 测试网络连通性,ps -ef 查看进程信息,top 查看系统资源占用。
🔶 压缩:tar -zxvf 压缩文件。

🥂 在并发任务中,不加锁可能会出现哪些问题?

🔶 当两个线程使用同一个全局变量时,可能会导致数据不一致的问题。
🔶 比如 A 线程修改了数据,但是 B 线程还是从缓存中读取未更新的数据,导致数据不一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的小老虎丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值