前言

其实没有想要发布出来的,但是我的几个粉丝和朋友私信我说非常期望我的面经贴,最后还是决定发布出来,希望能够帮助后人。

本篇将博主整个秋招的面经总结给发布了出来,篇幅很大,超过10W字,涵盖了我平日的积累与各大公司的面试真题,如果你能背完大部分,那八股文部分大概率没有问题了,反正博主在面试的时候八股文基本都答了上来。这篇发出来属于是最后燃烧自己了。当然如果你发现某一点有歧义或者错误,也欢迎你在评论区说出来大家一起讨论。


C++

1. 静态变量在哪初始化的?

静态变化在程序开始时初始化,内存分配是在编译期间完成的,占用内存大小固定。在内存的静态/全局区,这部分用于存储全局变量和静态变量。

2. 说一下编译的过程,那静态变量是在哪个阶段初始化的?

  1. 预处理:宏替换、头文件包含等
  2. 编译:转为汇编
  3. 汇编:转为机器语言
  4. 链接:将多个文件和需要的库文件链接成一个可执行程序

全局静态变量的初始化值存储在可执行文件中,而函数内的局部静态变量在函数第一次调用时进行初始化。

为什么需要链接这个过程?

  • 编译器将源文件转换成目标二进制文件,但是这些文件是独立的。可执行文件将这些文件和库文件中的目标文件整合成一个单独的文件,需要链接
  • C++需要使用各种动态库,库中的代码一般已经编译程二进制文件,只有在链接阶段才能将它们与我们的代码合并,而非在编译阶段。
  • 既然有了链接这个过程,方便我们将代码分类,放到多个文件中,最后组装起来,让代码维护更加方便

3. map和set的区别和底层实现,map取值的find、[]、at方法的区别

mapset都是关联式容器,底层实现是红黑树,增删查改的时间复杂度都为O(logn)。其中,map是键值对关联容器,set是集合容器。

取值方法区别

  1. find:指向值的迭代器,若不存在,返回end(),安全
  2. []:若不存在,会自动插入
  3. at:若不存在,会抛出std::out_of_range异常,安全。

4. 智能指针的循环引用问题?

两个智能指针指向互相指向对方,导致双方不能正常析构,引起内存泄漏。可以使用weak_ptr,他不会增加引用计数。

shapre_ptr的创建、复制、销毁是线程安全的。但是,当存在共享对象的时候,还是需要加锁;总之,shared_ptr的部分操作是线程安全的。shapred_ptr的引用计数是通过原子操作来保证线程安全的

5. 虚函数的底层实现,多继承(一个派生类继承多个基类)的虚指针的虚表的形式

虚函数的底层依赖虚指针(vptr)和虚函数表(vtable)。多继承是一个派生类继承多个基类,多继承中的虚指针和虚函数为如下:

  1. 虚指针(4/8字节):每个含有虚函数的类都会有一个虚指针,指向虚函数表
  2. 虚函数表:虚函数表是一个函数指针数组,存放着类的虚函数指针。每个继承自基类的派生类都有自己的虚函数表,虚函数表中的函数指针顺序与基类的函数声明顺序一致。(有多少个虚函数,就有多少个函数指针)

虚析构函数:在删除指针的时候,会先调用派生类的析构函数,再调用基类的析构函数。如果基类的析构函数不是虚的,那么删除基类指针时只会调用基类的析构函数,从而导致资源未正确释放以及内存泄漏的问题。

如果一个类什么都没有,则sizeof()是1,如果有一个或多个虚函数,则sizeof()是8,因为虚指针8个字节。虚函数指针通常位于对象的开始位置。

另外,虚函数表存储在常量区的数据段(只读数据段)。它是一个静态数据结构,在编译式就确定了,后面不会修改

6. 左值和右值?

左值

  1. 可以取地址
  2. 具有持久性

右值

  1. 将亡值
  2. 不能取地址
  3. 表示临时结果,字面值或不能被赋值的对象

7. 涉及atoi函数要考虑哪些问题?

这是将字符串转换为整数(const char* -> int),需要考虑:

  1. 空字符串处理
  2. 空格处理
  3. 数值范围
  4. 符号,可能数字前面有正负号,以确定返回的整数应该是正数还是负数

1、该函数首先会丢弃尽可能多的空白字符,直到找到第一个非空白字符,然后,从这个字符开始,取一个可选的初识加号或者减号,后跟尽可能多的十进制数字,并将他们返回一个int类型的数值。
2、若该字符串是在整数的字符后包含其他字符,则这些字符将会被忽略,返回其他字符之前的整数,并且不会对该函数造成任何影响。
3、若该字符串中第一个非空字符序列表示有效的整数,或是一个空指针,或只包含空白字符,则不执行任何转换,并且返回零。

8. new和malloc的区别?

  1. new是C++的一个操作符,malloc是库函数
  2. new会调用构造函数,malloc只是分配内存
  3. 关于返回值,new会返回对应类型的指针,malloc返回void*
  4. 在回收内存的时候,new采用delete或delete[],malloc采用free
  5. 使用new的时候先调用malloc,再调用构造;delete时,先调用析构,再调用free

9. define和const的区别?

const在C++中推荐用来代替掉define,因为define实现的功能const都能实现。define是宏定义,在预编译时期替换,但是const变量具有类型更加安全

define在预编译阶段,cosnt是常量,在编译阶段。

10. 智能指针的运用场景?

  1. 资源共享:当多个对象需要共享相同的资源时,可以采用shared_ptr,例如观察者模式
  2. 管理独占资源:可以采用unique_ptr
  3. 实现RAII:使得程序员不用担心内存泄漏的问题
  4. 解决循环引用问题,:使用weak_ptr,引用不计数;主要是为了防止重复释放资源

11. C++11的单例模式?

  • 线程安全:C++11的单例模式提供了线程安全的初始化和使用手段,确保在多线程环境下单例的正确性。主要是因为C++11引入了新的内存模型和原子操作。在以前,竞态局部变量的初始化可能导致竞争条件。但在C++11中,静态局部变量的初始化在多线程中只会执行一次,其他线程会等待初始化完成。
class Singleton {public:static Singleton& getInstance() {static Singleton instance; // 在C++11中,这里的初始化是线程安全的return instance;}private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;};

以上是一种Magic Statics的实现方式,在C++11中是线程安全的,不需要懒汉(一开始就创建)、饿汉(需要时创建 ,可能有线程安全问题)。

12. 什么是布隆过滤器?

他是一个数据结构(很长的二进制向量) ,可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。他只能告诉我们一个元素绝对不在集合内或可能在集合内。布隆过滤器很难实现删除操作。

布隆过滤器主要是为了防止redis缓存击穿问题(前端要查询一个数据,但是redis没有这个数据,就会去数据库查询,数据库可能承受不了这么大的流量就挂掉了)。有了布隆过滤器,就能判断哪些数据不在数据库中,防止缓存击穿。

13. 重载、重写(覆盖)、隐藏 ?

  • 重载是函数名相同,但是参数或返回值不同,可以有不同地实现。

  • 重写是多态的内容,是父类写了虚函数,子类有自己的实现。

  • 隐藏是在子类中定义了父类完全相同的变量或函数,导致父类的同名元素被隐藏。

14. 我在调用fork之前打开了一个文件,那我子进程和父进程对这个文件描述符进行写入是追加还是重写?

二者共享相同的文件偏移量,所以不会相互覆盖,而是追加到彼此写入的内容中。但是为了避免意外,还是加锁更加安全。

15. vector中push_back和emplace_back的区别?

push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。但是,如果是传递对象,那么二者都是一样的。

16. 左值引用和右值引用的意义?

  • 左值引用:避免对象拷贝(函数传参、函数返回值)

    • const左值引用能指向右值,但不能修改
  • 右值引用:实现移动语义、完美转发

    • 移动语义:std:move():左值变右值,这样就不用深拷贝了,避免资源的重新分配(堆、服务器数据库连接)

    通过移动构造以及移动拷贝构造(&&)

    • 完美转发:std::forward():在函数模板中保持参数的类型和值类别(左值或右值)不变(主要解决通过引用的方式接收左右属性的值

      引用折叠规则:参数为左值/左值引用,T&&将转化为int&;参数为右值/右值引用,T&&将转化为int&&

声明出来的左值引用、右值引用都是左值

总结

  • 移动语义避免不必要的对象拷贝和资源转移
  • 完美转发能正确保留参数属性

std::move和std::forward的区别是什么?在使用上有什么考虑么?

move主要用于有效传输资源,避免不必要的资源拷贝;forward用在函数模板中,保持参数类型一致,避免不必要的性能损失和提高代码的正确性

17. 多态是什么?怎么实现?

多态简单来说是让一个函数作用于多种类型的对象,使得这些对象可以以不同的方式响应同一个函数的调用,这样增加了程序的灵活性和扩展性。

18. 红黑树讲一下,怎么翻转?

红黑树的特性:

  1. 节点是红色或黑色的
  2. 根节点都是黑色的
  3. 所有叶子节点(null、空节点)都是黑色的
  4. 如果一个节点是红色,那么他的两个子结点必然是黑色
  5. 从任意节点到叶子节点的黑色节点数相同

翻转

  • 左旋:以某个节点(A)为轴,将其右节点(B)上移至A的位置,将A作为B的左子结点。原B的左子节点变为A的右子结点
  • 右旋:以某个节点(A)为轴,将其左节点(B)上移至A的位置,将A作为B的右子节点。原B的右子节点变为A的左子节点

19. 讲一下vector和迭代器

vector是STL的一个动态数组,可以让数组尾部插入数据。

迭代器从逻辑上类似于指针,通过迭代器可以很方便的遍历容器中的元素。

20. Vector扩容机制了解吗?

当vector容量不够时,vector会进行两倍扩容,重新分配内存,将之前内存的数据搬到另一块。

21. 内存泄漏的查找方法

  1. 检查代码,查看是否有分配内存但是没有释放的情况
  2. 内存分析工具,比如C++的Valgrind,Go内置的pprof功能
  3. 使用RAII,比如C++的只能指针

一般堆内存泄露就是申请了内存资源没有释放,栈内存泄漏比较少见,一般发生在栈溢出的情况,是由于递归过深导致的,或者用了过大的局部变量,需要检查代码。

22. 指针和引用的区别,引用初始化后是可变的吗

指针是一个变量的地址,引用是一个变量的别名,引用必须初始化,因为在之后无法改变引用所指的对象。

引用在本质上 是一个常量指针,即所指的对象不能发生改变,但可以改变指向对象的值。

23. sizeof引用和sizeof指针的区别,指针和引用占用内存空间吗?理解引用是啥意思吗

sizeof指针返回的是指针的大小,4个字节,sizeof引用返回的是指向对象的大小,比如一个float就是8字节。

24. 内存什么时候回收?显式delete才会回收吗?什么时候对象生命周期到达?栈里面的对象会自动回收吗?常量会自动回收吗?

C++是这样的,显示delete回收。对于栈中的对象,无需显示删除,当函数返回时,会自动回收。

全局常量在整个程序运行周期都在,局部常量与作用域有关。也不需要显示回收。

25. 讲讲协程,协程一定无锁吗?

协程是更轻量级的线程,主要是他可以在用户态调度,这样说明协程的创建、销毁、调度可以由程序员自己控制,而非操作系统。

协程是否一定无锁

协程不能保证代码完全无锁。协程降低了多线程编程中一些潜在的并发隐患,但访问共享资源时,仍然需要加锁。

26. 多态和继承什么情况下使用?

  • 继承在一个类有另一个类一样的功能,但是又有一些那个类没有的功能,就可以继承该基类

  • 多态是当我们希望使用统一的接口处理不同类型的对象时,可以用一个相同的接口,实现不同的方法。同时,希望通过扩展代码而不改变原始代码的时候使用。

27. 除了多态和继承还有什么面向对象方法

  • 封装:将对象属性和行为放到一个对象中,并限制对这些属性的访问,隐藏细节
  • 抽象:通过创建外界接口,隐藏底层细节
  • 组合:一个类包含另一个类,二者是同生死的关系
  • 聚合:弱组合,可以单独存在

28. 讲一讲智能指针?

是C++11中一种对象,主要目的在于帮助程序员管理指针内存,这样就不用担心因为忘记手动释放而造成的内存泄漏问题了。是一种RAII机制。

29. 讲一讲RAII?

用于确保资源的正确获取和释放。RAII的关键思想是将资源的生命周期绑定到对象的生命周期,并在对象的构造和析构函数中实现资源的管理。

30. vector、stack、deque的内存情况

  • vector是一种基于动态数组的顺序容器,元素在内存中连续存储。当空间不足时,会自动扩容(2倍)。
  • stack是是一种容器适配器,将底层容器的功能封装为栈,通常用deque或vector作为底层容器。
  • deque是一种线性结构的容器,允许在两端插入和删除。deque不一定是内存连续的,而且deque没有capacity

31. 详细解释一下动态绑定,怎么绑定的,绑定的是什么

动态绑定是指运行时决定调用哪个函数。这是通过基类的指针或引用间接调用虚函数来实现的。在这种情况下,编译器无法确认调用的哪个派生类的函数,必须等到程序运行时才确定。绑定的是基类指针或引用和具体派生类对象中相应的虚函数。

Animal animal = new Dog(); animal->eat();

32. 抽象类与纯虚函数,作用,有函数体是否编译

抽象类不能被实例化,只能被继承。纯虚函数是没有函数体的虚函数,目的就是让派生类重写。当一个类有纯虚函数的时候,就是抽象类。

纯虚函数:virtual float getArea() = 0;

33. override和final的作用

  • override用于指定派生类中一个函数是用来重写的,以确保子类函数和父类函数名才相同,表明了函数重写的意图
  • final作用于类的时候,表明该类不能被其他类继承,这样可以防止类的继承层次过深。同时,有final的函数,不能在派生类中重写。

34. memcpy函数实现,memmove呢

void *memcpy(void* dest, const void* src, size_t n) {char* d = (char*)dest;const char* s = (const char*))src;for(size_t i = 0; i < n; i++) {d[i] = s[i];}return dest;}

memmove是防止memcpy的地址重叠问题的,解决方法是:如果源地址小于目标地址,则从后向前复制,如果源地址大于目标地址,则从前向后复制,从而避免使用额外的缓冲区。

35. 细讲static,包括static成员变量与函数

static成员变量被类的所有对象共享。

static成员函数是属于类的,在没有创建类的实例时也可直接只用。它们不能访问类的非静态成员,因为非静态成员需要类的实例。

需要注意的是,static成员遍历和函数的作用域仅限于定义它们的类。要在类外部访问,需要加::

36. 给定类,判断大小,包括空类,加入static,加入虚函数,考虑到内存对齐

  • 空类的大小为1
  • static成员变量不影响类的大小,因为它们是类的所有实例共享的。静态成员变量在子类的实例之外单独分配内存
  • 虚函数:有虚函数的类会比没有虚函数的类多一个8字节的虚函数表指针
  • 内存对齐:根据不同平台和编译器,编译器可能会在类成员之间添加填充字节,以确保数据对齐。

37. 开辟位图大数组就直接存放在main函数里面吗

定义为全局变量是最好的,这样有助于内存管理,更容易跟踪数组的用途以及存取数组的所有位置。

38. unordered_map中哈希冲突的解决方法

  • 链地址法:键值添加到相应索引处的链表(unordered_map实现的方法)

    • 优点:对哈希函数要求低,删除操作简单

    • 缺点:元素过多会导致查询效率降低;由于链表使得数据访问不连续,对CPU缓存不友好

      优化方法

      • 使用平衡树代替链表
      • 自适应库容/缩容:随着元素增加,哈希表动态调整其大小,比如rehash
  • 开放地址法:试图找到冲突索引后的数组位置的一个空位

    • 优点:无需额外的空间存储链表,空间利用率高;可以有效利用CPU缓存
    • 缺点:对哈希函数要求较高;删除操作相对复杂

unordered_map使用的哈希函数是使用的std::hash作为哈希函数。

39. 如何让类只能在堆上创建,不能在栈上创建?

  1. 将构造函数设为私有
  2. 提供一个static外部接口函数,该函数返回一个指向新创建对象的指针
  3. 在析构函数中,销毁该对象
class HeapOnly {private:HeapOnly() {} // 私有构造函数~HeapOnly() {} // 私有析构函数public:// 使用静态成员函数在堆上创建对象的实例static HeapOnly* Create() {return new HeapOnly();}// 添加成员函数以释放对象占用的内存void Destroy() {delete this;}};

40. 如果将一个模板类的,h文件和.cpp文件分开,别的文件调用该包,会出现什么错误?为什么普通的类就可以分开呢?

模板类是在编译的时候生成的,如果将定义放在.cpp文件中会导致编译器在处理其他源文件的时候找不到完整的模板定义对于模板类,编译器在编译时并不知道可能的所有类型参数,因此需要在使用模板时生成具体实例。所以,我们只有保留模板类的声明,并在同一个文件中添加实现。

模板类是在编译期间确定的,不是动态绑定的。

为什么普通的类就可以分开呢?

生成程序的时候有编译到链接的机制,链接阶段会把这些文件连接起来,而模板类在编译时并不知道可能的所有类型参数,因此需要在使用模板时生成具体实例。在链接阶段,编译器会检查所有具体类型的模板实例是否生成了相应的代码,如果找不到实例,就会报链接错误。总结:链接的时候不知道具体类型,无法整合

41. 栈上能动态分配内存吗?

alloca()函数用于申请栈空间,这也意味着深情地对空间在当前函数结束后会自动释放

42. 拷贝构造函数和赋值运算符重载的区别?

拷贝构造函数是构造函数,我还没有这个对象,我需要构建出来;而赋值的话,是我已经拥有了这个对象,我将另外一个对象的属性给赋值到我现在的对象上。

43. 静态链接和动态链接的区别?

  • 静态链接:在编译期将库文件的所有代码嵌入到最终可执行文件中,体积大,快 (.a .lib)
  • 动态链接:在程序执行过程中遇到了再链接(.so .dll),体积小,慢

44. 讲讲struct和union的区别

  • 存储方式:struct各个成员有自己独立的内存地址,union所有成员共享相同的内存地址
  • 初始化:struct可以同时访问多个成员,union只能访问一个
  • 适用场景:struct适用于存储不同变量,并在同一时刻访问它们;union适用于不同场合使用不同变量,比较节省内存

在union中,会使用内存对齐,即union的大小由最大成员的大小决定

45. vector是线程安全的吗?

不是,所以在使用的时候需要考虑竞态条件,加锁。

46. decltype、auto

  • decltype:根据表达式推测类型
  • auto:根据初始化自动推导变量类型

47. noexcept

在函数声明后面加这个关键字,说明该函数不会抛出异常,可以提高性能。

48. 为什么unique_ptr删除了拷贝构造、赋值构造

因为unique_ptr只允许复制一次,同一时间只能由一个智能指针操作这个对象。如果采用拷贝构造这些的话,会调用超过一次析构函数,对同一块内存释放多次内存,就成了操作野指针。 一般用:unique_ptr p = make_unique(); 这也是auto_ptr不安全,被unique_ptr替换掉的原因。

49. 为什么用make_shared不用new

如果通过new再传递给shared_Ptr,内存是不连续的,会造成内存碎片化

50. 解释下循环以用

class MyClass {public:std::weak_ptr other; // 使用 weak_ptr 而不是 shared_ptr~MyClass() { std::cout << "Destructor called!" << std::endl; }};int main() {{std::shared_ptr obj1 = std::make_shared();std::shared_ptr obj2 = std::make_shared();obj1->other = obj2; // obj1 和 obj2 互相引用obj2->other = obj1; // 不再创建循环引用} // obj1 和 obj2 离开作用域// 此时当 obj1 和 obj2 离开作用域时,引用计数变为零,然后析构函数调用并释放内存资源。// 使用 weak_ptr 可以避免引用计数的循环增加,从而避免了内存泄漏。return 0;}

51. C++类型转换4个

  • static_cast:会执行隐式转换(普通类型转换、子类转换为基类)
  • const_cast:用于更改const属性
  • dynamic_cast:多态类之间的转换,即在类的继承层次之间,可以从基类到派生类,会检查类型
  • reinterpret_cast:指针和整数类型转换

简单数据类型用static_cast,多态类之间的转换用dynamic_cast,确保转换安全

B *b = new A();A *a = dynamic_cast(b); // 这是一个有效的操作,因为 b 实际上指向一个 A 对象

52. 隐式转换(自动)和显示转换(强制)的区别?

  1. 可见性:隐式转换由编译器自动完成,显示转换需要程序员明确指定
  2. 自动化:隐式转换通常在类型之间的自动转换中发生,显示转换要人为干预
  3. 控制成都:显示转换使程序员更精确的控制类型转换过程,隐式转换的行为由编译器定义

53. 全局静态变量和局部静态变量的区别

  1. 全局静态遍历在整个程序文件可见,局部静态变量只有在内部函数可见。
  2. 全局静态变量生命周期从开始执行到结束,局部静态变量也是如此。
  3. 二者都是在编译的时候就初始化分配了内存。

54. std::string缺陷

  • 内存管理:使用动态内存存储字符串数据,这意味着频繁进行字符串操作时,可能会频繁产生内存的分配和转移,造成性能下降
  • 拷贝开销:当进行字符串拼接、复制的时候,每次都会创建新的字符串对象,涉及大量的内存拷贝操作
  • 不支持多字节字符:string内部使用单字节字符来表示字符串,对于多字节字符集的处理可能不够方便

55. 智能指针与原生指针开销一样吗?

不一样。智能指针是一种用于动态管理内存分配和释放的对象,是一个类。包含了额外的开销,例如引用计数、指针管理等,以便跟踪指针的使用情况。而原生指针是直接指向内存地址的指针,没有附加的开销。

56. vector的内存管理?

在初始化的时候,vector的容量大小为0。

  • resize

    用于改变vector的大小,包括增加元素或删除元素。如果新的大小小于旧的大小,resize()会删除一些元素。如果新的大小大于旧的大小,resize()会添加一些新的元素,新元素的初始值将由所提供的新元素类型的默认构造函数确定。

  • reserve

    这个函数效率更高,一般用来提前预留空间,需要注意的是,reverse的空间大小必须比原来大才行。

57. 静态多态和动态多态

  • 静态多态:在编译期间完成,它主要是通过函数重载和模板来实现的
  • 动态多态:运行时期完成,它主要是通过虚函数和指针/引用调用实现的。

58. 联合体和结构,联合体什么时候使用?

联合体

  • 当多个基本数据类型或符合数据结构要占用同一片内存时
  • 当多种类型、多个对象、多个事物只取其中一个时

结构体

  • 当你需要表示一个事物的多方面,例如学号、姓名
  • 当你需要多个事物组合在一起的时候

59. i++和++i的区别和实现

// 前缀自增操作符Integer& operator++() {++value;return *this;}// 后缀自增操作符Integer operator++(int) {Integer tmp = *this;// 取一个临时值++value;return tmp;}

所以++i效率会比i++效率更高,而且从返回值也可以看出前置++返回的是左值,后置++返回的是右值

一般操作符重载后面的参数是操作符后面的参数,但是i++和++i却是反的

60. c++友元函数

友元函数不是类的成员,而是外部的一个函数。他允许该函数访问自己类中的私有和保护成员。

61. struct和class的区别

  • struct默认是public,class默认是private
  • struct默认不会初始化成员,所以是随机值,而class默认初始化为0(编译器行为)

那为什么要用class?

当你想要强调封装和隐藏实现细节时,class是更好的选择。class只暴露必要的接口,这就是封装的一部分。不过C++中基本上二者相同。

62. 内存对齐有什么好处?

内存对齐是计算机科学中的一个重要概念,指的是数据在内存中按其自然边界排列。为了提高存取速度和性能,许多处理器都要求某种类型的数据(如整数、浮点数)要安排在特定的内存地址上。

对齐规则

  1. 第一个成员在结构体偏移量为0的地址处,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int4字节,要从4的整数倍地址开始存储)
  2. 结构体A嵌套了结构体B,要从B的最大元素整数倍开始存储
  3. 结构体总大小必须是内部最大成员的整数倍,不足的要补齐
  4. 带虚函数的类,虚函数表指针必须和最大成员变量对齐

为什么要内存对齐?(本质上就是空间换时间)

寄存器只能从整除以4的地址开始读取数据,没有对齐会导致寄存器访问2次内存地址,性能下降。内存对齐多少字节是可以自己设计的。

  • 加速数据访问:某些处理其访问与内存系统对齐的数据时,会更加高效。
  • 减少内存访问冲突:在多处理器系统中,内存对齐可以帮助减少处理器因访问同一缓存行中共享数据而引发的竞合。

63. 虚函数可以定义为内联函数吗?

可以被声明为内联函数。但是如果这个虚函数在派生类中被覆盖,那么内联可能不会发生。对于虚函数,编译器基本上需要保留对它的间接调用(通过虚函数表),以确保动态绑定的正确性。所以,虚函数可以被定义为内联,只要不是用动态绑定,使用静态绑定即可。

64. C++程序运行起来如果发现内存使用不断在长,怎么确定问题位置?

可能是发生了内存泄漏问题,通过审查代码,观看有new的地方是有delete。当时我们团队在做一个课程的大作业的时候,我有一个功能是调用接口打开摄像头进行人脸识别,可是次数多了过后程序就会崩溃,究其原因发现是内存不断地增大,应该就是发生了内存泄漏问题。VS有一个内存分析器可以告诉我内存泄漏的位置,我当时也是发现当调用某个线程的时候,他创建了资源,但是我没有释放掉,而是在主函数关闭后去释放,就造成了内存泄漏问题。后面就尝试定义成一个静态变量。

65. volatile是线程安全的吗

volatile是告诉编译器每次访问变量都从内存中去取,而不是使用优化后的寄存器缓存。然而,它不会防止并发读取和修改操作时的数据竞争。例如,当A读取变量,B也读取到变量,然后A、B分别对变量+1,由于竞争问题,最终的值只增加了1。

66. vector缩容方法

vector::shrink_to_fit,将容器空间缩减为size大小。

清空内存:可以先clear,再shrink_to_fit;也可以vector().swap(v)

67. 虚函数和纯虚函数的区别?在设计模式中有什么应用?

二者都是在子类中进行重写,但区别在于纯虚函数在虚函数后面加=0,表明没有具体实现,这样的类被称为抽象类,不能实例化。

纯虚函数在设计模式中应用广泛:工厂方法模式。基类定义了一个纯虚函数作为创建对象的抽象接口,派生类提供具体的对象创建实现。

class Product {public:virtual void ShowProduct() = 0;};class ConcreteProductA {public:void ShowProduct() {cout<<"using A product"<ShowProduct();// 具体产品的功能调用}

68. new和malloc出错的话(比如内存不够了)

在C++中,new操作如果无法分配所需内存,会抛出一个std::bad_alloc异常,可以用try-catch捕捉。

69. 原子操作是怎么做的?

atomic a(10),该操作能保证是原子的,在多线程环境下的逻辑运算的时候是安全的,不需要加锁保护。

70. 怎么避免哈希冲突?

  • 链地址法
  • 开放寻址法
  • 二次哈希
  • 动态调整哈希表大小:当哈希表的负载因子超过阈值时,调整哈希表大小,已重新分补键值对并减少冲突

71. .bss和.data的区别?

  • .data:存储已经初始化的全局变量和静态变量。
  • .bss:存储未初始化的全局和静态变量。

72. 使用map迭代器,erase删除某个元素之后,这个迭代器有效的还是无效的

会变得无效。在调用erase后,不应该再使用该迭代器。通常的额做法是再调用erase时将返回值赋值(一个指向已删除元素之后的新迭代器)返回给原有的迭代器。

73. 宏函数的选择分支会不会增加代码的圈复杂度?

圈复杂度是一种用于评估代码复杂度的度量方法,它根据程序控制流图测量代码的分支数量。一个具有较高圈复杂度的程序可能更难理解和维护。

宏函数是在预处理阶段展开的,会用宏定义替换他们的调用。展开后的代码如果包含分支语句,会影响圈复杂度。

74. define宏定义和const是存放在哪里的?

  • define会在与编译阶段被扩展或替换掉,所以执行程序时,实际上并没有名为“宏”的实体存在。所以在内存中没有固定的存储位置。

  • const在编译期间和运行期间,存储在常量区,这个区域的数据只可读不可修改。

75. 你一般使用vector有什么新技巧,清空vector有什么方法?

  • 使用emplace_back()添加元素,可以避免额外的复制和移动操作(添加右值元素时,直接调用构造,没有拷贝或移动的操作)
  • 预先分配容量reserve:避免后续添加元素时动态扩容
  • 使用swap()清空cevtor:和一个空的vector交换:vector().swap(v)
  • 使用shrink_to_fit调整容量:减少capacity到size大小

76. 野指针和空指针

  • 野指针
    • 指向已释放或无效内存的地址,通常出现在指针未初始化或者释放指针内存后,没有置为nullptr的情况。野指针可能导致意想不到的程序行为、内存泄漏、数据损坏等
  • 空指针
    • 即nullptr。如果对空指针进行解引用,会导致程序崩溃,因为没有验证它指向的内存是否有效。一般会通过抛出异常或报错来处理这种情况,例如段错误或者空指针异常。

77. sizeof和strlen的区别?

  • sizeof是一个操作符,strlen是一个库函数,在string.h里面

  • sizeof的参数可以是数据类型,也可以是变量,而strlen只能以\0结尾的字符串作为参数

  • 编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来

  • sizeof计算包括\0,strlen不包括

78. C++空类有什么函数?

有默认构造函数、析构函数、拷贝构造、拷贝赋值运算符、移动构造、移动赋值

79. 迭代器为什么可以遍历任何类型的数据?

因为模板,C++提供迭代器模板来抽象容器中的元素访问,这使得相同的代码可以适用于不同类型的容器和数据类型。

80. 迭代器支持++和–吗?

对于随机访问迭代器,比如vector,支持,而且还支持+=

对于end(),vector不支持–操作,可以用-1来代替

81. 哪些函数不能被用作虚函数?

  • 构造函数:虚函数表在构造函数中产生,如果将构造函数设置为虚函数,无法生成虚函数表
  • 内联函数:普通的内联函数可以,但是如果这个虚函数在派生类中被覆盖,那么内联可能不会发生。
  • 静态函数:静态成员函数不依赖于类的实例对象,并且在所有类的实例之间共享。因此,不适合将它们声明为虚函数,因为它们属于整个类,而不是特定实例。

82. 函数内部创建了一个临时对象a,最后return move(a),问返回值类型分别是A, A&, A&&时分别会发生什么?

  1. 返回值类型为 A: 在这种情况下,由于我们使用 std::movea 转换为右值,编译器执行移动构造(如果类A有移动构造函数)。函数func回的是一个具有A类型的新对象。这种方式相对于传统的拷贝构造更高效,因为它允许我们在某种程度上“窃取”临时对象的资源。
  2. 返回值类型为 A&(A的引用): 在这种情况下,代码会产生错误,因为返回局部对象(在函数内声明的对象)的引用将导致未定义行为。当函数返回时,局部对象a将被销毁,所以返回的引用将指向一个无效的对象。你应该避免这种返回类型。
  3. 返回值类型为 A&&(A的右值引用): 这种情况也会导致错误,原因同样是返回局部对象的引用,这在函数返回后不再有效。在函数中创建的a对象在函数返回后被销毁,所以返回的右值引用指向一个已销毁的对象,导致未定义行为。

总结:当涉及到移动语义时,最佳实践是返回值类型设置为类的类型(如 A),这样编译器会自动进行移动构造或复制构造。避免返回局部对象的引用或右值引用,因为这会导致未定义行为。

83. 垃圾回收算法?

  • 引用计数:跟踪一个对象被多少个其他对象引用。当计数变为0时,该对象被清楚。缺点是会出现循环引用问题。
  • 标记-清除(Mark and Sweep):这种算法由两个阶段组成。在标记阶段,所有存活的对象都会被标记。在清除阶段,所有未标记的对象会被回收。这种算法可以处理循环引用,但可能产生内存碎片。
  • 复制 (Copying):这种算法将内存分为两部分。当一块内存充满时,存活的对象会被复制到另一块内存中。然后,旧内存中的所有对象都会被回收。这种算法避免了内存碎片,但需要额外的内存空间。(vector().swap(v))

84. 数组访问越界会怎样?

会导致未定义的行为:因为访问的是未知地址,可能会导致程序崩溃、错误输出等;也可能导致其他数据被恶意覆盖,导致数据损坏

85. 构造函数注意事项

  • 若要初始化类的成员变量,请使用成员初始化列表。这比在构造函数体内分配初始值更有效。
  • 构造函数不能是虚函数
  • 构造函数可以重载

86. coredump是什么

coredump是当程序出错而异常中断时,OS会把程序工作的当前状态存储成一个coredump文件。包含了程序运行时的内存、寄存器状态、堆栈指针、内存管理信息等。

如何用gdb调试?

gdb [binfine ][coredump file]

调试思路:

  1. 查看调用堆栈,寻找崩溃原因
  2. 根据崩溃点,查找代码,分析原因;bt查看调用堆栈显示,f 0跳转到0位置,p str打印字符
  3. 修复代码

87. 讲讲函数返回引用类型?

C++的函数无法返回局部变量的引用,即要么返回全局变量/静态变量的引用 ,要么返回指针指向内容的引用。

88. vector和list有什么区别” />89. unordered_map底层

unordered_map底层原理使用了哈希表作为数据结构。当将一个键插入到哈希表时,首先要使用一个哈希函数将键从其原始范围映射到较小的范围(称为桶)。这个函数应该均匀地将输入映射到可能的桶中,以避免哈希冲突。当发生哈希冲突即多个键映射到同一个桶时,采用了链地址法:每个桶都维护了一个链表,哈希冲突时将新的键值对添加到该桶对应链表的末尾,查找任意键时只需要遍历属于同一桶的链表即可。

90. const左值引用和右值引用的区别?

  • const左值引用可以绑定右值,但不能修改右值
  • 右值引用可以绑定右值,同时可以修改。

91. 什么叫悬垂引用?

悬垂引用是指一个引用变量指向了一个已经销毁的对象或函数。悬垂引用是一种未定义行为,因为它可能会导致程序崩溃或产生意外结果。在使用指针或引用时,我们应该尽量避免出现悬垂引用的情况。

92. shared_ptr 的基本数据结构可以讲讲么?

分2层,直接成员是

  1. 对象指针:这是一个指向实际分配的资源的指针。通常我们会将sp与new一起使用

  2. 控制块指针。控制块主要包含 ① 被管理的对象 (或指针),② deleter、③ allocator、④ shared 引用计数、⑤ weak 引用计数。

控制块部分是线程安全的。底层的引用计数会使用原子操作保证同步。而托管对象则不是,所以需要确保互斥的访问对象指针。

93. make_shared 和构造函数传裸指针的区别?

首先标准建议 make_shared 同时申请控制块与对象的内存 (直观可减少一次内存申请);否则内存不连续,防止产生内存碎片;

其次是异常安全,C++17 之前形如 f(sp(new A), g()) 的执行顺序可能是 new A、g()、sp(),一旦 g() 中发生异常,那么 new A 将无法被回收,使用 make_shared 可以避免。

94. 说到线程安全性,在语言层面,你一般使用什么手段?

主要借助各种同步原语,或者一开始从设计上消除 data race。

95. 怎么理解inline的?(主要从C++17的可重复定义角度讲)

  • 允许重定义函数和遍历,无需担心重定义问题
  • 提高代码效率:编译器将程序中出现的内联函数调用表达式用内联函数的函数体替代,类似宏展开,不用压栈。
  • C++17中,还可以修饰变量,内联静态成员变量的主要目的是避免多个源文件中重复定义静态成员变量。我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的

96. const和constexpr有什么区别?

  • const表示常量,一旦被声明不能修改
  • constexpr表示编译时常量表达式。旨在在编译时计算所定义的表达式,而不是等到运行时。(包括局部constexpr)

在可以在编译时计算的情况下,通常推荐使用 constexpr,因为它可以在编译时优化并提高程序运行效率。然而,如果表达式的值在编译时无法确定,那么使用 const 更合适。

97. C++11提供的统一初始化方式叫什么?自己用的多么?

C++11引入了一种成为统一初始化语法的特性,它主要包括列表初始化(花括号初始化或大括号初始化),旨在简化对象的初始化操作。

int a{5};std::vector v{1, 2, 3};struct MyStruct{int x;int y;}; MyStruct ms {10, 20};

98. 初始化列表为什么更快一些?

因为成员变量在构造函数体之前完成了初始化,避免了在构造函数中用内部赋值的方式对已初始化的成员进行重新赋值。

99. vector什么时候会迭代器失效?迭代器失效是什么意思?说说你的理解?

迭代器失效是指迭代器不能再用来有效地访问容器中的元素。

  1. 向向量添加元素,例如使用push_back()emplace_back(),可能导致存储空间重新分配,从而使现有迭代器失效。
  2. 使用erase()删除向量中的一个或多个元素,此时从删除点起的后面部分的迭代器将失效。(返回的是erase元素的下一个)
  3. 使用clear()清空容器,将使所有迭代器失效。
  4. 使用resize()reserve()改变向量容量,可能导致存储空间重新分配,进而使迭代器失效。
  5. 使用swap()std::swap()交换两个向量,将导致它们的迭代器失效。

100. C++ list的底层逻辑?实现原理?

list是一个双向链表,每个元素有两个指针,指向前驱和后继节点。在使用的时候,通过迭代器封装了起来,我们可以使用++和–操作进行移动。另外,list是一个链式数据结构,其内存的分配是离散的,所以list可能比其他连续性容器更容易产生内存碎片。

101. 和vector相比,list有什么区别?在读写元素时有什么优劣么?各自适用于什么场景?

vector

  • 需要频繁访问或修改容器中的元素
  • 主要在末尾进行插入和删除
  • 缓存友好,内存连续

list

  • 需要在容器的中间位置频繁插入删除
  • 不需要随机访问容器中的元素(一半搭配哈希表 LRU)

102. 引用和指针分别适用于什么场景?

  • 引用
    • 函数传递参数,特别是大型数据结构的时候,可以避免数据拷贝
    • 当需要为一个变量创建别名或者实现类似于传统指针的功能
  • 指针
    • 操作数组或其他内存分配的数据结构
    • 实现多态,使用基类指针指向派生类指针
    • 当需要传递一个可选(可以为空)的参数
    • 在一些特定场景下,需要对内存进行直接操作

103. emplace_back更好?能完美替代push_back吗?

在创建临时对象插入时,emplace_back更好,不需要移动构造这一步。

不能完美替代push_back,会有安全问题。

vector<vector> vec(1);vec[0].push_back(1<<20);vec[0].emplace_back(1<<20);//vec.push_back(1<<20);// won't compilevec.emplace_back(1<<20);// created a vector of size 2^20vec.push_back(vector(1<<20));// same as above

104. 介绍下菱形继承

例如,考虑一个具有四个类“A(基类)”,“B”和“C”(两个间接子类)及”D”(直接子类)的情况。假设类B和C都继承类A,并分别对类A的某一个方法进行了重载或覆盖。现在,如果类D同时继承类B和C,那么D对于这个重载的方法应该如何处理就成为了问题。这个问题就是菱形继承问题。这也是C++多继承带来的问题。

C++提供了虚继承(virtual inheritance)来解决菱形继承问题。在虚继承中,子类将只继承一个基类的复制品,从而消除歧义带来的问题。在虚继承中,基类会被最底层的子类直接继承,而不是通过中间类。这样可以确保最底层的子类只有一个基类实例

class Base {public:void baseFunction();};class Der1 : virtual public Base {// ... };class Der2 : virtual public Base {// ... };class Bottom : public Der1, public Der2 {// ... };

105. strcpy和memcpy的区别

strcpy会一直复制,知道遇到\0结束,可能会造成溢出,不安全。memcpy则多了一个要复制的字节数,不会检查终止符。

果你需要复制的是字符串,并且能确保源字符串能够以\0结束,那么strcpy更合适。如果你需要复制的是一段内存,或者不能保证字符串会以\0结束,那么memcpy更合适。

char* my_strcpy(char* dst, const char* src) {char* saved = dst;while ((*dst++ = *src++)); // 结束条件是源字符串的'\0'字符return saved;}

106. C和C++的区别?

  • C++完全兼容C
  • C++包含面向对象,包括封装继承多态
  • 泛型编程,模板
  • STL,各种库函数包
  • 二进制兼容性比C差

107. 多态的实现原理?

  • 静态多态:编译时完成,通过函数重载和函数模板实现
  • 动态多态:运行期间决定。为每一个包含虚函数的类创建一个虚函数表。虚函数表的各项存放着各个虚函数的指针。该类的每个对象会在构造的时候设置一个虚指针,指向该虚函数表。调用虚函数的时候,先利用虚指针找到虚函数表,确认调用的虚函数的入口地址,获取入口地址然后完成调用。

108. const和static的用法

  • const:指定一个语义约束,告诉编译器某个对象不应该被改变。
    • 修饰全局遍历,修饰函数,修饰指针或者指向的值(const int*不能修改值;int* const不能修改地址)
  • static
    • 在全局变量和函数签使用,可以限制其作用范围在当前文件内,避免与其他文件同名的变量或函数冲突
    • 在类中,静态成员变量属于类,静态成员函数只能访问静态成员变量
    • 在函数内部,static可以定义静态局部变量。这种变量会持久存在(即使函数调用结束),只在初始化时赋值一次。

109. 说一下deque

deque是双向队列,结合了vector和list。在内部,他维护了一个指针数组,每个指针指向一个连续的内存块,每一个内存块可以存储多个元素。使得deque在两端插入或删除元素都是常数级的,同时因为内存的存储(同一个内存块中)连续,所以性能也较好。

110. 数组名和指针

数组名是数组首元素的地址,是固定的,不能改变,指针存储的是一个地址。虽然数组名可以像指针一样使用(比如,进行解引用操作和加法操作),并且可以自动地转换成一个指向第一个元素的指针,但数组名并不是能改变的指针。换句话说,数组名是常量指针,数组名是该数组类型的一个左值。

111. 继承

子类继承父类的所有属性和方法,即子类可以访问父类的共有和保护成员。私有成员虽然可以继承,但无法访问。

继承可以提供得到好处有:1. 代码复用:减少冗余代码;2. 代码组织:通过创建类层次结构,可以更好地组织和维护代码。但是继承会破坏代码的封装性。

112. 面相对象三大特征,说说理解

  • 封装:在一个对象中,代码和数据被封装在一起。对象的方法提供了与对象内部状态交互的唯一方式。隐藏了内部信息,提高了代码的安全性和可管理性
    • 隐藏内部细节、增加安全性、简化接口、提高模块化
  • 继承:一个新的类可以从一个已存在的类中继承属性和方法。这样可通过父类的代码对其进行扩展。
  • 多态:多态允许我们进行父类引用的方式来处理一个子类对象。提高了代码的扩展性和复用性。

113. C++的多态,多态要满足什么条件

  1. 继承:要有一个基类和派生类,派生类从基类继承了其定义和行为
  2. 虚函数:在基类中定义虚函数。派生类可以选择重写这些虚函数。当通过基类指针调用这些函数时,实际上调用的将是派生类中重写的函数。
  3. 基类指针:使用基类指针指向派生类对象,这样当基类指针调用虚函数时,就实现了动态绑定,也就实现了多态。

114。 typedef和using的区别,using在什么情况下使用?

  • typedef用于为现有类型创建别名。
  • using是在c++11中引入的,提供了与typedef相同的功能,同时增加了模板别名的功能。

在新的C++代码中,推荐使用 using,由于其提供了模板别名和类型更可读的优点。在特定情况下,比如需要定义模板别名,或者需要以更清晰的方式向右阅读和解析类型时,应该使用 using

templateusing Vec = std::vector;Vec myVector; // equivalent to std::vector myVector;

115. 指针常量和常量指针问题

\1. const int a; //指的是a是一个常量,不允许修改。

\2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变

\3. int const *a; //同const int *a;

\4. int *const a; //a指针所指向的内存地址不变,即a不变

\5. const int *const a; //都不变,即(*a)不变,a也不变

116. 桶的扩容机制是什么?

  1. 初始大小: 创建一个哈希表时,它通常会被初始化为一定大小。这个大小一般是2的整数次幂。
  2. 负载因子: 一般哈希表有一个被称为“负载因子”的概念,用于衡量哈希表的“满程度”。负载因子通常被定义为哈希表中已经存储的项数与哈希表大小的比值。比如,如果我们有一个大小为16的哈希表,并且它已经存储了8个项,那么它的负载因子就是0.5。
  3. 自动扩容: 当哈希表的负载因子达到某个阈值(例如0.75)时,就会发生“扩容”。扩容通常通过创建一个新的更大的哈希表(通常为原来的两倍),然后将旧哈希表中的所有项重新哈希到新哈希表中来实现。这个过程的目的是为了保持哈希表的查询/插入/删除等操作的效率。

117. 指针数组和数组指针?

  • 指针数组:他是一个数组,其元素都是指针。int* arr[3],这里又3个元素,每个元素都是int*
  • 数组指针:是一个指针,int (*p)[10],着个指针指向了一个个包含10个整数的数组的地址

118. 模板有哪几种?

  • 函数模板
template T Add(T a, T b){return a + b;}
  • 类模板
template class SampleClass{private:T data;public:SampleClass(T d){data = d;}};

119. 函数传参访问结构体中的元素用指针好还是引用好?

  • 使用指针传递的好处:
    • 允许传递空指针,这是引用做不到的
    • 指针参数可以被重新指向不同的对象
    • 可以实现函数指针、回调等
  • 使用引用的好处
    • 代码更简洁
    • 不需要解引用

120. 指针访问数组怎么预防越界?

  • 使用迭代器的标准算法:可以不直接处理指针,减少错误的可能性
  • 使用标准库函数:例如vector::at()会自动进行边界检查

121. double转float精度损失

double精度在15~17位,float在6 ~ 9位,如果从double转到float,会导致截断,产生不正确的结果。

122. C继承自B,B继承自A,为什么构造C,会先构造A和B?

理由是派生类可能依赖于从基类继承的一些成员或行为。这些成员或行为可能在基类的构造函数中被特殊处理。如果不先构造基类,那么派生类无法正常工作,因为他继承自基类的部分还未被正确初始化。

123. 深拷贝浅拷贝区别?

  • 深拷贝会赋值对象内部的所有数据,重新拷贝一份新的,修改原始对象不会影响深拷贝的对象
  • 浅拷贝仅仅只是赋值了原始对象的引用,因此,如果修改了原始对象,那么浅拷贝的对象也会跟着改变

124. 静态变量作用?

  • 持久性寸尺:静态变量只会被初始化一次,即使在函数多次调用的过程中,他的值也只会被初始化一次
  • 生命周期:静态变量的生命周期和程序的生命周期相同,存储在全局静态区,其内存地址不会发生改变
  • 类相关的数据:在类中,静态函数只能调用静态变量,静态变量不属于某个对象,而是属于该类,也节省了内存空间。在单例模式中很常用

125. C++内联函数和宏函数的区别?和普通函数的区别?

  • 内联函数的调用是传参,而宏定义只是简单的文本替换
  • 内联函数可以在程序运行时调用,而宏定义会在预编译阶段进行替换
  • 内联函数运行时有类型检测,更加安全

相比于普通函数,内联函数的主要特点是他们在编译器中的指令是内联的,减少了函数调用压栈的开销。但是,如果内联函数体非常庞大,那么他的调用会变得非常低效。

126. 两个.c文件都有一个同名的static函数上,编译链接时期会不会报错?

不会,static作用域只在其所在的源文件中,不会被其他源文件访问到。

127. c++ const和constexpr的区别

  • const用于声明一个运行时常量
  • constexpt用于声明一个编译时的常量,效率更高

Goland

1. golang管道怎么用?

golang的管道是一种用于在不同goroutines之间通信的数据结构。使用channel,可以避免手动同步数据传输和竞争条件。

使用的时候先定义和创建管道ch := make(chan int),然后向管道发送数据ch <- 42,另一边接收数据value := <- ch

2. golang的goroutine泄露

当一个goroutine在生命周期结束后还继续占用内存资源时,就会发生泄漏。可以使用sync.WaitGroup等待所有goroutine结束,确保在启动新的goroutine时sync.WaitGroup递增,Done递减。

3. map的底层?

Go的map底层是基于哈希表是新鲜的。在Go的哈希表实现中,底层会自动进行扩容与收缩操作。此外,为了处理哈希冲突,Go采用了开放地址法,即从当前位置开始探测,直到找到一个空闲的位置来存储新的键值对。

4. go slice底层和array

slice是动态数组,它是一个更加灵活和用户友好的数据结构。它们的底层实际上是由固定长度的数组实现的。

s := make([]int, 5, 10)// 创建一个int型slice,长度为5,容量为10

当空间不足的时候,go会创建一个新的数组,将原来数组的元素复制到新数组中,再追加新的元素。如果元素小于1024,则两倍扩容,反之1.25倍扩容。此外,Go还可以选择以2的幂次方增长,避免了内存碎片化的问题。

5. map扩容

map扩容机制遵循以下原则:

  1. 负载因子:是map中当前元素数量除以map的容量(桶数)。go中,默认负载因子阈值是6.5.当map负载因子超过阈值时,会触发扩容操作。
  2. 桶:go中的map数据结构采用桶来存储键值对。每个桶可以存储8个键值对,当一个桶存满8个元素后,新增元素将导致哈希冲突,需要解决冲突保持性能。

扩容策略

  1. 翻倍
  2. 分布扩容:在某些情况下,为了减小一次性扩容带来的性能影响,Go 语言采用渐进式方法来进行扩容。每次更新 map 时,将会将一部分旧 map 的元素移动到新的 map 中。这样,扩容过程可以在多次更新操作中逐渐完成,降低对性能的冲击。

6. 协程怎么调度的

Go 运行时实现了一种名为 M:N 的调度模型,其中 M 代表操作系统线程,N 代表 Goroutine。

在 M:N 的调度模型中,存在以下关键组件:

  1. M(机器):操作系统线程。这些线程是由操作系统调度器来调度的,但是它们的数量在 Go 程序中通常相对较少。Go 语言的运行时会在程序开始时根据可用的 CPU 核心数创建一些线程。
  2. G(Goroutine):Go 协程。这些是由用户代码创建的轻量级线程。Go 运行时负责在 M 之间调度和平衡这些 Goroutine。
  3. P(处理器):表示 Go 运行时中的上下文,它用于将 Goroutine 调度到可用的 M。P 的数量在程序启动时根据可用的 CPU 核心数确定。每个 P 都有自己的本地队列,用于存储等待运行的 Goroutine。此外,还有一个全局队列,用于存储还未分配给 P 的 Goroutine。

以下是 Go 协程的调度过程:

  1. 当一个 Goroutine 开始运行时,运行时会把它添加到当前 P 的本地队列。
  2. 当 M 空闲时,它会查找与它关联的 P 的本地队列中的 Goroutine。如果有等待的 Goroutine,它将运行 Goroutine。如果本地队列为空,则会尝试从全局队列或其他 P 的队列窃取工作。
  3. 如果一个 Goroutine 在运行时阻塞(例如通过输入/输出操作、通道、互斥锁等情况),M 则会将其放回队列,并尝试调度其他 Goroutine 继续执行。
  4. 被阻塞的 Goroutine 在解除阻塞时会被重新调度,等待资源可用后恢复执行。

总之,Go语言中的协程调度机制采用了M:N模型和抢占式的调度方式,使得协程的创建和销毁更加高效,并且可以更好地利用多核处理器的优势。

7. go的协程一直不退出会怎么样,会被系统回收吗?

不会被系统自动回收。这可能会导致资源泄漏,最终可能导致内存耗尽或系统响应缓慢。为了避免该问题,需要使用 sync.WaitGroup、或通道(channel)等机制来协调协程的执行和退出。当协程完成其任务并结束时,Go 运行时会正确地回收资源。

8. 介绍一下select

select 语句用于处理多个 channel 操作。它用于在多个通道上进行等待发送或接收操作。当任何一个通道成功发送或接收后,select 语句自动执行其中的相应代码块。如果多个通道同时满足条件,那么 select 将随机选择一个满足条件的通道执行。

select {case <-channel1:// 当从 channel1 中成功接收数据时执行此代码块case channel2 <- someValue:// 当成功向 channel2 中发送数据时执行此代码块default:// 如果以上任何一种操作均未执行,则执行此代码块}

9. Go和C++有什么不同?

二者都是编译型语言,支持指针操作和内存管理。C++更注重底层,需要程序员自己处理许多细节,例如内存的申请和释放等。而Go提供了内存回收机制,且上手容易,且有goroutine协程的存在,可以高效开发高并发的程序。

  • 类型系统:Go是一种静态类型语言,这意味着在编译时需要直到所有变量的类型。C++是静态类型语言,但也支持动态类型,比如多态。
  • 内存管理:Go有内置的垃圾回收机制,大大减少了程序员因内存管理错误而引发的问题。
  • 包和导入:Go语言中的源文件可以作为一个包,包含多个源文件;而在C++中,每个源文件通常都是独立的
  • 适用场景:C++适合底层开发,Go适合服务器高并发开发、分布式、云平台等

设计模式

1. 什么时候要用到设计模式

  1. 扩展代码
  2. 自己开发模块,根据业务变化方向,选定设计模式
  3. 解决重复出现的代码,期望修改少量的代码,就可以适应需求的变化

2. 策略模式?(不同节假日打折)

定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换。该模式使得算法可独立于使用他的客户程序而变化

(多态的最基本使用,将算法抽象成接口,子类各自实现该接口)

3. 观察者模式?(通知不同气象台,气象台执行自己的操作)

定义对象间的一种一对多的关系,以便当一个对象的状态发生改变时,所有**依赖(不是继承)**于他的对象都能得到通知并能够自动更新。

实现:

  • 接口指针:容器接收不同的观察者
  • 依赖注入AttachDetach

4. 写一个单例模式?

单例模式的核心是保证类的实例在程序中只有一个。我们可以确保在整个程序声明周期内,这个特定的实例是唯一可访问的,从而减少程序中的资源消耗和避免错误。

// C++11 线程安全class Singleton {public:static Singleton* getInstance() {static Singleton single;return &single;}private:Singleton(const Singleton& single) = delete;Singleton() = delete;Singleton& operator=(const Singleton&) = delete;};
  • 饿汉式:类文件加载的时候已经创好了,如果一直没使用,则会浪费空间(直接在类外初始化,对象在类中,静态变量)
class Singleton {public:static Singleton* getInstance() {static Singleton single;return &single;}private:Singleton(const Singleton& single) = delete;Singleton() = delete;Singleton& operator=(const Singleton&) = delete;};Singleton Singleton::instance;// 全局创建,这样在编译阶段就创建了int main() {Singleton& instance1 = Singleton::getInstance();instance1.display();Singleton& instance2 = Singleton::getInstance();instance2.display();return 0;}
  • 懒汉式:使用的时候才创建,多线程访问不安全,因为创建实例的过程不是原子操作(调用的时候创建,双检查锁null、lock、null)
    • 为什么要加锁双重判断,只判断一次不行吗:可以,但是性能会比较低,因为你每次都需要加锁再判断是否创建实例
  • C++11:懒汉模式不用加锁,因为C++11对于static变量,自带使用了单例模式,则不需要我们再检查了

5. 设计模式几大原则?

  1. 单一职责原则:一个类只有一个原因引起变化
  2. 开放封闭原则:对扩展开放,对修改封闭(通过添加新代码来扩展功能)
  3. 里氏替换原则:子类能够代替父类(父 = new 子
  4. 接口隔离原则:不应该强迫用户依赖他们不适用的方法
  5. 依赖倒置原则:高层不能依赖低层,他们都应该依赖抽象(接口或抽象类)

6. 具体的设计模式?

  • 创建型模式
    • 工厂模式
    • 单例模式
  • 结构型模式
  • 行为型模式
    • 观察者模式
    • 策略模式

7. 谈谈你对工厂方法模式的理解

工厂模式是创建型设计模式的一种。避免简单工厂模式的区别,不能满足开闭原则,引申出了工厂方法模式。工它将对象的创建过程封装到抽象工厂类中的一个接口方法(也称为工厂方法)。具体的工厂类继承抽象工厂类并实现工厂方法,负责实例化特定类型的对象。客户端通过调用工厂方法获得所需对象,而无需直接创建。这样,如果需要更改对象的创建方式,只需要修改工厂类的子类即可,而无需修改客户端代码,客户端也不需要关心产品对象的具体细节。

优点

  • 就是降低了客户端代码和具体产品间的耦合,使得添加新产品比较容易。

  • 符合开放封闭原则,在不修改源代码的情况下,可以扩展新的产品类。

应用场景

当编写代码的过程中,如果无法阈值对象确切类别以及依赖关系的时候,可以使用工厂方法。

8. 设计模式有什么了解?

设计模式实在软件开发中,用于解决特定环境下、重复出现的、特定问题的解决方啊,。设计模式可以避免重复造轮子的问题,解决代码间的高耦合问题,使得开发者在开发代码和后续维护上更加方便。

9. 比较一下组合和继承?组合和聚合呢?

  • 聚合

    聚合关系是has-a,是一种弱的拥有关系,B不是A的一部分,生命周期不同

  • 组合

    二者是part-of的关系,生命周期相同(比如在A初始化时实例化B)

10. 策略模式和观察者模式

都属于行为模式。

  • 策略模式主要用于根据上下文动态控制类的行为,一方面可以解决多个if else带来得到代码复杂性和维护问题,另外各方面把类的不同行为进行封装,使得程序可以进行动态的扩展和替换,增加了程序的灵活性
  • 观察者模式主要是用在一对多的对象依赖关系中,实现某一个对象状态变更之后的感知的场景,一方面可以降低对象依赖关系的耦合度,弱化依赖关系,另一方面通过这种状态通知机制,可以保证这些依赖对象之间的状态协同

操作系统

1. 计算机内存分为哪几部分?

  • 栈区(stack):

用来存储函数调用时的临时信息的结构,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。

  • 堆区(heap):

一般由程序员分配、和释放,用来存储程序运行时分配的变量。

  • 全局区(静态区static):

存放全局变量、静态数据。程序结束后由系统释放。
全局区分为已初始化全局区(data)和未初始化全局区(bss)。

  • 常量区(文字常量区):

存放常量字符串,程序结束后由系统释放。

  • 代码区:

存放函数体(类成员函数、静态函数和全局函数)的二进制代码。

https://blog.csdn.net/qq_49286390/article/details/126561732

2. 进程、线程的区别?

  1. 进程是资源分配的基本单位,线程是调度的基本单位

  2. 一个进程可以有多个线程,至少有一个主线程

  3. 线程的系统开销小,启动速度快,更轻量

  4. 同一线程共享的有:堆、全局变量、静态变量、指针、引用等,独自占有:TCB、寄存器、栈

  5. 通信上来说,进程通信用到管道、套接字、共享内存等,比较复杂;线程共享数据段,所以通信方便

什么时候用进程、线程

  • 创建销毁频繁,用线程

  • 并行操作多用线程,要求稳定用进程

3. 进程、线程、协程的区别?

进程是资源分配的基本单位,线程是轻量级的并发执行单元,协程更轻量级,它通常在用户态进行切换,因此可以更高校地执行多任务并发,这种编程模型在处理大量 I/O-bound 或网络操作的任务时非常有益。

协程拥有自己的寄存器上下文和栈,一直处于用户态。

4. 不同进程之间是否可以共享内存?父子进程也不可以吗?

不同进程间不能共享内存,因为不同进程有独立的虚拟地址空间,可以采用进程间通信(IPC),如共享内存、管道、socket、信号、消息队列等。

父子进程也不会共享内存,可以使用管道进行通信。

5. 软链接和硬链接的区别?

  • 软链接文件与源文件的inode号不同,软链接相当于快捷方式
  • 硬链接的inode号与源文件相同,相当于源文件的另一个文件名,它允许文件系统中具有多个指针,这意味着删除指向数据的一个硬链接不会导致数据本身被删除,除非所有硬链接都没了,有点类似于引用计数为0了才释放。

各自的应用场景

  • 软链接
    • 链接到目录
    • 创建快捷方式
  • 硬链接(无法跨文件系统)
    • 文件系统跨目录共享文件
    • 备份和版本控制:创建不同版本的文件集合,对其中一个版本的修改不会影响到其他版本
    • 减少磁盘空间使用:硬链接不会创建原始文件的副本,只在文件系统种创建新的索引。

6. 755含义

三个数字分别代表用户、组、其他用户的权限,一个数字为3个二进制数字,每一位对应可读、可写、可执行。

这里的755就说明文件拥有者有完全的权限,组内其他用户和其他组用户只能读取和执行,不能写入。

7. 改变文件权限命令?

chmod

使用方法:chmod 755 example.txt

8. 通过网卡抓数据包命令?

linux上可以通过tcpdump命令通过网卡抓取数据包,windows上可以用Wireshark

9. 进程通信的优缺点?

  • 管道:简单,父子进程间通信;但只能半双工
  • 信号:轻量级;但无法传递参数
  • 共享内存:高性能;但需处理一致性问题
  • 套接字:全双工、跨平台;但复杂
  • RPC:可以远程过程调用;但设计操作系统和应用程序间的交互

10. 线程同步手段

  • 互斥锁
  • 条件变量:搭配锁使用,以确保资源获取;还可以设置超时时间
  • 信号量:管理有限资源的访问

11. 堆栈溢出可能的情况?怎么处理?

堆栈溢出(stack overflow)是计算机程序在运行过程中遇到的一种错误。这种错误通常发生在程序使用过多的内存或者递归层数过深时,导致计算机无法为程序分配更多的栈内存。

  1. 避免递归过深:检查代码,是否有出递归的路口
  2. 增大程序的可用栈空间
  3. 检查数据结构:或许该数据结构过大,考虑优化、采用链表

12. 什么是虚拟内存?

虚拟内存使得每个程序认为自己独占内存,从逻辑上增大了内存量,使得多道程序能够同时运行,只需将运行程序的基本少数页面放入内存即可,当需要时再从外存中调入内存。这样也可以防止其他程序访问,保护了程序。

13. 数据从磁盘到内核的整个过程

  1. 寻道:磁盘会移动磁头以找到数据所在的磁道
  2. 旋转延迟:此时,磁头在正确的磁道,但需要等待磁盘转到数据开始的地方
  3. 读取数据
  4. 数据传输:将数据传输到内存中
  5. 操作系统内核处理数据:此时操作系统会处理读取的数据

14. 条件变量,信号量的底层实现。

  1. 条件变量:允许一个线程等待特定条件的发生。操作系统为条件变量提供了一组系统调用。POSIX里面是pthread_cond_系列函数
  2. 信号量:用于控制堆共享资源的访问,维持一个计数器,底层实现依赖于原子操作、自旋锁(尝试获得锁,如果已被占有,他会在一个循环中自旋,持续检查锁的状态,避免线程切换的开销,以及避免了线程阻塞和唤醒带来的成本)

15. 讲讲零拷贝,有哪些函数?

零拷贝是一种避免在数据传输过程中进行不必要的数据复制的技术,

  1. mmap + write:可以将一个文件映射到内存,直接访问文件内容,不需要进行缓冲区的拷贝。

16. 虚拟内存怎么映射到物理内存的?

  1. 分页:将虚拟内存和物理内存分成固定大小的块4KB。使得不连续的物理内存可以映射到看似连续的虚拟内存
  2. 页表:表示虚拟内存到物理内存的映射关系。MMU通过查询页表将虚拟内存地址转换为物理内存地址
  3. 页面置换算法:当发生缺页时,使用页面置换算法换入换出页面。

17. 操作系统的中断和异常?

  • 中断:中断是硬件设备产生的外部事件,用于通知操作系统执行某些任务,如键盘输入、处理网络请求等。执行步骤如下:
    • 保存当前执行的程序状态
    • 切换到内核模式
    • 执行预先定义的中断处理程序
    • 恢复原始程序状态并继续执行
  • 异常:异常是程序执行过程中遇到的错误或异常,如除以0、内存访问错误等。异常是一个同步事件。
    • 保存当前执行的程序状态
    • 切换到内核模式
    • 执行预先定义的异常处理程序
    • 根据处理结果,恢复原始程序并继续执行

二者区别在于,中断是由外部硬件产生的,异常是程序产生的

18. CPU密集型采用多进程还是多线程?

  • CPU密集型:是指一个任务主要依赖CPU的计算性能,它再执行过程中消耗大量CPU资源。

  • IO密集型:依赖IO操作的速度,如磁盘读写和网络通信。

19. 死锁产生的条件,怎么判断是否产生死锁

  • 互斥条件:资源不能被共享访问
  • 不剥夺条件:进程未使用完资源前,不能被其他线程抢夺
  • 请求并保持条件:进程已经获得了一个资源,但是又提出了新的资源请求,但该资源被其他线程占有,此时请求进程被阻塞,但不是放自己的资源
  • 循环等待条件:存在一种进程资源的循环等待链。

以上四个条件必须同时满足才能死锁。

如何避免死锁

  1. 资源分配优先级
  2. 定时请求
  3. 预先分配:在进程开始时分配所需资源,避免等待其他进程释放资源的情况
  4. 检测和恢复:允许死锁发生,当检测到时,释放部分资源以解除死锁

20. 你了解哪些锁?

  • 互斥锁:一个线程只能独占锁
  • 读写锁:读锁可以被多个线程获取,写锁只能被一个获取。当写锁占领时,读锁不能获取。
  • 自旋锁:是互斥锁的一种变体。线程试图获得已被锁住的资源,它会不断地忙等待,直到获取锁。它不会让线程处于休眠状态,在某些情况下可以减少上下文切换的开销。

数据库:

  • 乐观锁:在数据处理前,不会加锁。而是在实际修改数据时,检查数据是否已被其他事务修改。若被修改,则进行错误处理。适用于读操作多、冲突少的场景
  • 悲观锁:任务资源冲突概率高。在读写操作开始时,就加锁保护数据。适用于写操作多、冲突较多的场景。

21. 既然栈比堆的效率高,为什么不全用栈

  1. 栈大小空间有限,如果程序需要更多内存,会出现栈溢出的情况,导致程序崩溃
  2. 栈的声明周期很短,一旦函数调用完成,就会回收。如果需要在函数外保留数据,就不能存储在栈上

22. 分页和分段的区别

分页分段
1在分页中,进程的地址空间被划分为固定大小的页面在分段中,进程的地址空间被划分为大小不同的段
2操作系统负责分页编译器负责分段
3页大小由硬件决定段大小由用户给出
4速度比分段块分段速度慢
5分页会导致内部碎片分段导致外部碎片
6分页中,逻辑地址被划分为页号和页偏移分段中,逻辑地址被划分为段号和段偏移
7分页包含一个页表,页表包含每个页的基地址分段包含段表,段表中包含段号和段偏移量
8分页对于用户不可见分段对于用户可见
9在分页中,处理器需要页号和页偏移来计算实际物理地址分段中,处理器使用段号和段偏移量计算地址
10分页是根据地址空间划分(物理上)分段是根据应用程序进行划分(逻辑上)
11分页是一维地址空间(基址)分段是二维地址空间(段长、基址)

23. 信号量和锁有什么不同?

  • 实现方式:锁是自旋等待,而信号量是进入睡眠等待
  • 适用场景:锁只允许一个线程拥有,而信号量可以根据资源数量让多个线程共同访问
  • 效率:锁效率较高,因为它不用进行状态切换

24. 操作系统分页机制

分页机制是一种内存管理技术,它将内存分为固定的块,成为页,每块4KB。有了这个技术,可以提高内存利用率,并实现进程间的地址隔离。每个进程都有一个页表,通过页表,我们可以将虚拟地址映射到物理地址上。不仅从逻辑上增加了内存空间大小,还保护了每个进程的内存被其他进程访问的风险,同时还降低了外部碎片。

25, 生产者消费者模型,应用?

生产者消费者模型是一种经典的并发设计模式,用于解决在多个线程间进行有限资源共享的问题。生产者负责生产数据,消费者线程负责处理数据。二者之间的通信通常用一个共享的缓冲区(例如队列)来实现,生产者将数据放入队列,消费者从队列中取出任务。

26. socket与其他通信方式有什么不同?

  • socket是面向连接的,而其他通信方式如共享内存、管道、消息队列等是无连接的
  • 数据传输方式:socket是通过TCP协议进行数据传输,是可靠的、面向字节流的;而其他通信方式的数据传输可能是不可靠的,例如共享内存、管道
  • 通信范围:socket可以实现不同主机的通信,而其他通信方式只能用于同意主机内的进程通信
  • tt哦那个新协议:socket用的是TCP/IP协议,而其他通信方式可能使用其他的协议。

27. 共享内存安全吗,有什么措施保证?

共享内存相对高效,但是存在风险,因为任何人都可以加载应用程序访问定义在文件中的共享内存分区。

措施

  • 访问权限:为共享趋于设置读/写权限,以防止非授权进程访问。另外,可以为共享内存对象设置访问权限,只允许特定用户房屋内。
  • 对数据进行加密

28. linux中内存分布有哪些,cpp呢?

linux

  • 物理内存
  • 内存管理单元(MMU)
  • 操作系统内核
  • 用户空间

cpp:cpp的内存分布是通过虚拟内存实现的

  • 全局静态区
  • 数据代码段
  • 常量区

29. 说一说共享内存

共享内存是进程间通信的一种方式。不同进程间共享的内存通常为同一段物理内存,进程可以将同一段物理内存连接到他们自己的地址空间中。所有的进程都可以访问共享内存中的地址。

优点:是进程通信方式中最快的一种。访问共享内存和访问进程独有的内存区域一样快,并不需要通过系统调用或者其他需要切入内核的过程来完成。同时它避免了对数据的各种不必要复制。

缺点:没有提供同步机制,使得我们在使用共享内存时,需要借助其他手段保证进程间的同步工作。(信号量、互斥锁等)

30. 虚拟内存怎么实现的?

虚拟内存通过软件和硬件的协作,为每个进程提供独立的地址空间,使得每个进程都可以像拥有整个内存一行使用内存,从逻辑上扩大了内存。虚拟内存的实现主要依赖于以下几个关键部分:

  1. 分页和分段:分页将物理内存划分为固定大小的页面,当进程访问虚拟内存时,操作系统会通过一个叫做“页表”的数据结构将虚拟页面映射到物理页面。
  2. 硬件支持:通过MMU查询页表,从虚拟内存地址映射到物理内存地址。
  3. 如果发生缺页,则触发缺页中断,通过页面置换算法进行调页。

31. 原子操作,你了解吗,底层原理是什么?

原子操作主要用于多线程保证数据一致性和同步,确保原子操作可以避免多线程代码出现竞态条件。

现代计算机底层硬件提供支持原子操作的指令,主要有以下类型:

  1. 原子读/写操作:这些类型的原子操作允许你从内存中原子性地读取或写入一个值。这意味着读取或写入操作在同一时间只能被一个线程执行,否则它会被阻塞。
  2. CAS操作:它可以原子性地检查某个内存地址上的值是否与给定值相等。如果相等,它会使用新值替换该内存地址上的值。否则,它返回失败。

原子操作的底层原理通常依赖于处理器架构和硬件指令集。通常,硬件提供一组特殊的原子性指令,用于在多处理器或多核系统中实现原子操作。

32. 栈的大小是多少?

栈的大小和操作系统有关。再64位的linux和windows中,线程栈的默认大小通常为2M或8M。

此外,在某些极端情况下(如深度递归),可以考虑将一些递归调用代码转换为非递归调用来避免栈溢出。

33. lru 虽然命中率很高,但实际效率不是很高为什么,有什么优化方法?

  • 预测不准确:LRU是基于访问数据的时间信息进行预测,但在实际情况中,历史访问模式并不总时准确预测未来访问模式
  • 预处理时间:对访问时间和顺序进行管理会带来一定额外开销。

优化方法:

  • 使用LFU:对每个数据项的使用频率进行记录,选择使用频率最低的进行替换,而不是仅仅保留最近使用过的数据。
  • 用2个缓存队列,一个用来存储短期访问的数据,另一个存储长期访问的数据。

34. 什么是内存碎片?如何减少内存碎片?

  • 外部碎片:外部碎片是未分配的内存,但因为内存太小了,无法分配给任何进程。
  • 内部碎片:已经被分配出去,能明确指出属于哪个进程,却不能被利用的内存空间。

操作系统为了防止运行时造成的内存地址冲突,引入了虚拟内存地址,为每个进程提供了一个独立的虚拟内存空间,使得进程以为自己独占全部内存资源。操作系统使用分段和分页的机制,管理虚拟地址与物理地址的映射关系。正是由于操作系统的这种内存管理机制导致了内存碎片的产生。

分段机制就是根据你的需求分配内存,需要多少给多少,所以不会有内部碎片。但是由于每个段的长度不固定,多个段不宜能能恰好使用所有的内存空间,所以就会有外部碎片。

内存分页将整个虚拟内存和物理内存空间分成一段段固定大小的片,虚拟内存和物理内存的映射以这个片为最小单位进行管理。在linux上,页的大小为4kb。但由于内存分页机制分配内存的最小单位为一页,所以即使不足一页大小的程序也会分配一页,导致页内会出现内存浪费,造成内存碎片。

如何减少内存碎片?

  • 采用段页式管理机制,虚拟内存首先被划分为段,然后每个段被进一步划分为固定大小的业面。每个段的大小取决于实际需要的内存大小,使得内部碎片较少发生。
  • 重用内存:重用已分配内存可以减少内存分配释放的次数,从而减少内存碎片。
  • 合并相邻的空闲块:在内存被释放后,合并相邻的空闲块以减少外部碎片。

35. 分段和分页区别?

分段和分页都是内存管理的技术,主要目的是提升内存利用率和保护程序。分段机制就是根据你的需求分配内存,需要多少给多少,不会产生内存碎片。但是由于每个段的长度不固定,多个段不能恰好使用完所有的内存空间,就会产生外部碎片。而分页是将虚拟内存和物理内存分成一块块固定大小的片,虚拟内存和物理内存的映射以这个位最小单位进行管理。在linux中,一页的大小为4kb,这些页在逻辑地址上是连续的,但是物理地址不一定是连续的,通过页表映射。但是由于内存分配机制分配的最小单位为一页,所以不足一页大小的内存也会分配一页,会导致内部碎片,造成内存浪费。

从地址映射上来看,页表是一维的,即页号和页内偏移量;而段表是二维的,包括段号、段长、段基址。

分段更适用于逻辑上已经划分好的程序结构,比如代码、数据、堆栈,对程序员是可见的;而分页对程序员是不可见的,适用于随时分配和回收内存的程序,并且允许触发缺页中断,进行页面置换。

36. cpu流水线

流水线将处理其中的指令执行分成若干个操作阶段,这些操作可以同时进行,以优化处理其性能。流水线设计的核心思想是将一条较长的操作拆分为几个较短的操作,以便每个操作在处理器中相对更短的时间内完成。

  1. 取指令
  2. 指令译码
  3. 执行/地址计算
  4. 内存访问
  5. 写回

37. 内存模型

  • 内存层次结构:内存按其存储和访问速度划分为多个层次。速度从低到高为:磁盘、内存、高速缓存、寄存器。价格也依次上涨。
  • 地址空间和内存模型:每个进程再操作系统中拥有独立的地址空间,这主要是依靠虚拟内存实现的。通过页表将逻辑地址映射到物理地址,不仅能保护进程,还能从逻辑上扩大内存容量,使多个进程在内存中同时运行。当实际内存空间不足时,就会触发缺页中断,将不常用的页面置换出去。

38. 操作系统的页面置换算法?

  • FIFO先进先出算法:顾名思义,将最早加载到内存的页面进行置换。
  • LRU最近最少使用算法:将最近一段时间最久未被访问的页面进行置换。
  • 时钟置换算法:与LRU相似,但效率更高。一个指针始终指向该环的一个位置。当需要置换页面时,算法会检查指针所指向的位置的页面。若该页面被访问过,其访问标志会被设置为0,然后指针向前移动一位。这个过程会循环进行,直到找到一个访问标志为0的页面。该页面会被置换,并将指针移动到新页面所在的位置。

39. 进程为什么切换慢?

  • 上下文保存:操作系统需要保存当前进程的上下文信息(CPU寄存器、内存管理器状态、程序计数器等),以便稍后恢复。
  • 上下文恢复:操作系统从准备好运行的进程队列中选取一个进程,将其上下文恢复到CPU和内存中,使其继续执行。
  • 缓存失效:当进程切换时,CPU缓存中的数据可能会失效或者变得不相关,由于缓存需要重新填充,会导致性能下降
  • TLB失效:TLB是内存管理硬件的一部分,加速了虚拟地址到物理地址的转换。进程切换可能导致TLB条目失效或需要更新,从而导致性能下降。

为什么进程切换比线程慢?

  • 线程间共享地址空间,而进程切换涉及不同的地址空间的转换,这需要操作系统进行内存映射和页表等的调整。
  • 数据同步和通信:进程间需要用IPC通信,相对较慢;而线程可以访问同一地址空间的数据。
  • 缓存利用:线程切换时,多线程可以在同一个核心上运行并利用高速缓存。而进程切换可能涉及到核心或处理器之间的切换,导致本地缓存失效,需要重新加载需要的数据,从而增加了延迟。

40. 操作系统打开文件夹,是怎么打开的,window打开文件夹大概是怎么实现的?

  1. 用户指令识别:当在图形用户界面上双击文件夹图标,或者在命令行输入相关命令时,系统会识别这为一个打开文件夹的指令。
  2. 查找文件夹位置:操作系统接下来会检查你希望打开文件夹的具体位置。对于桌面环境,每个图标都对应一个特定的路径。在命令行中,路径会被明确指定。系统会查找文件系统来定位这个文件夹。
  3. 读取索引信息:操作系统会通过文件系统来读取文件夹的索引信息以获取文件夹内容
  4. 显示文件夹内容:操作系统将文件夹内容显示在文件管理器或命令行界面。在图形界面,这会是图标和文件/文件夹名的列表;在命令行界面,这通常是包含文件/文件夹名和一些基本信息的文本列表。

41. 说一下进程被动、主动释放CPU的场景

  • 主动释放
    • 等待IO完成,如磁盘读写、网络数据接收。
    • 进程进入休眠状态sleep()或等待某个条件满足
  • 被动释放
    • 进程的CPU时间片用完了,操作系统会中断该进程的运行,并将CPU分配给下一个进程
    • 高优先级进程需要用CPU,抢占该进程的CPU

42. malloc是如何分配内存的?

  • 如果用户分配的内存小于128KB,则通过brk()申请内存(从堆区映射)
  • 如果用户分配的内存大于128KB,则通过mmap()申请内存(从文件映射区映射)

43. 内存碎片如何处理?

  • 内存整理:通过物理移动程序和数据,将空闲内存组合成一个大的块。比较耗时
  • 对于分配的内存空间回收后,不急着放回内存,下次申请时重复利用该区域
  • 分段分页:分页减少外部碎片,分段减少内部碎片

44. 信号量怎么来同步线程的?

信号量内部维护了一个计数器,当信号量大于0的时候请求资源会减一,释放资源会加一。当信号量为0了,再请求资源就会阻塞,等待其他线程释放资源。该加减操作都是基于原子完成的。

45. 锁底层怎么实现?

锁是用系统级别的原子操作实现的。这些原子操作保证多个线程之间进行同步操作时,操作都是不可中断的,依赖于硬件和操作系统。

46. 什么场景下用多进程,什么场景下用多线程?

多进程

  • 计算密集型任务:充分利用CPU性能,使得每个CPU都能都在各自的地址空间内独立运行,安全性高
  • 任务隔离:如果需要隔离任务以提高稳定性,那么多进程是适合的。每个进程都有自己的内存空间,这意味着一个进程崩溃不会影响别的进程

多线程

  • IO密集型任务:线程间切换成本更低,能更有效处理阻塞和请求并发
  • 共享数据:如果有大量数据共享和操作,多线程更方便,因为多线程共享同一个内存空间。但需要考虑资源竞争数据同步问题

47. 解释下系统调用

系统调用是运行在用户模式的程序请求操作系统内核为其服务的一种机制,可以看作是程序与操作系统之间的接口。

在现代操作系统中,由于安全和稳定性的原因,系统被分为至少两种运行模式:用户模式(User Mode)和内核模式(Kernel Mode)。用户模式下,程序所能使用的指令和能直接访问的内存空间都受到了限制,例如它无法直接对硬件设备进行操作,必须通过内核提供的接口。这时,如果用户态的程序需要使用到某些由内核提供的服务(如文件操作、网络操作、设备操作、内存管理等),就需要通过系统调用。

  • 文件操作:open()、read()、write()、close()等。
  • 进程和线程管理:fork()、exec()、exit()、wait()、创建线程、结线程等。
  • 网络通信:socket()、send()、receive()等。
  • 设备操作:这通常是通过对文件的操作来完成的。在许多系统中,设备被视为特殊的文件。

48. 为什么会发生用户态和内核态的切换?

操作系统设计的一个主要目标是确保计算机系统的稳定性和安全性。因此,为避免用户级应用程序直接操作硬件,通常会将操作系统的功能分为用户模式(User mode)和内核模式(Kernel mode)。

用户态和内核态之间的切换主要出现在以下几种情况:

  1. 系统调用:例如,当一个程序需要打开一个文件时,它需要通过系统调用告诉操作系统。此时,程序需要从用户态切换到内核态,以便操作系统可以安全地执行这个操作。
  2. 中断:当硬件发出一个中断请求时,操作系统需要切换到内核态以处理中断。
  3. 异常:当程序执行出错,如分页错误(page fault)或者除零错误时,会触发异常,此时也需要切换到内核态来处理异常。

39. mmap是做什么的?

允许我们将一个普通文件映射到进程的地址空间,此后,进程就可以像访问普通内存一样对文件进行访问,而无需使用read()、write()等操作。总的来说,mmap的主要作用是为了提高内存使用效率和文件读写速度,它通过将磁盘上的数据以页为单位映射到进程的虚拟地址空间,使得对数据的访问更加高效。

计算机网络

1. 拥塞控制和流量控制的区别

他们都是保证TCP可靠的方法。流量控制是防止发送方过快导致对方不能接收的问题,防止包丢失,属于通信双方的协议;拥塞控制是防止链路上的资源太多,导致网络负载过大,作用于整个网络。

2. tcp,udp,ip头部有什么?

  • IP:40个字节(挑几个说)
    • 版本号(4位)
    • 头部长度(4位)
    • 生存时间(TTL)(8位)每经过一个路由器,TTL减一,变成0直接丢弃该包
    • 源IP地址(32位):发送数据包的设备的IP地址
    • 目标IP地址(32位):接收数据包的设备的IP地址
    • 标识:用于分片重组。不同分片的标识值不同。每发送一个IP包,它的值也会逐渐递增
    • 标志(3位):控制分片的标志位。
    • 片偏移:用于标识被分片的每一个分段相对于原始数据的位置
  • TCP
    • 源端口(16位)
    • 目的端口(16位)
    • 序列号(32位):数据包中第一个字节的序列号(seq)
    • 确认号(32位):确认已收到对方发送的数据,并期望接收下一个字节的序列号(ack = seq+1)
    • 校验和(16位):用于校验TCP头部和数据是否被破坏
    • 控制位(6位):SYN,ACK,FIN,RST,PSH,URG
  • UDP
    • 源端口(16位)
    • 目的端口(16位)
    • 长度(16位):整个UDP数据包的字节数
    • 校验和(16位):用于检查UDP头部和数据是否被破坏

校验和作用:可以检测到数据包在传输过程中出现的位错误,如噪声等

3. TCP,IP几个字节?

IP首部和TCP首部都是20-60个字节。所以TCP/IP首部的最小总长度是40字节,最大总长度是120字节.

4. 如何修改socket接收缓冲区的大小

C语言可以用setsockopt修改

#include int sockfd; int recv_buffer_size = 1024 * 1024;// 1 MB// 创建一个 socketsockfd = socket(AF_INET, SOCK_STREAM, 0);// 设置接收缓冲区大小(以字节为单位)setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (const char *) &recv_buffer_size, sizeof(recv_buffer_size));

5. 用户态和内核态

用户态和内核态是操作系统中两种CPU运行模式,内核态拥有最高级别特权。

  1. 在用户态下,用户访问受限,不能直接访问硬件,如用于程序需要访问一些资源或执行敏感操作,需要变为内核态
  2. 内核态可以分配内存资源、处理硬件中断和处理系统调用等任务来确保整个系统的正常运行。

6. https和http的区别?https过程是什么

https是ssl/ tls+hhtp,有一个证书加密,默认端口号是443。

HTTPS的过程

  1. 客户端向服务器发送https连接请求,请求报文包含了客户端支持的加密算法
  2. 服务端收到请求,然后选择一个客户端支持的加密算法。此后,服务器会向客户端发送他的公钥证书。证书中包含了服务器的公钥和证书颁发机构(CA)的签名
  3. 客户端收到服务器发送的证书,进行验证。如果证书有效,那么客户端将**使用服务器的公钥加密一个“预主密钥”**并将其发给服务器。
  4. 服务器收到加密过后的密钥后,使用自己的私钥解密并得到原始的预主密钥。
  5. 现在,客户端和服务器都有了相同的预主密钥,他们将使用这个密钥进行加密和解密后续的数据
  6. 接下来,客户端和服务器之间的数据交换都将通过加密的信道,这意味着即使数据在传输过程中被监听,攻击者也无法解密。

7. SQL注入攻击是什么?

是一种网络安全漏洞,它利用了应用程序对用户输入数据的处理不当,从而允许攻击者执行恶意的SQL语句。例如在where后加入1=1来攻击和操纵数据库。

为了防止这种情况,需要:

  1. 限制用户输入:限制输入的长度、字符类型等
  2. 输入验证:在后端对用户输入尽心严格的检查和过滤
  3. 定期监控和更新
  4. 赋予最小权限:数据库账号只具备完成任务的最低权限
  5. 参数化查询和预编译语句: 这是预防SQL注入攻击的最直接并且最有效的方法。参数化查询要求你先定义所有的SQL代码,然后在你执行查询前将每一个参数添加到查询中。这样,即使攻击者试图注入恶意的SQL,他们也只能影响查询的参数,而不能改变查询本身的结构。

8. TCP可靠的原因是什么?

  1. 三次握手,确保双方都做好了发送和传输的准备
  2. 流量控制:利用滑动窗口机制控制发送方的发送速率
  3. 拥塞控制:避免网络拥塞
  4. 序号确认重传:将TCP报文段分断;确认收到后再收下一个;未收到会进行重传
  5. 校验和

9. 三次握手是为什么?两次为什么不行?会造成什么问题?

保证双方都确保自己和对方的接收和发送功能没问题。如果两次的话,服务端无法确认自己的发送是否有问题。

有一个场景就是:若客户端发起连接请求,这个请求长时间滞留,于是客户端重新发起新的请求,这一次双方建立连接了;当双方断开连接后,这个滞留的请求来了,服务器向该客户端发送报文建立连接(两次握手),但客户端不发送数据,服务器一直等待,造成了资源的浪费。反之若采用三次握手,服务器没收到第三次握手,就会建立连接失败。

10. IP已经分片了,TCP为什么要分段?

二者处于不同的层级上,有不同的目的和方法

  • IP分层(MTU):由于数据包超过本地网络最大传输单元(MTU),就会分片。
  • TCP分段(MSS):使得可靠传输变为可能,TCP分段的目的是将前后关联的数据分组在一起,以便网络中如果出现乱序等问题时,只需要发送该片段即可。一般MSS小于MTU。

11. 超时重传几种策略,快速重传后为什么是阈值+3

  • 固定时间重传
  • 指数型时间重传:如果没在规定时间内收到确认,每次等待时间翻倍,直到达到阈值
  • 快速重传:在收到3个重复的确认报文时,直接重发数据包

+3是因为,如果只基于1个或2个重复的确认来触发快重传,会存在误报情况,因此要求连续收到3个重复确认,降低了误报的可能性;另外,假设一个数据包丢失,但这个分组的确认仍然收到了,根据累计确认,至少有2个新的、乱序的数据包已经发给接收方。

12. tcp怎么判断丢包?

  1. 序列号和确认号:每一个TCP包都是独一无二的
  2. 超时重传:如果一定时间内还未收到接收方的确认,则认为丢失,发送方重传
  3. 快速重传:收到一个序列号不连续的数据包时(也就是在连续数据包到达之间丢失了一个或多个数据包),它会立即发送重复确认(多次确认同一个序列号)。发送端收到多个相同确认号时,会立即重新发送丢失的数据包,而不必等待超时重传的超时。

13. HTTPS中对称加密和非对称加密都怎么使用的?为什么 HTTPS 不直接用非对称加密

  1. 非对称加密

    非对称加密使用一对密钥:一个公开的公钥和一个私有的密钥。公钥用于加密数据,私钥用于解密数据。在HTTPS握手阶段,客户端和服务端使用非对称加密进行安全通信,并协商一个对称密钥

    1. 客户端向服务端发送连接请求。
    2. 服务端响应并发送其公钥证书。
    3. 客户端验证该证书的有效性(确保它是由受信任的证书颁发机构发出的)。
    4. 客户端使用服务端的公钥加密一个随机生成的对称密钥,并将其发送给服务端。
    5. 服务端使用私钥解密收到的密文,从而获得对称密钥。
  2. 对称密钥

    对称密钥使用相同的密钥进行加密和解密。它的性能更优,因此适用于加密大量数据。在HTTPS握手阶段完毕后,客户端和服务端共享会话密钥。接下来的数据用这个对称密钥进行加密解密。

为什么https不直接用非对称加密?

  1. 性能问题是一个瓶颈
  2. 效率:对于大量的数据交换,对称加密更高效

总结

建立连接用非对称加密,传输数据用对称加密。

前面采用非对称加密,是为了得到只有双方知道的预主密钥,方便后面对称加密使用

14. tcp拥塞控制实现

用于防止网络拥塞,提高网络性能

有四种算法:

  1. 慢启动:每次成功收到ACK确认包后,拥塞窗口大小增一倍。在达到阈值后,改用拥塞避免算法
  2. 拥塞避免:达到门限之后,拥塞窗口加一
  3. 快重传:收到3个相同的ack后,直接重传对方未收到的报文段
  4. 快恢复:收到3个重复的ack后,将门限值设为拥塞窗口的一半,采用拥塞避免算法

15. HTTP 不用 HTTPS 怎么保证接口的一些安全问题

  1. 使用加密算法

    发送数据前,使用数据加密,接收端收到后,再进行解密。

  2. 采用监控、日志

    记录所用访问API的尝试,并定期审计以检测可疑活动。

16. TCP的流量控制原理

是一种基于端到端的控制发送方传输速率的协议。流量控制最主要通过三个方法实现:

  1. 滑动窗口:发送方一个滑动窗口,接收方一个滑动窗口,接收方收到数据,就会通知发送方滑动窗口向前滑动。保证了发送方和接收方之间的同步。
  2. 接收窗口大小:通过调整滑动窗口大小,限定发送方速率

17. TCP粘包问题

TCP粘包是指TCP进行数据传输的过程中,发送方发送的多个数据包会被接收方一次性接收,导致多个数据包黏在一起,这通常是因为tcp传输是基于字节流的,不保证数据的边界所引起的。解决方法:

  • 消息定界符:在每个消息的末尾添加一个特殊字符
  • 固定长度消息:每条消息长度固定
  • 长度前缀:在头部添加长度的信息
  • 使用应用层协议:HTTP或者FTP等协议,这些协议都已经解决了粘包问题。

放数据的速度 > 应用层拿数据的速度

UDP会发生粘包吗?

UDP不会发生粘包。UDP是基于数据报的协议,发送的数据包是独立的,有明确的边界。而且UDP头部有数据长度,会根据数据报的边界进行数据处理。

udp为什么会有乱序现象?

udp没有滑动窗口,序列号,确认应答和超时重传机制,由于网络抖动,不能保证数据包是按照先后顺序到达的。

为什么先发的包不会先到呢?

数据包需要经过多个路由器和交换机才能到达目的地。这些设备会根据当前网络状况来调整数据的路径。因此,即使从统一源地址发送,它们的路径可能不同,每条路径的拥塞情况,数据包丢失情况和重传情况都不同。

18. TTL是什么?有什么用处?

是生存时间(最大生存时间),每经过一个路由就会减一,作用有:可以防止无限循环、跟踪网络。

19. TCP的重发机制怎么实现的?

  1. 滑动窗口机制,确立收发的边界,能让发送方知道已经发送了多少、尚未确认的字节数
  2. 选择重传,用于堆传输出错的序列进行重传

20. 谈谈你对滑动窗口的理解

滑动窗口是一种流量控制计数。他的大小意味着接收方还有多大的缓冲区用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少自解的数据。当滑动窗口为0了,发送方就不能再发送数据报了。

流量控制就是利用滑动窗口实现的,保证接收方来得及接收。

21. 利用udp实现tcp的协议的话,有什么优点?

  1. 避免拥塞控制:UDP没有拥塞控制
  2. 低延迟:UDP是无连接的,没有三次握手的过程,所以可以低延时
  3. 更好的灵活性:可以根据TCP的功能实现一部分,能在可靠性、延迟上做取舍

22. SYN报文什么情况下会被丢弃?

  1. TCP队列满了(半连接队列),导致SYN报文被丢弃
  2. 开启tcp_tw_recycle参数,并且在NAT环境下,导致SYN报文被丢弃

23. 超时重传时间(RTO)是固定的吗?

不是固定的,是动态变化的。RTO是根据RTT(报文往返时间)计算的。RTT是数据包发送时刻到接收到确认的时刻的差值,即包的往返时间。

24. 快重传存在效率问题吗?

存在,如果有几个连续的报文都丢失了,那么确认方需要挨个收到连续三个重复的ack再发起重传,非常浪费时间。

而如果选择重传之后所有的报文,则会把接收到的一些报文又重传一次,等于做了一次无用功,浪费资源。

25. HTTP/1.1的优缺点?

优点

  • 简单:http基本报文格式是header+body,头部信息也是kv形式
  • 灵活和易于扩展:http工作在应用层,1.0和2.0使用的是TCP协议,3.0改用了UDP协议
  • 跨平台

缺点

  • 明文传输,不安全

HTTP/1.1相比于1.0,多了长连接,改善1.0短连接的性能开销。

26. HTTP/2、HTTP/3做了什么优化?

HTTP/2基于HTTPS,所以HTTP/2的安全性也有保障。

  • 头部压缩:如果同时发出多个请求,它们的头是相似的,那么协议会帮你消除重复的部分。
  • 二进制格式:HTTP/2不像1.1那样纯文本,采用了二进制,统称为帧,对计算机而言效率更高
  • 并发传输
  • 服务器推送

由于HTTP/1和2都有队头阻塞问题,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后, 才能处理下一个请求,这属于 HTTP 层队头阻塞。所以HTTP/3把HTTP下层的TCP改成了UDP。这样就更快速了。

目前广泛使用的还是1.1版本。

27. 既然有HTTP,为什么还要有RPC?

  • RPC本质上不算是协议,而是一种调用方式,而像gRPC这种具体实现,才是协议。目的是希望程序员能像调用本地方法那样调用远端的服务方法。RPC不一定基于TCP。
  • 从发展史来看,HTTP主要用于B/S架构,RPC更多用于C/S架构。但现在二者在慢慢融合。很多软件同时支持多端,所以对外一般用HTTP,而内部集群的微服务之间采用RPC协议进行通讯。因为HTTP中的header信息有太多冗余内容,而如果约定好头部的第几位是 Content-Type,就不需要每次都真的把”Content-Type”这个字段都传过来。而RPC定制程度更高,就不需要像HTTP那样考虑服务器的各种行为,因此性能更好。这也是在公司内部微服务抛弃HTTP,使用RPC的主要原因。

28. HTTPS了解吗,具体怎么实现,CA是什么

HTTPS是通过SSL/TLS协议来对数据进行加密解密,同时需要一个受信任的第三方机构CA来堆证书进行签发和认证。

所以CA就是:1. 验证服务器身分;2. 为服务器颁发证书;3. 为客户端提供证书查询服务

29. 服务端出现大量close_wait状态, 什么原因?有什么危害?应该怎么办?

close_wait是服务端(被动断开方)收到客户端的FIN请求,回复确认后,直到自己下一次发送FIN的这段时间。该问题转化为:为什么被动断开的服务方,不调用close函数(发送FIN报文,close接口),关闭掉已经四次挥手中的连接呢?可能有:

  1. 服务端的线程阻塞了,没有办法调用close函数
  2. 在close之前有大量耗时的逻辑

大量的close_wait会占用系统资源,因为服务端每给客户端一个连接就会创建一个fd,如果没有关闭,fd就不会释放

30. 在tcp四次挥手中,如果服务器出现宕机或者断网,当服务器重启和重新联网,这个挥手还会继续吗?

不会自动继续。当服务器重启并重新联网后,它通常会重新初始化所有网络连接。之前中断的四次挥手过程将不会自动继续。

在这种情况下,客户端会等待服务器响应,如果服务器未发送FIN,客户端会进行重传,达到最大值;如果服务器在重启之前发送了FIN分组,客户端可能认为连接已终止并释放资源。然而,如果服务器未在宕机前成功发送FIN,客户端可能需要依靠超时重传来判断连接失败。

31. rpc和http的区别?gprc是什么?

  • http用于传输网页、图片、数据等资源;而rpc是远程调用,可以通过多种底层协议实现。
  • http使用tcp/ip作为传输层协议;RPC协议可基于任意套接字层次
  • HTTP性能较低,RPC具有更好的性能,因为它解决了客户端与服务器之间远程调用方法的问题。

grpc是谷歌开发的一个开源rpc框架,基于http/2.

32. TCP如何优雅关闭?

  1. 发送方向连接的一端执行shutdown,关闭写的方向。
  2. 接收方收到一个带有FIN的包,并返回ACK。
  3. 接收方也执行shutdown,关闭连接的读和写方向。即调用close。
  4. 发送方收到FIN的包,然后发送ACK作为回应。
  5. 最后,对方收到ACK后,就完成了优雅关闭。

33. DDoS了解吗?

是一种网络攻击手段,攻击者通过招募上千万太计算机组成的僵尸网络,用以耍宝目标服务器的网络带宽,从而使正常用户无法访问这个服务。比如说TCP连接时,拒绝第三次握手,使得服务器受到SYN泛洪攻击。

解决办法有:防火墙、对流量过滤和清洗。同时,ISP(Internet Service Provider,互联网服务提供商)也可能采取措施来阻止攻击,并协助企业及时应对安全风险。

34. 如何让UDP变得可靠?

UDP想要可靠,就是程序员在应用层仿照确认应答和超时重传

  1. 添加seq/ack机制,确保数据传送到对端
  2. 添加发送和接收缓冲区,主要用于用户超时重传
  3. 添加超时重传机制:可以避免无限制重发数据包,限制每个数据包的重传次数或总等待时间
  4. 重组和分片:对于较大的数据包,分片传输和接收端重组可能有助于减少单个丢失数据包对整体数据流的影响
  5. 校验和:为数据包附加校验和,已检测数据包在传输过程中是否损坏

35. UDP最大能发送的数据是多少?如果超过最大值怎么办?

醉翁之意不在酒,看似问UDP,其实是问IP分片长度

UDP首部有一个16位的UDP总长度,换算一下就是2^16字节,65536。所以,一个UDP数据总长度就是65536字节。而这个总长度还包括了UDP8字节的固定长度,也就是数据量还要减8字节,65528字节 。也就是说当我们发送的字节超过了65528,就需要将数据在应用层拆分成符合UDP大小的数据包,分别传输。

这里,我们还需要考虑分片的设计,比如

  1. 怎么标识分片的属于同一个应用层数据
  2. 怎么表示分片后还有数据

36. cookie和session的区别?

cookie是客户端浏览器用来保存服务器的一些数据的一种机制,当我们通过浏览器访问时,服务器可以把一些状态数据以KV的形式写入cookie,存储到客户端浏览器,然后下一次再访问服务器的时候,我们可以携带这些状态数据,发送到服务器端,服务器端可以根据cookie里面携带的内容去识别使用者。

session表示一个会话,它是属于服务器端的一个容器对象。默认情况下,它会针对每一个浏览器的请求,分配一个session对象。它用来存储当前会话的一些状态数据。我们都知道,http是一个无状态协议,也就是说服务器端并不知道客户端发送过来的多次请求是属于同一个用户。所以session是用来弥补http我要状态的不足。所以session可以用来存储客户端在同一个会话里面产生的多次请求的一个记录。

二者的结合我们就能去实现一个有状态的http协议。

它的过程是:浏览器第一次访问服务端的时候,服务端创建一个会话,并生成唯一的一个会话ID来标注这个会话。然后服务器端把这个sessionID写入浏览器的cookie里面。在后续的请求里面呢,每一次都会携带sessionID,服务器端可以根据sessionID识别当前会话的一个状态。所以总的来看,cookie是客户端的存储机制,session是服务端的存储机制。

37. 非对称加密的公钥和私钥的区别

  • 公钥是公开的,可以在互联网上自由传播,用来加密数据
  • 私钥只有自己才有,用来解密数据

公钥就像是邮箱顶上的缝隙,人人都可以用来投放信件;私钥则像是钥匙,只有持有者才能打开看到别人的信件。

38. 网关有什么作用?

它充当数据通信的中介,允许不同网络架构和协议之间传输数据。功能有:

  1. 协议转换
  2. 路由:负责将数据包从发送端路由到接收端,在网络中的两个节点间正确的数据传输路径
  3. 数据包过滤:可以阻止特定类型的数据包进入或离开网络控制访问权限
  4. 负载均衡:防止网络拥堵

39. websocket了解吗?为什么有http还要有websocket?

websocket是一种网络通信协议。因为http是半双工通信协议,客户端先发送请求报文,服务器再发送响应报文。而websocket是全双工通信协议,它基于TCP,允许服务器和客户端双向通信。如果我们要玩网页游戏、刷视频就得用websocket。

websocket是长连接,直到连接关闭或断开。

40. 一个get请求走https,如果我在中间代理服务器抓到这个包。请求参数会被加密吗?请求行类似协议版本,协议方法会被加密吗?cookie会被加密吗?

会的,当使用https进行get请求时,所有传输的数据都会被加密(参数、请求行、方法、协议版本、cookie)。即使再代理服务器上抓到了数据包,但由于没有预主密钥,仍然无法解析。

41. tcp和udp的区别

  1. 连接方式
    TCP 是面向连接的协议,它会在数据传输前建立一个连接。这可以确保数据包按照顺序发送和接收。相反,UDP 是无连接的,这意味着数据可以在没有先建立连接的情况下发送和接收。
  2. 可靠性:
    TCP 是一种可靠的协议,它通过应答、重传、流量控制和拥塞控制等手段保证数据的完整性和顺序。UDP 不提供类似的机制,所以在通信过程中可能丢失、乱序或重复数据包,因此可靠性相对较低。
  3. 速度
    由于 TCP 需要确认机制来确保数据传输的可靠性,这使得其传输速度相对较慢。而 UDP 不需要确认机制,因此传输速度相对较快。
  4. 用途
    TCP 常用于对可靠性和准确性要求较高的应用,如文件传输、电子邮件和网页浏览等。UDP 则更适用于需要快速传输和低延迟的应用,例如视频流、音频流、在线游戏和 VoIP 等。
  5. 复杂性
    TCP 更为复杂,因为它需要管理连接和各种可靠性保证机制。而 UDP 更简单,因为没有这些额外的机制。
  6. 数据包大小
    TCP 传输数据时,它会将应用层数据分割成适当大小的数据包由接收方重新组装。UDP 则不会分割数据包(除非超过2^16),由应用层来确定数据报的大小,在中继过程中可能会因为数据报大小受到限制。

42. 为什么快速恢复是直接进入拥塞避免,而不是慢启动开始?

这样做的原因是,拥塞导致丢包通常是因为网络出现了一定程度的拥塞。事实上,发送方的数据发送速率与网络当前能够处理的速率大致相当,但可能稍微超过了这一阈值导致了丢包。因此,为了快速恢复到一个稳定的发送速率,而不是从头再次开始慢启动过程,发送方选择直接进入拥塞避免阶段。

43. time-wait状态,什么是MSL,timewait过多怎么办?

time-wait是最后一次挥手发送过后到后面的2MSL阶段,以确保对方受到自己的请求,并可以重发丢失的ACK数据包。

MSL表示一个TCP报文段在网络中可以存在的最长时间。MSL主要用于防止过时的数据包再网络中滞留过久,导致一些问题。

timewait过多怎么办?

  • 设置TIME-WAIT端口周期,设置更短的时间
  • 启用端口复用:启用SO_REUSEADDR 可允许套接字在 TIME_WAIT 阶段再次被使用,以减少端口耗尽的问题。
  • 使用连接池:将连接放入连接池中,减少新连接和关闭连接的频率

44. 出现多次ACK什么原因?

  • 网络拥塞:导致数据传输变慢,接收方一直没收到数据
  • 信号失真:导致传输过程中数据出现误差
  • 数据包丢失/乱序:接收方一直没收到下一个包,于是重复发送ack报文

45. 讲讲1xx,2xx,3xx,4xx,5xx状态码以及对应的含义

  • 1xx:表示请求已被就饿瘦,还需要继续处理。
  • 2xx:请求成功处理
  • 3xx:重定向
  • 4xx:客户端错误
  • 5xx:服务端错误

46. get和post的区别和你的理解。

  • get用于请求数据,post用于发送数据,如表单或更新数据
  • get请求参数在url后,post请求数据在请求体
  • get请求对数据长度和类型有限制,url限制2048个字符;post没有限制
  • get是幂等的,post不是幂等的

47. 服务端不进行accept,最多有多少个连接可以建立成功?

取决于listen的第二个参数backlog,这个参数是全连接队列的最大长度。在Linux 2.2版本 以后,该参数仅表示已完成队列的大小,不包含未完成连接的大小。

当客户端主动连接(accept()),linux内核就会自动完成TCP三次握手,将建立好的连接放入队列中。

半连接队列长度是指服务器处于SYN_RCVD状态的套接字个数,半连接到全连接的通过设置重传次数来判断是否超时。

48. 三次握手可以携带数据吗?

前两次不可以,因为如果可以的话,有人恶意攻击服务器,那么他每次在第一次握手中的SYN报文中加入大量数据,服务端收到大量的SYN报文,会花费服务端很多时间和空间,浪费服务端资源。第三次握手可以携带数据。因为第三次握手是客户端发送给服务端的,只是为了让服务端确保服务端自己的发送能力没问题。

49. ACK数据包,消耗TCP的序号吗?

  • 携带ACK的数据包:协议自发发送的TCP数据包消耗一个序列号(SYN、SYN+ACK…),而应用层数据逐字节进行编号,比如abc的序号是100,b的序号是101.

  • ACK数据包:但是,如果是纯的ACK数据包(三次握手的第三次握手),则不消耗序号

50. TCP三次握手过程

  • 第一次握手客户端给服务端发送SYN报文,起始序号x(表明是客户端维护的序号,表明是给服务端发送的数据),告诉服务端想要建立连接,进入SYN_SENT状态
  • 服务端收到后,会给客户端回复一个SYN+ACK报文,起始序号y,确认序号x+1,告诉对方我收到了,并确认我也想和你建立连接,此时进入SYN_RCV
  • 客户端收到后,给服务端恢复了一个ACK报文,起始序号x+1(并未被使用,下次发送数据还是x+1),确认序号y+1,告诉服务端我收到了

51. 四次挥手可不可以发送应用层数据呢?

可以的,在两次回收后,被动断开连接方可以发送数据。

52. 什么是滑动窗口机制?

在没有滑动窗口之前,发送方只有等待接收方发送确认报文后,再发送下一个报文,效率很低。(确认应答机制)

滑动窗口允许窗口内的多个分组,不需要前一个分组的确认书举报,就可以发送到网络中,提高TCP发送方发送的数据量。从TCP角度看,滑动窗口将发送缓冲区分成了不同的区域:已发送已收到ACK、已发送没收到ACK、可以发送但还没发送、不能发送。收到确认后,就可以将窗口进行滑动,此时就有新的数据可以发送了。

53. TCP如何提高传输效率?

  • 流量控制:利用滑动窗口机制代替确认应答机制,控制发送方的发送数据量
  • 拥塞控制:通过不同的策略,不断探测网络的转发能力,调整发送方的发送数据量,有四种算法,慢启动、拥塞避免、快重传、快恢复

54. 两端传输文件,如何保证文件传输正确?

实际上实在问可靠传输的方式

  • 校验和:在发送时候计算文件校验和,接收端业绩算文件校验和,若不同说明传输过程中出错
  • 确认应答机制:将文件分成多个块,每一块都有一个序号,等待接收端发送ack后,再发送下一个
  • 超时重传机制:若超过一定时间未收到ack,自动重传

55. 反向代理服务器如何保证安全性?

  • 加密传输:使用https协议,ssl/tls加密
  • 身份验证和授权:确保只有身份验证通过的用户才能访问
  • 日志和监控:记录和分析日志,迅速发现和应对安全威胁

56. 拔掉网线后,TCP连接还存在吗?

双方建立连接后,操作系统内核会维护对方的socket结构体,只要连接后,就会一直维护。

拔掉网线后,客户端机器和服务端机器针对这个TCP连接对应的struct socket结构体一定是存在的,除非经历了四次挥手才会销毁掉结构体。所以

  • 拔掉网线后,有数据传输
    • 触发超时重传机制,重发到一定次数后(tcp_retries2默认是15次),发送方会认为网络断开。程序员通过通知进行后续操作
    • 在超时重传上限内,网络恢复了,则程序员无感知
  • 拔掉网线后,没有数据传输
    • 超过2小时空闲,则触发心跳机制:每隔75s发送一个探测报文,如果9次探测报文都没收到数据包,则认为连接断开了

57. RTO和RTT?

  • RTO:超时重传时间,即从发送数据时刻起,超过这个时间便执行重传
  • RTT:往返时间,即数据发送时刻到接收到确认的时刻的差值

58. TCP协议将数据交给IP协议后,IP协议需要分片传输吗?UDP呢?

TCP

MSS是TCP分段大小,MTU是IP报文最大长度

MSS一定小于MTU。

当IP数据包大小>MTU,就需要分片传输。

假设IP协议将TCP数据进行了分片,那么丢失一个分片,对于TCP来说都是整个数据包的丢失。都要重传整个数据包。suo亿,TCP严格按照MSS进行传输,进而IP协议一定不会针对TCP的数据进行分片传输,更何况MSS的长度是一定小于MTU的,不会发生分片现象。

UDP

UDP数据长度最大为2^16次方即65536字节。当数据包的长度大于MTU,就需要分片传输。所以如果IP数据包(UDP+IP头部)字节大小>MTU,就需要分片传输。

59. 子网掩码有什么作用?

子网掩码标识了网络号和主机号的范围,说白了就是用子网掩码标识网络号使用多少个比特位,主机号使用多少个比特位。

60. HTTP和HTTPS的区别,SSL/TLS解决了什么问题?

  • HTTP是非安全的,采用明文传输;HTTPS是安全的,传输过程中使用了SSL/TLS加密。
  • 端口号不同,HTTP是80,HTTPS是443.

SSL/TLS解决了以下问题

  • 数据保密性:通过加密数据保护数据传输过程中的信息
  • 数据完整性:确保数据在传输过程不会被修改或破坏,并可以检测道对数据的任何未经授权的更改
  • 身分验证:SSL/TLS证书验证了网站的身份,验证服务器确实是我们所期望访问的服务器。

61. 客户端如何保证CA机构的合法性?

客户端浏览器通常包含许多预安装的根证书。这些根证书来自收信人的证书颁发机构。当客户端验证CA时,会检查证书的根证书是否在预安装的信任列表中,如果存在,客户端则认为CA是合法的。

62. 为什么我们的IP都是192.168开头的?

因为我们的IP地址优先,为了有效利用这些有限的IP地址,可以将网络分为局域网和广域网,将IP分为了私有IP和公网IP。一个局域网里面的多台机器都可以公用一个广域网IP,而内部则用的是私有IP,即192.168,这就是我们的NAT协议。这样大大增加了可用IP数量,一个小区就共用一个公网IP,而且一般数量不多,所以用的是C类地址即192.168开头的地址。

如果要使用私网IP访问局域网外的共有IP,就需要IP转换,即NAT路由器。

63. CSRF攻击是什么,怎么预防

跨站点请求伪造(CSRF)是一种网络攻击奇数,攻击者通过诱导用户加载包含受害网站请求的页面来利用用户以获得的身份验证权限。当受害者访问含有恶意请求的页面时,攻击者利用受害者的凭据向受害网站发出恶意操作请求,如更改账户信息、发送而已消息等。

预防措施

  • 用户操作验证:在执行敏感操作前要求用户重新验证凭据,如验证码等一次性机制
  • 双重cookie防御:在用户浏览器和服务器中同时设置一个随机值的ecookie,服务器通过检查两个cookie是否匹配来验证请求的来源。
  • 同站请求验证(SameSite Cookie Attribute):设置 SameSite 属性可以限制 cookies 只在同一站点下发送,从而防止跨站请求攻击。

64. 为什么使用拥塞控制后,网络还是会拥挤?

  • 总带宽不足:如果多个数据流共享同一条链路,且总数据传输需求超过链路带宽,将导致拥塞。
  • 路由器缓冲限制:路由器在转发数据包时,需要将数据包暂存在缓冲区。如果数据包传输速率大于路由器转发速率,缓冲区会满,从而导致丢包。

65. HTTP常见字段有哪些?

  • connection:连接管理,可以是Keep-Alive或者close
  • content-length:用于读取post请求的消息体长度
  • Content-Type:服务器响应时,告诉客户端本次数据是什么格式Content-Type: text/html; charset=utf-8

66. 一次完整的HTTP请求的步骤

  • 域名->IP地址:浏览器缓存、系统缓存、hosts文件、路由器缓存、本地域名服务器递归、根域名服务器迭代
  • 建立TCP/IP连接(三次握手)
  • 浏览器发送HTTP请求
  • 经过路由器转发,通过服务器的防火墙,HTTP请求到达服务器
  • 服务器处理HTTP请求,发送HTTP响应报文,包括了发送方想要的资源
  • 浏览器解析HTML,CSS,并显示在浏览器端
  • 服务器关闭TCP连接

67. 如何求两个节点最短路径

  • 迪杰斯特拉算法:从起点开始,通过接连地考虑最近的未访问过的节点,以逐步更新个节点的最短路径
  • 弗洛伊德算法:在路径中添加中间节点,以进一步减小距离

68. TIME-WAIT的作用

  • 保证双方可以正常关闭:如果对方没收到FIN-ACK,可能会重发FIN,那么TIME-WAIT状态可以重新发送ACK,并不会重启关闭流程或打断新连接。
  • 防止错误接收历史报文:如果立刻断开连接,并建立了新的连接,但此时由于网络问题,网络中还残留着旧连接中的数据包,新连接接收了旧的数据包,那么就会导致不可预期的问题。TIME_WAIT状态的等待已经超过了数据包在网络中的最大生存时间,因此可以保证不会出现这种情况。

69. 什么是ICMP?

ICMP是网络层协议,用于传输网络层控制信息的协议来达到对网络信息进行诊断,以及发送错误报告的目的。

比如ping和traceroute就是通过icmp来对网络质量进行评估。

还有当数据包无法到达目的地时,路由器或目标主机就会发送目的地不可达的消息,有助于识别网络问题,通过这种方式来达到发送错误报告的目的。

70. 交换机是怎么做转发的?

交换机工作再数据链路层,它将数据帧从一个端口转发到另一个端口,以有效地将网络连接到其中的设备。步骤如下:

  1. 学习:当交换机接收到其某个端口上的数据帧时,它会查看源MAC地址,并将这个地址与接收该帧的端口号关联起来。交换机把这些信息存储在其转发数据库中。这就是所谓的MAC地址学习。
  2. 转发决策:当交换机需要转发一个数据帧时,它会查看该帧的目标MAC地址,然后在其转发数据库中搜索该地址。如果找到了与目标MAC地址对应的端口,那么交换机就会把数据帧转发到那个端口。如果没有找到,它就会广播这个帧到除了接收该帧的端口外的所有端口。
  3. 过滤/段内交换:交换机通过查看MAC地址表,确定数据包的目标地址是否在与源地址相同的网络段(局域网)内。如果是,交换机会阻止这个数据包在其他网络段(局域网)上的广播,即过滤该数据包。这样可以大大减少不必要的网络流量,提高网络的整体性能。

71. ping底层原理

  1. 发送ICMP请求,这个数据包包含了一些数据,比如数据报的大小,发送时间
  2. 路由和传送:这个数据报被放入到IP数据包中发送到目标网络。期间可能经过多个路由器。
  3. 接收并返回ICMP应答:目的主机收到后,会回复一个应答

除了ping还有什么命令可以去检测该主机网络是否正常,具体命令

  • telnet
  • netstat:显示你的网络连接,以及哪些端口正在被监听,哪些网络接口正在运行

72. SYN泛洪攻击怎么解决?

  • SYN cookies:服务器发送SYN-ACK消息前,不预分配完整的TCP数据链路,而是基于客户端的IP地址、端口号和自身信息,使用哈希函数计算一个cookie。将这个cookie作为初始序列号,发送SYN-ACK消息给客户端。如果客户端是和合法的,那他会返回包含这个初始序列号的ACK包。
  • 防火墙:限制从单一来源接收到的SYN的数量。
  • 缩短超时时间值:调整TCP/IP连接的超时时间,使得半连接更早的被清除。

73. IP地址有哪几类?

  • A类:IP地址第一位为0,网络号8位,主机号24位
  • B类:IP地址前两位为10,网络号16位,主机号16位
  • C类:IP地址前3位为110,网络号24位,主机号8位
  • D类:IP地址前4位为1110,被用作多播地址
  • E类:前四位为1111,被保留作为未来使用

74. MAC地址是什么?

MAC地址,即媒体访问控制地址,是计算机网络上设备的唯一识别码,通常被内嵌在网卡中,用于计算机网络中实现物理地址的定位和标识。MAC地址由6个字节(48位)组成。

75. UDP为什么不需要流量控制和拥塞控制?

UDP是基于无连接的,尽最大努力交付,所以无法对网络状态做出实时调整改变发送速率。比如流量控制是因为接收方会发送确认报文,来进行调整。

76. 什么是TCP连接复用,有什么意义?

TCP连接复用是一种优化网络性能的技术,通过将多个客户的请求复用到一个TCP连接上,以减少服务器的性能负载和延时。具体来说,在客户端发送HTTP请求之前,通常需要先与服务器进行TCP三次握手来建立连接。然后,服务器处理请求并将结果返回给客户端,最后双方互相发送FIN包并在收到确认后关闭连接。

优势:

  • 降低服务器性能负载:复用一个TCP连接,减少与服务器之间新建连接带来的开销
  • 减少延时:不需要频繁连接和关闭连接
  • 降低资源占用:减少了客户端对后端服务器的并发连接数的请求,进一步降低了服务器的资源占用

数据结构与算法

1. 前中后序遍历,知道其中两个就可以还原二叉树吗?

只有前中和中后才行,知道其中2个就能还原二叉树,但是必须要当每个值不重复才行。通过递归的方式还原。拥塞控制可以平衡每个连接的传输速率,但如果总带宽需求远超链路容量,仍然会出现拥塞。

前序和后序遍历通常无法唯一地还原二叉树,因为我们无法通过这两种遍历来准确确定根节点与左右子树的界限。

2. 什么是时间轮?请你说一下对时间轮的理解?

时间轮是用来存储一系列定时任务的环状数组。它的整个工作原理和我们的钟表的表盘类似。它由两部分组成,第一个是环状数组,另外一个是遍历整个环状数组的一个指针。首先定义一个固定长度的环状数组,然后数组的每一个元素代表一个时间刻度;然后是这个指针,这个指针按照顺时针无限循环这个数组,每隔最小的时间单位前进一格数组索引。环状数组的每一个元素都是用来存储定时任务的一个容器,当我们向时间轮去添加一个定时任务的时候,我们会去根据定时任务的执行时间,计算他所在的存储数组的下标,有可能在某个时间刻度上,存在多个定时任务,那么采用双向链表来进行存储。当指针指向某个数组的时候,就会把这个数组里面存储的任务取出来遍历这个链表,逐个去运行里面的每个定时任务。如果某个定时任务的执行时间大于环状数组转一圈的时间,那么一般可以使用一个圈数来表示这个任务的延迟执行的时间。也就是说如果一圈8秒,他是16秒,那么意味着要第二圈才执行该任务。

使用时间轮的好处:

  • 减少定时任务添加和删除的时间复杂度,提升性能
  • 可保证每次执行定时器任务都是O(1)复杂度,在定时器任务密集情况下,性能优势明显

当然,时间轮不适合时间精度要求高的任务(取决于最小时间精度)

3. 大小端区别?

举一个例子,在32位数字0x12 34 56 78在内存中的表示形式为:

1)大端模式: 低地址 --> 高地址 0x12 |0x34| 0x56|0x782)小端模式: 低地址 -------------------------> 高地址0x78 |0x56| 0x34|0x12

4. 同余操作怎么做?

加法和乘法操作完直接%mod;减法则相减后+m再%mod。

5. 讲讲什么是快速排序算法?

快排是一种交换排序算法,是冒泡排序算法的改进。最好的时间复杂度能达到O(nlogn),基于分治的思想,每次选准一个基准值,放入到对应的位置,使得左边的元素比他小,右边的元素比他大,然后再对两边递归执行相同的操作。

从快排延申出了快速选择排序算法,这个算法是快排的一个小改进,通常可以用来处理TOP K问题。

6. 介绍下冒泡排序,他和快排有什么区别?

冒泡排序的工作原理是重复地遍历排序序列,一次比较两个元素,直到进行到没有交换为止。时间复杂度是O(n2)。

快排和冒泡排序一样,都是交换排序算法,快排采用了分治地思想,将一个元素作为基准,将小于基准地元素移到基准以前,将大于基准的元素移到基准之后,然后递归地对基准前后地子序列进行相同的操作。

冒泡排序是稳定的排序算法,快排是不稳定的排序算法。

7. 讲一下归并排序,使用的场景是什么?

归并排序是一种典型的分治算法。他将数据分成多个子序列,然后解决子问题,最后将子问题的结果合并得出最终结果。

使用场景主要有:

  • 大数据排序:对于大规模数据无法在内存中一次性比较所有数据,因此将数据拆分成小的部分进行排序。然后再将小的已排序的列表组合成一定顺序的大列表。
  • 外部排序:待排序的文件无法一次装入内存,需要在内存和外部存储设备之间进行多次数据交换,以达到排序整个大文件的目的。归并排序特别适合对外部数据进行排序。
  • 稳定排序
  • 并行计算: 数据可以在不同的节点上进行分割和排序,然后再合并。在需要进行大规模排序时,这可以显著提高效率。

8. 什么排序是稳定的,什么是不稳定的?

稳定性指算法在排序过程中保持相等元素的相对顺序。即如果两个元素如果相等,原本在前的元素排序后还是在前。

稳定的排序算法有:插入排序、冒泡排序、归并排序、基数排序

不稳定的有:选择排序、快速排序、堆排序、希尔排序

9. 动态规划与递归的联系?

二者都有递归性质,而且存在重叠子问题。动态规划可以理解为递归的一种优化,将重叠的子问题记录了下来,下次访问时直接返回无需重新计算。

10. 快排有什么优缺点?

  • 优点
    • 平均时间复杂度可以达到O(nlogn)
    • 原地排序:不需要额外的空间存储(除了递归栈)
  • 缺点
    • 不稳定
    • 最坏情况下复杂度为n2,通常是在输入数组基本完全有序的情况下,导致选择分界元素的时候,全部分到了另一边,没有达到二分的效果。可以通过随机选取元素来解决。
    • 递归:通过递归实现,可能导致栈溢出。

11. AVL树和红黑树区别?

二者都是可以自行调整使得平衡条件得以满足的二叉查找树。但他们的平衡条件和平衡方式有所不同。

  • 平衡条件
    • AVL树:任何节点的两个子树高度最大差别为1.
    • 红黑树:每个节点都带有颜色,保证最长的可能路径不多于最短可能路径的两倍长度
  • 平衡方式
    • AVL树:当我们插入或者删除的时候,需要通过旋转保持树的平衡,有4种旋转操作
    • 红黑树:在插入或删除的时候,颜色的规则可能会破坏,需要修复。通过颜色变换和旋转两种方式保持平衡。
  • 应用
    • AVL:平衡性较好,查找效率更高,所以AVL更适合查找操作频繁的情况
    • 红黑树:平衡性较差但是调整平衡的操作较少,适合插入、删除操作频繁的情况。

12. 数组和链表的区别?

  • 存储方式

    • 数组是一块连续的内存区域,且大小在创建时就固定
    • 链表由一系列节点组成,每个节点包含数据和指向下一个节点的指针。在内存中离散存储
  • 插入和删除

  • 数组插入或删除未O(n),较麻烦

    • 链表插入未O(1)

13. 各个排序算法的速度

在平均情况下,快速排序最快;
在最好情况下,插入排序和冒泡排序最快;
在最坏情况下,堆排序和归并排序最快

14. 哈希是无序的,怎么保证有序?

LRU,双向链表,插入删除的时候将目标节点放置表头或表尾

15. 冒泡排序、快速排序的适用场景?

  • 冒泡排序:适用数据规模小的情况,且数据基本有序
  • 快速排序:数据规模大、数据乱(越乱越快)、原地排序

16. dfs和bfs流程和各自应用场景?

  • DFS
    • 回溯
    • 连通性
  • BFS
    • 最短路径
    • 连通性

Linux

1. accept函数是阻塞的还是非阻塞的?

默认阻塞的,但是可以通过fcntl将套接字设置为非阻塞。

2. git pull和git fetch的区别?

  • git fetch
    • 从远程仓库下载到最新的提交记录和分支信息到本地,不会合并到当前分支
    • git fetch更安全,因为他不会更改当前工作区的内容
  • git pull
    • 包含了git fetch的功能,还会自动合并远程仓库最新的更新到当前工作目录中的分支
    • 相当于执行了git fetchgit merge的组合

3. 信号你知道吗?常用的信号有哪些?

通信的一种方式。一般来源于中断。接收到信号后处理方式有忽视、强迫系统执行中断程序。

常用的信号有:

  • SIGINT:终止信号(ctrl+c)
  • End of File:ctrl+d,表示输入流已经结束,用于关闭程序的快捷方式
  • SIGCHLD:子进程退出,父进程会收到
  • SIGKILL:杀死进程 kill -9
  • SIGSEGV:段错误

4. Linux的ps, top,netstat, free等等命令?

  • ps:显示当前系统的进程状态,如果搭配grep使用,就能根据特定关键字来过滤进程列表
  • top:实时查看和管理运行中的进程。此命令可以帮助你识别系统资源的消耗情况。通过使用 top 命令,你可以轻松找到占用 CPU、内存等资源的进程。
  • netstat:显示网络连接、路由表、接口状态等相关信息
  • free:用于查询系统内存的使用情况

5. 讲讲listen() 第二个参数,read() 返回值,socket编程中怎么判断对方已经close

int listen(int sockfd, int backlog);,第二个参数是最大半连接队列(又称SYN队列) ,当有新的连接请求时,如果连接队列已满,客户端就会收到拒绝连接的错误。

read()会返回实际读取的字节数,如果返回0,说明对方已经关闭了,如果小于0,则说明发生了错误。

当然,还可以用I/O多路复用来监视socket是否关闭。将套接字设置为EPOLLIN|EPOLLRDHUP如果epoll返回EPOLL_IN事件,读取时为0,则可以关闭。(EPOLLRDHUP:表示TCP的半关闭或远端关闭)

6. 多进程fork后不同进程会共享哪些资源

实际上,子进程会获得父进程的一些资源的副本,而不会共享,比如一些堆空间、栈空间的变量这些,都是从父进程重新拷贝了一份新的,而不会共享实际的内存地址

7. 五种IO模型,同步阻塞

  • 同步阻塞:发起IO操作的线程在发出后,会一直等待操作系统完成数据手法,阻塞在那,无法执行其他任务
  • 同步非阻塞:允许程序执行其他任务,而无需等待IO操作完成。当一个任务需要等待时,放入等待队列,并处理其他任务。
  • I/O复用
  • 信号驱动I/O
  • 异步I/O:不同于同步非阻塞,异步通常使用回调函数或事件驱动的方式通知发起者任务已经完成。

总结

同步阻塞方式会导致发起者等待任务的完成,同步非阻塞方式允许发起者在等待任务完成时执行其他事物,但需要轮询任务状态,而异步方式则通过回调函数或事件通知完成情况,避免了轮询

8. exec()

exec能够运行另一个程序。如execl("/bin/ls", "ls", "-l", (char *) NULL);

9. 孤儿进程与僵尸进程,怎么解决

  • 孤儿进程:父进程在子进程之前退出;孤儿进程会被init进程接管
  • 僵尸进程:执行完毕后仍占有资源的子进程;需要让父进程回收子进程:通过调用wait或捕捉SIGCHLD信号。

10. linux如何设置后台进程,后台进程和守护进程的区别?

在启动程序时,使用&将程序放置后台运行。例如:./my-program &

  • 后台进程
    • 由用户在操作终端启动,并在后台执行
    • 当用户退出终端时,会收到SIGHUP信号,导致进程终止
  • 守护进程
    • 通常在系统启动时启动,并在后台运行
    • 不与用户交互,也没有与某个终端绑定
    • 当用户退出终端时,依旧可以继续运行

11. linux查看进程打开的文件

可以使用lsof,他是”list open files”的缩写。使用方法:lsof -p [PID]

12. IO复用什么意思?

复用的含义:用最少的物理要素实现最多的功能。

IO复用允许一个程序同时处理多个IO操作,适用于多个文件描述符或网络套接字。

IO复用的核心思想是将阻塞式I/O调用转换为非阻塞I/O调用,让单个线程或进程可以并发地管理多个I/O通道。这样就避免了为每个I/O操作创建独立的线程和进程,降低了资源消耗和调度开销。

为什么要采用非阻塞IO呢?

  1. 并发性:非阻塞IO允许应用程序同时处理多个IO操作,这说明当等待一个数据时,应用程序可以继续执行其他IO操作
  2. 资源利用率:某个IO阻塞时,应用程序其他部分仍可以工作
  3. 可扩展性:对于高并发场景,IO复用可以更好的扩展,因为他减少了线程数量。

13. 一个终端与多个终端传大文件怎么实现,P2P架构?

  1. 分片:将大文件分成多个较小的分片
  2. 创建节点列表:每个节点需要知道其他节点的网络地址,以便通信
  3. 建立连接:各个节点需要建立连接,以便相互传输数据。
  4. 分布式哈希表:使用分布式哈希表来存储和查找节点之间的文件分片信息
  5. 数据传输:在节点之间传输数据分片
  6. 合并文件:当一个节点收到文件所有分片后,重组分片获取到原来的文件

14. linux磁盘、IO、网络相关命令

磁盘相关命令

  • df:显示磁盘空间使用情况
  • du:查看文件和目录磁盘使用情况
  • fdisk:分区管理工具,用于创建和管理磁盘分区

IO相关命令

  • iostat:监控系统输入/输出设备和CPU使用情况
  • iotop:实时监控磁盘IO使用情况,分析哪些进程在正确使用磁盘(top本身就是用来查看进程资源情况的)
  • vmstat:显示关于内存、进程、cpu和文件系统的统计信息

网络相关命令

  • ifconfig:查看网络接口信息,例如IP、子网掩码
  • ping:测试网络连接
  • tcpdump:监视网络流量并实时分析数据包
  • netstat:显示网络连接、路由表、接口统计等信息

15. 网络编程中设计并发服务器,使用多线程和多进程,有什么区别?

根本区别在于,多进程每个进程有自己的地址空间,而多线程共享地址空间。所有其他区别都由此产生:

  1. 速度:线程速度切换快,因为它们在同一个地址空间
  2. 资源利用率:线程的资源利用率较高
  3. 同步问题,线程更好
  4. 稳定性:多进程更稳定

16. socket编程,如果client断电了,服务器如何快速知道

  1. 设置超时时间:如果在超时时间内客户端没发来消息,则认为已经断开
  2. 心跳:服务器每过一段时间向客户端发送心跳,等待客户端的回应
  3. 使用socket选项SO-KEEPALIVE:当你在套接字(socket)上设置了 SO_KEEPALIVE 选项时,TCP 能够在指定的时间间隔内,自动发送心跳包(keepalive 数据包),以确保双方依然保持活动连接。

17. select文件描述符限制

select的主要限制是FD_SETSIZE,默认情况下,FD_SETSIZE是1024,所以最多可以监视1024个文件描述符。FD_SETSIZE是一个宏定义,专门用于select函数的,可以进行修改。

18. 半连接队列是几次握手?

版连接队列是指哪些进行了一部分三次握手过程的请求。处在第一步和第二部之间,即客户端发送了SYN,服务端还未响应SYN。半连接队列的存在有助于服务器更有效地管理和处理各个阶段的连接请求。在高并发和网络延迟的情况下,由于更好地减轻了服务器的压力,它可以提高服务器性能。

19. 内核态和用户态区别,作用,没有内核态会发生什么,为什么没有内核态会发生系统崩溃

  1. 区别

内核态

  • 权限高,可以访问全部系统资源,如内存、硬件
  • 任务调度

用户态

  • 权限低
  • 运行用户程序和应用程序,如浏览器等
  1. 作用

内核态的作用是执行关键任务,访问中断等,保护系统资源,确保操作系统稳定运行。用户态为普通应用程序提供了一个受限的环境,确保应用程序不会影响到系统。

  1. 没有内核会发生什么?
  • 安全性降低:恶意程序可以直接访问内核资源,控制硬件
  • 资源管理困难,导致资源竞争
  1. 为什么没有内核态会发生系统崩溃

内核态能通过操作系统控制硬件,如果没有内核态,无法处理中断,也无法处理资源竞争、内存泄漏的问题。因此,内核态提供了系统级别的隔离和保护,维持整个系统的稳定性。

20. 介绍I/O多路复用机制

IO多路复用的核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是出发了读/写事件,就通知应用程序,去获取这个就绪的连接进行读写操作。

常见的IO多路复用机制有select poll epoll

其中,select和poll基于轮询的方式获取连接,epoll基于事件驱动的方式获取连接。从性能上来看,基于事件驱动的方式要更优。

21. 描述符是什么

描述符是计算机编程中用于标识和管理资源的一个整数,这些资源可能是文件、管道、套接字等。可用这个描述符来进行各种IO操作。

22. 两台linux传文件用什么命令?

scp(secure copy)命令。它是一个在本地主机和远程主机之间迅速将文件复制的命令,基于SSH协议

23. 查找所有.xxx后缀的的文件

find /路径 -name "*.xxx"

24. 怎么查询24小时内修改过的文件?

find /path -type f -mtime 0

这里的 -type f 参数表示查找文件,而 -mtime 0 参数表示查找最近 24 小时内修改过的文件。

25. 我想查端口占用情况用什么命令?

netstat -tuln

26. 如果同一台机器上,不同进程需要大量通信,选哪种方式

  • 套接字:使用AF_UNIX(本地进程间通信)套接字
  • 管道:管道有匿名和命名管道,可用来传递数据。

27. 讲讲linux的权限

linux权限分为三类:分别是用户、用户组、其他。每类用户都有3种权限:读、写、执行。我们可以通过ls -l来查看不同文件的权限是怎样的。也可以通过chmod来为用户修改权限。

  • chown new_owner file.txt:该命令用于更改文件或目录的所有者/group。可以帮助确保只有具有适当权限的用户才能访问或修改文件目录。
  • chmod 777 file.txt:修改访问权限

简言之,chown 更改文件或目录的所有者和/或所属群组,而 chmod 更改它们的访问权限。

28. 如何查找某个端口是否被占用,然后关闭占用?

sudo lsof -i :端口号,如果该端口被的占用,可以用kill 端口号关闭进程。

29. kill -9 和 kill的区别

kill的信号是SIGTERM,被称为优雅退出,用于请求程序终止;kill -9是SIGKILL,被称为强制杀死进程。

30. 什么指令查看线程?linux你是如何感知进程线程的?

ps -eL。-e:选择所有进程。

top :查看所有进程的CPU、内存、网络等使用情况。

linux其实没有线程,posix只是对线程的一层封装。

31. 如何排序,怎么去重?

sort test.txt -u# Sort将文件的每一行作为一个单位,相互比较,比较原则是从首字符向后,依次按ASCII码值进行比较,最后将他们按升序输出。 -u去除重复行

32. 怎么去查看内存泄露?

用valgrind。

对编译后的程序执行

valgrind --leak-check=full ./mytest
  1. Valgrind将会运行程序,并在运行完成后报告潜在的内存泄漏。注意,Valgrind会略微降低程序的运行速度。

33. 条件变量和互斥锁?

  • 互斥锁

    互斥锁只允许一个线程拥有互斥锁,其他线程只有等待。当抢锁失败时,线程会主动放弃CPU进入睡眠状态。

  • 条件变量

    互斥锁的缺点是只有两种状态:锁定和非锁定。当条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,通常搭配互斥锁使用,实现同步操作。当线程获取到锁但是条件不满足时,会释放锁并阻塞线程等待其他线程唤醒。

总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

34. read返回值的总结

1.当read返回值大于0时,返回读到数据的实际字节数
2.返回值等于0时,表示读到文件末尾。
3.返回值小于0时,返回-1且设置errno
当errno = EINTR,表示被信号中断,并且对信号的处理方式为捕捉。对于read函数处理方式可以选择重启或退出。
errno = EAGAIN ,表示以非阻塞方式读并且没有数据。
errno为其他值时,表示错误,可以perror和exit。

35. linux查找并删除/opt下以txt结尾的文件

find /opt -name "*.txt" -type f -delete

-type:按文件类型查找,f是file

-name:按文件名称查找

grep搜索的是文本,find搜索的是文件,换句话说就是grep是查找匹配条件的行,find是搜索匹配条件的文件

36. ls-l a.txt 和 du -sh a.txt为什么显示的大小不一样

  • ls -l显示的是文件的大小,这是文件内容的字节数量。
  • du -sh显示的是文件在磁盘的使用空间。每个块4KB,所以即使一个文件只有1字节,也需要4KB.这就是为什么你在使用du -sh 查询很小的文件的时候,结果显示的文件大小可能比实际的要大。

37. 僵尸进程可以被强杀吗?该怎么处理?

僵尸进程就是子进程先于父进程退出。

kill -9 不能强杀掉僵尸进程,因为僵尸进程本身就是已经退出的进程,kill -9相当于是鞭尸

父进程等待+自定义信号处理,在自定义信号处理中调用wait(NULL),回收子进程。

38. top有哪些字段?

top可用于监控系统活动和当前正在运行的进程。

  • PID:立即调度以运行的进程的进程ID。
  • USER:进程的拥有者用户名
  • %CPU:上一次更新到现在的CPU时间占用比力
  • %MEM: 进程占用的物理内存和总内存的百分比

39. linux 查看端口占用情况

  • netstat:netstat -tuln | grep 端口号

    • -t (tcp) 仅显示tcp相关选项

      -u (udp)仅显示udp相关选项

      -n 拒绝显示别名,能显示数字的全部转化为数字

      -l 仅列出在Listen(监听)的服务状态

  • lsof:lsof -i :端口号

40. docker和vmware有什么区别?

docker不是虚拟机,而是一种轻量级的虚拟化技术,只虚拟出来容器所需要的资源,而不需要像虚拟机一样虚拟一整套硬件。

具体来说,Docker是基于Linux内核的容器化技术,它允许开发者将应用程序、依赖项和配置文件打包到一个可移植的容器中,然后在任何支持Docker的系统上运行。这使得应用程序可以在不同的环境中保持一致性,并且更容易部署和管理。相比之下,VMware是一种完全虚拟机化的技术,它需要在主机操作系统之上运行多个不同的从操作系统,每个操作系统都可以运行自己的应用程序和服务。

MySQL

1. MySQL的隔离级别?

事务隔离级别是为了解决多个并行事务竞争导致的数据安全问题的一种规范。具体来说,多个事务竞争可能会产生三种不同的一个现象。

第一个,假设两个事务同时执行,T1事务可能读取到T2事务未提交的数据,但是最后T2回滚了,导致T1读取到了一个未存在的数据,从而导致脏读现象;第二个是说T1和T2同时执行,但是T1在读取同一行数据的两次结果不一样,从而导致不可重复读的问题;第三个是说假设两个事务T1/T2同时执行,那么T1在执行范围查询或范围修改的时候,T2插入了一条属于T1范围内的数据,并且提交了,这个时候T1查询的时候发现多了一条数据,造成了幻读现象。这三种现象在实际应用中可能不能接收某些存在,所以MySQL提供了四种隔离级别。

  • 读未提交:直接读取数据版本链中的最新版本
  • 读已提交:使用快照读,即按照MVCC读取符合ReadView要求的版本数据,生成多个ReadView,解决了脏读
  • 可重复度:使用快照读,只会在首次查询时生成ReadView,解决了不可重复读
  • 串行化:读取时加入共享表锁,更新时加入独占表锁,解决了幻读

Innodb默认是可重复读,因为他需要保证事务ACID特性中的隔离性。在该级别下会发生幻读(前后读取的数量不同),通过Next-key避免幻读。

MVCC就是在事务启动的时候对数据库拍了个快照,保留了那个时候的状态,这个事务后续的读取都从快照中获取,哪怕加了新的数据,也不会影响。

但是如果别的事务删除了某条记录,我们就不能用快照了,要用间隙锁。

小结

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
  • 针对当前读(当前读会读取最新数据)(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。

MVCC可以确保某一时刻事务见到的数据是一致的,但如果在事务执行期间,另一个事务对数据库执行了插入或删除操作,MVCC无法阻止原事务发现这种新插入的数据,导致发生幻读。

2. 你知道意向锁吗?

意向锁是一种用于数据库管理系统,它试图在锁定资源之前表明事务希望锁定该资源。当一个事务想要获得锁时,数据库会检查是否存在资源的意向锁。

3. 快照隔离是什么?

是一种事务隔离级别,用在数据库中确保不同事务之间的数据一致性和隔离性,能够防止脏读(读到了另一个事务未提交的数据)、不可重复读(两次读取不一样,主要是改)、幻读(主要是增删)。

4. 为什么用B+树不用红黑树这些?

  • 因为B+树查询效率更稳定,每个结点只保留了关键字,那么每次IO读取时就能读取更多的关键字,一个Innodb页默认16kb,一般情况下一颗两层的B+树可以存2万行左右的数据。

  • B+树矮很多,那就意味着访问磁盘IO次数会少很多,效率就快很多

  • 且B+树叶子节点就是数据,MySQL中的B+树叶子节点类似于一个双向链表,我们经常会有范围查询,那么效率会非常高。(在物理地址上是连续的)

  • 所有的数据都存储在叶子节点,那么B+树的全局扫描能力要强很多

5. B+树有什么缺点呢?

  • 插入和删除的开销:插入和删除可能导致结构变动,此时需要一些额外的开销修改树
  • 空间利用率:为了保持平衡,部分结点可能没有被填满
  • 索引维护开销

6. mysql回表是什么,什么时候会回表,如何避免回表

回表是指二级索引,你通过当前索引不能查询到想要的数据,需要再查找一个主键主键索引才能找到。

避免回表的方法:在索引中包含查询所需的所有字段,这样,在查询时,不需要再回原表。

7. mysql-explain?各个字段的含义

mysql-explain语句用于查看查询的执行计划,通过这个语句可以看到查询语句的优化效果,有助于我们进一步优化。

  • extra
    • distinct:在select部分使用了distinc关键字
    • Using filesort:当 Extra 中有 Using filesort 时, 表示 MySQL 需额外的排序操作, 不能通过索引顺序达到排序效果. 一般有 Using filesort, 都建议优化去掉, 因为这样的查询 CPU 资源消耗大.
    • Using index
      “覆盖索引扫描”, 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错
    • Using temporary
      查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不高, 建议优化.

8. mysql锁有哪些

  • 全局锁:加锁后整个数据库实例都处于只读状态。

    • 共享锁:读锁。

      select xxx LOCK IN SHARE MODE

    • 排他锁:写锁。只有一个事务能够获得排他锁。

      select xxx FOR UPDATE

    • 自增锁:通常时针对MySQL当中的自增字段。如果有事务回滚这种情况,数据就会回滚,但是自增序列不会回滚

  • 表锁

    锁整张表,锁粒度最大,并发度低。MyISAM和Innodb都支持。

  • 行锁

    锁某行数据,锁粒度最小,并发度高,但是加锁资源开销比较大,Innodb支持。


常见的锁算法:user:userid(1,4,9) update user set xxx where userid=5; REPEATABLE READ间隙锁锁住(5,9)

  • 记录锁:锁一条具体的数据
  • 间隙锁:RR隔离级别下,会加间隙锁,锁一定范围;防止幻读;划分为(-xx,1](1,4](4,9](9,xxx),左开右闭
  • Next-key:间隙锁+右记录锁(-xx,1] [1,4] [4,9] [9,xxx),实现左闭右闭

9. 慢查询如何优化?

  1. 是否用了索引,该索引是否是最左索引
  2. 是否返回了过多字段,查出了多余数据
  3. 检查数据是否过多,是否应该进行分库分表
  4. 检查数据库实例所在的机器性能
  5. 修改配置文件:在my.ini文件中,慢查询的定义事件是超过2秒,我们可以修改慢查询的定义
  6. 优化数据库结构:合理的数据库结构不仅可以使数据库占用更小的磁盘空间,而且能够使查询速度更快。
  7. 少使用join语句:比如左连接必须先查左表全表扫描,然后一条条到外表去查询
  8. 利用explain的key,看看到底使用了哪个索引,把用不到的索引删除掉
  9. 如果是联合索引,考虑是否满足最左前缀原则,尽量少使用聚合函数
  10. like模糊查询的%不可以放在最前面,不要使用or、union(去重)、not in这些条件查询

一、慢查询原因
要对慢查询进行优化,首先要搞清楚慢查询的原因,原因主要有三:

(1)加载了不需要的数据列

(2)查询条件没有命中索引

(3)数据量太大

二、优化方案
优化也是针对这三个方向的:

(1)先分析语句,看看是否加载了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,如果有这些问题,则对语句进行分析、重写

(2)分析语句的执行计划,获得其使用索引的情况,然后修改语句或修改索引,使得语句尽可能地命中索引

(3)如果对语句的优化都已经无法进行了,可以考虑是否是表中数据量太大引起的慢查询,如果是,则可以进行横向或者纵向分表

10. 索引设计的原则?

  • 出现在where子句的列
  • 基数较小的表,没必要建立
  • 更新频繁的字段不适合创建索引
  • 不要过度索引,索引会占用空间
  • 定义外键的数据一定要建立索引

11. 索引覆盖是什么?

索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,所有要查询的字段在当前索引对应的字段中都包含了,不用回表。

12. MySQL聚簇和非聚簇索引的区别?

二者都是B+树的数据结构

  • 聚簇索引:将数据存储与索引放在一块,找到了索引也就找到了数据
  • 非聚簇索引:叶子节点不存储数据,存储的是数据行地址,需要回表,相当于字典查询中的按照偏旁查找

Innodb一定有主键,主键是聚簇索引,不手动设置,则会使用unique索引,若没有unique索引,则会使用内部一个行的隐藏id来当作主键索引。

MyISAM使用的非聚簇索引。

13. 什么是MVCC?

是多版本并发控制:通过为所有事务操作的数据对象分配一个时间戳或版本号,从而使每个事务在自己的快照上执行,而不影响其他事务。MVCC 允许多个读操作和写操作并发执行,提高了系统的吞吐量和性能。(不用加锁)

MVCC直在READ COMMITTED (已提交读)和 REAPEATABLE READ(可重复读)下工作。

MVCC就是一种乐观锁的机制,它通过对于不同事务生成不同的快照版本,然后通过UNDO的版本链进行管理;高版本看得到低版本的事务变更,反之看不到。从而去实现了不同事物之间的数据隔离,解决了幻读的问题。

14. 最左前缀原则是什么?

如果一个SQL想要利用索引,就必须提供该索引所对应字段中最左边的字段,比如针对abc建立了一个联合索引,那么在写sql的时候就一定要提供a字段的条件。

15. Innodb是如何实现事务的?原理?

事务就是数据库中一系列操作的组合,具有4大特性。。

  1. 收到update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
  2. 执行update语句,修改Buffer Pool中的数据(即内存中的数据)
  3. 针对update生成一个RedoLog对象(持久化日志),并存入LogBuffer中(保证持久性)
  4. 针对update生成undolog,用于回滚(保证原子性)
  5. 如果事务提交,则把redolog对象持久化
  6. 如果事务回滚,则利用undolog日志回滚

原理

mysql的事务原理就是InnoDB如何保证ACID的特性的。

首先A是原子性,为了保证原子性,要么都成功,要么都失败,失败就意味着对原本执行成功的数据回滚,所以有一个UNDO_LOG表,在事务执行过程中,把修改之前的数据快照,保存到UNDO_LOG中,一旦出现错误,直接从UNDO_LOG进行反向操作。

其次就是C一致性,这个主要是依赖业务层面的保障。

然后是I隔离性,多个并行事务对同一个数据进行操作的时候,如何去避免多个事务的干扰。SQL提供了四种隔离级别来实现,Innodb默认采用可重复读+MVCC机制解决了脏读和不可重复读的问题,然后用间隙锁解决了幻读的问题。

最后D持久性,只要事务提交成功对数据的影响是永久的。按理说直接写入磁盘即可,但是效率非常低。于是Innodb设计了Buffer Pool缓冲区来进行优化,也就是说数据发生变化的时候先更新内存缓冲区,然后在合适的时间再持久化到磁盘中。但在这个过程中可能会出现宕机,于是就引入了Redo_LOG,这个文件存储了数据库变更之后的值,当我们通过事务进行数据更改的时候,除了修改内存缓冲区里面的数据之外,还会把本次修改的值追加到RedoLog中,当事务提交的时候,直接把redo_log里面的日志刷新到磁盘里面,进行持久化。一旦宕机,在重启后可以直接用Redo_log保存的重写日志再重写一遍。

16. Redis和MySQL如何保证数据一致?

  1. 先更新mysql,再更新redis;但是如果更新redis失败,可能不一致(不推荐),而且会产生并发问题,导致脏数据
  2. 先更新缓存,再更新DB:如果DB异常,导致缓存数据和DB数据不一致(同上)
  3. 先删除redis缓存数据,再更新mysql,再次查询的时候再将数据添加到缓存 (有可能redis读取到的值是旧值)
  4. 延时双删:先删redis,再更新mysql,延迟一段时间再删redis,这样就算在更新mysql时,有其他线程读了mysql,把老数据读到了redis中,也会被删掉,从而保证数据一致

17. 什么是脏读、幻读、不可重复读?要怎么处理?

  • 脏读:读到了其他事务未提交的数据

  • 不可重复读:在一个事务中,多次查询的结果不一致(强调改)

  • 幻读:在同一个事务中,用同样的操作查询数据,得到的记录数不相同(强调增删)

处理方式:加锁、事务隔离、MVCC

加锁:

  1. 脏读:在修改时加排他锁,直到事务提交才释放。读取时加共享锁,读完释放锁。
  2. 不可重复读:读数据时加共享锁,写数据时加排他锁。(同上)
  3. 幻读:加范围锁。

18. MySQL一页有多大?

一页(一个节点)16kb。每次从磁盘取数据,都是一页一页的取。(操作系统一页是4KB)在插入时,会根据主键自增自动排序

19. 高度为3的B+树可以存储多少数据?

一个主键4字节,一个指针6字节,那么一页(一个节点)能存储16kb/10 = 1638个页(节点、page)

假设一行记录1kb,那么两层就能存储1638*(16kb/1kb)= 26208

那么假设是三层,就有1638 * 1638 * 16,大概4K多万条。

一般B+树就设置为3层,多了查询就很缓慢了。

20. order by为什么使索引失效?

  1. 可能索引失效,最左前缀原则没有用到
  2. 可能该索引不是主键索引,而我们查询的字段不能直接通过该索引得到,需要回表,会非常慢

21. SQL语句执行效率低,怎么分析并优化它?到服务器程度了吗?

  1. explain 查询sql执行语句,看看是否走索引
  2. 优化表结构,添加索引
  3. 优化sql语句,避免使用select *, 优化JOIN和子查询等
  4. 考虑硬件和网络原因

一般情况下,分析和优化SQL语句就能大幅度提升性能,无需涉及服务器层面。不过如果服务器性能太差,也得考虑更新服务器硬件设施。

22. 为什么sql语句join会很慢?

  1. 索引缺失:没有合适的索引
  2. 数据量过大,此时可以将大表拆分成小表,这样可以减少需要查询处理的数据量
  3. join设计多表,内存不足的情况下,数据库可能需要将部分结果存储在磁盘上,这会导致操作变得很慢

23. 为什么要遵循最左前缀原则?

因为联合索引在B+树中是查询第一个索引,第一个索引值相同的情况下,再查找第二个索引。如果直接跳过第一个索引,索引就会失效,就无法根据索引查询了。

另外,最左前缀原则会一直向右匹配知道遇到范围查询就停止匹配了,所以我们可以先等值查询放到范围查询后面。

24. 索引失效了解吗?怎么知道索引失效了没有?

索引失效就是查询语句没有用到索引,走的全表查询。

可以在sql语句前面加explain,如果Extra出现了Using filesortUsing temporaryUsing join buffer等,说明查询语句存在问题,需要优化;还可以看key列,如果出现了索引名称,说明用到了索引,如果是NULL则没有用到索引

25. 我建了abc的联合索引,再单独用了a=什么,b=什么,c=什么,它会走索引吗?a=什么 and b=什么 会走索引吗

单独用a=什么,可能会走索引,这取决于数据库优化器的决定,如果优化器认为走索引开销更小就走索引。其他两个不会走索引

对于a=什么 and b=什么,也会走索引,同上,取决于优化器。

具体都可以用explain来看。

26. InnoDB引擎和MyISAM引擎的区别

  1. Innodb支持事务,myisam不支持
  2. myisam支持表锁,innodb支持行锁,对于高并发读写环境下,性能更高
  3. myisam不支持外键,innodb支持外键
  4. innodb不保存表的具体行数,执行select count(*)需要全表扫描。而myisam保存了整个表的行数,执行上述语句速度很快。

27. 索引的优缺点?

索引能够帮助mysql高效地去从磁盘去检索数据的数据结构,在mysql的innodb引擎中采用的是B+树的结构来实现索引和数据的存储。

优点有很多,我简单罗列几点:

  1. 通过B+树的结构来存储数据,可以大大减少数据检索时的磁盘IO次数,从而提升数据查询时的性能
  2. B+树索引在进行范围查找的时候,只需要找到起始节点,然后基于叶子节点的双向链表结构往后读取,查询效率高
  3. 通过唯一索引的约束,可以保证数据表中每一行数据的唯一性

索引的不合理使用也会带来很多缺点,诸如:

  1. 数据的增删改的时候,需要修改索引,当数据量很大的时候,索引的维护会带来较大的开销
  2. 一个表只能有一个聚簇索引,多个非聚簇索引。索引数量不能太多,否则维护成本很高
  3. 创建索引的时候,需要考虑索引字段值的分散性,如果字段重复数据过多,创建索引反而会带来性能降低

28. mysql的分库分表?

将一个庞大的数据库通过某种策略分割成多个较小的表。这些较小的表分布在多个数据库服务器上,提高系统性能,扩展数据库存储容量,并降低数据在单个节点上的风险。

  • 分表

    • 垂直分表:将数据垂直划分为多个表,这样单个页里面能存放的数据更多,所需要的IO次数就更少

    • 水平分表:将一个表的数据分成多个表

      • 取模:后续扩展非常麻烦
      • 根据ID范围分表:根据ID自增来决定放入哪张表里面。缺点是大多时候只有一两张表频繁读写,其他表都很空闲,出现读写热点问题
      • 同时结合ID取模分表和ID范围分表:先根据数据量范围分表,然后再对id取模决定具体的表,比如user1_0表。如果部署到多台机器上,就是分库分表。我们同过一个中间层逻辑做路由(一般是proxy),把这部分逻辑封装起来,这样对于业务代码来说,就只知道在读取一张表。不过会出现读扩散问题,即出现select *的时候,需要挨个查询。可以对对应的数据再建立一个索引,查询时就进行一次回表,缺点就是要维护两个索引,开销大。

29. MVCC过程中会加锁吗?

multi-version concurrency control 在MVCC中,通常不需要加锁来控制并发的访问,相反,每个事务都可以读取到已提交的快照,而不需要去获取共享锁或者排他锁。在修写操作时,MVCC会用到写时复制的技术,即在修改数据之前先将数据复制一份,从而创建一个新的快照。当一个事务需要修改数据的时候,MVCC会首先检查修该数据的快照版本号,是否与该事务的快照版本一致,如果一致就可以修改这条数据,否则这个事务需要等待其他事务完成对该数据的修改。另外,其他事务也可以读取相同版本的快照数据,防止了脏读和不可重复读的问题。以上就是我的理解。

30. mysql的锁有哪些?

  • 锁模式lock_mode

    • 共享锁
    • 排他锁
    • 意向锁(表级锁):用于表明在某个表上持有了特定类型的锁,他不直接影响其他事务对表的读取和写入操作,而是一种指示
      • 作用:当事务A有行锁,mysql会自动为该表加意向锁,事务B如果想申请整张表的写锁,就可以直接判断是否存在意向锁,增强性能
      • 意向共享锁
      • 意向排他锁
    • 自增锁
  • 锁粒度

    • 全局锁:flush tables with read lockunlock tables

    • 表锁:lock tables employee read/write;性能下降,并发能力差,写操作影响大(MyIsam只有表锁没有行锁)

    • 行锁:只能在事务中使用,for update in share mode

      • 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。
      1. 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。
      • 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持。

      我们通常选用innodb存储引擎,主要使用行级锁,可以提供更好的并发性能,并且再一定程度上减少了锁争用的问题。而且InnoDB支持事务,可以保证数据的一致性和完整性。

  • 行子类型rec_lock_type

    • 记录锁:锁一条具体的数据
    • 间隙锁:RR隔离级别下,会加间隙锁,锁一定范围;防止幻读;划分为(-xx,1](1,4](4,9](9,xxx),左开右闭
    • 临建锁Next-key:间隙锁+右记录锁(-xx,1] [1,4] [4,9] [9,xxx),实现左闭右闭
  • 乐观锁和悲观锁

    • 乐观锁:冲突概率低,因此再操作数据时不会立即锁定,而是在提交数据更改时检查(加一个版本号字段)是否又其他事务修改了这条数据。如果没有,则提交更改,否则回滚事务。
      • update employee set name='张三', version=version+1 where id = 1 and version = 0;
      • 低冲突环境、读多写少、短事务、分布式系统(网络延迟)、互联网应用(大多是读取操作),缺点是发生冲突时只有一个事务可以提交
    • 悲观锁:先加锁,保证数据完整性和一致性;适合写比较多、并发冲突高、业务需要强一致性的场景;使用就是for update lock in share mode
      • 缺点:性能开销、并发度降低、死锁、锁超时
    • 自旋锁(CAS):尝试获得的数据未改变,则写入
      • ABA问题:会导致数据不一致的问题。(有的业务会关心中间值,则会发生问题)。解决策略有:加版本(version bool)
      • 保障CAS操作的原子性问题(lock指令):原子级别

31. 数据库的分页怎么做?

  • limit/offset:通过limit和offset指定查询结果的起始位置和结束位置,从而实现堆数据库的分页。
  • 游标:将查询结果存储在游标中,然后根据游标的指向获取每页的数据。

32. MySQL有哪几种日志,描述下

  • 慢查询日志:记录了执行时间超过某个阈值的查询。
  • 二进制日志:记录了对数据库进行更改的执行语句,这些信息可以用于故障恢复、主从复制
  • 错误日志:记录了服务器启动、运行、关闭过程中遇到的问题。

33. MySQL复制原理

  1. 主服务器记录日志:金鲁道二进制日志中
  2. 从服务器请求日志:请求主服务器发送从服务器未有的日志,从服务器会记录从哪个二进制日志开始
  3. 主服务器发送日志:主服务器响应从服务器的请求,发送日志
  4. 从服务器应用日志:收到二进制日志事件后,从服务器解析并应用到自己的服务器中,从而保持与主服务器的数据同步

34. 讲一下聚簇索引吧。聚簇索引和哈希索引的区别?

聚簇索引就是按照每张表的主键构造了一棵B+树,同时叶子节点将索引和数据存储在了一起,找了索引就找到了数据。而非聚簇索引存储讲数据和索引分开,索引结构的叶子节点指向了数据的对应行,需要进行回表,效率较低。

35. 联合索引有什么规则?

  1. 选择性高的列优先:在创建联合索引时,建议将区分度高的列放在索引最前面。
  2. 最左前缀原则:在查询条件中最左边开始的列匹配你的索引第一个字段。

36. mysql的大表分页查询如何做优化

  1. 避免使用offset语句:使用大的offset值得limit语句可能导致性能问题,因为mysql需要处理并跳过offset之前得所有记录
  2. 使用索引覆盖:确保查询可以使用索引覆盖

37. 乐观锁实现方式

有以下几种

  • 版本号:在数据表中添加一个版本号字段,每次更新数据时,版本号+1.在进行更新操作的时候,检查当前的版本号是否与之前读取的版本号相同,如果相同则执行更新操作,否则表示数据已被其他人修改,更新失败,MVCC就是基于此。
  • 时间戳:在数据表中添加一个时间戳字段,记录每次数据更新的时间。在进行更新操作时,检查当前的时间戳是否与之前读取的时间戳相同,如果相同则执行更新操作,否则表示数据已被其他人修改,更新失败。

38. mysql分库分表后怎么进行查找?

分库,当表的数量很多导致数据系统的单个数据库很大,这时候需要根据不同业务将表拆分到多个数据库中;分表,当表中的数据太多的时候导致单个表的太大,这时候需要将表中的数据拆分到多个表中。

分库分表策略一般有几种,使用与不同的场景:

  • range范围:缺点是会出现热点问题,比如最近一个月的订单都在一张表的id范围内,那就都打到这张表上了
  • hash取模
  • range+hash取模混合

分库分表主键问题

  • 用redis进行自增 incr orderId
  • 雪花算法:雪花算法是一种Twitter开源的分布式ID生成算法,是long类型的ID,占用8字节的空间,由时间信息+机器信息+流水号组成。因为前面是时间信息所以保证了ID的有序性,同时占用空间要比前面两种小。因为有时间信息所以要保证多个服务器时间同步。

怎么进行查找?

  • 应用层的分片查询

    根据分片规则,应用程序可以根据查询中的某个值(如用户ID)来确定目标分片(分库和分表)。一旦找到目标分片,应用程序会直接对该分片进行查询。(是否可以用redis存储每一个库的ID自增数在哪了,根据redis来判断在哪一个数据库哪一个片)

39. 索引的分类?

  • 数据结构
    • 树索引
    • 哈希索引
  • 物理存储角度
    • 聚簇索引
    • 非聚簇索引
  • 逻辑角度
    • 唯一索引
    • 主键索引
    • 联合索引

40. 谈谈你对数据库读写分离的理解?

读写分离常用代理方式实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。

读写分离能提高性能的原因在于:

  1. 主从服务器负责各自的读和写,极大程度缓解了锁的争用
  2. 增加冗余,提高可用性。

41. MySQL数据库CPU飙升到500%的话怎么处理?

  1. 列出所有进程,观察,干掉一些没有变化的。
  2. 查看超时日志或者错误日志(一般会是查询以及大批量插入导致CPU与IO上涨,当然不排除网络状态断了,导致一个请求服务器直接受到一半)
  3. 优化SQL查询效率
  4. 限制连接数:在WEB上设置连接池,限制连接数量

42. MVCC +undo logo解决了什么问题?

  1. 保证了事务的原子性:UNDO日志记录数据修改操作的逆向操作。当事务需要回滚时,UNDO日志用于撤销已执行的指令。
  2. 保证事务的隔离性:每个事务在开启前会生成数据的一个快照,保证我们各个事务在不用加锁的情况下,读取到当时事务对应的数据版本。因此,也解决了事务一致性的问题。

43. 乐观锁和悲观锁

  • 乐观锁默认不会加锁,只会在提交的时候判断一下是否在此期间别人修改了数据,如果修改了则放弃操作,否则则执行。乐观锁的实现方式主要有2种:CAS和版本号机制。
  • 悲观锁则相反,在操作数据的时候直接把数据锁住,直到操作完成后才释放锁。

44. 读提交和可重复读实现上的区别?

区别在于MVCC生成数据快照时,读提交会在每次事务修改的时候生成一个readview,而可重复度只会在事务开启时生成一个,后续的操作都是在这个readview上的。

45. 了解过redo日志的存储格式吗?

分为以下几种:

  • 通用格式。大部分redo日志都有着通用的结构,存储着日志类型、表空间ID、页号以及这条redo日志的具体内容。
  • 简单redo日志。一些简单的redo日志只需要记录在某个页面的某个偏移量处修改了几个字节的值,具体修改后的内容是什么。这种简单的redo日志称为物理日志,根据在页面中写入数据的多少划分了多种类型。
  • 复杂redo日志。有时执行一条语句时会修改很多页面,包括系统数据页面和用户数据页面。比如一个INSERT语句,表中有多少索引就可能更新多少棵B+树,针对某棵B+树,可能更新叶子节点页面,也可能更新内节点页面。

46. 一条SQL的执行步骤?

  1. 客户端请求
  2. 连接器(验证用户身份,给予权限)
  3. 查询缓存:若存在缓存直接返回,MySQL8.0以后已经删除(缓存失效代价高)
  4. 分析器:对SQL语法分析
  5. 优化器:对执行的SQL优化选择最优的执行方法
  6. 执行器:执行时会看用户是否有执行权限,有才去使用这个引擎提供的接口
  7. 去引擎层获取数据返回

47. 快照读为什么不会幻读 当前读为什么会出现?

快照读是事务开始时,该事务生成一个全局数据的快照,后续的操作都是在该快照上的,所以就消除了幻读的可能,因为即使其他事务插入或删除了符合查询条件的数据,从事务开始时的数据快照看,这些更改并不存在。

当前读每次查询都尝试获取数据库的当前状态,这就意味着如果其他事务提交了修改,当前读会看到这些已经提交的修改,查询多次返回结果不同,从而导致了幻读的问题。

48. limit底层实现原理有了解过吗?

  1. 执行查询:MySQL 执行器根据优化器的建议开始从数据表中查找数据。当执行器找到第一条结果记录时,它会检查 LIMIT 子句中的起始值(如果有的话)。如果查询需要跳过一定数量的记录,执行器将继续扫描并忽略这些记录,直到达到该起始值。
  2. 获取并返回结果:当达到 LIMIT 子句中的起始值时,MySQL 开始向结果集中添加记录。每次找到新的记录时,MySQL 都会检查是否达到了结果集的限制。一旦达到限制,MySQL 将停止查找,并返回结果集。

49. 讲一下explain中索引命中的类型

type字段,显示查询使用了何种类型,从好刀叉依次为:

  1. system:表只有一行数据,这是最理想的查询类型,几乎不消耗任何资源。
  2. const:唯一性索引使用(包括主键索引),仅需要读取一行数据。表示至少一个索引列有常数值,这种查询非常快。
  3. eq_ref:多表联结查询中的情况,即在连接操作中,对于前一张表的每一个结果,后一张表都只有一条结果与之对应。这种情况一般出现在使用主键或唯一性索引作为连接条件的场景。查询性能较好。
  4. ref:对于前一张表中的每一行结果,在后一张表中可能有多行结果与之对应。连接时使用的条件不是唯一性索引。查询性能良好,但不如eq_ref。
  5. range:表示本查询范围扫描,一般是使用了范围条件(如 BETWEEN<><=>= 等),但只限定了索引的部分列。
  6. index:全索引扫描。MySQL这种情况下会遍历整个索引树,这比全表扫描要快些,因为索引树的数据量通常比数据表本身小。
  7. ALL:全表扫描,MySQL 遍历整个数据表,查找匹配的行。全表扫描效率低,需要尽量避免。

50. 索引为什么用B+树不用跳表?

二者查询的复杂度都是logn,而且数据都是有序的。但是B+树是一个多路平衡树,当新插入一个数据时,整个B+树会做调整,进行一个平衡操作;而跳表的多级索引是随机的,理论上来说上一层的索引是下一层的二分之一,那么如果有三层的跳表,最上层插入索引的概率就是四分之一,当数据量够大时,就符合二分之一。跟B+树不一样,跳表是否新增层数全靠随机函数。

B+树一页16KB,能存储很多索引信息,所以扇出每个索引页都能指向1000多个子页,三层左右就可以存储2000W数据,所以要查询一个数据最多三次磁盘IO;而跳表如果最底层要存放2000W数据,且每次查询都要达到二分查找的效果,那么2000W大概就等于2的24次方左右,所以高度是24次,那么最坏的情况下查一次数据要24次磁盘IO。因此存放同样多的数据,B+树高度要低得多。不过跳表的写入性能要比B+树高。

51. mysql中常用的数据类型

  • 整型
    • INT:4个字节
    • BIGINT:8个字节
  • 浮点型
    • FLOAT:单精度
    • DOUBLE:双精度
  • 时间和日期类型
    • DATE:用于存储日期,格式为 ‘YYYY-MM-DD’。
    • TIME:用于存储时间,格式为 ‘HH:MM:SS’。
    • DATETIME:用于存储日期和时间,格式为 ‘YYYY-MM-DD HH:MM:SS’。
    • TIMESTAMP:也用于存储日期和时间,但其范围较小,并且可以在记录插入或更新时自动设置为当前时间。
    • YEAR:用于存储年份,范围从 1901 到 2155。
  • 字符串类型
    • CHAR:定长字符串,最多存储255个字符,当数据长度固定时,char性能更好
    • VARCHAR:可变长度字符串,当数据长度存在显著差异时,varchar数据类型具有更好的性能,因为他节省存储空间并消耗较少的IO资源
    • TEXT:最长可达 65535 个字符的字符串
  • 二进制类型
    • BLOG:类似于TEXT,但用于存储二进制数据
    • BINARY:与CHAR类似,但用于存储二进制字节串而非字符

52. mysql主从复制

mysql主从赋值时mysql中最基本的高可用和故障恢复策略之一。该策略工作原理是:主数据库进行所有的数据更改,然后这些更改会记录在二进制日志中,从数据库将这些更改赋值到自己的数据集中。这也可以避免单点故障,提高读取性能,以及简化备份流程。

53. redis和mysql的区别

  • 数据模型:一个是表格、一个是kv
  • 数据持久化:mysql提供较强的数据持久化功能,redis提供了rdb和aof
  • 性能:redis在内存中,所以快得多
  • 应用场景:mysql适用于复杂查询、事务处理等场景,redis用作缓存、发布订阅等高并发场景
  • 事务:mysql的事务支持ACID,redis虽然也支持事务,但不具备ACID

54. 如果把MySQL的硬盘存储换成内存存储,可以跟Redis比吗?

仍然不行。

  • MySQL是关系型数据库,数据以表单的形式存储,适用于复杂的数据关系和事务操作。而redis是kv数据库,数据结构简单,适合存储小块数据。
  • 持久化:虽然redis也有持久化,但是redis对数据的持久化没有mysql完备,mysql进行持久化的操作会更耗时
  • 事务支持:MySQL有非常强大的事务支持能力,它可以处理复杂的增删改查和事务性任务。但Redis的事务功能较弱。
  • 查询语言:redis查询简单,而mysql支持复杂查询

55. 数据库事务隔离级别为什么是4种,不是更多的级别?

因为再处理并发事务的时候,主要面临三种问题:脏读、不可重复读、幻读。这四种不同级别的隔离能够解决不同的并发问题,四种隔离级别是比较合适的设计。如果添加更多的隔离级别,就没有必要了,因为还会带来更复杂的管理和维护问题,同时也会对性能带来一定的影响。

56. MySQL常用的存储引擎

  • InnoDB:默认的存储引擎。提供了四种隔离级别,支持事务,支持行锁。默认采用可重复读+next-key锁来解决三种并发问题。
  • MyISAM:是MySQL最早的存储引擎。不支持事务和行级锁,不过MyISAM有统计表行数,所以count(*)会更块。在只读少写的场景下,MyISAM的性能由于InnoDB。

57. 当前读和快照读的区别?

  • 当前读:当前读是一种行级锁定读取方式,读取的是最新的数据。当你有一个事务正在对一个记录进行读取或写入操作时,其他事务不能对这个记录进行修改。在MySQL中,SELECT… FOR UPDATE 和 SELECT… LOCK IN SHARE MODE 就是进行的当前读。
  • 快照读:读取一个事务开始时的数据版本快照,即使在此期间其他事务对数据进行了修改,也不影响这个数据的操作。

当前读需要用next-key(行锁+间隙锁)解决幻读,而快照读情况下,mvcc避免了幻读。

58. 分库分表准则?

  • 业务逻辑分割:过程中优先考量业务之间的关联性,尽可能地保证数据独立性。
  • 均衡性:【数据分布均衡】,要考虑数据的分布是否均匀;【负载均衡】,确保新的子表或库的负载是均衡的,以避免热点的出现。
  • 数据一致性:需要确保数据在增删改地操作下,数据一致。

Redis

1. 为什么单线程能有这么好的性能 ?

redis的性能瓶颈不在于CPU,而在于内存、网络I/O的影响,早期版本中,他们能够用单线程实现高性能,就没有使用多线程了。而且多线程还需要考虑锁的开销。

2. Redis可以设置过期时间,过期的删除策略有哪些?

  • 定时删除:给某个键设置过期时间,默认每秒进行10次过期扫描,过期扫描不会遍历过期字典中的所有key,而是采用一种贪心策略

    • 从过期字典中随机20个Key(可能扫不到过期的key,所以才有惰性删除)
    • 删除着20个key中已经过期的key
    • 如果过期的key比率超过1/4,那就重复步骤1

    如果同一时间大量key过期,会一直循环扫描过期字典,造成卡顿现象。所以业务开发人员要注意过期时间,如果有大批量的key过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期

  • 惰性删除:检查一个键是否过期时,并不主动删除过期的键。而是在访问的时候,如果发现已经过期,再删除。减少了CPU负担,但不能保证过期的一定会删除,可能造成内存浪费。

实际上,redis的过期删除结合了二者,并根据系统的负载进行调整。

3. redis的哨兵?

用来监控和管理redis服务器。对主从复制集群进行监控、通知、自动故障转移。

  • 监控:哨兵会周期检查redis主从服务器是否正常运行
  • 通知:被监控的结点发生故障时,sentienl通知管理员
  • 故障转移:如果一个主服务器被检测失效,会从从结点中选举一个新的主服务器
  • 配置服务发现:客户端可以询问sentinel获取当前可用的redis服务地址和端口。

4. redis为什么这么快?

关于这个问题,我是这样思考的。我从两个点来说:一个系统,他无非就是两种操作,一种是读写操作,一种是计算操作。关于计算操作,它是基于内存的,所以计算对于CPU来说不是瓶颈,真正的瓶颈在于IO。然后就是读写操作,读写操作他做了很多优化,读写操作又分为2部分,一个是网络IO一个是磁盘IO。磁盘IO这块呢,是redis基于做持久化的时候他用另一个线程去写入磁盘,但是对于主线程来说不影响啊;至于网络IO,他采用了多路复用IO,用epoll同时监听多个socket的IO请求。包括后续redis6.0出了多线程,增加的多线程只是用来处理网络IO事件,对于指令的核心执行过程,仍然是主线程单线程来处理

  1. 基于内存,绝大部分请求是纯粹的内存操作,非常快。
  2. 数据结构简单,对数据操作有简单
  3. 采用单线程,避免不必要的上下文切换和竞争条件,也不存在多进程/多线程之间导致的切换而消耗cpu,不需要考虑各种锁的问题
  4. 使用I/O多路复用,非阻塞IO
  5. 使用底层模型不同,它们之间底层实现方式以及客户端之间的通信协议不一样,redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的事件去移动和请求。

还有一点,就是渐进式ReHash、缓存时间戳

我们知道,redis本质就是一个大的哈希表,当需要进行扩容的时候,需要将所有数据进行迁移,对于大量数据来说,这个过程是会很缓慢的。redis采用了渐进式rehash,即把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

这个过程是这样的:redis默认使用了两个全局哈希表,默认使用哈希表1,此时哈希表2没有被分配空间。当数据增多的时候,进行rehash,给哈希表2分配更大的空间,例如是哈希表1的两倍,然后把哈希表1的数据重新映射到哈希表2中,释放哈希表1的空间。在访问的过程中,如果第一张哈希表没找到,就找第二张

使用时间戳的时候不进行系统调用,因为系统调用会很慢,因此redis对事件进行了缓存,由一个定时任务,每毫秒更新一次时间缓存,获取时间都是从缓存中拿。

5. redis的持久化机制是什么?

有两种,一个是RDB快照,一个是AOF日志。

  • RDB快照:xx时间如果超过xx条数据更改,则将当前redis生成一个快照保存

    • 载入RDB文件是为了在redis重启后还原之前存储在redis的数据
    • 如果AOF关闭,服务器采用RDB还原;如果开启了AOF,则服务器优先使用AOF文件还原数据
  • AOF日志:保存redis服务器执行的写命令,默认AOF是关闭的。

    • redis只需要读入并重新执行一次AOF的写命令,就可以还原redis服务器关闭之前的数据

RDB记录的是结果,AOF记录的是过程,所以AOF会更大,redis提供了AOF重写功能来减小AOF文件体积,优点是回复大数据集的速度比AOF快,适合需要做冷备份,对数据恢复要求不高,。

AOF持久化的安全性要比RDB持久化的安全性高,即如果宕机,AOF要比RDB丢失的数据少(最多丢失1s之内写入的数据)。优点是更加稳定,数据完整性更好,更适用于更加稳定、对数据完整性要求高的场景

6. 什么是缓存穿透?如何解决?

缓存穿透是指缓存和数据库都没有的数据,导致所有数据都落到数据库上,造成数据库崩溃。

解决方案

  • 在接口层增加校验,比如如果id <= 0,则直接拦截

  • 可以将key-value对 写成key-null,缓存有效时间可以设置短一点,这样可以防止攻击者反复用同一个id暴力攻击

  • 布隆过滤器放在redis前面,将所有可能存在的数据存到足够大的bitmap中,一个一定不存在的数据就会被拦截掉,从而避免了对底层存储系统的查询压力(选择多个hash函数计算多个hash值,如果全部命中,则说明可能存在)

7. redis 支持事务吗,什么命令?什么数据类型的支持事务?

redis支持事务,包括的命令有:MULTI开启新事务、EXEC执行事务的命令、DISCARD取消事务、WATCH监视一个或多个键

8. redis的zset底层结构

ZSet是一种有序集合,底层基于跳表和哈希表

  1. 跳表是用于维护有序数据的数据结构,能够快速查询、删除、搜索
  2. 哈希表直接访问内存存储的位置。在ZSet中,哈希表用于存储和快速访问成员。

9. redis怎么保证数据一致性?

  1. 持久化:用RDB、AOF或二者结合的方式,将数据写入磁盘
  2. 主从复制:从节点复制主节点的数据,当主节点宕机时,切换节点。
  3. 分布式一致性:对于分布式的redis集群,需要考虑数据一致性。
  4. 容错和故障切换:哨兵监视着各个节点,保证在宕机时选取新的节点。

10. redis的淘汰策略?

当内存满了,就会执行淘汰策略删除一些旧数据

  1. LRU:最近最少使用的键
  2. 随机删除一个键
  3. 无驱逐:直接报错
  4. 删除TTL即将过期的键

11. redis底层的数据结构

redis我们直到是一个KV键值对数据库嘛,他其实本质就是一个全局哈希表+链表的结构。比如set bob 1我们默认V是用的string类型,而这里的V其实有很多种数据结构类型供我们选择。

  • 字符串string

    • int:字符串长度小于等于20且能转换为整数
    • raw:字符串大于44
    • embstr:字符串长度小于等于44(64减去19个编码信息)
    • cpu缓存的基本单位为cacheline64个字节
    • 使用场景:
      • 文章博客阅读数自增:INCR bob(只有int的string才可以)这是原子自增操作
      • mysql分库分表后可能每个表的id有相同的,用mysql进行自增不现实,针对每个分表,你可以在 Redis 中创建一个 key,这个 key 对应分表的自增字段。当你向分表中插入数据时,使用 Redis 的 INCR 命令为对应的 key 进行原子性自增。然后,将自增后的值用作分表中新数据的自增字段。
  • 列表list:使用双向链表实现

  • 集合

    • 微信点赞、收藏、标签
  • 有序集合:使用跳表和哈希表组合实现。跳表负责排序,哈希表负责映射

    • 跳表:数量大于128或者有一个字符长度大于64
    • 压缩列表:节点数量小于等于128且字符串长度小于等于64
  • 哈希hash

    • 字典:节点数大于512或字符串长度大于64
    • 压缩列表:节点数小于等于512且字符串长度小于等于64

    针对string那种json存储和哈希根据字段存储,2种方案的优缺点?如果是余额类型的,只有余额经常变化,用哈希更好,因为采用string需要先将键值对拿出来,转成对象,设置属性,再存入redis,而如果用哈希这种分字段的,可以直接HMSET命令搞定

12. skiplist和红黑树性能分析

  • 跳表结构简单,适用于高并发

  • 红黑树结构复杂,空间紧凑。

跳表结构简单,性能更高,但需要更多空间存储指针;红黑树保证了查找性能的稳定性以及空间使用方面具有优势,但实现复杂度较高

13. redis分布式集群

redis的分布式集群目的是在多台服务器上分配数据,提高应用程序的性能,同时以一种容错的方式运行程序。一个集群至少需要3个节点。redis集群有这些特征:

  1. 自动数据分片:通过哈希映射到不同的节点上,保证数据均衡分布
  2. 主从复制:redis集群各个节点可以配置为其他节点的从节点。从节点会复制主节点的数据,用于容错。
  3. 高可用和容错性:redis集群通过各个节点之间的数据冗余和心跳检测确保服务的高可用。如果一个节点故障,哨兵检测到后会将另一个节点升为主节点。
  4. 客户端重定向:当请求的数据在另一个节点时,redis集群能够通知客户端应该连接到哪个节点。

14. redis分布式锁如何实现,命令

Redis分布式锁是一种分布式环境下的互斥操作的解决方案。可以确保多个客户端同一时间只有一个在进行同步操作,避免发生冲突。

  1. 尝试获取锁:发送一个带有 NXPX 选项的 SET 命令,设置一个键和对应的过期时间(这样锁就有了自动释放的保障,如果服务器挂掉了会一直阻塞)若key不存在,则能存储成功,若存在,则不成功。例如:
SET resource_lock_name lock_value NX PX 10000// 这里的 resource_lock_name 是锁的名称,lock_value 是客户端生成的唯一标识(通常是 UUID),以便确保只有锁的持有者能够释放锁EX seconds:设定过期时间,单位为秒PX milliseconds:设定过期时间,单位为毫秒NX:仅当 key 不存在时设置值XX:仅当 key 存在时设置值set 命令的 nx 选项,就等同于 setnx 命令
  1. 检查是否成功获取到锁:客户端需要检查 Redis 的 SET 命令是否返回 OK。 如果返回的是 OK 说明成功获取到了锁。 否则说明锁已被其他客户端持有。PX是毫秒操作

  2. 执行保护的操作:如果成功获取到了锁,客户端就可以执行要保护的操作。

  3. 释放锁:操作完成后,客户端需要释放锁。

分布式锁的其他实现方案

  1. 数据库
    1. 锁表
    2. 悲观锁
    3. 乐观锁
  2. Zookeeper

15. redis存在线程安全问题吗?为什么?

不会。redis server本身是线程安全的kv数据库,也就是说在redis server上执行的指令,不需要任何同步机制(单线程)

尽管后面的版本中,增加了多线程模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是主线程来处理。

至于为什么不用多线程:

  • redis本身性能瓶颈是网络IO和内存,cpu不是redis瓶颈,没必要用多线程。
  • 采用多线程还需考虑同步安全问题,加锁的开销对性能影响反而更大。

16. redis主从复制的原理?

redis主从复制是指在redis集群里面,master节点和slave节点的数据同步的一种机制,即把一台redis服务器的数据复制到其他redis服务器里面。

在redis中,提供了全量复制和增量复制两种模式:

  • 全量复制一般在slave节点初始化阶段,需要把master所有数据copy一份
    • slave服务器向master发送一个SYNC的命令,master收到后生成数据快照
    • 把数据快照发送给slave,slave收到后丢弃旧的数据,重新加载新数据,然后对外提供服务
  • 增量复制是master收到数据变更后,会把变更的数据同步给所有slave节点
    • master和slave都会维护一个复制偏移量offset,用来表示master向slave传递的字节数量,每发送一定的字节数,双方的offset就会增加对应的数量。redis只需要根据offset即可实现增量数据的同步

17. redis切片集群中,数据量多了,加实例还是加内存,为什么?

各有优劣势。

加实例

  • 优势
    • 分布式处理可以提高性能和吞吐量
    • 提高集群容错能力
  • 劣势
    • 集群的管理和维护更加复杂
    • 高可用性和数据一致性需要更多的监控

加内存

  • 优势
    • 成本较低,简化了维护和管理
    • 降低网络时延
    • 可以提高单个redis性能和缓存容量
  • 劣势
    • 单点故障风险较高
    • 随着内存占用增加,备份和恢复可能需要更长时间

18. 缓存雪崩和缓存穿透

  1. 缓存雪崩

缓存雪崩是在相对短的时间内,缓存中大量数据同时过期失效。导致大量请求直接进入数据库,使得数据库崩溃

解决方法

  • 设置不同的过期时间:为缓存的数据项设置不同的过期时间,以避免它们同时失效
  • 使用分布式锁:当某个缓存过期时,使用分布式锁机制,让当前请求处理过程获得锁后负责加载数据并重新填充缓存,其他并发流程等待数据加载完毕
  1. 缓存穿透

缓存和底层数据都没有的数据,导致数据库崩溃

解决方法

  • 布隆过滤器
  • 设置空结果缓存:即使结果是空的,也可以在缓存中存储一个特殊的值,设置一段较短的过期时间,以避免数据源有新数据时缓存过期。

19. redis为什么用跳表而不是b树?

  • 实现简单:相比于B树,跳表的实现更加简单直接
  • 适合并发:跳表对并发操作有较好的表现。而B树需要加锁解锁,影响性能。
  • 有较好的插入和删除策略:跳表的平均复杂度分别是logn级别的,非常适用于缓存和存储的场景。

20. redis除了做缓存还能做什么?

  • 数据结构存储:redis有五种数据结构,可以轻松实现对多种数据类型的存储和查询
  • 分布式锁:redis提供了SETNX命令,有助于实现分布式锁的功能,以确保在分布式环境中的执行任务是原子性的
  • 会话缓存:管理用户会话的缓存系统,以提高用户体验和系统性能
  • 时间序列数据存储:使用ZSet存储时间序列数据,例如气象数据、股票数据等
  • 排行榜:ZSET有序集合
  • 计数器:INCR,原子操作

21. 讲讲zset底层数据原理?

zset底层数据结构有压缩列表和跳表。

压缩列表本质上就是一个数组,只不过他增加了列表长度、尾部偏移量、列表元素个数、以及列表的结尾的标志,这样就有利于快速找到列表的首尾节点,但是对于其他普通元素中间元素效率仍没有很高,只能挨个遍历。当有序集合保存元素数量小于128,或者有序集合保存的所有元素长度小于64字节的时候,就会采用压缩列表,否则采用跳表。

跳表是什么?

跳表在在链表的基础上跟增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素。我们可以建立多级索引。当数据量特别大的时候,跳表的查找时间复杂度是O(logn)

为什么不用红黑树或者二叉树呢?

  • zset有一个核心操作即范围查找:我们可以找到区间的起点,然后根据起点往后面遍历就行了,但是红黑树范围查找效率没有条标高
  • 跳表的实现比红黑树简单易懂,可以有效控制跳表的索引层级来控制内存的消耗

22. redis 的哈希是有序的吗?

Redis 的哈希表(Hashes)本质上是无序的,因为它们是根据键值对的哈希值存储的。然而,有时我们可能需要以某种顺序(如字母顺序)检索哈希表中的键和值。

  1. HGETALL:获取哈希表种的所有键和值,并将其存储在某种数据结构中
  2. 将存储在数据结构的哈希表键值对按照排序顺序打印

23. 谈谈你对redis的理解

redis是基于内存的KV键值对的NoSQL数据库。它提供了5种常用的数据类型,string、map、set、list、zset。不同的数据结构可以解决不同的应用场景,因此他可以覆盖应用开发里面的大部分的业务场景,比如top10问题、好友关注列表、微博点赞、排行榜(zset)、计数器(string原子自增)等等。其次呢,由于redis是一个基于内存的存储,并且在数据结构上做了大量的优化,所以在IO上面性能比较好,所以我们常拿来做应用和数据库之间的一个分布式缓存中间件,并且它又是一个非关系型数据库,不存在表之间的关联,所以他可以很好的去提升应用程序的数据IO效率。对于企业开发来说,他又提供了主从复制+哨兵,以及集群的方式去实现高可用。在redis集群里面通过hash槽的方式去实现数据的分片,做到负载均衡。以上就是我对redis的理解。

24. 哨兵机制和集群(cluster)有什么区别?

  1. 主从复制(Replication):
    Redis 主从复制是指将一个 Redis 节点(主节点,Master)的数据复制到一个或多个其他 Redis 节点(从节点,Slave)。从节点可以接受客户端的读请求,这样可以有效分担主节点的读压力。当主节点出现故障时,可以手动或自动将从节点提升为新的主节点。
  2. 哨兵(Sentinel):
    Redis 哨兵是一个分布式系统,用于监控并管理 Redis 主从复制集群。哨兵的作用主要有两个:故障检测和自动故障转移。当主节点出现故障时,哨兵系统会根据预设策略自动选举新的主节点。然后,其余的从节点会将自己重新指向新的主节点。此外,哨兵还可以定期执行类似主节点和从节点的合规性检查。
  3. 集群(Cluster):
    虽然哨兵模式解决了master选举的问题,但是仍然没有解决在线扩容的问题。于是就有了redis Cluster。Redis 集群是一种水平扩展技术,通过将数据分散在多个节点上,提高 Redis 的性能和可用性。在 Redis 集群模式下,数据被切分为多个小片(称为哈希槽),每个节点负责管理一部分哈希槽。当数据量增长时,可以通过添加更多的节点并重新分配哈希槽来实现线性扩展。

哨兵集群和cluster的区别

  • 哨兵集群基于主从复制实现,可以实现读写分离,分担读操作的压力;而cluster只是实现冷备的机制,只有在master宕机后才会工作
  • 哨兵集群无法在线扩容,并发压力受限于单个服务器的配置;cluster提供了基于slot槽(默认16384)的数据分片机制,可以实现在线扩容,提升读写性能
  • 哨兵集群是一主多从;cluster是多主多从

25. 怎么解决缓存穿透、缓存雪崩、缓存击穿等问题

缓存雪崩:某一个键失效,新的缓存未到的期间,同一时刻大量请求查询数据库,导致数据库崩溃

  • 使用布隆过滤器
  • 缓存空对象:当数据不存在时,将返回的空对象缓存起来,同时设置一个过期时间
  • 加互斥锁

缓存穿透:查询缓存和数据库中都不存在的数据,导致所有请求都落到数据库上

  • 布隆过滤器
  • 缓存空值
  • 前端过滤掉恶意请求

缓存击穿:热点数据失效的瞬间,持续的大并发穿破缓存,直接请求数据库,就像屏幕上凿开了一个洞。或者客户端恶意发起大量不能存在的key的一个请求,由于访问的key对应数据本身不存在,所以每次访问都会到数据库上。

  • 热点数据永不过期,或者在访问数据的时候对数据过期时间进行续期
  • 对于热点数据设置多级缓存
  • 使用分布式锁,当发现缓存失效时,先获取分布式锁,获得分布式锁的线程先从数据库查询数据后写回缓存里面(牺牲了一定性能,但是确保了数据库的稳定性)
  • 采用布隆过滤器

26. 雪崩用二级缓存解决

为数据设置两个缓存层。一个是一级缓存(例如本地缓存),另一个是二级缓存(例如分布式缓存)

  1. 当请求进入数据时,先检查一级缓存
  2. 如果一级缓存不存在,再检查二级缓存,如果存在,将数据写入一级缓存
  3. 如果二级缓存也不存在,则从数据库获取数据,获取后放入一级缓存和二级缓存

26. 分布式id生成策略

在分布式系统中,生成全局唯一ID的需求很常见,比如订单ID、用户ID等,需要保证生成的ID是唯一的、按照时间顺序排列的。可以用数据库的自增字段自己增长,但会受到数据库性能的可用性的限制;也可以用Redis来实现。通过redis的原子自增操作INCR命令在redis中递增。这种方案简单易用、性能良好。

27. mongoDB用在什么场景?

mongoDB是一个文档型NoSQL数据库。

  • 内容管理系统(CMS):MongoDB 适用于存储和管理各种多媒体内容,如文字、图片和视频等。
  • 大数据存储与分析:MongoDB 的分布式特性允许在多个服务器上进行数据存储,分担 I/O 和请求负载。这对于需要处理海量数据并进行实时分析的项目尤为贴切。

28. 单机单线程的redis并发量极限是多少?

redis的响应时间为100纳秒,能够支持8W~10W的QPS。

如果是大公司,需要更大的QPS,用到了IO多线程(内部执行命令还是单线程)

为什么不采用分布式架构,缺点是什么?

  • 服务器数量多,维护成本高
  • redis命令不适用于数据分区
  • 数据分区,无法解决热点读写的问题

29. redis如何解决key冲突?

  • 业务隔离

    业务A SET A1,key区分出来

  • key的设计

  • 分布式锁

30. 怎么提高缓存命中率?

  • 提前加载
  • 增加缓存的存储空间,提高缓存的数据、提高命中率
  • 调整缓存的存储类型(哈希存储类型)
  • 提升缓存的更新频次

31. memcached与redis区别?

  • 数据结构:memcached只支持字符串,redis支持更丰富的数据结构,string、list、hash、set、zset
  • 数据持久性:redis支持rdb、aof两种持久化机制,memcached没有
  • 线程模式:memcached使用多线程,每个CPU使用一个线程处理请求,redis使用单线程,但是redis内部使用了大量优化,因此redis单线程性能更高

32. redis hashmap渐进式rehash流程?

  1. 初始化新的哈希表:当redis的hash结构需要扩容或缩容的时候,会先初始化一个新的更大的hash表
  2. 运行时将旧哈希表的键迁移到新哈希表:当对hash结构进行增删查改操作时,redis会顺带做一些rehash操作,这就是所谓的渐进式rehash。分多次迁移,这样就可以保证一次操作不会因为迁移太多数据造成阻塞现象

33. zset的底层数据结构,为什么不用B+树?

和B+树相比,查询效率相当,都是logN,同时,支持范围查询,这对于ZSET这类需要排序的数据非常重要。而且,对于数据的插入来说,B+树需要进行调整,性能非常低。另外,跳表数据结构简单,实现简单,占用的内存也比B+树小。

34. 怎么提升缓存命中率?

  • 优化缓存策略:采用LRU、LFU等
  • 定期检查和优化你的查询,避免使用没有索引的查询
  • 多级缓存机制:主存储和redis之间建立多级缓存
  • 使用哈希分片:将数据分布到多个redis节点,可以有效提升缓存命中率
  • 热点数据长期存留

35. Redis最大的问题是什么,要避免哪些问题?

  • 单线程调度模型:Redis使用单线程模型来处理命令,这就意味着一次只能处理一条命令。这在需要大量并发处理的场景下可能会成为性能瓶颈。

分布式

1. 一致性哈希了解吗

一致性哈希是一种分布式哈希方案,可以解决一些分布式系统中的负载均衡和数据存储问题。一致性哈希的优点在于:当有新的节点加入或现有节点离开系统时,只需要在哈希环上重新分配一小部分数据项,而不需要重新分配全部数据,这就减少了系统中的数据迁移两和负载重新分布的开销。

2. 限流、熔断、降级是什么含义

  • 限流(Rate Limiting):限流是一种控制资源访问速率的策略,旨在阻止异常流量对系统造成过大压力。通过限制资源访问的频率,限流能够确保系统在高流量情况下保持稳定,从而为用户提供更好的服务质量。限流常见于应对分布式拒绝服务攻击(DDoS)以及对公共API或请求频繁的服务进行流量控制的场景。
  • 熔断(Circuit Breaking):熔断是一种基于自我保护的方式来阻止调用失败的服务,以防止故障扩散到其他部分的系统。熔断器模式类似于电路熔断器,当检测到服务出现问题(如连续失败、响应超时等)时,熔断器将“打开”,屏蔽对该故障服务的调用。同时,熔断器在一段时间后会尝试重新连接服务,检测服务是否恢复。若服务恢复,熔断器重新“闭合”,系统继续正常运行。
  • 降级(Degradation):降级策略指的是在系统遇到一些紧急情况(例如资源紧张、系统过载等)时,自动减少系统的部分功能,从而确保关键核心服务的可用性。降级有多种方式,比如对某个功能进行实时计算改为使用缓存数据,或者关闭一些次要功能。降级策略通常在短时间内能够缓解系统压力,保护关键服务但在长期内应考虑修复根本原因。

限流和降级的区别:前者是出现问题前,为了防止系统过载降低速率;后者是已经出现问题,通过降低服务质量保证系统继续正常运行。

3. 分布式事务知道吗

分布式事务指的是再计算机网络中涉及多个参与者、组件或系统的事务。由于多个组件可能位于不同的机器上,需要跨越网络。分布式事务同样满足ACID特性。

4. Paxos和Raft

二者都是分布式系统共识算法。

Paxos有三个角色

  1. 提议者:发起提案,这些提案在分布式系统中的副本中传递。提议者的任务提出一个值,以寻求其他节点的同意。
  2. 接受者:接受者接收提议者提交的值。为确保提案的一致性,接受者需要在某一伦茨中接受最新的提案。
  3. 学习者:学习者接收到一致性达成的通知,并采取响应的行动。学习者在系统中的其他角色也可能充当学习者。

Paxos算法有两个阶段:

  1. 准备阶段:在此阶段中,提议者发送具有唯一提案编号的 “准备” 请求给接受者。接受者接收该请求后,做出承诺,不再接受使用旧编号的提案,并返回之前可能已接受的提案。
  2. 接受阶段:如果提议者从多数接受者那里收到了 “准备好了(OK)” 的回应,提议者会根据已接受的提案选择一个值。然后,提议者将选定的值与之前的提案编号一起发送给接受者。接受者根据这个新的提案编号和它之前的承诺来决定是否应接受该提案。如果多数接受者接受了提案,那么这个值就被选定,学习者被告知结果,并对系统产生实际影响。

因为Paxos的复杂性和不直观性,在实用性上很有挑战。 Raft算法是他的变种,简单易懂,易实现。

5. 谈谈什么是消息队列MQ

概念:MQ分布式消息队列它是一种应用之间异步通信的方式,主要由三部分组成:1. 生产者。负责消息所承载业务信息的一个实例化,是我们整个消息的发起方;2. broker代理:是我们整个消息的服务端,主要是去处理我们的这个消息单元,负责消息的存储、投递以及一些其他的附加功能,是整个消息队列里面最核心的组成部分;3. 消费者:主要负责消息的一个消费,具体根据消息所承载的一个信息去处理各种业务逻辑。

场景

  1. 异步处理:主要是应用在实时性要求不严格的一些场景,比如用户注册发送验证码、下单通知发送优惠券,服务方只需要将协商好的消息发送到消息队列,剩下的是由消费者的消息服务去处理,不需要等待消费者的返回结果就可以直接返回给客户端
  2. 应用解耦:可以看作成把一些相关的但是耦合度不高的一些系统关联起来。比如说订单系统与优惠券,有关联度但是没那么紧密,每个系统只需要把一些约定的消息发送到MQ,另外的系统直接取消费者的消息即可。他可以解决各类系统之间采用不同的一些框架来实现,从而增加系统的灵活度
  3. 流量削峰:一般用在大流量入口的一些短时间的业务需求处理不完的一些服务,为了去权衡高可用把大量的一些并行任务发送MQ,根据MQ的存储和分发功能去平稳地处理后续的业务,起到一个大流量缓冲的作用。

6. 分布式的CAP理论” />7. 分布式解决什么问题?

  • 可扩展性:随着用户数量的增长,单个服务器无法应对所有请求。分布式系统将任务分散到各个节点上来扩展处理能力
  • 容错性:分布式系统能够在一台机器宕机时,仍然以一种容错的方式让整个系统运行下去
  • 数据一致性:分布式系统需要保证各个节点的数据一致性,这就是我们的raft算法的用处
  • 负载均衡:避免单个节点过载,分布式系统在各个节点之间有效分配数据。

8. http和grpc的区别?

  • 传输协议
    • http最初基于http/1.1,而grpc基于http/2,提供了多路复用、头部压缩等特性,能显著提高性能、减少延迟
  • 适用场景
    • gpr适用于微服务,http适用于网站web服务等
  • 通信方式
    • grpc是一个远程过程调用框架,允许客户端像调用本地程序一样调用远程服务,http是一种基于请求-响应的通信模式。

9. 分布式事务

分布式事务同样需要满足ACID属性,而且还要考虑跨节点通信和协调。一种常见的处理分布式事务的方法是使用两阶段提交(2PC)或三阶段提交(3PC)。

  • 两阶段提交(2PC):在第一阶段,协调者(通常是发起事务的节点)询问其他所有的节点,他们是否可以提交事务。如果所有节点都回答 “yes”,则进入第二阶段,协调者向所有节点发送提交事务的请求,如果有节点回答 “no”,则协调者向所有节点发送中止事务的请求。这种协议可以确保所有节点最后的状态是一致的。
  • 3PC:
    • 投票阶段:事务协调者向所有的事务参与者发送预提交请求,询问它们是否准备好提交事务了。
    • 预提交阶段:事务参与者回复协调者,表明它们已经准备好预提交了。紧接着协调者会向所有参与者发送一个正式的提交请求。这个阶段,协调者会牵涉到超时机制,如果在设定的时间内没有收到所有参与者的确认信息,那么就会中止事务。
    • 提交阶段:事务参与者收到正式的提交请求后,将事务状态从预提交改为已提交,然后向协调者返回提交了的确认,协调者在接收到所有参与者的确认后,整个事务完成。

10. 分布式系统中如何设计唯一ID?

  • UUID:UUID是128位长的,可以保证全球唯一性。大部分编程语言针对UUID都有内建支持。
    • 优点:生成的主键全局唯一,跨服务器合并数据方便
    • 缺点:不是递增有序的数据,数据写入IO随机性大,索引效率下降
  • 雪花算法:包含了时间、工作节点的ID,以及节点上的序列号
    • 优点:高性能高可用
  • 数据库自增索引
    • 优点:主键自增,IO写入连续性好
    • 缺点:存储在磁盘,效率不高
  • redis自增:INCR命令实现原子自增
    • 优点:使用内存,并发性能好
    • 缺点:内存存储,数据易丢失

11. 如何设计一个分布式系统?

  • 确定系统的目标和需求:看看支持多少用户、需要处理多大的数据量
  • 考虑系统的可扩展性:加入未来用户或数据量增大,你的系统应当容易扩容
  • 设计恢复策略:如果一台机器宕机,应当有处理错误的策略,让其他机器顶替该任务
  • 负载均衡:让数据平衡打到每一个节点上

12. rpc通信中,如果两个进程互相发送rpc请求陷入死锁怎么办?

  • 设置超时时间,如果在规定的时间内没有得到响应,那么就会自动放弃该请求
  • 当一个RPC请求失败后,可以尝试重新发送该请求。

13. 一致性理论

  • 强一致性:读写操作后,能立刻看到更新过的数据
  • 弱一致性:能容忍部分或者全部数据看不到
  • 最终一致性:经过一段时间后,能够看到更新后的数据

场景题

1. 面对海量数据,有许多重复数据,怎么查询?

  1. 首选去重:采用哈希表,当然,这一步也能在后面多路分治映射起帮助,能够让数据均分到不同的数据块当中,而且相同的数据也会放到同样的文件中(MapReduce思想)

关于数据去重,可以采用的是hashtable、bitmap、布隆过滤器(二者的结合应用)

采用bitmap的话,能够比数组、哈希表节省大部分的空间,因为一个int型要4字节,而如果用bit来表示,则会少很多。

  1. bitmap: 假设5个整数组成的序列{2,3,200,7000,12000},则我们可以将这个序列保存在二进制数组当中,第n位如果为1,则表示n存在于这个序列中。 然后在根据这个二进制数组转化为整数,依次从小到大排序更新到外部文件。

    但缺点就是不能有重复的数,而且bitset有限制,存入的数最大限制是10亿

  2. 采用多路归并算法(内部排序+外部排序)

多路归并算法用于:无法一次性将数据全部放入内存中排序(场景题中内存很小,磁盘很大)

于是,我们可以先在内存中将N个小文件排序好(例如十万级别的数据),再通过合并K路对外部文件归并排序,外部排序本指就是将K个已经排序号的文件块,整合到一个大文件中,(这就是力扣的23题,合并K个升序链表),这个算法本质,也就是维护一个有K个节点的堆,每次将K个文件的第一个值放进堆中,然后取最小值,再将这个值对应的次最小值放入堆中,以此类推。

完整流程:

把磁盘上的1TB数据分割为40块(chunks),每份25GB。(注意,要留一些系统空间!)

②、顺序将每份25GB数据读入内存,使用quick sort算法排序。

③、把排序好的数据(也是25GB)存放回磁盘。

④、循环40次,现在,所有的40个块都已经各自排序了。(剩下的工作就是如何把它们合并排序!)

⑤、从40个块中分别读取25G/40=0.625G入内存(40 input buffers)。

⑥、执行40路合并,并将合并结果临时存储于2GB 基于内存的输出缓冲区中。当缓冲区写满2GB时,写入硬盘上最终文件,并清空输出缓冲区;当40个输入缓冲区中任何一个处理完毕时,写入该缓冲区所对应的块中的下一个0.625GB,直到全部处理完成。

2. 海量数据,取出重复数最多的K条。

https://love6.blog.csdn.net/article/details/124851241

其实就是第K大的问题。

  1. 分而治之,用哈希映射,将大文件映射到不同的小文件中,还能将不同长度的key归到长度相同的不同的桶中
  2. unordered_map统计各个数据出现的次数,复杂度O(n),哈希表一般占用内存至少两倍左右
  3. priority_queue来做排序,决出热点数据。用小顶堆,堆大小为K,如果遍历到的词频大于堆顶,就将该词替换掉堆顶的词,然后重新调整小顶堆。

总结:

  1. 分而治之/哈希映射
  2. unordered_map统计词频
  3. 求解最大的 TopN 个,用小顶堆;求解最小的 TopN 个,用大顶堆

3. 从数据库并发读数据然后转成特定格式文件有什么优化?

  1. 使用连接池,减少数据库连接的创建和销毁
  2. 数据缓存:如果近期读取过的相同的数据,可以采用缓存策略
  3. 索引优化:读取数据更快

4. 计算机内存只有1M,有一个文件1G,怎么排序?

  1. 将1G的文件分成1024份1M的文件
  2. 对每个文件进行排序(快排、归并、堆)
  3. 一次从每个已排序的子文件中读取一个元素,将它们按顺序合并到一个临时缓冲区中。每次合并后,将缓冲区的内容写回磁盘,以便为下一组元素腾出空间。

5. 一个公司很多员工,电梯很少,去每层楼的人数差不多,想个办法,让每层楼的人能同时抵达。

  • 每部电梯固定楼层,如1号去1-10,2号去10-20等等
  • 辛苦下员工,电梯只能去特定楼层,如1、5、10、15,然后员工爬楼梯

6. 存储海量qq号登录状态的数据结构,估算一下40亿个qq号的空间内存大小

用64位整数来存储QQ比用字符串存储更省空间,一个QQ占8个字节,然后再考虑登录状态占用一个字节。

那么对于40亿的QQ,占用的空间就是大概(8+1)*(4*10^9)约36GB

7. 有一个服务,专门有一个服务器来进行大量的图形渲染;大概客户端50个,怎么设计服务器?

  1. 采用GPU
  2. 这是典型CPU密集型服务,采用多进程,因为我们要考虑一个稳定性,他进行图形渲染会大量使用CPU资源,而且50个客户端也不多,不需要考虑高并发。

8. 大数排序

可以用string存放大数,然后进行排序

9. 如果游戏卡顿,技能放出去了但是没有响应,怎么办?

  1. 动态调整发送速率:根据网络情况调整网络的发送速率,有助于避免丢包、网络拥塞情况
  2. 切换成TCP暂时,在低延迟的时候再用UDP

10. 大文件传输有什么解决方案

TCP没有数据边界,分段去传,在头部加入长度,还能防止粘包。

文件突然断网了,续传怎么办?

先存在本地或者url,或者存临时文件。

11. 一个TCP连接断了,代码的角度,你会怎么排查?

通过perror错误码来看。

int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("Error creating socket");return -1;}

比如这个就是创建套接字失败。

12. 有一千万个ip地址,他们是离散的不是连续的,输入一个ip地址,如何快速判断它是否在这一千万个里面,这个函数你会如何设计

  • 哈希表,但是可能会消耗大量存储空间。而且考虑到哈希冲突问题,可以引入布隆过滤器去筛选一下。
  • 二分查找,适用于存储空间有限的情况,

13. 给 40 亿个不重复的 unsigned int 的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那 40 亿个数当中?

使用bitmap:申请512M的内存,512M(2^32)内存是42亿多bit,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应的bit位是否为1.

14. 在海量日志数据中,提取出某日访问百度次数最多的那个IP

  1. IP地址最多有2^32=4G中取值,如果内存小于4G,则不能全部加载到内存中。
  2. 按照IP地址的Hash(IP)%1024,把海量数据存储到1024个小文件中,这样每个小文件最多包含4M个IP地址。
  3. 对于每一个小文件,采用unordered_map,统计频数,并根据value进行排序
  4. 将每个小文件中最大的前K个取出来,最后进行常规的排序,就得到了总体出现次数最多的IP

15. 项目上线需要注意哪些问题,需要做哪些改进

  1. 需求分析和规划:项目需要明确,指定详细的项目计划
  2. 设计与架构:设计合理的项目架构方便后续扩展、维护和修改
  3. 版本控制:使用git管理代码
  4. 测试:单元测试、集成测试、系统测试和压力测试
  5. 代码优化与审查:检查代码质量
  6. 文档编写:编写完整的项目文档,包括技术文档、用户手册等,方便后续维护以及后面开发人员更好的了解项目

16. 哈希表是有序的吗?如何有序打印?

哈希表是无需的,无法保证按照插入顺序遍历元素。我们可以用一个数组/链表和哈希表来共同维护,其中数组/链表用来存储KV,哈希表用来存储Key所在的下标。这样我们就既能通过哈希快速查找元素,又能按照插入元素打印了。

17. 设计高并发秒杀服务,怎么从系统架构设计(后端)

  1. 分布式架构:要支持高并发访问,首先采用分布式架构,这可以使你的系统利用多态服务器的计算能力,提高承载能力
  2. 数据库优化:考虑使用部分预先生成的数据,以减轻数据库负担。例如,将信息保存在缓存中。对数据库进行分库分表,可以提高性能并减轻压力。
  3. 容错和熔断机制:引入熔断器或者降级策略,防止系统雪崩
  4. 对于热点数据,采用分布式锁或乐观锁等并发控制方法,避免出现超卖等严重问题

18. 用redis做页面访问量的技术统计,怎么实现

可以根据不同的IP用户创建对应的键值对,并用INCR命令来为该业面的对应键名增加访问计数。

19. 百万并发的QPS需要哪些进一步的处理?

  • 负载均衡:使用负载均衡器分发流量负载以避免单一节点的瓶颈
  • 缓存:高效使用缓存技术,减少数据库和后端服务的压力
  • 数据库优化:对数据库进行分片,读写分离,对SQL语句优化
  • 无状态:确保你的应用程序/服务是无状态的,这样可以轻松地水平扩展系统

20. 服务器性能调优方法?

  1. 了解性能瓶颈,通过性能监控工具、日志分析等方式定位
  2. 硬件调优:内存、处理其、存储(SSD)
  3. 网络调优:IO多路复用、负载均衡、增加网络带宽
  4. 软件调优:使用缓存、并发处理、数据库SQL优化

21. 2个线程交替打印

bool printNum = false;mutex mtx;condition_variable cond;void printA() {for(int i = 1; i <= 26; i++) {unique_lock lock(mtx);cond.wait(lock, []{return printNum == true;});cout<<i;printNum = false;cond.notify_one();}}void printB() {for(char i = 'A'; i <= 'Z'; i++) {unique_lock lock(mtx);cond.wait(lock, []{return printNum == false;});cout<<i;printNum = true;cond.notify_one();}}int main() {thread a(printA);thread b(printB);a.join();b.join();return 0;}

22. 交替打印ABC

mutex mtx;condition_variable cond;char currentChar = 'A';void printChar(char ch) {for(int i = 0; i < 10; i++) {unique_lock lock(mtx);cond.wait(lock, [&]{return currentChar == ch;});cout<<ch;currentChar = currentChar == 'C' ? 'A' : currentChar+1;cond.notify_all();}}int main() {thread t1(printChar, 'A');thread t2(printChar, 'B');thread t3(printChar, 'C');t1.join();t2.join();t3.join();return 0;}

23. 如何实现浏览器的后退和前进功能?

可以用2个栈来实现,一个前进栈,一个后退栈。

  • 当点开一个新页面时,将url推入后退栈;
  • 当返回时,将后退栈pop到前进栈,然后导航到后退栈的头部
  • 当前进时,将前进栈pop到后退栈,然后导航到后退栈的头部
  • 如果前进栈为空,就说明无法再前进了,如果后退栈为空,就说明无法再后退了

24. 扫码登陆怎么实现?

  1. 在网页端打开登陆界面,展示一个二维码,这个二维码有一个唯一编号是服务端生成的,然后浏览器定时轮询这个二维码的状态,如果一定时间内没有扫码,该二维码就会失效,需要重新生成一个二维码
  2. APP扫描二维码,把app的token信息和二维码id发给服务端,服务端收到请求后会更新二维码的状态,并生成一个临时token
  3. 此时web端会更新二维码的状态,更新为已扫码,待确认
  4. app端确认同意登陆,将临时携带的token发送给服务端,服务端修改二维码状态并为网页端生成授权token,放在服务端缓存中
  5. web端通过轮询获取到二维码的状态变化,并且拿到token,写入到日志程序,从而完成扫码授权。

25. 让你设计一个分布式的支付系统,你会怎么设计?

  • 使用负载均衡器(nginx)在各个服务之间分发流量,以解决单点故障问题
  • 采用事件驱动架构,以处理高并发交易和异步通知,同时将事件存储在分布式消息队列

26. 设计一个高并发系统

  1. 系统拆分:将一个系统拆分成多个子系统,用rpc来搞。然后每个系统连接一个数据库。
  2. 缓存:大部分的高并发场景,都是读多写少,采用redis做缓存,毕竟redis轻轻松松单机几万的并发。
  3. 消息队列:将大量的写请求灌入MQ中,后边系统消费后慢慢写,控制在MySQL承载范围之内。
  4. 分库分表:将数据库拆分成多个库,将一个表拆分成多个表;提高SQL性能
  5. 读写分离:搞一个主从架构,主库写入,从库读取。

27. 在 2.5 亿个整数中找出不重复的整数。

bitmap

2.5亿的内存大小为:2.5e8/1024/1024/1024* 4=3.72GB。采用bitmap,每个数用2个bit表示,00表示没出现过,01表示出现1次,10表示出现多次。那么2.5亿个数,就需要2b*2^32=1GB,因此,当内存超过1G时,可以采用位图法。

该题也可以改为在大量数据中判断一个数是否存在,若采用分治法则是unordered_set

总结

求解数据重复问题,用bitmap

求解top k问题,用分治

28. 如何统计不同电话号码的个数?

一个号码8位数,8位电话号码可以表示的号码数有108个,即1亿个。我们每一个号码用一个bit来表示,总共需要1亿个bit,即12M.

将对应bit的电话号置为1,最后总的1个数就是不同的电话号个数。

29. 邮件系统如何设计

  • 用户通过登陆API或者WEB解面与邮件系统交互
  • 用户在界面上的操作(比如写和发送、查看收件箱等),通过邮件系统的接口和后端通讯
  • 用户发邮件操作转交给SMTP服务器处理,SMTP服务器将邮件系统发送到对应的POP/IMAP服务器
  • 用 务器提供邮件资源,经由用户界面展示
  • 所有的邮件内容,用户数据,邮件状态等信息存储在数据存储系统中
  • 每次有偶见的传输都经过安全和防垃圾邮件的系统筛选和处理

30. 如果两个类功能基本一样,但是处理的场景稍微不同,但是各自的private函数也必须存在(有用),你要怎么抽象呢

可以将两个类的一些公用的函数接口抽象出来,让他们继承一个基类。通过里氏替换原则,创建后,实现接口函数。对于他们类内部各自的实现逻辑,可以自己实现。

31. 多个线程同时操作一个共享变量,加锁后串行访问的方式会降低程序的吞吐量,你要如何解决

  • 使用读锁:如果多个线程只是读取该变量,可以用读锁同时读取,但是在写入时,只允许一个线程访问。
  • 原子操作:原子增加或减少一个值,能够保证数据一致性。
  • 乐观锁:乐观锁假设冲突不会发生,不会加锁;当写入时发现有冲突,那么久操作失败,线程回滚重试,直到成功。

32. 要做一个给大量用户使用的库 应该考虑那些方面

  • 版本控制:使用诸如git这样的版本控制系统
  • 社区的设立和维护:鼓励社区用户提交错误报告,请求新特性
  • 性能:不仅涉及到执行速度,还包括内存消耗、启动时间等
  • 说明文档

33. 手撕读写锁

class RWLock {public:RWLock() : readCount(0) {}void lockRead() {unique_lock lock(mtx);readCV.wait(lock, [this](){ return !writerCount; });++readCount;}void unlockRead() {unique_lock lock(mtx);readCount--;if(!readCount) writeCV.notify_one();// 读没了,唤醒写}void lockWrite() {unique_lock lock(mtx);++writeCount;writeCV.wait(lock, [this](){ return !isWriting && !readCount; });isWriting = true;}void unlockWrite() {unique_lock lock(mtx);isWriting = false;--writeCount;if(writeCount == 0) readCV.notify_all();// 唤醒所有读锁else writeCV.notify_one();// 否则随机唤醒一个写锁}private:mutex mtx;condition_variable readCV, writeCV;int readerCount, writerCount;bool isWriting = false;};

34. 线程池的弊端?

  • 复杂性增加:线程池的管理和维护很复杂,需要处理工作线程的调度问题,避免发生数据竞争、死锁等问题
  • 响应时间可能变长:对于个别任务,如果线程池中的所有线程都在忙碌,那么该任务则需要一直等待(即性能可能会降低
  • 资源消耗:如果线程池的线程数量过多,仍然会消耗大量的内存和CPU资源

智力题

1. 3L瓶子和5L瓶子怎么接出4L水

  1. 先用3L的装满水,倒给5L瓶子
  2. 再用3L的装满水,给5L瓶子倒满,此时3L瓶子还剩1L
  3. 把5L瓶子的水全部倒掉,然后把3L瓶子的1L倒给5L瓶子
  4. 再将3L瓶子装满,倒给5L的瓶子

2. 1瓶毒药,1000瓶饮料,十只小白鼠,怎么实验找出毒药

用二进制来进行。1000瓶饮料,正好可以用2的10次方来完全表示。我们将10只小白鼠按照比特位去喝特定的毒药,如果01就让小白鼠1喝,10就让小白鼠2喝,11就让小白鼠1和2喝。这样的话,假如1和3号小白鼠死了,那么我们就可以知道,转换为十进制是5,就是第五瓶水有毒。

3. 7个小球,其中有一个重量和其他小球不同,如何快速找出来?

假设这个异常小球的重量偏重一点。

  1. 将7个小球分成3组:A(3个球),B(3个球),C(1个球)。
  2. 先将A组和B组各3个球放在平衡秤的两端。此时有三种情况:
    • 如果A组和B组平衡,说明C组的那个球是重量不同的球。
    • 如果A组重于B组,则重量不同的球在A组。
    • 如果B组重于A组,则重量不同的球在B组。
  3. 若重量不同的球在A组或B组,我们只需要找出这组中异常的球。将这组中的3个球再分成3份:1个球、1个球,及1个球和剩下的球(可以称之为D组)。将第1个和第2个球放在平衡秤的两边进行比较:
    • 如果它们平衡,说明第三个球是异常球。
    • 如果其中一个球重,那么那个更重的球就是异常球。

4. n个不同颜色盒子和n个不同颜色球,球不能放对应颜色盒子里,有多少种方法?DP

现在,我们知道第一个盒子中有一个不同颜色的球,但是剩下的 i-1 个盒子也要满足条件。我们可以分析两种可能的情况:

  1. 选中的替换球与颜色相同的盒子中的球交换。这时,我们将原问题缩小为一个规模为 i-2 的子问题。在这种情况下有 dp[i-2] 种方法。
  2. 选中的替换球不与颜色相同的盒子中的球交换。此时,剩下的 i-1 个盒子的问题与原来的问题相似。在这种情况下有 dp[i-1] 种方法。

因此,我们可以得出递推公式:

dp[i] = (i-1) * (dp[i-1] + dp[i-2])

5. 蚂蚁问题

一根线上若干蚂蚁([1,0,0,0,-1,-1,1,0,-1,0,0,1]),有往左的(1)有往右(-1)的,每两只蚂蚁碰撞会掉头,蚂蚁速度都一样(一秒移动一格)

  1. 什么时候最后一只蚂蚁掉下去?
  2. 最后一只蚂蚁掉出去的方向是什么?
  3. 最后掉出去的是哪只蚂蚁?

蚂蚁相撞可以堪称无障碍地穿过对方,所以:

  1. 比较最左侧走向右边的和最右侧走向左边的,最右侧那只蚂蚁是最后走完的,总共耗时就是数组长度
  2. 就是初始方向,左边
  3. 如果模拟的话,就是最右边的那只蚂蚁?

6. 若干根一个小时燃烧完的绳子测量十五分钟

  1. 取两根绳子(A和B),将其点燃:
    a. 点燃绳子A的一端;
    b. 同时点燃绳子B的两端;
  2. 当绳子B完全燃烧完毕时(这将花费30分钟),此时绳子A已经过去了30分钟,还剩下30分钟的燃烧时间。
  3. 接下来,立刻点燃绳子A的另一端。现在,绳子A的两端都在燃烧,它将在接下来的15分钟内燃烧完毕。
  4. 当绳子A燃烧完时,已经过去了15分钟。

7. 有一种万年历是通过两个六面骰子表示01-31,请问每个骰子上各有哪些数字。

这其实是一个很有趣的问题,我们需要通过两个六面骰子(我们记作 A 和 B)来代表 01 到 31 的日期,要包括所有可能的日期组合。我们可以根据以下规则来设定每个骰子面的数字:

  • 骰子 A: 0, 1, 2, 3, 4, 5
  • 骰子 B: 0, 1, 2, 6, 7, 8

其中,6 可以翻转使用作为 9。这样我们就可以得到 01 至 31 的所有数字了。对于 1 到 9 的日期,我们可以将骰子 A 上的 0 与骰子 B 上的 1 到 9 结合,以及 10 到 15 可以用 A 上 1 到 5 与 B 上的 0 结合,以此类推,我们可以表示所有日期。

8. 25匹马,找前3名

  1. 把25匹马分为5个组,每个组5匹马。比赛5次,选出每组的赛马顺序。
  2. 再进行一次比赛,这次比赛的参赛马是每一组前一名的马。这样我们就知道了所有马中速度最快的那匹。
  3. 选择冠军马所在的小组里的2,3名
    冠军赛里第2名马所在小组的1,2名
    冠军赛里第3名马所在小组的第1名共5匹马进行比赛

END