其实没有想要发布出来的,但是我的几个粉丝和朋友私信我说非常期望我的面经贴,最后还是决定发布出来,希望能够帮助后人。
本篇将博主整个秋招的面经总结给发布了出来,篇幅很大,超过10W字,涵盖了我平日的积累与各大公司的面试真题,如果你能背完大部分,那八股文部分大概率没有问题了,反正博主在面试的时候八股文基本都答了上来。这篇发出来属于是最后燃烧自己了。当然如果你发现某一点有歧义或者错误,也欢迎你在评论区说出来大家一起讨论。
静态变化在程序开始时初始化,内存分配是在编译期间完成的,占用内存大小固定。在内存的静态/全局区,这部分用于存储全局变量和静态变量。
全局静态变量的初始化值存储在可执行文件中,而函数内的局部静态变量在函数第一次调用时进行初始化。
为什么需要链接这个过程?
map
和set
都是关联式容器,底层实现是红黑树,增删查改的时间复杂度都为O(logn)。其中,map是键值对关联容器,set是集合容器。
取值方法区别
find
:指向值的迭代器,若不存在,返回end(),安全[]
:若不存在,会自动插入at
:若不存在,会抛出std::out_of_range
异常,安全。两个智能指针指向互相指向对方,导致双方不能正常析构,引起内存泄漏。可以使用weak_ptr
,他不会增加引用计数。
shapre_ptr
的创建、复制、销毁是线程安全的。但是,当存在共享对象的时候,还是需要加锁;总之,shared_ptr的部分操作是线程安全的。shapred_ptr的引用计数是通过原子操作来保证线程安全的。
虚函数的底层依赖虚指针(vptr)和虚函数表(vtable)。多继承是一个派生类继承多个基类,多继承中的虚指针和虚函数为如下:
虚析构函数:在删除指针的时候,会先调用派生类的析构函数,再调用基类的析构函数。如果基类的析构函数不是虚的,那么删除基类指针时只会调用基类的析构函数,从而导致资源未正确释放以及内存泄漏的问题。
如果一个类什么都没有,则sizeof()
是1,如果有一个或多个虚函数,则sizeof()
是8,因为虚指针8个字节。虚函数指针通常位于对象的开始位置。
另外,虚函数表存储在常量区的数据段(只读数据段)。它是一个静态数据结构,在编译式就确定了,后面不会修改。
左值
右值
这是将字符串转换为整数(const char* -> int),需要考虑:
1、该函数首先会丢弃尽可能多的空白字符,直到找到第一个非空白字符,然后,从这个字符开始,取一个可选的初识加号或者减号,后跟尽可能多的十进制数字,并将他们返回一个int类型的数值。
2、若该字符串是在整数的字符后包含其他字符,则这些字符将会被忽略,返回其他字符之前的整数,并且不会对该函数造成任何影响。
3、若该字符串中第一个非空字符序列表示有效的整数,或是一个空指针,或只包含空白字符,则不执行任何转换,并且返回零。
const在C++中推荐用来代替掉define,因为define实现的功能const都能实现。define是宏定义,在预编译时期替换,但是const变量具有类型更加安全。
define在预编译阶段,cosnt是常量,在编译阶段。
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中是线程安全的,不需要懒汉(一开始就创建)、饿汉(需要时创建 ,可能有线程安全问题)。
他是一个数据结构(很长的二进制向量) ,可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。他只能告诉我们一个元素绝对不在集合内或可能在集合内。布隆过滤器很难实现删除操作。
布隆过滤器主要是为了防止redis缓存击穿问题(前端要查询一个数据,但是redis没有这个数据,就会去数据库查询,数据库可能承受不了这么大的流量就挂掉了)。有了布隆过滤器,就能判断哪些数据不在数据库中,防止缓存击穿。
重载是函数名相同,但是参数或返回值不同,可以有不同地实现。
重写是多态的内容,是父类写了虚函数,子类有自己的实现。
隐藏是在子类中定义了父类完全相同的变量或函数,导致父类的同名元素被隐藏。
二者共享相同的文件偏移量,所以不会相互覆盖,而是追加到彼此写入的内容中。但是为了避免意外,还是加锁更加安全。
push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。但是,如果是传递对象,那么二者都是一样的。
左值引用:避免对象拷贝(函数传参、函数返回值)
右值引用:实现移动语义、完美转发
std:move()
:左值变右值,这样就不用深拷贝了,避免资源的重新分配(堆、服务器数据库连接)通过移动构造以及移动拷贝构造(&&)
完美转发:std::forward()
:在函数模板中保持参数的类型和值类别(左值或右值)不变(主要解决通过引用的方式接收左右属性的值)
引用折叠规则:参数为左值/左值引用,T&&将转化为int&;参数为右值/右值引用,T&&将转化为int&&
声明出来的左值引用、右值引用都是左值
总结
std::move和std::forward的区别是什么?在使用上有什么考虑么?
move主要用于有效传输资源,避免不必要的资源拷贝;forward用在函数模板中,保持参数类型一致,避免不必要的性能损失和提高代码的正确性
多态简单来说是让一个函数作用于多种类型的对象,使得这些对象可以以不同的方式响应同一个函数的调用,这样增加了程序的灵活性和扩展性。
红黑树的特性:
翻转
vector是STL的一个动态数组,可以让数组尾部插入数据。
迭代器从逻辑上类似于指针,通过迭代器可以很方便的遍历容器中的元素。
当vector容量不够时,vector会进行两倍扩容,重新分配内存,将之前内存的数据搬到另一块。
一般堆内存泄露就是申请了内存资源没有释放,栈内存泄漏比较少见,一般发生在栈溢出的情况,是由于递归过深导致的,或者用了过大的局部变量,需要检查代码。
指针是一个变量的地址,引用是一个变量的别名,引用必须初始化,因为在之后无法改变引用所指的对象。
引用在本质上 是一个常量指针,即所指的对象不能发生改变,但可以改变指向对象的值。
sizeof指针返回的是指针的大小,4个字节,sizeof引用返回的是指向对象的大小,比如一个float就是8字节。
C++是这样的,显示delete回收。对于栈中的对象,无需显示删除,当函数返回时,会自动回收。
全局常量在整个程序运行周期都在,局部常量与作用域有关。也不需要显示回收。
协程是更轻量级的线程,主要是他可以在用户态调度,这样说明协程的创建、销毁、调度可以由程序员自己控制,而非操作系统。
协程是否一定无锁
协程不能保证代码完全无锁。协程降低了多线程编程中一些潜在的并发隐患,但访问共享资源时,仍然需要加锁。
继承在一个类有另一个类一样的功能,但是又有一些那个类没有的功能,就可以继承该基类
多态是当我们希望使用统一的接口处理不同类型的对象时,可以用一个相同的接口,实现不同的方法。同时,希望通过扩展代码而不改变原始代码的时候使用。
是C++11中一种对象,主要目的在于帮助程序员管理指针内存,这样就不用担心因为忘记手动释放而造成的内存泄漏问题了。是一种RAII机制。
用于确保资源的正确获取和释放。RAII的关键思想是将资源的生命周期绑定到对象的生命周期,并在对象的构造和析构函数中实现资源的管理。
动态绑定是指运行时决定调用哪个函数。这是通过基类的指针或引用间接调用虚函数来实现的。在这种情况下,编译器无法确认调用的哪个派生类的函数,必须等到程序运行时才确定。绑定的是基类指针或引用和具体派生类对象中相应的虚函数。
Animal animal = new Dog(); animal->eat();
抽象类不能被实例化,只能被继承。纯虚函数是没有函数体的虚函数,目的就是让派生类重写。当一个类有纯虚函数的时候,就是抽象类。
纯虚函数:virtual float getArea() = 0;
override
用于指定派生类中一个函数是用来重写的,以确保子类函数和父类函数名才相同,表明了函数重写的意图final
作用于类的时候,表明该类不能被其他类继承,这样可以防止类的继承层次过深。同时,有final的函数,不能在派生类中重写。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的地址重叠问题的,解决方法是:如果源地址小于目标地址,则从后向前复制,如果源地址大于目标地址,则从前向后复制,从而避免使用额外的缓冲区。
static
成员变量被类的所有对象共享。
static
成员函数是属于类的,在没有创建类的实例时也可直接只用。它们不能访问类的非静态成员,因为非静态成员需要类的实例。
需要注意的是,static
成员遍历和函数的作用域仅限于定义它们的类。要在类外部访问,需要加::
。
定义为全局变量是最好的,这样有助于内存管理,更容易跟踪数组的用途以及存取数组的所有位置。
链地址法:键值添加到相应索引处的链表(unordered_map实现的方法)
优点:对哈希函数要求低,删除操作简单
缺点:元素过多会导致查询效率降低;由于链表使得数据访问不连续,对CPU缓存不友好
优化方法
开放地址法:试图找到冲突索引后的数组位置的一个空位
unordered_map使用的哈希函数是使用的std::hash
作为哈希函数。
class HeapOnly {
private:
HeapOnly() {} // 私有构造函数
~HeapOnly() {} // 私有析构函数
public:
// 使用静态成员函数在堆上创建对象的实例
static HeapOnly* Create() {
return new HeapOnly();
}
// 添加成员函数以释放对象占用的内存
void Destroy() {
delete this;
}
};
模板类是在编译的时候生成的,如果将定义放在.cpp文件中会导致编译器在处理其他源文件的时候找不到完整的模板定义。对于模板类,编译器在编译时并不知道可能的所有类型参数,因此需要在使用模板时生成具体实例。所以,我们只有保留模板类的声明,并在同一个文件中添加实现。
模板类是在编译期间确定的,不是动态绑定的。
为什么普通的类就可以分开呢?
生成程序的时候有编译到链接的机制,链接阶段会把这些文件连接起来,而模板类在编译时并不知道可能的所有类型参数,因此需要在使用模板时生成具体实例。在链接阶段,编译器会检查所有具体类型的模板实例是否生成了相应的代码,如果找不到实例,就会报链接错误。总结:链接的时候不知道具体类型,无法整合
alloca()
函数用于申请栈空间,这也意味着深情地对空间在当前函数结束后会自动释放。
拷贝构造函数是构造函数,我还没有这个对象,我需要构建出来;而赋值的话,是我已经拥有了这个对象,我将另外一个对象的属性给赋值到我现在的对象上。
在union中,会使用内存对齐,即union的大小由最大成员的大小决定
不是,所以在使用的时候需要考虑竞态条件,加锁。
在函数声明后面加这个关键字,说明该函数不会抛出异常,可以提高性能。
因为unique_ptr只允许复制一次,同一时间只能由一个智能指针操作这个对象。如果采用拷贝构造这些的话,会调用超过一次析构函数,对同一块内存释放多次内存,就成了操作野指针。 一般用:unique_ptr p = make_unique();
这也是auto_ptr不安全,被unique_ptr替换掉的原因。
如果通过new再传递给shared_Ptr,内存是不连续的,会造成内存碎片化
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;
}
static_cast
:会执行隐式转换(普通类型转换、子类转换为基类)const_cast
:用于更改const属性dynamic_cast
:多态类之间的转换,即在类的继承层次之间,可以从基类到派生类,会检查类型reinterpret_cast
:指针和整数类型转换简单数据类型用static_cast,多态类之间的转换用dynamic_cast,确保转换安全
B *b = new A();
A *a = dynamic_cast(b); // 这是一个有效的操作,因为 b 实际上指向一个 A 对象
不一样。智能指针是一种用于动态管理内存分配和释放的对象,是一个类。包含了额外的开销,例如引用计数、指针管理等,以便跟踪指针的使用情况。而原生指针是直接指向内存地址的指针,没有附加的开销。
在初始化的时候,vector的容量大小为0。
resize
用于改变vector
的大小,包括增加元素或删除元素。如果新的大小小于旧的大小,resize()
会删除一些元素。如果新的大小大于旧的大小,resize()
会添加一些新的元素,新元素的初始值将由所提供的新元素类型的默认构造函数确定。
reserve
这个函数效率更高,一般用来提前预留空间,需要注意的是,reverse的空间大小必须比原来大才行。
联合体
结构体
// 前缀自增操作符
Integer& operator++() {
++value;
return *this;
}
// 后缀自增操作符
Integer operator++(int) {
Integer tmp = *this; // 取一个临时值
++value;
return tmp;
}
所以++i
效率会比i++
效率更高,而且从返回值也可以看出前置++返回的是左值,后置++返回的是右值
一般操作符重载后面的参数是操作符后面的参数,但是i++和++i却是反的
友元函数不是类的成员,而是外部的一个函数。他允许该函数访问自己类中的私有和保护成员。
那为什么要用class?
当你想要强调封装和隐藏实现细节时,class是更好的选择。class只暴露必要的接口,这就是封装的一部分。不过C++中基本上二者相同。
内存对齐是计算机科学中的一个重要概念,指的是数据在内存中按其自然边界排列。为了提高存取速度和性能,许多处理器都要求某种类型的数据(如整数、浮点数)要安排在特定的内存地址上。
对齐规则
为什么要内存对齐?(本质上就是空间换时间)
寄存器只能从整除以4的地址开始读取数据,没有对齐会导致寄存器访问2次内存地址,性能下降。内存对齐多少字节是可以自己设计的。
可以被声明为内联函数。但是如果这个虚函数在派生类中被覆盖,那么内联可能不会发生。对于虚函数,编译器基本上需要保留对它的间接调用(通过虚函数表),以确保动态绑定的正确性。所以,虚函数可以被定义为内联,只要不是用动态绑定,使用静态绑定即可。
可能是发生了内存泄漏问题,通过审查代码,观看有new的地方是有delete。当时我们团队在做一个课程的大作业的时候,我有一个功能是调用接口打开摄像头进行人脸识别,可是次数多了过后程序就会崩溃,究其原因发现是内存不断地增大,应该就是发生了内存泄漏问题。VS有一个内存分析器可以告诉我内存泄漏的位置,我当时也是发现当调用某个线程的时候,他创建了资源,但是我没有释放掉,而是在主函数关闭后去释放,就造成了内存泄漏问题。后面就尝试定义成一个静态变量。
volatile是告诉编译器每次访问变量都从内存中去取,而不是使用优化后的寄存器缓存。然而,它不会防止并发读取和修改操作时的数据竞争。例如,当A读取变量,B也读取到变量,然后A、B分别对变量+1,由于竞争问题,最终的值只增加了1。
vector::shrink_to_fit
,将容器空间缩减为size大小。
清空内存:可以先clear,再shrink_to_fit;也可以vector
二者都是在子类中进行重写,但区别在于纯虚函数在虚函数后面加=0
,表明没有具体实现,这样的类被称为抽象类,不能实例化。
纯虚函数在设计模式中应用广泛:工厂方法模式。基类定义了一个纯虚函数作为创建对象的抽象接口,派生类提供具体的对象创建实现。
class Product {
public:
virtual void ShowProduct() = 0;
};
class ConcreteProductA {
public:
void ShowProduct() {
cout<<"using A product"<ShowProduct(); // 具体产品的功能调用
}
在C++中,new操作如果无法分配所需内存,会抛出一个std::bad_alloc
异常,可以用try-catch
捕捉。
atomic
,该操作能保证是原子的,在多线程环境下的逻辑运算的时候是安全的,不需要加锁保护。
.data
:存储已经初始化的全局变量和静态变量。.bss
:存储未初始化的全局和静态变量。会变得无效。在调用erase后,不应该再使用该迭代器。通常的额做法是再调用erase
时将返回值赋值(一个指向已删除元素之后的新迭代器)返回给原有的迭代器。
圈复杂度是一种用于评估代码复杂度的度量方法,它根据程序控制流图测量代码的分支数量。一个具有较高圈复杂度的程序可能更难理解和维护。
宏函数是在预处理阶段展开的,会用宏定义替换他们的调用。展开后的代码如果包含分支语句,会影响圈复杂度。
define会在与编译阶段被扩展或替换掉,所以执行程序时,实际上并没有名为“宏”的实体存在。所以在内存中没有固定的存储位置。
const在编译期间和运行期间,存储在常量区,这个区域的数据只可读不可修改。
emplace_back()
添加元素,可以避免额外的复制和移动操作(添加右值元素时,直接调用构造,没有拷贝或移动的操作)reserve
:避免后续添加元素时动态扩容swap()
清空cevtor:和一个空的vector交换:vector().swap(v)
shrink_to_fit
调整容量:减少capacity到size大小sizeof是一个操作符,strlen是一个库函数,在string.h里面
sizeof的参数可以是数据类型,也可以是变量,而strlen只能以\0
结尾的字符串作为参数
编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来
sizeof计算包括\0
,strlen不包括
有默认构造函数、析构函数、拷贝构造、拷贝赋值运算符、移动构造、移动赋值
因为模板,C++提供迭代器模板来抽象容器中的元素访问,这使得相同的代码可以适用于不同类型的容器和数据类型。
对于随机访问迭代器,比如vector,支持,而且还支持+=
。
对于end()
,vector不支持–操作,可以用-1来代替
A
: 在这种情况下,由于我们使用 std::move
将 a
转换为右值,编译器执行移动构造(如果类A有移动构造函数)。函数func
返回的是一个具有A类型的新对象。这种方式相对于传统的拷贝构造更高效,因为它允许我们在某种程度上“窃取”临时对象的资源。A&
(A的引用): 在这种情况下,代码会产生错误,因为返回局部对象(在函数内声明的对象)的引用将导致未定义行为。当函数返回时,局部对象a
将被销毁,所以返回的引用将指向一个无效的对象。你应该避免这种返回类型。A&&
(A的右值引用): 这种情况也会导致错误,原因同样是返回局部对象的引用,这在函数返回后不再有效。在函数中创建的a
对象在函数返回后被销毁,所以返回的右值引用指向一个已销毁的对象,导致未定义行为。总结:当涉及到移动语义时,最佳实践是返回值类型设置为类的类型(如 A
),这样编译器会自动进行移动构造或复制构造。避免返回局部对象的引用或右值引用,因为这会导致未定义行为。
会导致未定义的行为:因为访问的是未知地址,可能会导致程序崩溃、错误输出等;也可能导致其他数据被恶意覆盖,导致数据损坏
coredump是当程序出错而异常中断时,OS会把程序工作的当前状态存储成一个coredump文件。包含了程序运行时的内存、寄存器状态、堆栈指针、内存管理信息等。
如何用gdb调试?
gdb [binfine ][coredump file]
调试思路:
bt
查看调用堆栈显示,f 0
跳转到0位置,p str
打印字符C++的函数无法返回局部变量的引用,即要么返回全局变量/静态变量的引用 ,要么返回指针指向内容的引用。
unordered_map底层原理使用了哈希表作为数据结构。当将一个键插入到哈希表时,首先要使用一个哈希函数将键从其原始范围映射到较小的范围(称为桶)。这个函数应该均匀地将输入映射到可能的桶中,以避免哈希冲突。当发生哈希冲突即多个键映射到同一个桶时,采用了链地址法:每个桶都维护了一个链表,哈希冲突时将新的键值对添加到该桶对应链表的末尾,查找任意键时只需要遍历属于同一桶的链表即可。
悬垂引用是指一个引用变量指向了一个已经销毁的对象或函数。悬垂引用是一种未定义行为,因为它可能会导致程序崩溃或产生意外结果。在使用指针或引用时,我们应该尽量避免出现悬垂引用的情况。
分2层,直接成员是
对象指针:这是一个指向实际分配的资源的指针。通常我们会将sp与new一起使用
控制块指针。控制块主要包含 ① 被管理的对象 (或指针),② deleter、③ allocator、④ shared 引用计数、⑤ weak 引用计数。
控制块部分是线程安全的。底层的引用计数会使用原子操作保证同步。而托管对象则不是,所以需要确保互斥的访问对象指针。
首先标准建议 make_shared 同时申请控制块与对象的内存 (直观可减少一次内存申请);否则内存不连续,防止产生内存碎片;
其次是异常安全,C++17 之前形如 f(sp(new A), g()) 的执行顺序可能是 new A、g()、sp(),一旦 g() 中发生异常,那么 new A 将无法被回收,使用 make_shared 可以避免。
主要借助各种同步原语,或者一开始从设计上消除 data race。
在可以在编译时计算的情况下,通常推荐使用 constexpr
,因为它可以在编译时优化并提高程序运行效率。然而,如果表达式的值在编译时无法确定,那么使用 const
更合适。
C++11引入了一种成为统一初始化语法的特性,它主要包括列表初始化(花括号初始化或大括号初始化),旨在简化对象的初始化操作。
int a{5};
std::vector v{1, 2, 3};
struct MyStruct
{
int x;
int y;
};
MyStruct ms {10, 20};
因为成员变量在构造函数体之前完成了初始化,避免了在构造函数中用内部赋值的方式对已初始化的成员进行重新赋值。
迭代器失效是指迭代器不能再用来有效地访问容器中的元素。
push_back()
或emplace_back()
,可能导致存储空间重新分配,从而使现有迭代器失效。erase()
删除向量中的一个或多个元素,此时从删除点起的后面部分的迭代器将失效。(返回的是erase元素的下一个)clear()
清空容器,将使所有迭代器失效。resize()
或reserve()
改变向量容量,可能导致存储空间重新分配,进而使迭代器失效。swap()
或std::swap()
交换两个向量,将导致它们的迭代器失效。list是一个双向链表,每个元素有两个指针,指向前驱和后继节点。在使用的时候,通过迭代器封装了起来,我们可以使用++和–操作进行移动。另外,list是一个链式数据结构,其内存的分配是离散的,所以list可能比其他连续性容器更容易产生内存碎片。
vector
list
在创建临时对象插入时,emplace_back更好,不需要移动构造这一步。
不能完美替代push_back,会有安全问题。
vector> vec(1);
vec[0].push_back(1<<20);
vec[0].emplace_back(1<<20);
//vec.push_back(1<<20); // won't compile
vec.emplace_back(1<<20); // created a vector of size 2^20
vec.push_back(vector(1<<20)); // same as above
例如,考虑一个具有四个类“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 {
// ...
};
strcpy会一直复制,知道遇到\0
结束,可能会造成溢出,不安全。memcpy则多了一个要复制的字节数,不会检查终止符。
果你需要复制的是字符串,并且能确保源字符串能够以\0
结束,那么strcpy
更合适。如果你需要复制的是一段内存,或者不能保证字符串会以\0
结束,那么memcpy
更合适。
char* my_strcpy(char* dst, const char* src) {
char* saved = dst;
while ((*dst++ = *src++)); // 结束条件是源字符串的'\0'字符
return saved;
}
const int*
不能修改值;int* const
不能修改地址)deque是双向队列,结合了vector和list。在内部,他维护了一个指针数组,每个指针指向一个连续的内存块,每一个内存块可以存储多个元素。使得deque在两端插入或删除元素都是常数级的,同时因为内存的存储(同一个内存块中)连续,所以性能也较好。
数组名是数组首元素的地址,是固定的,不能改变,指针存储的是一个地址。虽然数组名可以像指针一样使用(比如,进行解引用操作和加法操作),并且可以自动地转换成一个指向第一个元素的指针,但数组名并不是能改变的指针。换句话说,数组名是常量指针,数组名是该数组类型的一个左值。
子类继承父类的所有属性和方法,即子类可以访问父类的共有和保护成员。私有成员虽然可以继承,但无法访问。
继承可以提供得到好处有:1. 代码复用:减少冗余代码;2. 代码组织:通过创建类层次结构,可以更好地组织和维护代码。但是继承会破坏代码的封装性。
在新的C++代码中,推荐使用 using
,由于其提供了模板别名和类型更可读的优点。在特定情况下,比如需要定义模板别名,或者需要以更清晰的方式向右阅读和解析类型时,应该使用 using
。
template
using Vec = std::vector;
Vec myVector; // equivalent to std::vector myVector;
\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也不变
int* arr[3]
,这里又3个元素,每个元素都是int*
int (*p)[10]
,着个指针指向了一个个包含10个整数的数组的地址template
T Add(T a, T b)
{
return a + b;
}
template
class SampleClass
{
private:
T data;
public:
SampleClass(T d)
{
data = d;
}
};
vector::at()
会自动进行边界检查double精度在15~17位,float在6 ~ 9位,如果从double转到float,会导致截断,产生不正确的结果。
理由是派生类可能依赖于从基类继承的一些成员或行为。这些成员或行为可能在基类的构造函数中被特殊处理。如果不先构造基类,那么派生类无法正常工作,因为他继承自基类的部分还未被正确初始化。
相比于普通函数,内联函数的主要特点是他们在编译器中的指令是内联的,减少了函数调用压栈的开销。但是,如果内联函数体非常庞大,那么他的调用会变得非常低效。
不会,static作用域只在其所在的源文件中,不会被其他源文件访问到。
golang的管道是一种用于在不同goroutines之间通信的数据结构。使用channel,可以避免手动同步数据传输和竞争条件。
使用的时候先定义和创建管道ch := make(chan int)
,然后向管道发送数据ch <- 42
,另一边接收数据value := <- ch
当一个goroutine在生命周期结束后还继续占用内存资源时,就会发生泄漏。可以使用sync.WaitGroup
等待所有goroutine结束,确保在启动新的goroutine时sync.WaitGroup
递增,Done
递减。
Go的map底层是基于哈希表是新鲜的。在Go的哈希表实现中,底层会自动进行扩容与收缩操作。此外,为了处理哈希冲突,Go采用了开放地址法,即从当前位置开始探测,直到找到一个空闲的位置来存储新的键值对。
slice是动态数组,它是一个更加灵活和用户友好的数据结构。它们的底层实际上是由固定长度的数组实现的。
s := make([]int, 5, 10) // 创建一个int型slice,长度为5,容量为10
当空间不足的时候,go会创建一个新的数组,将原来数组的元素复制到新数组中,再追加新的元素。如果元素小于1024,则两倍扩容,反之1.25倍扩容。此外,Go还可以选择以2的幂次方增长,避免了内存碎片化的问题。
map扩容机制遵循以下原则:
扩容策略
Go 运行时实现了一种名为 M:N 的调度模型,其中 M 代表操作系统线程,N 代表 Goroutine。
在 M:N 的调度模型中,存在以下关键组件:
以下是 Go 协程的调度过程:
总之,Go语言中的协程调度机制采用了M:N模型和抢占式的调度方式,使得协程的创建和销毁更加高效,并且可以更好地利用多核处理器的优势。
不会被系统自动回收。这可能会导致资源泄漏,最终可能导致内存耗尽或系统响应缓慢。为了避免该问题,需要使用 sync.WaitGroup
、或通道(channel)等机制来协调协程的执行和退出。当协程完成其任务并结束时,Go 运行时会正确地回收资源。
select 语句用于处理多个 channel 操作。它用于在多个通道上进行等待发送或接收操作。当任何一个通道成功发送或接收后,select 语句自动执行其中的相应代码块。如果多个通道同时满足条件,那么 select 将随机选择一个满足条件的通道执行。
select {
case <-channel1:
// 当从 channel1 中成功接收数据时执行此代码块
case channel2 <- someValue:
// 当成功向 channel2 中发送数据时执行此代码块
default:
// 如果以上任何一种操作均未执行,则执行此代码块
}
二者都是编译型语言,支持指针操作和内存管理。C++更注重底层,需要程序员自己处理许多细节,例如内存的申请和释放等。而Go提供了内存回收机制,且上手容易,且有goroutine协程的存在,可以高效开发高并发的程序。
定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换。该模式使得算法可独立于使用他的客户程序而变化
(多态的最基本使用,将算法抽象成接口,子类各自实现该接口)
定义对象间的一种一对多的关系,以便当一个对象的状态发生改变时,所有**依赖(不是继承)**于他的对象都能得到通知并能够自动更新。
实现:
Attach
、Detach
单例模式的核心是保证类的实例在程序中只有一个。我们可以确保在整个程序声明周期内,这个特定的实例是唯一可访问的,从而减少程序中的资源消耗和避免错误。
// 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;
}
父 = new 子
)工厂模式是创建型设计模式的一种。避免简单工厂模式的区别,不能满足开闭原则,引申出了工厂方法模式。工它将对象的创建过程封装到抽象工厂类中的一个接口方法(也称为工厂方法)。具体的工厂类继承抽象工厂类并实现工厂方法,负责实例化特定类型的对象。客户端通过调用工厂方法获得所需对象,而无需直接创建。这样,如果需要更改对象的创建方式,只需要修改工厂类的子类即可,而无需修改客户端代码,客户端也不需要关心产品对象的具体细节。
优点
就是降低了客户端代码和具体产品间的耦合,使得添加新产品比较容易。
符合开放封闭原则,在不修改源代码的情况下,可以扩展新的产品类。
应用场景
当编写代码的过程中,如果无法阈值对象确切类别以及依赖关系的时候,可以使用工厂方法。
设计模式实在软件开发中,用于解决特定环境下、重复出现的、特定问题的解决方啊,。设计模式可以避免重复造轮子的问题,解决代码间的高耦合问题,使得开发者在开发代码和后续维护上更加方便。
聚合
聚合关系是has-a,是一种弱的拥有关系,B不是A的一部分,生命周期不同
组合
二者是part-of的关系,生命周期相同(比如在A初始化时实例化B)
都属于行为模式。
用来存储函数调用时的临时信息的结构,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。
一般由程序员分配、和释放,用来存储程序运行时分配的变量。
存放全局变量、静态数据。程序结束后由系统释放。
全局区分为已初始化全局区(data)和未初始化全局区(bss)。
存放常量字符串,程序结束后由系统释放。
存放函数体(类成员函数、静态函数和全局函数)的二进制代码。
https://blog.csdn.net/qq_49286390/article/details/126561732
进程是资源分配的基本单位,线程是调度的基本单位
一个进程可以有多个线程,至少有一个主线程
线程的系统开销小,启动速度快,更轻量
同一线程共享的有:堆、全局变量、静态变量、指针、引用等,独自占有:TCB、寄存器、栈
通信上来说,进程通信用到管道、套接字、共享内存等,比较复杂;线程共享数据段,所以通信方便
什么时候用进程、线程
创建销毁频繁,用线程
并行操作多用线程,要求稳定用进程
进程是资源分配的基本单位,线程是轻量级的并发执行单元,协程更轻量级,它通常在用户态进行切换,因此可以更高校地执行多任务并发,这种编程模型在处理大量 I/O-bound 或网络操作的任务时非常有益。
协程拥有自己的寄存器上下文和栈,一直处于用户态。
不同进程间不能共享内存,因为不同进程有独立的虚拟地址空间,可以采用进程间通信(IPC),如共享内存、管道、socket、信号、消息队列等。
父子进程也不会共享内存,可以使用管道进行通信。
各自的应用场景
三个数字分别代表用户、组、其他用户的权限,一个数字为3个二进制数字,每一位对应可读、可写、可执行。
这里的755就说明文件拥有者有完全的权限,组内其他用户和其他组用户只能读取和执行,不能写入。
chmod
使用方法:chmod 755 example.txt
linux上可以通过tcpdump
命令通过网卡抓取数据包,windows上可以用Wireshark
堆栈溢出(stack overflow)是计算机程序在运行过程中遇到的一种错误。这种错误通常发生在程序使用过多的内存或者递归层数过深时,导致计算机无法为程序分配更多的栈内存。
虚拟内存使得每个程序认为自己独占内存,从逻辑上增大了内存量,使得多道程序能够同时运行,只需将运行程序的基本少数页面放入内存即可,当需要时再从外存中调入内存。这样也可以防止其他程序访问,保护了程序。
pthread_cond_
系列函数零拷贝是一种避免在数据传输过程中进行不必要的数据复制的技术,
二者区别在于,中断是由外部硬件产生的,异常是程序产生的。
CPU密集型:是指一个任务主要依赖CPU的计算性能,它再执行过程中消耗大量CPU资源。
IO密集型:依赖IO操作的速度,如磁盘读写和网络通信。
以上四个条件必须同时满足才能死锁。
如何避免死锁
数据库:
分页 | 分段 | |
---|---|---|
1 | 在分页中,进程的地址空间被划分为固定大小的页面 | 在分段中,进程的地址空间被划分为大小不同的段 |
2 | 操作系统负责分页 | 编译器负责分段 |
3 | 页大小由硬件决定 | 段大小由用户给出 |
4 | 速度比分段块 | 分段速度慢 |
5 | 分页会导致内部碎片 | 分段导致外部碎片 |
6 | 分页中,逻辑地址被划分为页号和页偏移 | 分段中,逻辑地址被划分为段号和段偏移 |
7 | 分页包含一个页表,页表包含每个页的基地址 | 分段包含段表,段表中包含段号和段偏移量 |
8 | 分页对于用户不可见 | 分段对于用户可见 |
9 | 在分页中,处理器需要页号和页偏移来计算实际物理地址 | 分段中,处理器使用段号和段偏移量计算地址 |
10 | 分页是根据地址空间划分(物理上) | 分段是根据应用程序进行划分(逻辑上) |
11 | 分页是一维地址空间(基址) | 分段是二维地址空间(段长、基址) |
分页机制是一种内存管理技术,它将内存分为固定的块,成为页,每块4KB。有了这个技术,可以提高内存利用率,并实现进程间的地址隔离。每个进程都有一个页表,通过页表,我们可以将虚拟地址映射到物理地址上。不仅从逻辑上增加了内存空间大小,还保护了每个进程的内存被其他进程访问的风险,同时还降低了外部碎片。
生产者消费者模型是一种经典的并发设计模式,用于解决在多个线程间进行有限资源共享的问题。生产者负责生产数据,消费者线程负责处理数据。二者之间的通信通常用一个共享的缓冲区(例如队列)来实现,生产者将数据放入队列,消费者从队列中取出任务。
共享内存相对高效,但是存在风险,因为任何人都可以加载应用程序访问定义在文件中的共享内存分区。
措施
linux
cpp:cpp的内存分布是通过虚拟内存实现的
共享内存是进程间通信的一种方式。不同进程间共享的内存通常为同一段物理内存,进程可以将同一段物理内存连接到他们自己的地址空间中。所有的进程都可以访问共享内存中的地址。
优点:是进程通信方式中最快的一种。访问共享内存和访问进程独有的内存区域一样快,并不需要通过系统调用或者其他需要切入内核的过程来完成。同时它避免了对数据的各种不必要复制。
缺点:没有提供同步机制,使得我们在使用共享内存时,需要借助其他手段保证进程间的同步工作。(信号量、互斥锁等)
虚拟内存通过软件和硬件的协作,为每个进程提供独立的地址空间,使得每个进程都可以像拥有整个内存一行使用内存,从逻辑上扩大了内存。虚拟内存的实现主要依赖于以下几个关键部分:
原子操作主要用于多线程保证数据一致性和同步,确保原子操作可以避免多线程代码出现竞态条件。
现代计算机底层硬件提供支持原子操作的指令,主要有以下类型:
原子操作的底层原理通常依赖于处理器架构和硬件指令集。通常,硬件提供一组特殊的原子性指令,用于在多处理器或多核系统中实现原子操作。
栈的大小和操作系统有关。再64位的linux和windows中,线程栈的默认大小通常为2M或8M。
此外,在某些极端情况下(如深度递归),可以考虑将一些递归调用代码转换为非递归调用来避免栈溢出。
优化方法:
操作系统为了防止运行时造成的内存地址冲突,引入了虚拟内存地址,为每个进程提供了一个独立的虚拟内存空间,使得进程以为自己独占全部内存资源。操作系统使用分段和分页的机制,管理虚拟地址与物理地址的映射关系。正是由于操作系统的这种内存管理机制导致了内存碎片的产生。
分段机制就是根据你的需求分配内存,需要多少给多少,所以不会有内部碎片。但是由于每个段的长度不固定,多个段不宜能能恰好使用所有的内存空间,所以就会有外部碎片。
内存分页将整个虚拟内存和物理内存空间分成一段段固定大小的片,虚拟内存和物理内存的映射以这个片为最小单位进行管理。在linux上,页的大小为4kb。但由于内存分页机制分配内存的最小单位为一页,所以即使不足一页大小的程序也会分配一页,导致页内会出现内存浪费,造成内存碎片。
如何减少内存碎片?
分段和分页都是内存管理的技术,主要目的是提升内存利用率和保护程序。分段机制就是根据你的需求分配内存,需要多少给多少,不会产生内存碎片。但是由于每个段的长度不固定,多个段不能恰好使用完所有的内存空间,就会产生外部碎片。而分页是将虚拟内存和物理内存分成一块块固定大小的片,虚拟内存和物理内存的映射以这个位最小单位进行管理。在linux中,一页的大小为4kb,这些页在逻辑地址上是连续的,但是物理地址不一定是连续的,通过页表映射。但是由于内存分配机制分配的最小单位为一页,所以不足一页大小的内存也会分配一页,会导致内部碎片,造成内存浪费。
从地址映射上来看,页表是一维的,即页号和页内偏移量;而段表是二维的,包括段号、段长、段基址。
分段更适用于逻辑上已经划分好的程序结构,比如代码、数据、堆栈,对程序员是可见的;而分页对程序员是不可见的,适用于随时分配和回收内存的程序,并且允许触发缺页中断,进行页面置换。
流水线将处理其中的指令执行分成若干个操作阶段,这些操作可以同时进行,以优化处理其性能。流水线设计的核心思想是将一条较长的操作拆分为几个较短的操作,以便每个操作在处理器中相对更短的时间内完成。
为什么进程切换比线程慢?
信号量内部维护了一个计数器,当信号量大于0的时候请求资源会减一,释放资源会加一。当信号量为0了,再请求资源就会阻塞,等待其他线程释放资源。该加减操作都是基于原子完成的。
锁是用系统级别的原子操作实现的。这些原子操作保证多个线程之间进行同步操作时,操作都是不可中断的,依赖于硬件和操作系统。
多进程
多线程
系统调用是运行在用户模式的程序请求操作系统内核为其服务的一种机制,可以看作是程序与操作系统之间的接口。
在现代操作系统中,由于安全和稳定性的原因,系统被分为至少两种运行模式:用户模式(User Mode)和内核模式(Kernel Mode)。用户模式下,程序所能使用的指令和能直接访问的内存空间都受到了限制,例如它无法直接对硬件设备进行操作,必须通过内核提供的接口。这时,如果用户态的程序需要使用到某些由内核提供的服务(如文件操作、网络操作、设备操作、内存管理等),就需要通过系统调用。
操作系统设计的一个主要目标是确保计算机系统的稳定性和安全性。因此,为避免用户级应用程序直接操作硬件,通常会将操作系统的功能分为用户模式(User mode)和内核模式(Kernel mode)。
用户态和内核态之间的切换主要出现在以下几种情况:
允许我们将一个普通文件映射到进程的地址空间,此后,进程就可以像访问普通内存一样对文件进行访问,而无需使用read()、write()等操作。总的来说,mmap的主要作用是为了提高内存使用效率和文件读写速度,它通过将磁盘上的数据以页为单位映射到进程的虚拟地址空间,使得对数据的访问更加高效。
他们都是保证TCP可靠的方法。流量控制是防止发送方过快导致对方不能接收的问题,防止包丢失,属于通信双方的协议;拥塞控制是防止链路上的资源太多,导致网络负载过大,作用于整个网络。
校验和作用:可以检测到数据包在传输过程中出现的位错误,如噪声等
IP首部和TCP首部都是20-60个字节。所以TCP/IP首部的最小总长度是40字节,最大总长度是120字节.
C语言可以用setsockopt
修改
#include
int sockfd;
int recv_buffer_size = 1024 * 1024; // 1 MB
// 创建一个 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置接收缓冲区大小(以字节为单位)
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (const char *) &recv_buffer_size, sizeof(recv_buffer_size));
用户态和内核态是操作系统中两种CPU运行模式,内核态拥有最高级别特权。
https是ssl/ tls+hhtp,有一个证书加密,默认端口号是443。
HTTPS的过程
是一种网络安全漏洞,它利用了应用程序对用户输入数据的处理不当,从而允许攻击者执行恶意的SQL语句。例如在where后加入1=1
来攻击和操纵数据库。
为了防止这种情况,需要:
保证双方都确保自己和对方的接收和发送功能没问题。如果两次的话,服务端无法确认自己的发送是否有问题。
有一个场景就是:若客户端发起连接请求,这个请求长时间滞留,于是客户端重新发起新的请求,这一次双方建立连接了;当双方断开连接后,这个滞留的请求来了,服务器向该客户端发送报文建立连接(两次握手),但客户端不发送数据,服务器一直等待,造成了资源的浪费。反之若采用三次握手,服务器没收到第三次握手,就会建立连接失败。
二者处于不同的层级上,有不同的目的和方法
+3是因为,如果只基于1个或2个重复的确认来触发快重传,会存在误报情况,因此要求连续收到3个重复确认,降低了误报的可能性;另外,假设一个数据包丢失,但这个分组的确认仍然收到了,根据累计确认,至少有2个新的、乱序的数据包已经发给接收方。
非对称加密
非对称加密使用一对密钥:一个公开的公钥和一个私有的密钥。公钥用于加密数据,私钥用于解密数据。在HTTPS握手阶段,客户端和服务端使用非对称加密进行安全通信,并协商一个对称密钥
对称密钥
对称密钥使用相同的密钥进行加密和解密。它的性能更优,因此适用于加密大量数据。在HTTPS握手阶段完毕后,客户端和服务端共享会话密钥。接下来的数据用这个对称密钥进行加密解密。
为什么https不直接用非对称加密?
总结
建立连接用非对称加密,传输数据用对称加密。
前面采用非对称加密,是为了得到只有双方知道的预主密钥,方便后面对称加密使用
用于防止网络拥塞,提高网络性能
有四种算法:
使用加密算法
发送数据前,使用数据加密,接收端收到后,再进行解密。
采用监控、日志
记录所用访问API的尝试,并定期审计以检测可疑活动。
是一种基于端到端的控制发送方传输速率的协议。流量控制最主要通过三个方法实现:
TCP粘包是指TCP进行数据传输的过程中,发送方发送的多个数据包会被接收方一次性接收,导致多个数据包黏在一起,这通常是因为tcp传输是基于字节流的,不保证数据的边界所引起的。解决方法:
放数据的速度 > 应用层拿数据的速度
UDP会发生粘包吗?
UDP不会发生粘包。UDP是基于数据报的协议,发送的数据包是独立的,有明确的边界。而且UDP头部有数据长度,会根据数据报的边界进行数据处理。
udp为什么会有乱序现象?
udp没有滑动窗口,序列号,确认应答和超时重传机制,由于网络抖动,不能保证数据包是按照先后顺序到达的。
为什么先发的包不会先到呢?
数据包需要经过多个路由器和交换机才能到达目的地。这些设备会根据当前网络状况来调整数据的路径。因此,即使从统一源地址发送,它们的路径可能不同,每条路径的拥塞情况,数据包丢失情况和重传情况都不同。
是生存时间(最大生存时间),每经过一个路由就会减一,作用有:可以防止无限循环、跟踪网络。
滑动窗口是一种流量控制计数。他的大小意味着接收方还有多大的缓冲区用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少自解的数据。当滑动窗口为0了,发送方就不能再发送数据报了。
流量控制就是利用滑动窗口实现的,保证接收方来得及接收。
不是固定的,是动态变化的。RTO是根据RTT(报文往返时间)计算的。RTT是数据包发送时刻到接收到确认的时刻的差值,即包的往返时间。
存在,如果有几个连续的报文都丢失了,那么确认方需要挨个收到连续三个重复的ack再发起重传,非常浪费时间。
而如果选择重传之后所有的报文,则会把接收到的一些报文又重传一次,等于做了一次无用功,浪费资源。
优点
缺点
HTTP/1.1相比于1.0,多了长连接,改善1.0短连接的性能开销。
HTTP/2基于HTTPS,所以HTTP/2的安全性也有保障。
由于HTTP/1和2都有队头阻塞问题,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后, 才能处理下一个请求,这属于 HTTP 层队头阻塞。所以HTTP/3把HTTP下层的TCP改成了UDP。这样就更快速了。
目前广泛使用的还是1.1版本。
HTTPS是通过SSL/TLS协议来对数据进行加密解密,同时需要一个受信任的第三方机构CA来堆证书进行签发和认证。
所以CA就是:1. 验证服务器身分;2. 为服务器颁发证书;3. 为客户端提供证书查询服务
close_wait是服务端(被动断开方)收到客户端的FIN请求,回复确认后,直到自己下一次发送FIN的这段时间。该问题转化为:为什么被动断开的服务方,不调用close函数(发送FIN报文,close接口),关闭掉已经四次挥手中的连接呢?可能有:
大量的close_wait会占用系统资源,因为服务端每给客户端一个连接就会创建一个fd,如果没有关闭,fd就不会释放
不会自动继续。当服务器重启并重新联网后,它通常会重新初始化所有网络连接。之前中断的四次挥手过程将不会自动继续。
在这种情况下,客户端会等待服务器响应,如果服务器未发送FIN,客户端会进行重传,达到最大值;如果服务器在重启之前发送了FIN分组,客户端可能认为连接已终止并释放资源。然而,如果服务器未在宕机前成功发送FIN,客户端可能需要依靠超时重传来判断连接失败。
grpc是谷歌开发的一个开源rpc框架,基于http/2.
shutdown
,关闭写的方向。shutdown
,关闭连接的读和写方向。即调用close。是一种网络攻击手段,攻击者通过招募上千万太计算机组成的僵尸网络,用以耍宝目标服务器的网络带宽,从而使正常用户无法访问这个服务。比如说TCP连接时,拒绝第三次握手,使得服务器受到SYN泛洪攻击。
解决办法有:防火墙、对流量过滤和清洗。同时,ISP(Internet Service Provider,互联网服务提供商)也可能采取措施来阻止攻击,并协助企业及时应对安全风险。
UDP想要可靠,就是程序员在应用层仿照确认应答和超时重传
醉翁之意不在酒,看似问UDP,其实是问IP分片长度
UDP首部有一个16位的UDP总长度,换算一下就是2^16字节,65536。所以,一个UDP数据总长度就是65536字节。而这个总长度还包括了UDP8字节的固定长度,也就是数据量还要减8字节,65528字节 。也就是说当我们发送的字节超过了65528,就需要将数据在应用层拆分成符合UDP大小的数据包,分别传输。
这里,我们还需要考虑分片的设计,比如
cookie是客户端浏览器用来保存服务器的一些数据的一种机制,当我们通过浏览器访问时,服务器可以把一些状态数据以KV的形式写入cookie,存储到客户端浏览器,然后下一次再访问服务器的时候,我们可以携带这些状态数据,发送到服务器端,服务器端可以根据cookie里面携带的内容去识别使用者。
session表示一个会话,它是属于服务器端的一个容器对象。默认情况下,它会针对每一个浏览器的请求,分配一个session对象。它用来存储当前会话的一些状态数据。我们都知道,http是一个无状态协议,也就是说服务器端并不知道客户端发送过来的多次请求是属于同一个用户。所以session是用来弥补http我要状态的不足。所以session可以用来存储客户端在同一个会话里面产生的多次请求的一个记录。
二者的结合我们就能去实现一个有状态的http协议。
它的过程是:浏览器第一次访问服务端的时候,服务端创建一个会话,并生成唯一的一个会话ID来标注这个会话。然后服务器端把这个sessionID写入浏览器的cookie里面。在后续的请求里面呢,每一次都会携带sessionID,服务器端可以根据sessionID识别当前会话的一个状态。所以总的来看,cookie是客户端的存储机制,session是服务端的存储机制。
公钥就像是邮箱顶上的缝隙,人人都可以用来投放信件;私钥则像是钥匙,只有持有者才能打开看到别人的信件。
它充当数据通信的中介,允许不同网络架构和协议之间传输数据。功能有:
websocket是一种网络通信协议。因为http是半双工通信协议,客户端先发送请求报文,服务器再发送响应报文。而websocket是全双工通信协议,它基于TCP,允许服务器和客户端双向通信。如果我们要玩网页游戏、刷视频就得用websocket。
websocket是长连接,直到连接关闭或断开。
会的,当使用https进行get请求时,所有传输的数据都会被加密(参数、请求行、方法、协议版本、cookie)。即使再代理服务器上抓到了数据包,但由于没有预主密钥,仍然无法解析。
这样做的原因是,拥塞导致丢包通常是因为网络出现了一定程度的拥塞。事实上,发送方的数据发送速率与网络当前能够处理的速率大致相当,但可能稍微超过了这一阈值导致了丢包。因此,为了快速恢复到一个稳定的发送速率,而不是从头再次开始慢启动过程,发送方选择直接进入拥塞避免阶段。
time-wait
是最后一次挥手发送过后到后面的2MSL阶段,以确保对方受到自己的请求,并可以重发丢失的ACK数据包。
MSL表示一个TCP报文段在网络中可以存在的最长时间。MSL主要用于防止过时的数据包再网络中滞留过久,导致一些问题。
timewait过多怎么办?
SO_REUSEADDR
可允许套接字在 TIME_WAIT
阶段再次被使用,以减少端口耗尽的问题。取决于listen的第二个参数backlog,这个参数是全连接队列的最大长度。在Linux 2.2版本 以后,该参数仅表示已完成队列的大小,不包含未完成连接的大小。
当客户端主动连接(accept()),linux内核就会自动完成TCP三次握手,将建立好的连接放入队列中。
半连接队列长度是指服务器处于SYN_RCVD状态的套接字个数,半连接到全连接的通过设置重传次数来判断是否超时。
前两次不可以,因为如果可以的话,有人恶意攻击服务器,那么他每次在第一次握手中的SYN报文中加入大量数据,服务端收到大量的SYN报文,会花费服务端很多时间和空间,浪费服务端资源。第三次握手可以携带数据。因为第三次握手是客户端发送给服务端的,只是为了让服务端确保服务端自己的发送能力没问题。
携带ACK的数据包:协议自发发送的TCP数据包消耗一个序列号(SYN、SYN+ACK…),而应用层数据逐字节进行编号,比如abc的序号是100,b的序号是101.
ACK数据包:但是,如果是纯的ACK数据包(三次握手的第三次握手),则不消耗序号
可以的,在两次回收后,被动断开连接方可以发送数据。
在没有滑动窗口之前,发送方只有等待接收方发送确认报文后,再发送下一个报文,效率很低。(确认应答机制)
滑动窗口允许窗口内的多个分组,不需要前一个分组的确认书举报,就可以发送到网络中,提高TCP发送方发送的数据量。从TCP角度看,滑动窗口将发送缓冲区分成了不同的区域:已发送已收到ACK、已发送没收到ACK、可以发送但还没发送、不能发送。收到确认后,就可以将窗口进行滑动,此时就有新的数据可以发送了。
实际上实在问可靠传输的方式
双方建立连接后,操作系统内核会维护对方的socket结构体,只要连接后,就会一直维护。
拔掉网线后,客户端机器和服务端机器针对这个TCP连接对应的struct socket结构体一定是存在的,除非经历了四次挥手才会销毁掉结构体。所以
tcp_retries2
默认是15次),发送方会认为网络断开。程序员通过通知进行后续操作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,就需要分片传输。
子网掩码标识了网络号和主机号的范围,说白了就是用子网掩码标识网络号使用多少个比特位,主机号使用多少个比特位。
SSL/TLS
加密。SSL/TLS解决了以下问题
客户端浏览器通常包含许多预安装的根证书。这些根证书来自收信人的证书颁发机构。当客户端验证CA时,会检查证书的根证书是否在预安装的信任列表中,如果存在,客户端则认为CA是合法的。
因为我们的IP地址优先,为了有效利用这些有限的IP地址,可以将网络分为局域网和广域网,将IP分为了私有IP和公网IP。一个局域网里面的多台机器都可以公用一个广域网IP,而内部则用的是私有IP,即192.168,这就是我们的NAT协议。这样大大增加了可用IP数量,一个小区就共用一个公网IP,而且一般数量不多,所以用的是C类地址即192.168开头的地址。
如果要使用私网IP访问局域网外的共有IP,就需要IP转换,即NAT路由器。
跨站点请求伪造(CSRF)是一种网络攻击奇数,攻击者通过诱导用户加载包含受害网站请求的页面来利用用户以获得的身份验证权限。当受害者访问含有恶意请求的页面时,攻击者利用受害者的凭据向受害网站发出恶意操作请求,如更改账户信息、发送而已消息等。
预防措施
Content-Type: text/html; charset=utf-8
ICMP是网络层协议,用于传输网络层控制信息的协议来达到对网络信息进行诊断,以及发送错误报告的目的。
比如ping和traceroute就是通过icmp来对网络质量进行评估。
还有当数据包无法到达目的地时,路由器或目标主机就会发送目的地不可达的消息,有助于识别网络问题,通过这种方式来达到发送错误报告的目的。
交换机工作再数据链路层,它将数据帧从一个端口转发到另一个端口,以有效地将网络连接到其中的设备。步骤如下:
除了ping还有什么命令可以去检测该主机网络是否正常,具体命令
telnet
netstat
:显示你的网络连接,以及哪些端口正在被监听,哪些网络接口正在运行MAC地址,即媒体访问控制地址,是计算机网络上设备的唯一识别码,通常被内嵌在网卡中,用于计算机网络中实现物理地址的定位和标识。MAC地址由6个字节(48位)组成。
UDP是基于无连接的,尽最大努力交付,所以无法对网络状态做出实时调整改变发送速率。比如流量控制是因为接收方会发送确认报文,来进行调整。
TCP连接复用是一种优化网络性能的技术,通过将多个客户的请求复用到一个TCP连接上,以减少服务器的性能负载和延时。具体来说,在客户端发送HTTP请求之前,通常需要先与服务器进行TCP三次握手来建立连接。然后,服务器处理请求并将结果返回给客户端,最后双方互相发送FIN包并在收到确认后关闭连接。
优势:
只有前中和中后才行,知道其中2个就能还原二叉树,但是必须要当每个值不重复才行。通过递归的方式还原。拥塞控制可以平衡每个连接的传输速率,但如果总带宽需求远超链路容量,仍然会出现拥塞。
前序和后序遍历通常无法唯一地还原二叉树,因为我们无法通过这两种遍历来准确确定根节点与左右子树的界限。
时间轮是用来存储一系列定时任务的环状数组。它的整个工作原理和我们的钟表的表盘类似。它由两部分组成,第一个是环状数组,另外一个是遍历整个环状数组的一个指针。首先定义一个固定长度的环状数组,然后数组的每一个元素代表一个时间刻度;然后是这个指针,这个指针按照顺时针无限循环这个数组,每隔最小的时间单位前进一格数组索引。环状数组的每一个元素都是用来存储定时任务的一个容器,当我们向时间轮去添加一个定时任务的时候,我们会去根据定时任务的执行时间,计算他所在的存储数组的下标,有可能在某个时间刻度上,存在多个定时任务,那么采用双向链表来进行存储。当指针指向某个数组的时候,就会把这个数组里面存储的任务取出来遍历这个链表,逐个去运行里面的每个定时任务。如果某个定时任务的执行时间大于环状数组转一圈的时间,那么一般可以使用一个圈数来表示这个任务的延迟执行的时间。也就是说如果一圈8秒,他是16秒,那么意味着要第二圈才执行该任务。
使用时间轮的好处:
当然,时间轮不适合时间精度要求高的任务(取决于最小时间精度)
举一个例子,在32位数字0x12 34 56 78在内存中的表示形式为:
1)大端模式:
低地址 -------------------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
2)小端模式:
低地址 -------------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
加法和乘法操作完直接%mod;减法则相减后+m再%mod。
快排是一种交换排序算法,是冒泡排序算法的改进。最好的时间复杂度能达到O(nlogn),基于分治的思想,每次选准一个基准值,放入到对应的位置,使得左边的元素比他小,右边的元素比他大,然后再对两边递归执行相同的操作。
从快排延申出了快速选择排序算法,这个算法是快排的一个小改进,通常可以用来处理TOP K问题。
冒泡排序的工作原理是重复地遍历排序序列,一次比较两个元素,直到进行到没有交换为止。时间复杂度是O(n2)。
快排和冒泡排序一样,都是交换排序算法,快排采用了分治地思想,将一个元素作为基准,将小于基准地元素移到基准以前,将大于基准的元素移到基准之后,然后递归地对基准前后地子序列进行相同的操作。
冒泡排序是稳定的排序算法,快排是不稳定的排序算法。
归并排序是一种典型的分治算法。他将数据分成多个子序列,然后解决子问题,最后将子问题的结果合并得出最终结果。
使用场景主要有:
稳定性指算法在排序过程中保持相等元素的相对顺序。即如果两个元素如果相等,原本在前的元素排序后还是在前。
稳定的排序算法有:插入排序、冒泡排序、归并排序、基数排序
不稳定的有:选择排序、快速排序、堆排序、希尔排序
二者都有递归性质,而且存在重叠子问题。动态规划可以理解为递归的一种优化,将重叠的子问题记录了下来,下次访问时直接返回无需重新计算。
二者都是可以自行调整使得平衡条件得以满足的二叉查找树。但他们的平衡条件和平衡方式有所不同。
存储方式
插入和删除
数组插入或删除未O(n),较麻烦
在平均情况下,快速排序最快;
在最好情况下,插入排序和冒泡排序最快;
在最坏情况下,堆排序和归并排序最快
LRU,双向链表,插入删除的时候将目标节点放置表头或表尾
默认阻塞的,但是可以通过fcntl
将套接字设置为非阻塞。
git fetch
git fetch
更安全,因为他不会更改当前工作区的内容git pull
git fetch
的功能,还会自动合并远程仓库最新的更新到当前工作目录中的分支git fetch
和git merge
的组合通信的一种方式。一般来源于中断。接收到信号后处理方式有忽视、强迫系统执行中断程序。
常用的信号有:
SIGINT
:终止信号(ctrl+c)End of File
:ctrl+d,表示输入流已经结束,用于关闭程序的快捷方式SIGCHLD
:子进程退出,父进程会收到SIGKILL
:杀死进程 kill -9SIGSEGV
:段错误int listen(int sockfd, int backlog);
,第二个参数是最大半连接队列(又称SYN队列) ,当有新的连接请求时,如果连接队列已满,客户端就会收到拒绝连接的错误。
read()
会返回实际读取的字节数,如果返回0,说明对方已经关闭了,如果小于0,则说明发生了错误。
当然,还可以用I/O多路复用来监视socket是否关闭。将套接字设置为EPOLLIN|EPOLLRDHUP
如果epoll返回EPOLL_IN
事件,读取时为0,则可以关闭。(EPOLLRDHUP:表示TCP的半关闭或远端关闭)
实际上,子进程会获得父进程的一些资源的副本,而不会共享,比如一些堆空间、栈空间的变量这些,都是从父进程重新拷贝了一份新的,而不会共享实际的内存地址
总结
同步阻塞方式会导致发起者等待任务的完成,同步非阻塞方式允许发起者在等待任务完成时执行其他事物,但需要轮询任务状态,而异步方式则通过回调函数或事件通知完成情况,避免了轮询
exec能够运行另一个程序。如execl("/bin/ls", "ls", "-l", (char *) NULL);
wait
或捕捉SIGCHLD
信号。在启动程序时,使用&
将程序放置后台运行。例如:./my-program &
可以使用lsof
,他是"list open files"的缩写。使用方法:lsof -p [PID]
复用的含义:用最少的物理要素实现最多的功能。
IO复用允许一个程序同时处理多个IO操作,适用于多个文件描述符或网络套接字。
IO复用的核心思想是将阻塞式I/O调用转换为非阻塞I/O调用,让单个线程或进程可以并发地管理多个I/O通道。这样就避免了为每个I/O操作创建独立的线程和进程,降低了资源消耗和调度开销。
为什么要采用非阻塞IO呢?
磁盘相关命令
df
:显示磁盘空间使用情况du
:查看文件和目录磁盘使用情况fdisk
:分区管理工具,用于创建和管理磁盘分区IO相关命令
iostat
:监控系统输入/输出设备和CPU使用情况iotop
:实时监控磁盘IO使用情况,分析哪些进程在正确使用磁盘(top本身就是用来查看进程资源情况的)vmstat
:显示关于内存、进程、cpu和文件系统的统计信息网络相关命令
ifconfig
:查看网络接口信息,例如IP、子网掩码ping
:测试网络连接tcpdump
:监视网络流量并实时分析数据包netstat
:显示网络连接、路由表、接口统计等信息根本区别在于,多进程每个进程有自己的地址空间,而多线程共享地址空间。所有其他区别都由此产生:
select的主要限制是FD_SETSIZE,默认情况下,FD_SETSIZE是1024,所以最多可以监视1024个文件描述符。FD_SETSIZE是一个宏定义,专门用于select
函数的,可以进行修改。
版连接队列是指哪些进行了一部分三次握手过程的请求。处在第一步和第二部之间,即客户端发送了SYN,服务端还未响应SYN。半连接队列的存在有助于服务器更有效地管理和处理各个阶段的连接请求。在高并发和网络延迟的情况下,由于更好地减轻了服务器的压力,它可以提高服务器性能。
内核态
用户态
内核态的作用是执行关键任务,访问中断等,保护系统资源,确保操作系统稳定运行。用户态为普通应用程序提供了一个受限的环境,确保应用程序不会影响到系统。
内核态能通过操作系统控制硬件,如果没有内核态,无法处理中断,也无法处理资源竞争、内存泄漏的问题。因此,内核态提供了系统级别的隔离和保护,维持整个系统的稳定性。
IO多路复用的核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是出发了读/写事件,就通知应用程序,去获取这个就绪的连接进行读写操作。
常见的IO多路复用机制有select poll epoll
其中,select和poll基于轮询的方式获取连接,epoll基于事件驱动的方式获取连接。从性能上来看,基于事件驱动的方式要更优。
描述符是计算机编程中用于标识和管理资源的一个整数,这些资源可能是文件、管道、套接字等。可用这个描述符来进行各种IO操作。
用scp
(secure copy)命令。它是一个在本地主机和远程主机之间迅速将文件复制的命令,基于SSH协议
find /路径 -name "*.xxx"
find /path -type f -mtime 0
这里的 -type f
参数表示查找文件,而 -mtime 0
参数表示查找最近 24 小时内修改过的文件。
netstat -tuln
linux权限分为三类:分别是用户、用户组、其他。每类用户都有3种权限:读、写、执行。我们可以通过ls -l
来查看不同文件的权限是怎样的。也可以通过chmod
来为用户修改权限。
chown new_owner file.txt
:该命令用于更改文件或目录的所有者/group。可以帮助确保只有具有适当权限的用户才能访问或修改文件目录。chmod 777 file.txt
:修改访问权限简言之,chown
更改文件或目录的所有者和/或所属群组,而 chmod
更改它们的访问权限。
sudo lsof -i :端口号
,如果该端口被的占用,可以用kill 端口号
关闭进程。
kill的信号是SIGTERM
,被称为优雅退出,用于请求程序终止;kill -9
是SIGKILL,被称为强制杀死进程。
ps -eL
。-e:选择所有进程。
top
:查看所有进程的CPU、内存、网络等使用情况。
linux其实没有线程,posix只是对线程的一层封装。
sort test.txt -u # Sort将文件的每一行作为一个单位,相互比较,比较原则是从首字符向后,依次按ASCII码值进行比较,最后将他们按升序输出。 -u去除重复行
用valgrind。
对编译后的程序执行
valgrind --leak-check=full ./mytest
Valgrind
将会运行程序,并在运行完成后报告潜在的内存泄漏。注意,Valgrind
会略微降低程序的运行速度。互斥锁
互斥锁只允许一个线程拥有互斥锁,其他线程只有等待。当抢锁失败时,线程会主动放弃CPU进入睡眠状态。
条件变量
互斥锁的缺点是只有两种状态:锁定和非锁定。当条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,通常搭配互斥锁使用,实现同步操作。当线程获取到锁但是条件不满足时,会释放锁并阻塞线程等待其他线程唤醒。
总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。
1.当read返回值大于0时,返回读到数据的实际字节数
2.返回值等于0时,表示读到文件末尾。
3.返回值小于0时,返回-1且设置errno
当errno = EINTR,表示被信号中断,并且对信号的处理方式为捕捉。对于read函数处理方式可以选择重启或退出。
errno = EAGAIN ,表示以非阻塞方式读并且没有数据。
errno为其他值时,表示错误,可以perror和exit。
find /opt -name "*.txt" -type f -delete
-type:按文件类型查找,f是file
-name:按文件名称查找
grep搜索的是文本,find搜索的是文件,换句话说就是grep是查找匹配条件的行,find是搜索匹配条件的文件。
ls -l
显示的是文件的大小,这是文件内容的字节数量。du -sh
显示的是文件在磁盘的使用空间。每个块4KB,所以即使一个文件只有1字节,也需要4KB.这就是为什么你在使用du -sh
查询很小的文件的时候,结果显示的文件大小可能比实际的要大。僵尸进程就是子进程先于父进程退出。
kill -9 不能强杀掉僵尸进程,因为僵尸进程本身就是已经退出的进程,kill -9相当于是鞭尸
父进程等待+自定义信号处理,在自定义信号处理中调用wait(NULL)
,回收子进程。
top可用于监控系统活动和当前正在运行的进程。
PID
:立即调度以运行的进程的进程ID。USER
:进程的拥有者用户名%CPU
:上一次更新到现在的CPU时间占用比力%MEM
: 进程占用的物理内存和总内存的百分比netstat:netstat -tuln | grep 端口号
-t (tcp) 仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数字的全部转化为数字
-l 仅列出在Listen(监听)的服务状态
lsof:lsof -i :端口号
docker不是虚拟机,而是一种轻量级的虚拟化技术,只虚拟出来容器所需要的资源,而不需要像虚拟机一样虚拟一整套硬件。
具体来说,Docker是基于Linux内核的容器化技术,它允许开发者将应用程序、依赖项和配置文件打包到一个可移植的容器中,然后在任何支持Docker的系统上运行。这使得应用程序可以在不同的环境中保持一致性,并且更容易部署和管理。相比之下,VMware是一种完全虚拟机化的技术,它需要在主机操作系统之上运行多个不同的从操作系统,每个操作系统都可以运行自己的应用程序和服务。
事务隔离级别是为了解决多个并行事务竞争导致的数据安全问题的一种规范。具体来说,多个事务竞争可能会产生三种不同的一个现象。
第一个,假设两个事务同时执行,T1事务可能读取到T2事务未提交的数据,但是最后T2回滚了,导致T1读取到了一个未存在的数据,从而导致脏读现象;第二个是说T1和T2同时执行,但是T1在读取同一行数据的两次结果不一样,从而导致不可重复读的问题;第三个是说假设两个事务T1/T2同时执行,那么T1在执行范围查询或范围修改的时候,T2插入了一条属于T1范围内的数据,并且提交了,这个时候T1查询的时候发现多了一条数据,造成了幻读现象。这三种现象在实际应用中可能不能接收某些存在,所以MySQL提供了四种隔离级别。
Innodb默认是可重复读,因为他需要保证事务ACID特性中的隔离性。在该级别下会发生幻读(前后读取的数量不同),通过Next-key避免幻读。
MVCC就是在事务启动的时候对数据库拍了个快照,保留了那个时候的状态,这个事务后续的读取都从快照中获取,哪怕加了新的数据,也不会影响。
但是如果别的事务删除了某条记录,我们就不能用快照了,要用间隙锁。
小结
select
语句),是通过 MVCC 方式解决了幻读。select ... for update
等语句),是通过 next-key lock
(记录锁+间隙锁)方式解决了幻读。MVCC可以确保某一时刻事务见到的数据是一致的,但如果在事务执行期间,另一个事务对数据库执行了插入或删除操作,MVCC无法阻止原事务发现这种新插入的数据,导致发生幻读。
意向锁是一种用于数据库管理系统,它试图在锁定资源之前表明事务希望锁定该资源。当一个事务想要获得锁时,数据库会检查是否存在资源的意向锁。
是一种事务隔离级别,用在数据库中确保不同事务之间的数据一致性和隔离性,能够防止脏读(读到了另一个事务未提交的数据)、不可重复读(两次读取不一样,主要是改)、幻读(主要是增删)。
因为B+树查询效率更稳定,每个结点只保留了关键字,那么每次IO读取时就能读取更多的关键字,一个Innodb页默认16kb,一般情况下一颗两层的B+树可以存2万行左右的数据。
B+树矮很多,那就意味着访问磁盘IO次数会少很多,效率就快很多。
且B+树叶子节点就是数据,MySQL中的B+树叶子节点类似于一个双向链表,我们经常会有范围查询,那么效率会非常高。(在物理地址上是连续的)
所有的数据都存储在叶子节点,那么B+树的全局扫描能力要强很多
回表是指二级索引,你通过当前索引不能查询到想要的数据,需要再查找一个主键主键索引才能找到。
避免回表的方法:在索引中包含查询所需的所有字段,这样,在查询时,不需要再回原表。
mysql-explain语句用于查看查询的执行计划,通过这个语句可以看到查询语句的优化效果,有助于我们进一步优化。
全局锁:加锁后整个数据库实例都处于只读状态。
共享锁:读锁。
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)
(-xx,1] [1,4] [4,9] [9,xxx)
,实现左闭右闭my.ini
文件中,慢查询的定义事件是超过2秒,我们可以修改慢查询的定义%
不可以放在最前面,不要使用or、union(去重)、not in这些条件查询一、慢查询原因
要对慢查询进行优化,首先要搞清楚慢查询的原因,原因主要有三:
(1)加载了不需要的数据列
(2)查询条件没有命中索引
(3)数据量太大
二、优化方案
优化也是针对这三个方向的:
(1)先分析语句,看看是否加载了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,如果有这些问题,则对语句进行分析、重写
(2)分析语句的执行计划,获得其使用索引的情况,然后修改语句或修改索引,使得语句尽可能地命中索引
(3)如果对语句的优化都已经无法进行了,可以考虑是否是表中数据量太大引起的慢查询,如果是,则可以进行横向或者纵向分表
索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,所有要查询的字段在当前索引对应的字段中都包含了,不用回表。
二者都是B+树的数据结构
Innodb一定有主键,主键是聚簇索引,不手动设置,则会使用unique索引,若没有unique索引,则会使用内部一个行的隐藏id来当作主键索引。
MyISAM使用的非聚簇索引。
是多版本并发控制:通过为所有事务操作的数据对象分配一个时间戳或版本号,从而使每个事务在自己的快照上执行,而不影响其他事务。MVCC 允许多个读操作和写操作并发执行,提高了系统的吞吐量和性能。(不用加锁)
MVCC直在READ COMMITTED (已提交读)和 REAPEATABLE READ(可重复读)下工作。
MVCC就是一种乐观锁的机制,它通过对于不同事务生成不同的快照版本,然后通过UNDO的版本链进行管理;高版本看得到低版本的事务变更,反之看不到。从而去实现了不同事物之间的数据隔离,解决了幻读的问题。
如果一个SQL想要利用索引,就必须提供该索引所对应字段中最左边的字段,比如针对abc建立了一个联合索引,那么在写sql的时候就一定要提供a字段的条件。
事务就是数据库中一系列操作的组合,具有4大特性。。
原理
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保存的重写日志再重写一遍。
脏读:读到了其他事务未提交的数据
不可重复读:在一个事务中,多次查询的结果不一致(强调改)
幻读:在同一个事务中,用同样的操作查询数据,得到的记录数不相同(强调增删)
处理方式:加锁、事务隔离、MVCC
加锁:
一页(一个节点)16kb。每次从磁盘取数据,都是一页一页的取。(操作系统一页是4KB)在插入时,会根据主键自增自动排序。
一个主键4字节,一个指针6字节,那么一页(一个节点)能存储16kb/10 = 1638个页(节点、page)
假设一行记录1kb,那么两层就能存储1638*(16kb/1kb)= 26208
那么假设是三层,就有1638 * 1638 * 16,大概4K多万条。
一般B+树就设置为3层,多了查询就很缓慢了。
一般情况下,分析和优化SQL语句就能大幅度提升性能,无需涉及服务器层面。不过如果服务器性能太差,也得考虑更新服务器硬件设施。
因为联合索引在B+树中是查询第一个索引,第一个索引值相同的情况下,再查找第二个索引。如果直接跳过第一个索引,索引就会失效,就无法根据索引查询了。
另外,最左前缀原则会一直向右匹配知道遇到范围查询就停止匹配了,所以我们可以先等值查询放到范围查询后面。
索引失效就是查询语句没有用到索引,走的全表查询。
可以在sql语句前面加explain
,如果Extra
出现了Using filesort
、Using temporary
、Using join buffer
等,说明查询语句存在问题,需要优化;还可以看key列,如果出现了索引名称,说明用到了索引,如果是NULL则没有用到索引
单独用a=什么,可能会走索引,这取决于数据库优化器的决定,如果优化器认为走索引开销更小就走索引。其他两个不会走索引
对于a=什么 and b=什么,也会走索引,同上,取决于优化器。
具体都可以用explain
来看。
select count(*)
需要全表扫描。而myisam保存了整个表的行数,执行上述语句速度很快。索引能够帮助mysql高效地去从磁盘去检索数据的数据结构,在mysql的innodb引擎中采用的是B+树的结构来实现索引和数据的存储。
优点有很多,我简单罗列几点:
索引的不合理使用也会带来很多缺点,诸如:
将一个庞大的数据库通过某种策略分割成多个较小的表。这些较小的表分布在多个数据库服务器上,提高系统性能,扩展数据库存储容量,并降低数据在单个节点上的风险。
分表
垂直分表:将数据垂直划分为多个表,这样单个页里面能存放的数据更多,所需要的IO次数就更少
水平分表:将一个表的数据分成多个表
multi-version concurrency control 在MVCC中,通常不需要加锁来控制并发的访问,相反,每个事务都可以读取到已提交的快照,而不需要去获取共享锁或者排他锁。在修写操作时,MVCC会用到写时复制的技术,即在修改数据之前先将数据复制一份,从而创建一个新的快照。当一个事务需要修改数据的时候,MVCC会首先检查修该数据的快照版本号,是否与该事务的快照版本一致,如果一致就可以修改这条数据,否则这个事务需要等待其他事务完成对该数据的修改。另外,其他事务也可以读取相同版本的快照数据,防止了脏读和不可重复读的问题。以上就是我的理解。
锁模式lock_mode
锁粒度
全局锁:flush tables with read lock
;unlock tables
表锁:lock tables employee read/write
;性能下降,并发能力差,写操作影响大(MyIsam只有表锁没有行锁)
行锁:只能在事务中使用,for update
in share mode
我们通常选用innodb存储引擎,主要使用行级锁,可以提供更好的并发性能,并且再一定程度上减少了锁争用的问题。而且InnoDB支持事务,可以保证数据的一致性和完整性。
行子类型rec_lock_type
(-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
聚簇索引就是按照每张表的主键构造了一棵B+树,同时叶子节点将索引和数据存储在了一起,找了索引就找到了数据。而非聚簇索引存储讲数据和索引分开,索引结构的叶子节点指向了数据的对应行,需要进行回表,效率较低。
有以下几种
分库,当表的数量很多导致数据系统的单个数据库很大,这时候需要根据不同业务将表拆分到多个数据库中;分表,当表中的数据太多的时候导致单个表的太大,这时候需要将表中的数据拆分到多个表中。
分库分表策略一般有几种,使用与不同的场景:
分库分表主键问题
incr orderId
怎么进行查找?
应用层的分片查询
根据分片规则,应用程序可以根据查询中的某个值(如用户ID)来确定目标分片(分库和分表)。一旦找到目标分片,应用程序会直接对该分片进行查询。(是否可以用redis存储每一个库的ID自增数在哪了,根据redis来判断在哪一个数据库哪一个片)
读写分离常用代理方式实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离能提高性能的原因在于:
区别在于MVCC生成数据快照时,读提交会在每次事务修改的时候生成一个readview,而可重复度只会在事务开启时生成一个,后续的操作都是在这个readview上的。
分为以下几种:
快照读是事务开始时,该事务生成一个全局数据的快照,后续的操作都是在该快照上的,所以就消除了幻读的可能,因为即使其他事务插入或删除了符合查询条件的数据,从事务开始时的数据快照看,这些更改并不存在。
当前读每次查询都尝试获取数据库的当前状态,这就意味着如果其他事务提交了修改,当前读会看到这些已经提交的修改,查询多次返回结果不同,从而导致了幻读的问题。
type字段,显示查询使用了何种类型,从好刀叉依次为:
system
:表只有一行数据,这是最理想的查询类型,几乎不消耗任何资源。const
:唯一性索引使用(包括主键索引),仅需要读取一行数据。表示至少一个索引列有常数值,这种查询非常快。eq_ref
:多表联结查询中的情况,即在连接操作中,对于前一张表的每一个结果,后一张表都只有一条结果与之对应。这种情况一般出现在使用主键或唯一性索引作为连接条件的场景。查询性能较好。ref
:对于前一张表中的每一行结果,在后一张表中可能有多行结果与之对应。连接时使用的条件不是唯一性索引。查询性能良好,但不如eq_ref。range
:表示本查询范围扫描,一般是使用了范围条件(如 BETWEEN
、<
、>
、<=
、>=
等),但只限定了索引的部分列。index
:全索引扫描。MySQL这种情况下会遍历整个索引树,这比全表扫描要快些,因为索引树的数据量通常比数据表本身小。ALL
:全表扫描,MySQL 遍历整个数据表,查找匹配的行。全表扫描效率低,需要尽量避免。二者查询的复杂度都是logn,而且数据都是有序的。但是B+树是一个多路平衡树,当新插入一个数据时,整个B+树会做调整,进行一个平衡操作;而跳表的多级索引是随机的,理论上来说上一层的索引是下一层的二分之一,那么如果有三层的跳表,最上层插入索引的概率就是四分之一,当数据量够大时,就符合二分之一。跟B+树不一样,跳表是否新增层数全靠随机函数。
B+树一页16KB,能存储很多索引信息,所以扇出每个索引页都能指向1000多个子页,三层左右就可以存储2000W数据,所以要查询一个数据最多三次磁盘IO;而跳表如果最底层要存放2000W数据,且每次查询都要达到二分查找的效果,那么2000W大概就等于2的24次方左右,所以高度是24次,那么最坏的情况下查一次数据要24次磁盘IO。因此存放同样多的数据,B+树高度要低得多。不过跳表的写入性能要比B+树高。
mysql主从赋值时mysql中最基本的高可用和故障恢复策略之一。该策略工作原理是:主数据库进行所有的数据更改,然后这些更改会记录在二进制日志中,从数据库将这些更改赋值到自己的数据集中。这也可以避免单点故障,提高读取性能,以及简化备份流程。
仍然不行。
因为再处理并发事务的时候,主要面临三种问题:脏读、不可重复读、幻读。这四种不同级别的隔离能够解决不同的并发问题,四种隔离级别是比较合适的设计。如果添加更多的隔离级别,就没有必要了,因为还会带来更复杂的管理和维护问题,同时也会对性能带来一定的影响。
当前读需要用next-key(行锁+间隙锁)解决幻读,而快照读情况下,mvcc避免了幻读。
redis的性能瓶颈不在于CPU,而在于内存、网络I/O的影响,早期版本中,他们能够用单线程实现高性能,就没有使用多线程了。而且多线程还需要考虑锁的开销。
定时删除:给某个键设置过期时间,默认每秒进行10次过期扫描,过期扫描不会遍历过期字典中的所有key,而是采用一种贪心策略
如果同一时间大量key过期,会一直循环扫描过期字典,造成卡顿现象。所以业务开发人员要注意过期时间,如果有大批量的key过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期
惰性删除:检查一个键是否过期时,并不主动删除过期的键。而是在访问的时候,如果发现已经过期,再删除。减少了CPU负担,但不能保证过期的一定会删除,可能造成内存浪费。
实际上,redis的过期删除结合了二者,并根据系统的负载进行调整。
用来监控和管理redis服务器。对主从复制集群进行监控、通知、自动故障转移。
关于这个问题,我是这样思考的。我从两个点来说:一个系统,他无非就是两种操作,一种是读写操作,一种是计算操作。关于计算操作,它是基于内存的,所以计算对于CPU来说不是瓶颈,真正的瓶颈在于IO。然后就是读写操作,读写操作他做了很多优化,读写操作又分为2部分,一个是网络IO一个是磁盘IO。磁盘IO这块呢,是redis基于做持久化的时候他用另一个线程去写入磁盘,但是对于主线程来说不影响啊;至于网络IO,他采用了多路复用IO,用epoll同时监听多个socket的IO请求。包括后续redis6.0出了多线程,增加的多线程只是用来处理网络IO事件,对于指令的核心执行过程,仍然是主线程单线程来处理。
还有一点,就是渐进式ReHash、缓存时间戳
我们知道,redis本质就是一个大的哈希表,当需要进行扩容的时候,需要将所有数据进行迁移,对于大量数据来说,这个过程是会很缓慢的。redis采用了渐进式rehash,即把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
这个过程是这样的:redis默认使用了两个全局哈希表,默认使用哈希表1,此时哈希表2没有被分配空间。当数据增多的时候,进行rehash,给哈希表2分配更大的空间,例如是哈希表1的两倍,然后把哈希表1的数据重新映射到哈希表2中,释放哈希表1的空间。在访问的过程中,如果第一张哈希表没找到,就找第二张
使用时间戳的时候不进行系统调用,因为系统调用会很慢,因此redis对事件进行了缓存,由一个定时任务,每毫秒更新一次时间缓存,获取时间都是从缓存中拿。
有两种,一个是RDB快照,一个是AOF日志。
RDB快照:xx时间如果超过xx条数据更改,则将当前redis生成一个快照保存
AOF日志:保存redis服务器执行的写命令,默认AOF是关闭的。
RDB记录的是结果,AOF记录的是过程,所以AOF会更大,redis提供了AOF重写功能来减小AOF文件体积,优点是回复大数据集的速度比AOF快,适合需要做冷备份,对数据恢复要求不高,。
AOF持久化的安全性要比RDB持久化的安全性高,即如果宕机,AOF要比RDB丢失的数据少(最多丢失1s之内写入的数据)。优点是更加稳定,数据完整性更好,更适用于更加稳定、对数据完整性要求高的场景
缓存穿透是指缓存和数据库都没有的数据,导致所有数据都落到数据库上,造成数据库崩溃。
解决方案
在接口层增加校验,比如如果id <= 0,则直接拦截
可以将key-value对 写成key-null,缓存有效时间可以设置短一点,这样可以防止攻击者反复用同一个id暴力攻击
布隆过滤器放在redis前面,将所有可能存在的数据存到足够大的bitmap中,一个一定不存在的数据就会被拦截掉,从而避免了对底层存储系统的查询压力(选择多个hash函数计算多个hash值,如果全部命中,则说明可能存在)
redis支持事务,包括的命令有:MULTI
开启新事务、EXEC
执行事务的命令、DISCARD
取消事务、WATCH
监视一个或多个键
ZSet是一种有序集合,底层基于跳表和哈希表
当内存满了,就会执行淘汰策略删除一些旧数据
redis我们直到是一个KV键值对数据库嘛,他其实本质就是一个全局哈希表+链表的结构。比如set bob 1
我们默认V是用的string类型,而这里的V其实有很多种数据结构类型供我们选择。
字符串string
INCR bob
(只有int的string才可以)这是原子自增操作 INCR
命令为对应的 key 进行原子性自增。然后,将自增后的值用作分表中新数据的自增字段。列表list:使用双向链表实现
集合
有序集合:使用跳表和哈希表组合实现。跳表负责排序,哈希表负责映射
哈希hash
针对string那种json存储和哈希根据字段存储,2种方案的优缺点?如果是余额类型的,只有余额经常变化,用哈希更好,因为采用string需要先将键值对拿出来,转成对象,设置属性,再存入redis,而如果用哈希这种分字段的,可以直接HMSET
命令搞定
跳表结构简单,适用于高并发
红黑树结构复杂,空间紧凑。
跳表结构简单,性能更高,但需要更多空间存储指针;红黑树保证了查找性能的稳定性以及空间使用方面具有优势,但实现复杂度较高
redis的分布式集群目的是在多台服务器上分配数据,提高应用程序的性能,同时以一种容错的方式运行程序。一个集群至少需要3个节点。redis集群有这些特征:
Redis分布式锁是一种分布式环境下的互斥操作的解决方案。可以确保多个客户端同一时间只有一个在进行同步操作,避免发生冲突。
NX
和 PX
选项的 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 命令
检查是否成功获取到锁:客户端需要检查 Redis 的 SET
命令是否返回 OK
。 如果返回的是 OK
说明成功获取到了锁。 否则说明锁已被其他客户端持有。PX是毫秒操作
执行保护的操作:如果成功获取到了锁,客户端就可以执行要保护的操作。
释放锁:操作完成后,客户端需要释放锁。
分布式锁的其他实现方案
不会。redis server本身是线程安全的kv数据库,也就是说在redis server上执行的指令,不需要任何同步机制(单线程)
尽管后面的版本中,增加了多线程模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是主线程来处理。
至于为什么不用多线程:
redis主从复制是指在redis集群里面,master节点和slave节点的数据同步的一种机制,即把一台redis服务器的数据复制到其他redis服务器里面。
在redis中,提供了全量复制和增量复制两种模式:
各有优劣势。
加实例
加内存
缓存雪崩是在相对短的时间内,缓存中大量数据同时过期失效。导致大量请求直接进入数据库,使得数据库崩溃
解决方法
缓存和底层数据都没有的数据,导致数据库崩溃
解决方法
SETNX
命令,有助于实现分布式锁的功能,以确保在分布式环境中的执行任务是原子性的zset底层数据结构有压缩列表和跳表。
压缩列表本质上就是一个数组,只不过他增加了列表长度、尾部偏移量、列表元素个数、以及列表的结尾的标志,这样就有利于快速找到列表的首尾节点,但是对于其他普通元素中间元素效率仍没有很高,只能挨个遍历。当有序集合保存元素数量小于128,或者有序集合保存的所有元素长度小于64字节的时候,就会采用压缩列表,否则采用跳表。
跳表是什么?
跳表在在链表的基础上跟增加了多级索引,通过多级索引位置的转跳,实现了快速查找元素。我们可以建立多级索引。当数据量特别大的时候,跳表的查找时间复杂度是O(logn)
为什么不用红黑树或者二叉树呢?
Redis 的哈希表(Hashes)本质上是无序的,因为它们是根据键值对的哈希值存储的。然而,有时我们可能需要以某种顺序(如字母顺序)检索哈希表中的键和值。
HGETALL
:获取哈希表种的所有键和值,并将其存储在某种数据结构中redis是基于内存的KV键值对的NoSQL数据库。它提供了5种常用的数据类型,string、map、set、list、zset。不同的数据结构可以解决不同的应用场景,因此他可以覆盖应用开发里面的大部分的业务场景,比如top10问题、好友关注列表、微博点赞、排行榜(zset)、计数器(string原子自增)等等。其次呢,由于redis是一个基于内存的存储,并且在数据结构上做了大量的优化,所以在IO上面性能比较好,所以我们常拿来做应用和数据库之间的一个分布式缓存中间件,并且它又是一个非关系型数据库,不存在表之间的关联,所以他可以很好的去提升应用程序的数据IO效率。对于企业开发来说,他又提供了主从复制+哨兵,以及集群的方式去实现高可用。在redis集群里面通过hash槽的方式去实现数据的分片,做到负载均衡。以上就是我对redis的理解。
哨兵集群和cluster的区别
缓存雪崩:某一个键失效,新的缓存未到的期间,同一时刻大量请求查询数据库,导致数据库崩溃
缓存穿透:查询缓存和数据库中都不存在的数据,导致所有请求都落到数据库上
缓存击穿:热点数据失效的瞬间,持续的大并发穿破缓存,直接请求数据库,就像屏幕上凿开了一个洞。或者客户端恶意发起大量不能存在的key的一个请求,由于访问的key对应数据本身不存在,所以每次访问都会到数据库上。
为数据设置两个缓存层。一个是一级缓存(例如本地缓存),另一个是二级缓存(例如分布式缓存)
在分布式系统中,生成全局唯一ID的需求很常见,比如订单ID、用户ID等,需要保证生成的ID是唯一的、按照时间顺序排列的。可以用数据库的自增字段自己增长,但会受到数据库性能的可用性的限制;也可以用Redis来实现。通过redis的原子自增操作INCR
命令在redis中递增。这种方案简单易用、性能良好。
mongoDB是一个文档型NoSQL数据库。
redis的响应时间为100纳秒,能够支持8W~10W的QPS。
如果是大公司,需要更大的QPS,用到了IO多线程(内部执行命令还是单线程)
为什么不采用分布式架构,缺点是什么?
业务隔离
业务A SET A1,key区分出来
key的设计
分布式锁
和B+树相比,查询效率相当,都是logN,同时,支持范围查询,这对于ZSET这类需要排序的数据非常重要。而且,对于数据的插入来说,B+树需要进行调整,性能非常低。另外,跳表数据结构简单,实现简单,占用的内存也比B+树小。
一致性哈希是一种分布式哈希方案,可以解决一些分布式系统中的负载均衡和数据存储问题。一致性哈希的优点在于:当有新的节点加入或现有节点离开系统时,只需要在哈希环上重新分配一小部分数据项,而不需要重新分配全部数据,这就减少了系统中的数据迁移两和负载重新分布的开销。
限流和降级的区别:前者是出现问题前,为了防止系统过载降低速率;后者是已经出现问题,通过降低服务质量保证系统继续正常运行。
分布式事务指的是再计算机网络中涉及多个参与者、组件或系统的事务。由于多个组件可能位于不同的机器上,需要跨越网络。分布式事务同样满足ACID特性。
二者都是分布式系统共识算法。
Paxos有三个角色
Paxos算法有两个阶段:
因为Paxos的复杂性和不直观性,在实用性上很有挑战。 Raft算法是他的变种,简单易懂,易实现。
概念:MQ分布式消息队列它是一种应用之间异步通信的方式,主要由三部分组成:1. 生产者。负责消息所承载业务信息的一个实例化,是我们整个消息的发起方;2. broker代理:是我们整个消息的服务端,主要是去处理我们的这个消息单元,负责消息的存储、投递以及一些其他的附加功能,是整个消息队列里面最核心的组成部分;3. 消费者:主要负责消息的一个消费,具体根据消息所承载的一个信息去处理各种业务逻辑。
场景:
A和C只能保证其一的原因:
如果保证了一致性(C),那我们就要保证客户端在任意时刻,在任意一个节点读到的值都是一致的;而可用性(A)是允许部分节点故障时,集群还能继续响应,那么就无法保证故障节点非故障节点之间的一致性了;反过来也是一样的道理,因此A和P无法共存;
分布式事务同样需要满足ACID属性,而且还要考虑跨节点通信和协调。一种常见的处理分布式事务的方法是使用两阶段提交(2PC)或三阶段提交(3PC)。
关于数据去重,可以采用的是hashtable、bitmap、布隆过滤器(二者的结合应用)
采用bitmap的话,能够比数组、哈希表节省大部分的空间,因为一个int型要4字节,而如果用bit来表示,则会少很多。
bitmap: 假设5个整数组成的序列{2,3,200,7000,12000},则我们可以将这个序列保存在二进制数组当中,第n位如果为1,则表示n存在于这个序列中。 然后在根据这个二进制数组转化为整数,依次从小到大排序更新到外部文件。
但缺点就是不能有重复的数,而且bitset有限制,存入的数最大限制是10亿
采用多路归并算法(内部排序+外部排序)
多路归并算法用于:无法一次性将数据全部放入内存中排序(场景题中内存很小,磁盘很大)
于是,我们可以先在内存中将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,直到全部处理完成。
https://love6.blog.csdn.net/article/details/124851241
其实就是第K大的问题。
unordered_map
统计各个数据出现的次数,复杂度O(n)
,哈希表一般占用内存至少两倍左右priority_queue
来做排序,决出热点数据。用小顶堆,堆大小为K,如果遍历到的词频大于堆顶,就将该词替换掉堆顶的词,然后重新调整小顶堆。总结:
unordered_map
统计词频用64位整数来存储QQ比用字符串存储更省空间,一个QQ占8个字节,然后再考虑登录状态占用一个字节。
那么对于40亿的QQ,占用的空间就是大概(8+1)*(4*10^9)
约36GB
可以用string存放大数,然后进行排序
TCP没有数据边界,分段去传,在头部加入长度,还能防止粘包。
文件突然断网了,续传怎么办?
先存在本地或者url,或者存临时文件。
通过perror错误码来看。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Error creating socket");
return -1;
}
比如这个就是创建套接字失败。
使用bitmap:申请512M的内存,512M(2^32)内存是42亿多bit,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应的bit位是否为1.
哈希表是无需的,无法保证按照插入顺序遍历元素。我们可以用一个数组/链表和哈希表来共同维护,其中数组/链表用来存储KV,哈希表用来存储Key所在的下标。这样我们就既能通过哈希快速查找元素,又能按照插入元素打印了。
可以根据不同的IP用户创建对应的键值对,并用INCR
命令来为该业面的对应键名增加访问计数。
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< lock(mtx);
cond.wait(lock, []{return printNum == false;});
cout<
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<
可以用2个栈来实现,一个前进栈,一个后退栈。
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问题,用分治
一个号码8位数,8位电话号码可以表示的号码数有108个,即1亿个。我们每一个号码用一个bit来表示,总共需要1亿个bit,即12M.
将对应bit的电话号置为1,最后总的1个数就是不同的电话号个数。
可以将两个类的一些公用的函数接口抽象出来,让他们继承一个基类。通过里氏替换原则,创建后,实现接口函数。对于他们类内部各自的实现逻辑,可以自己实现。
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;
};
用二进制来进行。1000瓶饮料,正好可以用2的10次方来完全表示。我们将10只小白鼠按照比特位去喝特定的毒药,如果01就让小白鼠1喝,10就让小白鼠2喝,11就让小白鼠1和2喝。这样的话,假如1和3号小白鼠死了,那么我们就可以知道,转换为十进制是5,就是第五瓶水有毒。
假设这个异常小球的重量偏重一点。
现在,我们知道第一个盒子中有一个不同颜色的球,但是剩下的 i-1 个盒子也要满足条件。我们可以分析两种可能的情况:
因此,我们可以得出递推公式:
dp[i] = (i-1) * (dp[i-1] + dp[i-2])
一根线上若干蚂蚁([1,0,0,0,-1,-1,1,0,-1,0,0,1]),有往左的(1)有往右(-1)的,每两只蚂蚁碰撞会掉头,蚂蚁速度都一样(一秒移动一格)
蚂蚁相撞可以堪称无障碍地穿过对方,所以:
这其实是一个很有趣的问题,我们需要通过两个六面骰子(我们记作 A 和 B)来代表 01 到 31 的日期,要包括所有可能的日期组合。我们可以根据以下规则来设定每个骰子面的数字:
其中,6 可以翻转使用作为 9。这样我们就可以得到 01 至 31 的所有数字了。对于 1 到 9 的日期,我们可以将骰子 A 上的 0 与骰子 B 上的 1 到 9 结合,以及 10 到 15 可以用 A 上 1 到 5 与 B 上的 0 结合,以此类推,我们可以表示所有日期。