C++ 基础八股

C++

C++的特点

  • RAII
  • STL
  • 面对对象

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++)
{
	// construct
	allocator_traits<decltype(alloco)>::construct(allco,&dataarr[i]);
	// destruct
	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) {
        // Copy constructor implementation
    }
};
  • 移动构造函数:接收同类型的右值引用
class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        // Move constructor implementation
    }
};
  • 转换构造函数:只接受一个其他类型参数,实现隐式转换
  • 拷贝赋值运算符:
class MyClass {
public:
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            // Copy assignment implementation
        }
        return *this;
    }
};
  • 拷贝构造和拷贝赋值对比
int main() {
    String str1("Hello");
    String str2 = str1; // Copy Constructor
    String str3("World");

    str3 = str1; // Copy Assignment Operator

    return 0;
}
  • 移动赋值运算符:
class MyClass {
public:
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            // Move assignment implementation
        }
        return *this;
    }
};
  • 移动构造和移动赋值运算符对比
int main() {
    // Create a buffer using the constructor
    Buffer buffer1(10);
    // Use move constructor to create a new buffer
    Buffer buffer2 = std::move(buffer1);
    // Create another buffer using the constructor
    Buffer buffer3(5);
    // Use move assignment operator to transfer ownership
    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; 	// 取址运算符 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; }); // Wait until even is printed
        
        std::cout << "Odd: " << i << std::endl;
        isOddPrinted = true;
        
        cv.notify_one(); // Notify the other thread to proceed
    }
}

void printEven() {
    for (int i = 2; i <= 10; i += 2) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return isOddPrinted; }); // Wait until odd is printed
        
        std::cout << "Even: " << i << std::endl;
        isOddPrinted = false;
        
        cv.notify_one(); // Notify the other thread to proceed
    }
}

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
#!/bin/bash
echo "Hello from the writer script" > my_pipe
#!/bin/bash
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级,只能使用非特权指令
    – 内核态的处理器资源不可抢占,用户态处理器资源可以被抢占
  • 切换方式:系统调用、异常(缺页中断)、外设中断(磁盘读写完毕)

信号

  • 生命周期:产生、注册、注销、处理

  • 产生:
    – 硬件产生:键盘输入ctrl+c、ctrl+\、ctrl+z
    – 软件产生:kill命令、kill函数

  • 注册:加入PCB的未决信号集合

  • 注销:在PCB中删除该信号的节点

  • 处理:执行信号处理回调函数

调试

  • 内置宏
    __TIME__ //当前时间 类型%s
    __FILE__ //显示当前文件 类型%s
    __DATE__//显示当前日期  类型%s
    __LINE__ //显示当前行数 类型%d
    __FUNCTION__ //显示当前函数  类型%s
    
  • coredump
    – 产生原因:数组越界、非法指针、堆栈溢出、assert失败
    – 更改core文件位置
    – core文件还原:使用gdb
    gdb /path/to/your/program /path/to/core/dump
    

网络

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锁(唯一索引等值查询)

    -- X lock
    SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
    update table .... where id = 1;
    delete from table where id = 1;
    -- S lock
    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 between1and3lock in share mode;
    select * from T where col_non_unique =5for 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 BYGROUP 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的状态,还可能取决于其他锁的状态。

什么是回调

如果一个函数被作为参数传进另一个函数,那这个函数就是一个回调函数。这一过程设计三个函数,首先是回调函数。然后是调用函数,即接收回调以及使用回调的函数。其次是把回调函数传给调用函数的函数,这个函数起到了组合两个函数的作用。这三个函数功能各不相同,回调函数能完成一个功能,但它并不知道自己什么时候被使用。调用函数需要某一个功能,但它无法直接找到完成这个功能的函数。组合函数知道如何把二者组合起来。

什么是并发

并发就是无序,无序就是无法预先知道顺序,即随机顺序。

你可能感兴趣的:(c++,后端)