C++
C++的特点
C++ vs C
- 语法扩充:namespace、引用、new/delete
- 面向对象:封装继承多态、自定义类型、函数和运算符重载
- 新机制:异常处理、类型检查、RAII、变参数模板
- 标准库:STL等
C++11 新特性
- 新语法:列表初始化、委托构造、范围for循环、lambda表达式
- 新关键字:nullptr、final、constexpr等
- 类型相关:using、auto、decltype、move、forward、右值引用
- 新组件:智能指针、无序容器、std::thread
C++关键字
- 存储说明(storage class specifiers):static、extern、thread_local
- 修饰符(modifier):const、constexpr、volatile、inline
- 模板:template、typename
- 类:class、public等
- 类型:int、bool等
- 流程控制:if、else等
const
const优先修饰左侧,其次修饰右侧
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
左值和右值
- 引用:别名、声明时初始化、通过引用修改原变量
- 左值和右值的含义:
–左值:等号左右,可取地址、具名(例:变量名、++i、返回引用的函数调用、赋值结果、解引用)
–右值:等号右、不可取地址、不具名(纯右值例:字面值、返回非引用的函数调用、i++、表达式)
–将亡值:触发移动构造和移动赋值构造的值
左值引用、右值引用
- 含义:左值引用是对左值的引用,右值引用是对右值的引用。const 左值引用可以指向右值,不可修改。右值引用可使用move指向左值。
- 应用场景:
–左:避免拷贝(函数传参、函数返回)
–右:移动和移动拷贝构造、完美转发(带模板函数可将参数准确地转发给内部函数)
–完美转发前提:借助T&&或auto,以引用的方式接收参数
malloc、new
- new:运算符可重载,malloc:函数
- new:先分配内存,再调用构造函数,最后返回对象类型的指针。malloc:只分配内存,需给定分配内存的大小,返回void*
- new:出错抛异常。malloc:出错返回NULL
- new:在free store上分配。malloc:小于128K,先在内存池分配,再调brk。大于128k,调mmap。
free、delete
- free:先把指针左移16字节,得到应释放大小。delete:根据对象类型指针计算释放大小,先析构、再释放空间。
- 共同点:利用mmap分配时,释放后直接回收。利用brk分配时,释放后归还内存池。
STL
- 含义:STL是一系列模板类,包含常用的数据结构和函数
- 组件:容器、算法、迭代器、仿函数、适配器、分配器
- 容器分类:顺序型(vector、deque、list)、关系型(set、multiset、map、multimap)、适配器(stack、queue、priority_queue)
迭代器iterator
- 含义:迭代器是一种对象,它可以指向并遍历某容器中的元素,其与指针不同,指针是记录其他变量地址的特殊变量
- STL迭代器分类:
– 输入迭代器:只能读取,每次读取迭代器递增
– 输出迭代器:只能写入,每次写入迭代器递增
– 前推迭代器:提供读写操作,每次写入迭代器递增
– 双向迭代器:提供读写操作,并能向前和向后操作。
– 随机访问迭代器:提供读写操作,并能在数据中随机移动。
- 对比指针:
– 指针记录内存地址,迭代器还可记录文件内的位置
– 指针可代数运算,迭代器不一定支持,如对输入迭代器递减
– 指针可以释放资源,迭代器不可以
- 失效:
– vector扩容:整体失效
– vector和deque的erase:某元素后全部失效
– 其他erase:当前迭代器失效
- 其他资料:https://getiot.tech/zh/cplusplus/stl/cpp-stl-iterators
分配器allocator
- 目的:
– 把对象的资源分配和构造分离
– 提供内存分配和释放的标准接口:填写模板参数,创建allocator对象,在用allocator管理的内存分配
- 使用:
– 建立allocator,调用allocate函数分配空间
– 建立allocator_traits,调用construct和destroy进行构造和析构
– 调用allocator的deallocate进行析构
std::allocator<MyClass> allco;
int size = 3;
auto dataarr = allo.allocate(size);
for(int i=0;i<size;i++)
{
allocator_traits<decltype(alloco)>::construct(allco,&dataarr[i]);
allocator_traits<decltype(alloco)>::destroy(allco,&dataarr[i]);
}
allo.deallocate(dataarr,size);
- 自定义allocator
– 实现内存共享、内存泄露检测、预分配对象存储、内存池
虚函数表
- 创建时机:一个类对应一个虚表,在编译时生成。虚表是一个函数指针数组。加载时,虚表被加载到全局数据区的只读数据段中。
- 虚函数表指针:每个对象有不同的的虚表指针,不同类的虚表内容相同。虚表指针在构造函数、拷贝构造函数和赋值拷贝构造函数中生成。构造时,虚表指针先指向父类的虚表,再指向自己的虚表。
- 浅拷贝导致不同对象指向同一个虚表的解决方案:1、避免浅拷贝。2、重载拷贝和复制拷贝。3、delete拷贝和赋值拷贝
- 不可为虚函数:构造函数、友元函数、静态成员函数
构造函数
- 默认构造函数:不含参数,自动生成
- 带参数构造函数:由用户定义
- 拷贝构造函数:接收同类型的引用(通常加const)
class MyClass {
public:
MyClass(const MyClass& other) {
}
};
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
}
};
- 转换构造函数:只接受一个其他类型参数,实现隐式转换
- 拷贝赋值运算符:
class MyClass {
public:
MyClass& operator=(const MyClass& other) {
if (this != &other) {
}
return *this;
}
};
int main() {
String str1("Hello");
String str2 = str1;
String str3("World");
str3 = str1;
return 0;
}
class MyClass {
public:
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
}
return *this;
}
};
int main() {
Buffer buffer1(10);
Buffer buffer2 = std::move(buffer1);
Buffer buffer3(5);
buffer3 = std::move(buffer2);
return 0;
}
- 空类的6大函数:默认构造、默认析构、拷贝构造、拷贝赋值运算符、取地址运算符、const取地址运算符
class Empty{
public:
Empty();
Empty( const Empty& );
~Empty();
Empty& operator=( const Empty& );
Empty* operator&();
const Empty* operator&() const;
};
拷贝
- 触发时机:1、A a=b;或 A a(b);。2、函数传参。
- 函数返回值:开启返回值优化,只会构造一次,不会拷贝。禁用返回值优化,则会优先调用移动构造、其次考虑拷贝构造。
- 何时生成默认拷贝函数:1、成员有默认拷贝构造函数。2、父类有默认拷贝构造函数。3、该类包含虚函数
智能指针
- 普通指针缺点
– 忘记释放
– 释放不置空:我释放,我不滞空,我野。我释放,他不置空,他悬。踩内存
– 多次释放
- 总体思想:RAII
- unique_ptr
– 特点:不支持拷贝和赋值,可以用release或std::move转移资源控制权。自定义分配函数和释放函数时,需要在模板里提供函数类型,因为unique_ptr在编译器绑定分配和释放函数。
– 函数传参:方法一:外部传unique_ptr的资源,函数接收引用。方法二:传递裸指针。方法三:传unique_ptr的引用。方法四:使用std::move
– 函数返回:可以直接返回unique_ptr,因为函数返回值是右值。
- shared_ptr
– 原理:shared_ptr包含一个计数器,如果直接初始化,则会在堆上创建化计数器。如果用另一个shared_ptr初始化,则新shared_ptr的计数器指针指向传入的计数器,并使计数器加1。析构时计数器减1,若计数器为0,则析构计数器。
– 特点:shared_ptr的内部指针初始为nullptr,不提供+、-、[]运算符。使用自定义分配和释放函数时,无需在模板内提供函数类型
– 场景:资源被多个对象持有
– 线程安全:多线程读同一个shared_ptr(安全),多线程写同一个shared_ptr(不安全),同一个资源被多个shared_ptr管理(安全)
- weak_ptr
– 用法:初始化一个shared_ptr,初始化一个weak_ptr。把shared_ptr赋值给weak_ptr。使用lock把weak_ptr升级成shared_ptr,进而访问资源。
unordered_map
- 负载因子(load factor):负载因子 = 容器存储的总键值对 / 桶数
- 扩容:当负载因子>1时,建立新buckets,大小为大于num_element的下一个质数,将原来buckets的元素迁移到新的buckets中。
- 哈希冲突:桶内元素小于8时采用链表,大于8时采用红黑树
vector
- 实现vector:https://blog.csdn.net/xbhinsterest11/article/details/125972503
条件变量运行过程
- Thread A 加锁
- Thread A 检查条件变量
- Thread A 条件变量为false,A释放锁
- Thread B 加锁,修改数据,修改条件变量,通知Thread A
- Thread A 被唤醒,继续执行
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cv;
bool isOddPrinted = false;
void printOdd() {
for (int i = 1; i <= 10; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !isOddPrinted; });
std::cout << "Odd: " << i << std::endl;
isOddPrinted = true;
cv.notify_one();
}
}
void printEven() {
for (int i = 2; i <= 10; i += 2) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return isOddPrinted; });
std::cout << "Even: " << i << std::endl;
isOddPrinted = false;
cv.notify_one();
}
}
int main() {
std::thread t1(printOdd);
std::thread t2(printEven);
t1.join();
t2.join();
return 0;
}
内联函数
- 普通函数:入栈,调用,出栈
- 内联函数:直接代码替换,同时做类型检查
- 不能包含循环、switch、递归
- 虚函数:只有虚函数不发生多态,内联才会起作用
- 类声明中的一些函数会隐式内联
- 优点:省略出入栈、有类型检查、可以调试
- 缺点:代码膨胀、内联最终取决于编译器
静态局部变量和全局变量
- 可比性:二者都可以作为一个函数的状态,多次调用同一个函数,该变量的值会累计
- 对比:静态局部变量更好,因为它只能由一个函数访问。
重载和重写
重载:同名不同参
重写:同名同参不同类(子类与基类有同名同参的函数)
隐藏:同名不同域(子类隐藏基类方法,类方法隐藏外部函数)
面向对象三特性
- 封装:
– 目的:隐藏细节、实现模块化
– 语言特性:访问特性、属性、方法
- 继承
–目的:基于父类,实现扩展
– 语言特性:权限继承(基类在子类内的最高权限)。friend和using(破坏访问权限,可见其他类私有成员)。多继承。接口继承。
- 多态
–目的:一个接口,多种形态,利用接口重用,提高扩展性
– 语言特性:静态多态(函数重载)。动态多态(虚函数)
类型转换
- static_cast:没有运行时类型检查,用于上行转换、基础类型间转换和转成void类型。
- dynamic_cast:用于父子类的指针转换,存在动态类型检查,指针转换失败返回nullptr,引用转换失败抛异常。
- reinterpret_cast:用于整型和指针、指针和数组以及指针和引用间的转化
- const_cast:用于去除const和volatile属性
内存泄露
- 分类:堆内存泄露。资源泄露(Socket)
- 产生原因:
– 操作不匹配:new错配free、new[]错配delete
– 父类没有虚析构
– 循环引用
- 避免:
– 避免在堆上分配资源
– 及时手动释放
– RAII:使用智能指针、使用STL
- 发现
– 日志:分配资源时打印指针地址
– 统计:用自定义的函数申请和释放内存,并在这些函数中加入计数信息
– 工具:使用valgrind
增强局部性
- 使用缓存友好的数据结构(数组、vector)
- 合理使用数组的结构 (SoA)和结构的数组 (AoS)
- 分割大循环
- 使用向量化编程(SIMD)
- 避免伪共享(false sharing):伪共享是指不同线程同时修改一个缓存行的数据。为了避免伪共享,应该避免不同线程的数据处于一个缓存行内。
- 使用内存池
- 减少使用指针
- 多使用局部变量,少使用全局变量
调用约定Calling convention
- 含义:用于规定函数传递参数、管理堆栈、以及处理寄存器等细节。
- 例如:C Calling Convention (cdecl)::参数逆序入栈,caller清理堆栈
操作系统
中断
- 软中断:实现进程切换(并发)、虚拟内存、系统调用的底层原理
– 分类:CPU异常(运行过程中发生的异常情况,例如除0、缺页异常)、指令中断(INT指令产生的中断,例如系统调用)
- 硬件中断:由各种外设引起,再由特殊芯片(如8259)传递给CPU的中断。例如:IO设备的中断、时钟中断
函数调用过程
- 将当前程序计数器和栈指针入栈
- 函数参数从右到左入栈
- 局部变量入栈
- 执行函数,返回值保存到返回值寄存器中
- 弹出栈帧,将程序计数器等寄存器复原
进程管理
- PCB:进程控制模块,用来描述进程的一个数据结构,
包括:进程标识符、用户标识符、进程当前状态、进程优先级、虚拟地址空间、文件句柄、IO设备、 各个寄存器的值。
不同PCB通过链表进行组织。
- 创建进程:新建PCB、分配资源、将PCB插入就绪队列
- 切换进程:对于当前进程,保存上下文,保存存储管理数据,修改PCB,对于新进程,修改PCB,切换存储管理数据,恢复上下文
- 进程内切换线程:不需处理存储管理数据(页表)
- 上下文信息:可执行fd, 栈, 数据段、堆, 进程状态, 优先级, 寄存器组、程序计数器等
- 进程vs线程:进程是资源分配的单位,线程是资源调度的单位。线程所属于进程。进程有独立的虚拟地址空间,线程共享所属进程的内存空间,线程有独立的栈和PC。线程容易切换,并发性高。线程会连锁崩溃,健壮性低。
- 写时复制(fork的执行):
–过程:复制父进程的页表(不复制物理内存),另父子进程的页表为只读。当某个进程进行写操作时,触发写保护中断,进而触发缺页中断,进而发生物理内存内的复制,最后修改页表的映射关系和读写权限(只读变为读写)。
进程调度
- 先来先服务:每次选取最先进入就绪队列的进程,直到该进程完成或阻塞。(利于长作业)
- 短作业优先:优先选择作业时间最短的进程。(利于短作业)
- 响应比优先:高响应比作业优先。响应比=(等待时间+要求服务时间)÷ 要求服务时间。(权衡长短作业)
- 高优先级优先:优先级在进程创建时或运行中确定。高优先级进程出现时,可以抢占低优先级进程,也可等待低优先级进程结束。
- 时间片轮转:每个进程公平分配运行时间。
- 多级反馈队列:队列内采用先到先服务,队列间采用时间片轮转,不同队列由不同优先级,先服务高级队列,再服务低级队列,高级队列时间片少。
- Linux内核的调度算法:CFS(完全公平调度)、RT(实时调度)、MLFQ(多级反馈队列)、RR(时间片轮转)
守护进程
- 含义:后台长期运行的进程
- 特点:后台运行,无交互,与具体用户无关,产期运行
- 例子:web server、MySQL、SSH、Init进程
进程vs线程vs协程
- 进程:资源分配的最小单元
- 线程:任务调度的最小单元。
- 协程:用户态的线程,由应用程序显示调度。
- 特点:
– 资源分配:进程有独立的内存空间,但有独立的栈、寄存器值和PC。
– 创建成本不同
– 切换成本不同
– 容错性:进程容错性强,线程会导致连锁崩溃
– 可扩展性:多进程更容易管理
- 多进程应用场景:
– CPU密集:仿真、并行计算
– 低切换:分布式
– 高容错:chrome浏览器
- 多线程应用:
– 高IO:GUI程序
X86 CPU寄存器
- 数量:45
- 上下文寄存器
– 通用寄存器:例如存放函数返回值、栈指针等
– 标志寄存器:例如进位等
– 指令寄存器:下一条指令的地址
– 段寄存器:和内存分段有关
- 其他寄存器、控制寄存器、调试寄存器、描述符寄存器、任务寄存器、MSR寄存器
锁
-
分类:
– 互斥锁与自旋锁:是否休眠
– 读锁与写锁:读锁与读锁兼容,写锁与读写锁互斥
– 乐观锁与悲观锁:乐观锁不真正加锁,只在提交修改时检查数据一致性。悲观锁等于互斥锁
– 可重入锁与不可重入锁:一个线程是否可以多次获取一个锁
-
互斥锁实现:
– 通过CAS(compare and swap)指令检验flag变量,得知是否已经加锁。
– 自旋一段时间,若锁没释放,则将线程休眠(使用Futex系统调用)
– 内核唤醒休眠的线程
原子操作
- CPU提供的原子操作
– cmpxchg:CAS (check and swap)
– xaddl:原子加法
– tas:test and set 修改某内存的值,返回修改之前的值
- 原理
– 缓存一致:某个cpu核的缓存数据会被同步到其他cpu核心
– 锁总线
进程通信
- 匿名管道 anonymous pipe:
管道是内核内存的一篇区域,形式上是一对fd,但没有文件实体。其用于父子进程通信。使用过程为
– 使用系统调用pipe创建2个fd
– 创建子进程,子进程会继承这两个fd
– 利用这两个fd进行读写
– 关闭fd
- 命名管道 nomed pipe:
命名官道有实际的文件实体,可以用于任意进程间通信,命名管道只能单向通信,使用过程为:
– 使用mkfifo创建命名管道
–打开命名管道
– 用read和write进行读写
– 关闭命名管道
例:在3个terminal中输入如下命令
mkfifo my_pipe
echo "Hello from the writer script" > my_pipe
read line < my_pipe
echo "Received: $line"
- 消息队列:
消息队列是内核空间的一个链表,其记录的消息存在类型,也可以随机访问,还可以双向通信。消息队列的使用方式与文件读写不同
– 利用ftok生成一个key
– 用msgget得到qid(作用类似于fd)
–用msgsnd或msgrcv进行读写
- 共享内存
多个进程同时映射同一块物理内存。
– 使用ftok生成一个key
– 用shmget和这个key申请共享内存
– 用shmat关联共享内存,获取指向共享内存的指针
– 操作共享内存
– shmdt取消关联
- 信号:软件中断
- 套接字:对网络的两端进行抽象
子进程继承资源
- 内存空间:创建之初时父子进程内存空间一致。
- 文件描述符:包括文件、Socket和其他IO资源
- 环境变量
- 信号处理程序
- 进程通信(IPC)资源
死锁
- 必要条件:资源互斥、不可剥夺、占有且请求、循环等待
- 检测:绘制资源分配图,检测环路
- 避免:
– 破坏互斥:允许资源共享(例:只读文件)
– 破坏不可剥夺:被迫或主动释放资源,(例:操作系统剥夺进程的CPU资源)
– 破坏占有且请求:一次请求所有资源
– 破坏循环等待:按顺序申请
- 银行家算法(避免循环等待,避免占有且请求)
– 进程请求资源
– 预判是否会发生死锁,会则拒绝请求,不会则同意请求,另进程进入等待状态。
– 当某资源被释放,系统把该资源分配给等待中的进程
- 恢复:回滚、杀死进程
内存
- 内存分布(低到高):保护区、代码区、已初始化静态变量、未初始化静态变量、堆区、文件映射区、栈区、内核区
- 虚拟内存:具体资源(例如string s)和虚拟地址的映射,
- 页表:虚拟地址和物理地址的映射。
- 虚拟内存作用:
– 使用内存大于物理内存
– 解决不同进程内存冲突
– 更可靠的内存访问:TLB加快内存映射,页表内记录读写权限和是否缺页
- 访问内存过程(写保护中断也可触发缺页异常)
– CPU请求一块内存,页表对请求的内存进行映射
– 若缺页,触发缺页异常
– 执行缺页异常处理函数:查找所缺页对应的磁盘位置
– 将磁盘页面与内存中的空闲页进行置换
– 置对应页表项为有效
– CPU访问物理内存
内存池
- 含义:向操作系统申请过量的内存,这块内存由应用程序自行管理
- 优点:减小内存碎片、分配时间可预估、可自定义分配策略
- 实现策略:定长分配器、哈希映射Freelist等
- 定长分配器:1、向操作系统申请一大块内存。2、把这块内存分为多个大小相同的块。3、当需要从内存池分配内存时,选取一个空闲的块,返回这个块的首地址。4、当内存池无可用内存时,再次向操作系统申请内存。
写入磁盘
- 缓存使用:数据量少用page cache进行缓存,数据量大则直接落盘。
- 读写过程:从用户缓冲区,到内核缓冲区,再到磁盘
- 系统调用:先write / fflush进内核,再sync / fsync刷磁盘
读取磁盘
- 系统调用:open、read、close
- 流程:先检查page cache,若未找到响应文件,触发缺页中断,DMA引擎进行内存置换,置换完毕再次触发中断,CPU把数据返回用户空间。
内核态与用户态
- 区别:
– 内核态特权为0级,可以使用特权指令,用户态特权为3级,只能使用非特权指令
– 内核态的处理器资源不可抢占,用户态处理器资源可以被抢占
- 切换方式:系统调用、异常(缺页中断)、外设中断(磁盘读写完毕)
信号
调试
网络
OSI模型
- 物理层:Ethernet、USB、RS232
- 数据链路层:MAC、Ethernet、Wi-Fi、ARP
- 网络层:IP、ICMP、OSPF
- 传输层:TCP、UDP
- 会话层:RPC
- 表示层:SSL/TLS、JPEG
- 应用层:HTTP、SMTP、FTP、DNS
IP地址
- 表示方法
– CIDR表示法:包含IP地址和子网掩码。
– IP地址分类法:把IP地址分成A、B、C、D四类(已弃用)
- 私有地址:
– Class A: 10.0.0.0 to 10.255.255.255
– Class B: 172.16.0.0 to 172.31.255.255
– Class C: 192.168.0.0 to 192.168.255.255
路由
- 过程:
– 用户向最近的路由器发送数据包
– 路由器根据路由表和路由算法选择下一个路由器
– 路由器发送数据包
- 路由表内容:
– 目标IP地址
– 子网掩码、
– 网关
– 接口:对应电脑上的一个网络适配器(网卡),(例如:以太网1、以太网2、无线网等)
– metric:路径优先级,metric越低,优先级越高
- 路由协议:用来构架路由表,并根据路由表确定网络路径
– OSPF(Open Shortest Path First):根据网络拓补数据(link-state),利用Dijkstra算法计算路径
– BGP (Border Gateway Protocol)::基于path vector的算法
– EIGRP (Enhanced Interior Gateway Routing Protocol):结合了link-state和path vector的算法
UDP vs TCP
- 相同点:传输层、全双工
- 面向连接:TCP需要先建立链接,UDP不用
- 服务对象:TCP点对点、UDP可一对多
- 传输方式:TCP字节流传输,包括TCP分段,IP分片,需要处理站包,UDP面向报文。
- 可靠性
– TCP可靠:序列号、校验和、滑动窗口、确认应答、超时重传
– UDP不可靠:只有读buf,没有写buf
- 首部开销:TCP首部大于20字节,UDP首部8字节
- 分片:TCP包大于MSS则会被分片,UDP包大于MTU则会再IP层分片。
- UDP应用:DNS、DHCP、NTP(Network Time Protocol)、音视频、游戏、IoT
- TCP应用:HTTPS、FTPS、SMTP、SSH、MySQL、VPN
TCP三次握手与四次挥手
- 握手
– 客户端:CLOSE --> SYN_SENT --> ESTABLISHED
– 服务端:CLOSE–> LISTEN --> SYN_RCVD --> ESTABLISHED
- 挥手
– 客户端:ESTABLISHED --> FIN_WAIT_1 --> FIN_WAIT_2 --> TIME_WAIT–>CLOSE
– 服务端:ESTABLISHED --> CLOSED_WAIT --> LAST_ACK --> CLOSE
TIME_WAIT
- 目的:防止历史连接、确保对端正确关闭
- 2MSL:可以允许ack2丢失一次,若ack2丢失,关闭端会再次收到fin2,从而再次发送ack2
- TIME_WAIT过多:大量的连接正在关闭,
– 引起原因包括:未开启HTTP长连接、大量长连接同时超时、长连接数目超上限。其危害是站内用系统和端口资源
TCP粘包
- 含义:发送方的多个数据包连续存储再在接收方的缓存中
- 原因:1、发送方的多分数据被打包发送。2、接收方未及时处理收到的数据。
- 解决:1、固定长度消息。2、添加特殊字符。3、制作数据包头
拥塞控制 Congestion control
- 含义:拥塞控制指的是发送方根据丢包情况对发送强度进行调整的机制。具体包含以下措施。
- 慢启动 slow start:连接建立时,每收到1个ack,cwnd大小加一
- 拥塞避免 congestion avoidance:当cwnd大小达到启动门限ssthresh后,其增长速率减为线性
- 超时重传:当发生超时重传后,ssthresh减为cwnd的一半,cwnd减为1。
- 快速重传:收到3个相同的ack时,cwnd = cwnd/2,ssthresh = cwnd。接着cwnd = ssthresh + 3,受到新的ack后,cwnd=ssthresh
滑动窗口
- 含义:发送方的缓冲区关联着两个指针,这两个指针的运动称作滑动窗口
- SND.UNA:指向发送方缓存中第一个已发送但未确认的数据
- SND.NXT:指向发送方缓存中第一个未发送但在接受范围内的数据
- SND.WND:发送方窗口大小
- RCV.WND:接收方窗口大小
- RCV.NXT:指向接收方期待的下一条数据在缓存中的位置
HTTP响应过程(输入URL后发生什么)
- 输入URL:判断输入是否合法,不合法则使用搜索引擎搜索
- 域名解析:先检查本地缓存和hosts文件,检查本地DNS服务器缓存,再查跟服务器找到顶级域服务器地址,再找到权威DNS服务器,得到IP
- 建立TCP连接:建立临时端口,开始三次握手
- 向服务器发送HTTP或HTTPS请求
- 接收服务器的应答,包括协议版本和状态码
- 接收实际数据
- 浏览器渲染页面
- 关闭TCP连接或继续保持连接
HTTP 状态码
Status Code |
Status Name |
1xx |
Informational |
100 |
Continue |
101 |
Switching Protocols |
2xx |
Successful Responses |
200 |
OK |
201 |
Created |
202 |
Accepted |
204 |
No Content |
3xx |
Redirection |
300 |
Multiple Choices |
301 |
Moved Permanently |
302 |
Found (Moved Temporarily) |
304 |
Not Modified |
307 |
Temporary Redirect |
4xx |
Client Errors |
400 |
Bad Request |
401 |
Unauthorized |
403 |
Forbidden |
404 |
Not Found |
405 |
Method Not Allowed |
409 |
Conflict |
5xx |
Server Errors |
500 |
Internal Server Error |
501 |
Not Implemented |
503 |
Service Unavailable |
504 |
Gateway Timeout |
HTTP 请求
- GET: 获取数据
- POST: 上传数据
- PUT: 替换或更新指定URI的内容(强调URI)
- DELETE: 删除数据
- HEAD: 只发送数据头
- OPTIONS: 获取可用的HTTP请求
- PATCH: 在指定资源上添加信息
GET vs POST
- 用途:GET从服务器收数据,POST发数据到服务端
- GET:对刷新免疫,幂等,请求可以被当作书签收藏,可以被缓存,还会被保存到浏览历史中。受URL长度限制。安全性低。只能用ASCII字符。
- POST:刷新后重新提交,不幂等,不可作为书签,不可缓存,不可作为历史,数据长度无限,字符类型不限,安全性高(用body传数据)。
Linux如何收发数据包
接收:
- 网卡:用DMA把数据写入内存的ringbuffer,发硬件中断
- CPU:找中断处理函数、屏蔽硬件中断,产生软件中断
- Linux内核:用ksoftirpd进程处理软件中断,把数据从ringbuffer转入sk_buff
- Linux内核:层层向上以此解析,把数据放入socket的读buff
- 应用程序:用系统调用(read,recvfrom)读socket的buff
- CPU:开启硬件中断
发送:
- 应用程序:把数据放入发送缓冲区,产生sk_buff
- (仅TCP)Linux内核:copy一个新的sk_buff
- Linux内核:层层向下加头信息,TCP分段,IP分片
- 网卡:把sk_buff的数据写到ringbuffer
- 网卡:发送成功,触发硬件中断
- CPU:清理数据。(TCP:ringbuffer,sk_buff拷贝,UDP:ringbuffer,sk_buff原件)
- (仅TCP)收到ack,清理sk_buff原件
网络IO模型
- 一般网络编程:a 绑定端口(bind,listen,)。b 连接客户端(accept)。c 等待数据到达(read)。d 发送数据(write)。e 关闭连接(close)
- 阻塞IO:一个连接对应一个线程。对于accept、read和write,数据未就绪则函数不返回
- 非阻塞IO:一个连接对应一个线程。对于accept、read和write,数据未就绪,函数返回-1
- IO多路复用:把多个真实的连接(cliendfd)抽象成一个连接集合(epollfd),对此集合进行非阻塞IO(reactor)或阻塞
- 异步
- 信号
丢包常见原因
- 问题来源:硬件、协议栈、应用
- 单核负载过高
- Ring Buffer溢出
- arp表满
- 防火墙
- TIME_WAIT过多:加速回收
- 全连接队列满:扩充队列
- 被syn flood攻击
- TCP超时:关闭Nagle
- TCP乱序丢包
- 拥塞控制丢包
HTTPS
- 特点:HTTPS进行加密和身份验证
- 与HTTP的区别:HTTPS会进行TLS握手、端口443、需向CA申请数字证书。
- 连接建立:
– ClientHello:发送随机数Client Random
– ServerHello:返回Server Random和数字证书(证书中包含公钥)
– 客户端回应:验证数字证书、提取公钥、发送加密的随机数(pre-master key)
– 服务端回应:用随机数生成会话密钥,通知握手结束。随后客户端也使用随机数生成会话密钥。
- 公钥与私钥
– 公钥加密私钥解密:保证传输安全。
– 私钥加密公钥解密:验证服务器身份。
HTTP2
- 头部压缩:HPACK算法
– 静态字典:对于常用字符,采用一个Indx替代,例如用54代替“server”
– Huffman编码:对于一般字符,采用Huffman算法进行压缩
– 动态字典:包含不在静态字典里的常用字符,其内容会动态改变。
- 二进传输
– 传输内容:每一条数据分为2帧,分别是头部帧和数据帧。
– 帧格式:每一帧包含帧头信息和具体数据内容。
- 并发传输:把一份完整的数据分成多个stream,不同stream可以乱序发送。
- 主动推送:请求一个资源,除了获得这份资源,还会获得其他资源
定时器
- 目的:依据事件,高效地检测多个事件是否应该出发。
- 实现要素:
– 容器:保存多个事件的触发时间(红黑树、最小堆)
–检测触发机制:带有阻塞功能的系统调用(select、poll、epoll)、sleep、Timerfd
RPC简介
- 定义:RPC是一类通讯协议,位于应用层和传输层,它通过对网络细节的封装,从而达到以调用本地服务的形式,来调用远程服务。
- 接口定义语言(IDL:interface definition language):IDL是一种特殊的语言,可以使不同操作系统的电脑,和不同编程语言的程序进行沟通。例如protobuf语言。
- RPC客户端(stub):stub是对网络细节和服务对端的封装,应用程序只与stub交互,而stub负责和网络另一端的电脑进行交互。
本地客户端——客户端stub——操作系统——|——远端操作系统——服务器stub——服务端应用程序
- 常用调用场景:客户端阻塞调用、客户端非阻塞调用,返回时服务器发送额外信号、客户端批量调用、客户端同时向多个客户端调用
MySQL
数据库范式
参考:https://zhuanlan.zhihu.com/p/275763405
- 1NF:每一字段不可分
- 2NF:在1NF基础上,每一列都和主键相关
- 3NF:在2NF基础上,非主键列之间不相关
SQL语句执行
1、服务器和MySQL建立连接,MySQL分配连接线程
2、解析SQL语句
3、预处理
4、优化阶段:产生执行计划
5、执行阶段:调用存储引擎
优化
- 查询语句优化
– 避免全表扫描和索引失效(SELECT *、%pattern、WHERE key IS NULL)
– 减少使用子查询(sub queries)
– 减少使用函数
- 索引优化:索引要适应使用场景
– 使用短索引
– 对于经常一起查询的字段,建立联合索引
- 表设计优化
– 合理选择主键:unique、narrow and stable
– 减少大字段
– 减少NULL
– 分表
- 参数配置优化
– MySQL参数:同时最大连接数、Buffer Pool大小、落盘频率
– Linux参数:TIME_WAIT时间、半连接队列长度、进程fd数量限制(默认1024)
- 使用缓存:例如redis、memcached
- 硬件优化:使用SSD、采用多机集群
InnoDB线程
- Master Thread
- Page Cleaner Thread:脏页刷盘
- Purge Thread:删除undo日志
- IO Thread:
为什么用B+树
1、目的:减少磁盘IO、实现范围查询和单点查询
2、B+树的优点:矮胖结构(多路,完全平衡)。减少磁盘IO。双向链表提速范围查询。叶子存数据,删除便捷。
3、B+树退化:退化成链表。
Buffer Pool
1、目的:异步刷盘更高效,所以用户程序写入磁盘时,需先用write向一个内核缓冲区page cache写数据,再使用fsync把page cache的数据转移到磁盘上。为了提高和磁盘交互的灵活性,MySQL创建了一个用户态缓冲区,即Buffer Pool(大小256MB)。
2、页面大小:16KB
3、存储内容:数据页、索引页、undolog页、自适应Hash、锁信息、插入缓存
事务隔离级别
- 连接建立:主线程使用IO多路复用(select、accept)建立连接,再把这个连接(clientfd)交给单独的线程进行处理。
- 目的:用于并发连接处理关联数据时
- 使用:语法begin、rollback、commit。单个语句默认是事务。
- 事务特性:ACID
–.原子性:事务的指令或全部成功或全部失败
–.一致性:保持数据库的约束(例:非空约束),
–.隔离性:不同事务不互相干扰
–.持久性:把指令持久化到磁盘
- 隔离级别
– 设置方法: SET TRANSACTION ISOLATION LEVEL READ COMMITED
– serializable:读写都加锁。
– repeatable read:
SELECT采用MVCC(可额外加X或S锁),insert加X锁,并采用auto increment lock(表级自增锁)和insert intention(表级插入意向锁)。update加X锁,未命中还会加gap锁。delete加X锁,删除不存在行还会加gap锁。
– read commited:读用MVCC,写加锁
– read uncommitted:读不加锁、写加锁
- 异常:
– 脏读:读到进行中事务对数据的修改
– 不可重复读:一次事务中,多次读某一行,结果不一致(读提交中,两个SELECT中间执另一个事务,这另一个事务执行力update,会造成不可重复读)
– 幻读:一次事务中,两次读多行,前后结果集不一致
原子性(Atomicity)
- uodo log:首先用undo log记录事务开始前的状态。
- redo log:在落盘之前,在redo log中记录事务对磁盘的更改
- commit:若事务成功,则根据redo log更改磁盘,否则根undo log回滚
隔离性(Isolation)
read commited
- 写:对受影响的行加exclusive record lock
- 读:SELECT开始时建立快照,SELECT执行过程中,其他事务对数据的修改被忽略。
- 异常:不可重复读、幻读
repeatable read
- 快照读(普通SELECT):事务开始时建立快照,依据快照和undo log,只读取事务开始前的数据。
- 当前读(SELECT…FOR UPDATE):对修改的数据加next-key lock。幻读可能发生
- repeatable read发生幻读:
– (快照读)例1:事务A开始后,其他事务插入id=5的记录,这条记录对A不可见。然后A修改这条记录,使id=5的trx_id下降,从而对A可见。导致A可查询到id=5的记录。
– (当前读)例2:事务A开始后,其他事务插入了一条数据,事务A执行SELECT…FOR UPDATE。由于这条命令执行过晚,next-key lock产生过晚,无法影响其他事务在SELECT…FOR UPDATE之前插入数据。
锁
-
全局锁:
– 分类:分为读锁(防止写)和写锁(防止读写)
– 用法:加锁:flust tables with read lock; 解锁:unlock tables;
-
表锁:
– 分类:分为读锁和写锁。MyISAM自动加表锁
– 场景:读密集写不密集、数据小、全表操作
– 触发操作:ALTER TABLE、DROP TABLE、TRUNCATE TABLE、LOCK TABLES
– 缺点:并发性能低、死锁(多表加锁顺序错误)
-
record lock(记录锁):
– 隔离级别:RC、RR
– 含义: 对某一行加读或写锁,依据读锁和写锁的规则,record lock作用域聚簇索引中的记录。
– 触发:“当前读”加X锁(SELECT FOR UPDATE、UPDATE、DELETE),SELECT in SHARE MODE加S锁(唯一索引等值查询)
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
update table .... where id = 1;
delete from table where id = 1;
SELECT c1 FROM t WHERE c1 = 10 in SHARE MODE;
-
gap lock(间隙锁):
隔离级别:RR
触发:范围查询,向两个值中间插入,删除 (由next-key lock退化产生)
不与gap lock冲突,与insert intention lock和record lock冲突。
select * from T where col between ‘1’ and ‘3’ lock in share mode;
select * from T where col_non_unique = ‘5’ for update;
-
next-key lock(临键锁):
– 隔离级别:RR
– 原理:对这条记录加读锁或写锁,同时依据索引,对这条记录前一个gap加读锁或写锁。next-key lock可以部分解决幻读。
– 触发:当前读(select for update)
SELECT * FROM T WHERE id > 100 FOR UPDATE;
-
insert intention lock(插入意向锁):
对插入的间隙加锁,不会和其他insert intention lock冲突,会与gap laock和record lock冲突。
INSERT INTO child (id) values (90),(102);
-
auto-inc lock(自增锁):
当一个事务向带有自增列的表插入时,会对被插入的表加锁,其目的是保证自增列序号正确分配。
-
死锁:
– 原因:一个事务对多行或多个表加锁,但加锁顺序不当。
– 自动处理:MySQL可以直接检测死锁并回滚事务,也可以利用timeout检测死锁。
– 解决:打印死锁信息。重做事务。使用短事务。降低隔离等级。注意加锁顺序。正确添加索引以降低扫描量。少加锁。用表锁实现序列化操作。建立一个充当信号量的表,该表只包含一行数据,每个事务处理前先更新这个信号量表
索引
- 含义:指向表行的指针,用来表示表的结构,从而提高查询速度
- 分类:
– 列数:单列索引(前缀索引Index Prefixes、全文索引FULLTEX indexes、空间索引Spatial Indexes)、联合索引
– 被索引对象:主键索引、唯一索引、普通索引、前缀索引
– 顺序:升序索引、降序索引
– 数据结构:B+树、HASH、倒排索引结构(用于full-text索引,是一种key-value数据结构,key是单词,value是这个单词出现的文档)
- 索引失效:or 语句、索引代数运算、%开头的查询、违反最左匹配、数据过少
- 添加方法:
– 定义主键自动添加
CREATE TABLE my_table (
id INT PRIMARY KEY,
name VARCHAR(50)
);
– 添加UNIQUE约束自动添加
CREATE TABLE unique_table (
email VARCHAR(100) UNIQUE,
username VARCHAR(50) UNIQUE
);
– 添加普通索引
CREATE INDEX idx_salary ON employees (salary);
– 添加前缀索引
CREATE INDEX idx_product_name_prefix ON products (product_name(4));
– 添加哈希索引
CREATE TABLE partitioned_table (
id INT,
name VARCHAR(50),
partition_key INT
)
PARTITION BY HASH(partition_key)
PARTITIONS 4;
– 添加联合索引
CREATE INDEX idx_name_age ON employees (first_name, last_name, age);
– 添加全文索引
CREATE FULLTEXT INDEX idx_fulltext_search ON employees (first_name, last_name);
– 添加Spatial索引
CREATE SPATIAL INDEX idx_spatial_location ON employees (location);
– 添加Unique Spatial索引
CREATE UNIQUE SPATIAL INDEX idx_unique_spatial_location ON employees (location);
- 触发条件(失效条件):
– 主要由WHERE触发
– 当使用同类型、同大小的列来JOIN两个表时,会触发索引
– MIN()和MAX()
– 使用索引字段的左前缀进行ORDER BY或GROUP BY
– 当查询多列数据时,这些列匹配了联合索引中左侧若干列,则触发索引。(最左匹配原则)
– 对索引本身搜索,必触发索引(覆盖索引原则)
优化
- 建立主键
- 用外键拆分大表
- 开启索引下推功能:先判断有索引的列是否满足SELECT的条件,再回表读取整行数据。
- 利用覆盖索引,对经常查找的列建立索引
- 利用前缀索引,对于长字符串,只对其前缀建立索引
- 适合建索引的字段:
– 经常where的字段
– 小字段
– 高选择性字段
- 不适合建索引的字段:
– 经常删除的字段
– 用单字段索引替代复合索引
日志
Undo Log
- 含义:用来撤销未提交的事务,实现了事务的原子性。
- 记录内容:一个undo log record记录了一个简单事务(只有一条读或写操作)的逆向操作,可以撤销一个事务。对于复杂事务,最多需要4个undo log才能记录撤销这个事务的操作。
- 生命周期:undo log服务未提交的事务,其在事务开始之前产生,于事务提交后准备销毁。undo log可能被持久化到磁盘,及时如此,其也最终被销毁。
- 存储:undo log存储于undo log page(InnoDB页的一种)中,undo log page存在于undo log segment中。
Redo Log
- 含义:redo log是一用于宕机后恢复的数据,redo log存储在磁盘里,redo log既不记录sql语句而是记录磁盘的物理变化(physical change)
- 宕机恢复:redo log 用于MySQL服务器宕机后的数据恢复,不适用于磁盘损坏的数据恢复,步骤为
– 搜索表空间 tablespace discovery
– 使用redo log恢复不在.ibd中的数据
– 恢复自增键的值
– 回滚未提交的事务
– 清除purge:删除redo log中的部分记录,例如已提交事务、以回滚事务的undo log等
Binary Log
- 含义:记录恢复数据库的SQL语句(不记录SELECT),用于数据恢复和主从复制。
- 主从复制:
– 主库写bin log,提交事务。
– 从库接收主库的bin log
– 从库回访bin log
主从复制
- 含义:把主库的数据复制到从库上,从而实现高可用、可扩展以及灾难恢复
- 复制模式:基于SQL语句、基于行、二者混合
- 主从复制模式
– 异步模式:主库更改数据,写binlog。从库根据binlog写relaylog。从库执行relaylog
– 同步模式:当所有从库均执行完一事务后,主库向用户返回成功。
– 半同步模式:至少一个从库执行完事务后,主库向用户返回成功。
– GTID模式:保证一个事务只在一个库执行一次。
缓存穿透、击穿、雪崩
- 穿透:查询不存在的数据,直达存储层。(缓存空对象、布隆过滤器)
- 击穿:热点数据失效(设置不失效)
- 雪崩:Redis崩溃(高可用)
数据结构
数组
- 适用于:下标访问频繁
- 时间复杂度:下标访问O(1)、查找O(n)、增删O(n)
链表
- 普通链表:
– 适用于:只进行顺序遍历
– 时间复杂度:头部插入删除O(1)。其他部位插入删除O(n)。查找O(n)
- 循环链表:适用于循环遍历,增删改查快于普通链表
- 双向链表:适用于双向遍历,增删改查快于循环链表
层式结构
- 常见层式结构:时间轮、跳表、LSM-Tree
- 时间轮:
–组成:包括一个环形数组,代表时刻。每一个时刻对应一个链表,对应多个任务
–设计:确定最小精度、确定最大时间范围、确定层级
–作用:实现海量定时任务
- 跳表
–组成:多层级有序链表
–优点:可以进行二分查找
–插入:先找到位置,再随机生成节点层数
红黑树
-
2-3-4树(4阶B树):
– 特点: 有3类节点,每类节点包含的元素个数为1、2或3,包含的子节点个数为2、3或4
– 操作:融合、裂变
– 与红黑树的转换:2节点对应一个黑节点。3节点对应两个节点,上黑下红。4节点转化为红黑树时,中间节点上移,作为黑节点,其他节点作为红节点。插入和裂变,对应的操作为,在红黑树中加入一个红节点,在进行整体的变色
-
基本性质:
– 根节点。叶节点以及红节点的子节点为黑
– 每条路径黑节点数目一致
– 红节点不连续
– 左右高度差小于2倍
-
变换操作:
– 左旋:右子树上升,右节点的左子树变为自身的右子树
– 右选:左子树上升,左节点的右子树变为自身的左子树
– 变色
-
插入
– 根据二叉搜索树的规则进行插入
– 调整:把红黑树的节点对应到2-3-4树的节点,再进行左右旋以及变色。2节点不用调整。3节点有6种情况,其中2种情况不用调整,2种情况需要一次旋转以及多次变色,2种情况需要两次旋转和多次变色。4节点对应4种情况,均需要调整。
-
搜索:中序遍历
-
适用于:动态增删,大量查找,元素有序
-
具体应用:索引、std::map、std::set、文件相关(epoll)
Redis
线程模型
- 主线程:执行命令
- 其他线程:关闭文件、AOF刷盘、释放内存、网络IO线程
robj(redis object)
- 含义:所有数据类型的原型
- 包含:type (eg. string)、encoding(eg. embstr)、ptr
Strings
- 应用:缓存、计数器、分布式锁、会话管理(session management)
- 使用:set、get、mset、mget、incrby
- 底层:SDS (Simple Dynamic Strings)
- SDS特点:O(1)获取长度、二进制存储、不会溢出(自动扩容)
- 编码方式:raw、int、embstr
Lists
- j简介:相当于list
- 使用:LPUSH、LPOP、RPUSH、RPOP、LLEN、LMOVE、LTRIM
- 应用:实现队列和栈
- 底层:quicklist
Sets
- 简介:相当于unordered_set
- 应用:跟踪独特信息(独特IP)、进行集合运算(交集、补集)
- 使用:sadd、srem、sismember、sinter、scard
- 底层:整数集合(数据少)或哈希表(数据多)
Hashes
- 简介:等于unordered_map
- 应用:用户信息、多个计时器
- 使用:hget、hset、hgetall、hmget、hmset、hincrby
- 底层:listpack(数据少)或哈希表(数据多)
Sorted sets
- 简介:有序集合,每一个元素对应一个分数
- 使用:zadd、zrange 、zrevrange 、zrangebyscore、zrank、zrevrank
- 应用:排行榜
- 底层:先使用跳表(数据量大)或listpack(数据量小),再使用哈希表
分布式理论
一致性
- 强一致性:所有进程都可以读取最新的数据
- 顺序一致性:所有进程可读取旧数据,但进程之间不矛盾,即各个进程的读写可序列化
- 弱一致性:不保证进程可读取最新数据
- 最终一致性:进程的读取结果缓慢趋近于最新数据。
– 因果一致性:一个进程可被其他进程通知,从而读取较新的数据
– 单调一致性:读取的数据只会越来越新
– 自写一致性(read-your-write):对于自己写入的值,只会读取最新数据
两阶段提交(Two-phase commit)
- 准备阶段:
– 协调者向参与者发送提交事务请求
– 参与者执行事务,记录redo log和undo log
– 参与者返回事务执行成功情况
- 提交阶段:
– 若所有参与者均成功执行任务,则执行者发送提交请求,否则发送回滚请求
– 参与者根据请求进行事务的提交或回滚
- 特点:中心化、强一致
- 缺点:
– 同步阻塞:一个节点阻塞会引起所有节点阻塞
– 单点故障:协调者在发送准备命令时宕机,会阻塞所有节点
– 数据不一致:协调者在发送提交命令后宕机,导致数据不一致
三阶段提交(Three-phase commit)
- 询问阶段(CanCommit):协调者询问参与者是否具备提交条件
- 预提交(PreCommit):若参与者均对CanCommit回复yes,则向所用参与者发送PreCommit信号。否则取消提交
- 提交阶段(DoCommit):若参与者均成功执行事务,则提交事务,并发送DoCommit信号,通知参与者正式提交事务。否则回滚事务
- 超时机制:
– 参与者PreCommit超时:若PreCommit超时,则放弃执行事务
– 参与者DoCommit超时:若DoCommit超时,则直接提交事务
– 协调者等待超时:若未收到参与者反馈,则回滚事务
二阶段锁(Two phase locking)
- 含义:2PL是一种并发控制协议,它使一个事务先逐渐获得所有锁,再执行任务,最后在逐渐释放所有锁。
- Basic 2PL:
– Growing phase:事务开始逐渐获取或升级所需的锁。
– Shrinking phase:事务逐渐释放获取的锁,此阶段不能获取新的锁。
– 缺点:连续回滚(cascading rollback)、死锁
- Rigorous 2PL:所有资源需要在事务commit或abort后释放,禁止中途释放
- Conservative 2PL:所有资源在事务开始前获取,可避免死锁
一致性哈希
- 含义:用于服务器分配。首先对服务器的IP取模(mod 2^32),得到服务器在HASH环上的位置。客户端进行请求时,某数进行取模,再沿HASH环找到距离最近的主机。
- 单调性:当插入新节点时,只有一个客户被重定位
- 高平衡性:通过虚拟节点,每台主机分配的客户数目相当
Raft
- Leader election
– 限制:只有Candidate包含所有已提交的记录才能胜选。投票时,若候选人最后一条log的term更大,或最后log的term相同但log个数更多,则给此候选人投票。
- Log replication
– Log内容:待执行命令、添加时Leader的term、在Log集合中的index
– 提交Log(commit):提交即执行Log包含的命令,当Leader把一条Log复制到多数Follower后,就会提交这条Log。Leader会保证这条Log之前的Log同样被提交。Follower根据Leader的commit index,提交本地的Log
– AppendEntries RPC:
发送内容:term、leaderId、prevLogIndex、prevLogTerm、entries[]
返回:term、success
– Log接收条件:Leader的term不小于自身的term。可以根据prevLogTerm和prevLogIndex匹配本地的一条历史Log
– 接收过程:根据这条匹配的历史Log,清理本地的错误Log。正式添加Log。根据LeaderCommit信息提交本地的Log
– Log提交:若一个log已经复制到半数以上的节点,则提交这个log,同时保证这个log之前的log也被提交。
– Log性质:在两个节点上,若两个Log具有同样的term和index,则这两条Log包含同样的指令,且两台机器上的这两条Log之前的Log也相同
百万并发
高可用
- 含义:降低系统停工时间的措施
- 特点:冗余、负载均衡
- 某架构分析
– 架构:client(客户端)----nginx(反向代理)----webserver(应用层)---- service(服务层)----cache, db(缓存、数据库)
– client–nginx:nginx冗余,试用keepalived+virtual IP保障高可用
– nginx–webserver:webserver冗余
– webserver–service:service冗余,简历service连接池
– service–cache:缓存数据冗余、主从复制、redis哨兵(检测存活)
– service–db:主从复制、读写分离
负载均衡
- 分类:
– HTTP重定向
– DNS负载均衡
– 反向代理(nginx、LVS)
- 算法:轮询、随机、一致性哈希
其他
Muduo
Muduo网络库工作流程
- 创建TcpServer对象:创建一个主EventLoop,一个InetAddress。用这二者创建TcpServer。TcpServer内聚合了主EventLoop对象,但主EventLoop和TcpServer可以独立存在。
- 设置TcpServer对象:绑定onConnection和onMessage回调,设置线程数
- 运行TcpServer:调用TcpServer的start()方法,和主EventLoop的loop()方法
- TcpServer的start()方法:这个方法完成两件事。首先,调用线程池EventLoopThreadPool的start(cb)方法,该线程池是TcpServer的成员变量。其次给调用主EventLoop的runInLoop(cb)方法,传入的cb是Acceptor的listen方法。Acceptor也是TcpServer的成员变量,目的是让EventLoop监听Acceptor的新用户连接事件。
Muduo关键组件
-
Thread:封装了一个函数对象。创建Thread对象时传入一个函数对象,调用start()函数开启线程。
-
EventLoopThread:
—主要成员:函数对象ThreadFunc,Thread对象,子EventLoop,回调cb(没什么用)。
–初始化时,用函数对象ThreadFunc构造Thread对象。
–调用startloop()函数时,Thread对象的start()方法被调用,Thread再调用ThreadFunc来构造EventLoop对象。
–ThreadFunc会调用EventLoop对象的loop()方法,开启one loop per thread
–与TcpServer不同,EventLoopThread不需要外部传入EventLoop,而是再ThreadFunc里创建。
-
EventLoopThreadPool:
–主要成员:主EventLoop,线程个数,线程集合,Eventloop集合。
–构造:从外部传入主EventLoop和线程个数。
–start():一句线程个数,创建多个EventLoopThread,把每个EventLoopThread加入线程集合,并把每个EventLoopThread对应的子EventLoop加入EventLoop集合。
-
Socket:用来封装socket对象
–成员:sockfd
–功能:对sockfd的bind、listen和accept。对于bind和listen,一旦发生错误,程序直接退出。accept会返回连接用户对应的fd。
-
Acceptor:包含一个Socket对象和一个Channel对象,并与主EventLoop关联。
–主要成员:Socket对象、主EventLoop、acceptChannel:sockfd对应的channel对象
–handleread():新用户连接时的回调方法,此方法注册为acceptChannel的read回调。此方法首先调用Socket对象的accept方法建立用户连接connfd,再调用处理新用户建立的回调方法。
-
Channel:封装了一个fd和这个fd上“读写错关”四类事件的回调。
–主要成员:某fd,子EventLoop,“读写错关”四类事件的回调函数
–handleEventWithGuard():根据到达事件revent_的值,选择对应的回调函数。
–update():Channel调用子EventLoop的updateChannel,子EventLoop调用EPollPoller的updateChannel,Poller调用自己的update,update里面调用epoll_ctl, epoll_ctl把Channel的某fd添加到Poller创建的epoll对象的感兴趣队列中
–remove():与update类似,经过一系列传到,最终调用epoll_ctl把某fd从epoll对象的感兴趣队列中删除。
-
EPollPoller
–构造:调用epoll_create获得epoll_fd。创建一个epoll_event数组(默认大小为16)。
–update():调用epoll_ctl,在内核的epoll对象里增加或删除fd
–poll():调用epoll_wait。此方法向epoll_wait传入epoll_fd,和event_event数组。epoll_wait返回后,epoll_event数组内记录了有事件发生的Channel,这些Channel被填入名为activeChannels的Channel数组,并最终被EventLoop获得。
Nuft
-
RaftNode类
– 关键成员:raft_message_client、RaftServer
– 构造:初始化一系列状态,开启on_timer线程,创建RaftServer
-
RaftNode::on_timer():
–leader:到某个时间,触发send_heartbeat(),(若用户注册了回调,则触发)
–非leader:到某个时间,触发do_election()。(若用户注册了回调,则触发)
–candidate:判断是否become_leader()。(若用户注册了回调,则触发)
-
RaftNode::send_heartbeat():触发do_append_entries
-
RaftNode::do_append_entries():
– 遍历每一个peer(其他RaftNode对象的指针)
– 若满足一定条件,触发do_send_install_snapshot(peer)
– 否则,准备一个AppendEntriesRequest
– 调用相应peer的raft_message_client的AsyncAppendEntries方法
-
raft_message_client类:
– 关键成员:RaftNode指针、stub指针(stub类由protobuf自动生成,作为grpc的客户端)
– 关键方法:AsyncRequestVote、AsyncAppendEntries、AsyncInstallSnapshot、AsyncCompleteRpc
– AsyncAppendEntries:调用stub的AsyncAppendEntries,该方法通过rpc连接RaftMessagesServiceImpl
-
RaftMessagesServiceImpl类:rpc的服务端,实现grpc的Service接口
– AppendEntries:调用RaftNode的on_append_entries_request
grpc
-
事件驱动模型:消息队列中包括request topic和response topic。调用方向request topic发送请求,并订阅response topic。被调用方订阅request topic,并向response topic发送请求。
-
使用方法:
https://grpc.io/docs/languages/cpp/basics/
– 在proto文件里定义message和service,编译proto文件
– server类:实现Service或AsyncService接口
– server类:运行server(创建Service,创建ServerBuilder ,创建并运行Server)
– client类:创建Channel。创建stub。进行RPC调用
感想
什么是锁:
锁的本质是状态机,可以理解为一个int,锁应该是各线程之间的公有变量。a.lock()和a.unlock()会改变锁a的状态。a.lock()俗称对a加锁,a.unlock()俗称对a解锁,和一般的int一样,锁可以除了可以改变状态,还可以被创建和销毁。a.lock()是否成功不仅取决于a的状态,还可能取决于其他锁的状态。
什么是回调
如果一个函数被作为参数传进另一个函数,那这个函数就是一个回调函数。这一过程设计三个函数,首先是回调函数。然后是调用函数,即接收回调以及使用回调的函数。其次是把回调函数传给调用函数的函数,这个函数起到了组合两个函数的作用。这三个函数功能各不相同,回调函数能完成一个功能,但它并不知道自己什么时候被使用。调用函数需要某一个功能,但它无法直接找到完成这个功能的函数。组合函数知道如何把二者组合起来。
什么是并发
并发就是无序,无序就是无法预先知道顺序,即随机顺序。