Skip to content

字节豆包C++后端一面,反转链表也太简单了吧。。。

来源:https://www.nowcoder.com/feed/main/detail/55321afde34d4659ba63b7a8e504db4b

C++

1、手撕:lc206链表反转

思路

这里给大家讲最经典的双指针法。

  1. 如果链表为空或者只有一个节点,直接返回头结点head。
  2. 初始化 pre 为 nullptr,cur 为头结点 head,node 为 cur 的下一个节点。
  3. 在循环中,不断更新 pre、cur 和 node 的值,使得 cur 的 next 指向 pre,然后将 pre、cur 和 node 分别向后移动一位。
  4. 当 cur 移动到链表末尾时,pre 就是反转后的新头结点。

参考代码

C++
c++
#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return head; // 如果链表为空或者只有一个节点,直接返回头结点
        }

        ListNode* pre = nullptr; // 初始化 pre 为 nullptr
        ListNode* cur = head; // 初始化 cur 为头结点
        ListNode* node = nullptr; // 初始化 node 为 nullptr

        while (cur != nullptr) {
            node = cur->next; // 保存当前节点的下一个节点
            cur->next = pre; // 当前节点的 next 指向 pre,完成反转
            pre = cur; // 更新 pre
            cur = node; // 更新 cur
        }

        return pre; // pre 就是反转后的新头结点
    }
};

int main() {
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(3);

    Solution solution;
    ListNode* newHead = solution.reverseList(head);

    while (newHead != nullptr) {
        std::cout << newHead->val << " ";
        newHead = newHead->next;
    }

    return 0;
}

2、手撕:lc70爬楼梯进阶,爬楼梯高级进阶:一次可以跳1-n阶台阶

思路

  1. 定义状态: 设 dp[i] 为到达第 i 阶台阶的方法总数。

  2. 状态转移方程: 到达第 i 阶的方法可以通过两种途径:

    • 从第 i-1 阶爬 1 阶到达。

    • 从第 i-2 阶爬 2 阶到达。

因此,状态转移方程为:

c++
dp[i]=dp[i−1]+dp[i−2]
  1. 初始状态:

    • dp[1] = 1,因为只有一种方法到达第 1 阶(直接爬 1 阶)。

    • dp[2] = 2,有两种方法到达第 2 阶(1 阶 + 1 阶 或者 直接爬 2 阶)。

  2. 最终结果: 我们要求的是到达第 n 阶的方法数,即 dp[n]

参考代码

C++
c++
#include <iostream>
using namespace std;

// 爬楼梯问题的函数,输入 n 表示楼梯的阶数,返回方法数
int climbStairs(int n) {
    // 如果楼梯阶数小于等于2,直接返回 n,因为方法数就是 n
    if (n <= 2) {
        return n;
    }

    // 初始化第1阶和第2阶的爬楼梯方法数
    int dp1 = 1; // 表示到达第1阶的方法数
    int dp2 = 2; // 表示到达第2阶的方法数

    // 临时变量用于存储当前阶数的爬楼梯方法数
    int current = 0;

    // 动态规划,从第3阶开始计算到第n阶的爬楼梯方法数
    for (int i = 3; i <= n; i++) {
        // 当前阶数的方法数是前两阶方法数之和
        current = dp1 + dp2;

        // 更新 dp1 和 dp2,准备计算下一阶
        dp1 = dp2; // dp1 向前移动
        dp2 = current; // dp2 向前移动
    }

    // 最终返回 dp2,表示第n阶的方法数
    return dp2;
}

int main() {
    int n;
    // 输入楼梯的阶数
    cout << "请输入楼梯的阶数:";
    cin >> n;

    // 调用函数计算方法数并输出结果
    int result = climbStairs(n);
    cout << "爬到第 " << n << " 阶楼梯的不同方法有:" << result << " 种。" << endl;

    return 0;
}

3、C++的多态实现原理是什么?

C++ 中的多态是一种面向对象编程的特性,允许同一接口(如函数或方法)通过不同类型的对象进行不同的行为。多态性在 C++ 中主要通过虚函数继承来实现,特别是运行时多态(也称为动态多态)。

多态的实现原理

多态的核心是虚函数表(Virtual Table,简称 vtable)虚指针(Virtual Pointer,简称 vptr)

分步骤讲解 C++ 中多态的实现原理:

1. 虚函数表(vtable)

当一个类中声明了虚函数时,编译器会为该类创建一个虚函数表(vtable)。这个表包含了该类的虚函数指针,指向具体实现的函数。

  • 每个声明了虚函数的类有一个独立的虚函数表。
  • 虚函数表中存储的是该类的虚函数的地址。
  • 如果一个子类重写了父类的虚函数,那么虚函数表中的对应条目会指向子类的函数实现。
2. 虚指针(vptr)

虚指针是指向虚函数表的指针。每个对象实例都有一个隐藏的成员,即虚指针(vptr)。当一个对象被创建时,编译器会将该对象的虚指针指向对应类的虚函数表。

3. 虚函数调用的过程

当我们通过指向基类的指针或引用调用虚函数时:

  • C++ 通过对象中的虚指针(vptr)找到该对象对应的虚函数表(vtable)。
  • 然后根据虚函数表中记录的函数地址,调用具体的函数实现。
  • 这就是运行时多态,即在运行时根据对象类型动态确定调用哪个函数。

例子:

c++
#include <iostream>
using namespace std;

class Base {
public:
    // 虚函数
    virtual void show() {
        cout << "Base class show function" << endl;
    }
    virtual ~Base() {} // 确保基类的析构函数也是虚函数
};

class Derived : public Base {
public:
    // 重写虚函数
    void show() override {
        cout << "Derived class show function" << endl;
    }
};

int main() {
    Base* basePtr; // 定义一个指向基类的指针
    Derived derivedObj;
    
    basePtr = &derivedObj;

    // 动态多态:调用的是 Derived 类的 show 函数
    basePtr->show(); // 输出:Derived class show function
    
    return 0;
}
运行机制分析
  1. Base 类中声明了一个虚函数 show(),因此编译器为 Base 类创建了一个虚函数表(vtable)。这个表中包含 Base::show() 的地址。
  2. Derived 类继承了 Base,并重写了 show()。因此编译器为 Derived 类也创建了一个虚函数表,表中的 show() 函数指针指向 Derived::show()
  3. 当在 main() 中使用 Base* 指针指向 Derived 类对象时,虚指针(vptr)指向了 Derived 类的虚函数表。因此调用 basePtr->show() 时,查找 Derived 类的虚函数表,最终调用的是 Derived::show() 而不是 Base::show()

4、多态分几种类型?

1. 静态多态

静态多态也称为编译时多态,是在编译时确定的。它通过函数重载和模板实现,表现为同一个函数名在不同的上下文中可以调用不同的函数版本。

  • 函数重载(Function Overloading):在同一个作用域中,定义多个函数名相同但参数列表不同的函数。编译器在编译时根据传递的参数类型和数量选择合适的函数版本。
c++
#include <iostream>
using namespace std;

void print(int i) {
    cout << "整数: " << i << endl;
}

void print(double f) {
    cout << "浮点数: " << f << endl;
}

int main() {
    print(5);     // 调用 print(int)
    print(3.14);  // 调用 print(double)
    return 0;
}
  • 函数模板(Function Templates):模板是泛型编程的一种形式,允许函数和类以通用方式实现。编译器根据传递的类型生成具体的函数版本。
c++
#include <iostream>
using namespace std;

template <typename T>
void print(T value) {
    cout << "值: " << value << endl;
}

int main() {
    print(10);        // 编译时生成 print<int>
    print(3.14);      // 编译时生成 print<double>
    print("hello");   // 编译时生成 print<const char*>
    return 0;
}
特点:
  • 编译时确定调用的函数。
  • 性能较好,因为编译时已经决定了调用的目标函数。
  • 不依赖虚函数表。

2. 动态多态

动态多态也称为运行时多态,是在程序运行时根据对象的实际类型动态决定调用哪个函数。动态多态主要通过继承虚函数实现。

  • 继承与虚函数:当基类中定义了虚函数时,子类可以重写该虚函数。在运行时,程序根据实际对象的类型动态决定调用基类版本还是子类版本的函数。
c++
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() {
        cout << "动物发出声音" << endl;
    }
};

class Dog : public Animal {
public:
    void sound() override {
        cout << "狗叫: 汪汪" << endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
        cout << "猫叫: 喵喵" << endl;
    }
};

int main() {
    Animal* animal;
    Dog dog;
    Cat cat;

    animal = &dog;
    animal->sound();  // 输出: 狗叫: 汪汪

    animal = &cat;
    animal->sound();  // 输出: 猫叫: 喵喵

    return 0;
}
特点:
  • 通过虚函数表(vtable)和虚指针(vptr)在运行时实现。
  • 能够在运行时根据对象类型动态选择合适的函数。
  • 稍微牺牲一些性能,因为调用时需要查找虚函数表。

静态多态与动态多态的对比

特性静态多态动态多态
实现方式函数重载、模板继承和虚函数
发生时间编译时确定运行时确定
性能快,编译期确定较慢,依赖虚函数表
可扩展性程序扩展时需要修改模板或重载易于扩展,通过继承增加新功能
示例函数重载、模板虚函数、基类指针或引用调用

5、多态的特点有哪些?

1. 接口一致性

  • 多态的核心是通过基类的接口来调用不同子类的行为,使用相同的函数调用接口来实现不同的功能。
  • 基类指针或引用可以调用子类的重写函数,而不需要知道对象的具体类型。这种统一的接口方式极大地提高了代码的可扩展性和可维护性。
c++
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "画圆形" << endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        cout << "画方形" << endl;
    }
};

void display(Shape* shape) {
    shape->draw();  // 使用统一接口调用,不同子类实现不同的行为
}

2. 运行时多态(动态绑定)

  • 通过虚函数实现的多态是在运行时决定调用哪个函数。这种机制称为动态绑定(late binding),即程序在运行时根据对象的实际类型确定要调用的函数。
  • 这使得程序能够根据实际的对象类型灵活调用相应的函数实现。
c++
Shape* shape = new Circle();  // shape 指向 Circle 对象
shape->draw();  // 调用 Circle 的 draw 方法,而非 Shape 的

3. 继承和虚函数的支持

  • 多态通常与继承密切相关。基类定义虚函数,子类可以选择重写这些虚函数。通过虚函数,C++ 可以在运行时根据对象的实际类型调用适当的函数版本。
  • 如果没有虚函数,多态无法在 C++ 中实现。
c++
class Animal {
public:
    virtual void speak() {
        cout << "Animal speaking" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Dog barking" << endl;
    }
};

4. 代码的可扩展性和可维护性

  • 多态允许代码在无需修改现有代码的情况下添加新的功能。通过继承基类并实现新的虚函数,新的子类可以扩展现有的系统。
  • 这使得程序可以随着需求的变化不断扩展,而不需要改动太多已有代码,提升了代码的可维护性和可扩展性。

示例:在添加新的 Triangle 类时,原来的代码无需修改,只需新定义类并重写虚函数即可。

5. 灵活的对象操作

  • 多态允许程序员通过基类指针或引用操作不同类型的对象。这种特性特别适合构建复杂的对象层次结构,允许统一管理和操作多种类型的对象。
  • 在一些设计模式中,如工厂模式、策略模式,多态经常被使用,以便根据运行时决定实际对象的类型和行为。

6. 虚函数表和运行时性能开销

  • C++ 中的多态依赖于虚函数表(vtable)和虚指针(vptr)来实现动态绑定。每个包含虚函数的类都有一个虚函数表,表中存储了虚函数的地址,而每个对象都包含一个虚指针指向它所属类的虚函数表。
  • 由于多态的实现需要查找虚函数表,动态多态会有一定的运行时开销。但这种开销一般较小,通常是可接受的。

7. 向下类型转换(Downcasting)

  • 在多态机制中,基类指针或引用可以指向子类对象。如果需要访问子类特有的成员,可以通过向下类型转换(downcasting)来完成。但这种操作通常伴随一定的风险,因此需要小心使用。
  • 在 C++ 中,dynamic_cast 可以进行安全的向下类型转换,并在不合法时返回 nullptr
c++
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal);
if (dog) {
    dog->speak();  // 安全地访问 Dog 对象
}

6、智能指针本质是什么?

本质是一个包装类,通过**RAII(资源获取即初始化)**原则来管理动态分配的资源,确保在智能指针不再使用时自动释放资源,避免内存泄漏问题。

  1. 封装了原始指针: 智能指针实际上是一个类,它内部封装了一个原始指针,该指针指向动态分配的对象。与普通的指针不同,智能指针在生命周期结束时会自动调用析构函数,释放资源,而无需手动调用 delete
  2. 自动内存管理: 智能指针通过析构函数来自动释放动态分配的内存,确保程序不会忘记释放资源,避免内存泄漏。每当智能指针离开其作用域或不再被引用时,析构函数会自动调用,删除封装的对象。
  3. 引用计数(对于 shared_ptrweak_ptr): 对于像 shared_ptr 这样的智能指针,内部维护了一个引用计数器,每当有新的 shared_ptr 指向相同的对象时,引用计数加一;当 shared_ptr 被销毁或指向其他对象时,引用计数减一;当引用计数为零时,释放资源。weak_ptr 则是一个不影响引用计数的弱引用,用来打破循环引用的问题。
  4. RAII(资源获取即初始化)机制: 智能指针通过 RAII 的方式管理资源:当智能指针对象构造时获取资源(动态分配的对象);当智能指针对象析构时,释放资源。RAII 的思想保证了资源的自动管理和正确释放,避免了资源泄漏问题。

常见的智能指针类型:

C++11 标准库提供了几种智能指针,用于不同的场景:

  1. std::unique_ptr(独占所有权)
    • unique_ptr 表示唯一所有权。一个 unique_ptr 只能拥有一个对象的所有权,不能共享。当 unique_ptr 被销毁时,自动释放它所管理的对象。
    • 不能复制或赋值,只能通过转移所有权(move semantics,移动语义)来传递所有权。
c++
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);  // 动态分配一个整型
    std::cout << *ptr1 << std::endl;  // 输出 10

    // std::unique_ptr<int> ptr2 = ptr1;  // 错误,不能复制
    std::unique_ptr<int> ptr2 = std::move(ptr1);  // 转移所有权
    if (ptr1 == nullptr) {
        std::cout << "ptr1 已经为空" << std::endl;
    }
    return 0;
}
  1. std::shared_ptr(共享所有权)

    • shared_ptr 允许多个智能指针共享同一个对象的所有权。它通过引用计数机制管理对象的生命周期。

    • 每个 shared_ptr 的复制操作都会增加引用计数,当所有 shared_ptr 都被销毁或重置时,引用计数为零,资源会自动释放。

c++
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);  // 动态分配一个整型
    std::shared_ptr<int> ptr2 = ptr1;  // 共享同一个对象

    std::cout << *ptr1 << std::endl;  // 输出 10
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 输出 2

    ptr2.reset();  // ptr2 不再指向对象
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;  // 输出 1
    return 0;
}
  1. std::weak_ptr(弱引用)

    • weak_ptr 是对 shared_ptr 的一种非拥有引用。它不影响引用计数,用来解决循环引用问题(两个对象通过 shared_ptr 互相引用,导致资源无法释放)。

    • weak_ptr 不能直接访问对象,需要先通过 lock() 方法转换为 shared_ptr,然后才能使用。

c++
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(10);
    std::weak_ptr<int> wp = sp;  // wp 是弱引用,不影响引用计数

    if (auto temp_sp = wp.lock()) {  // 通过 lock() 获取 shared_ptr
        std::cout << *temp_sp << std::endl;  // 输出 10
    } else {
        std::cout << "对象已被释放" << std::endl;
    }

    sp.reset();  // 重置 shared_ptr,释放对象
    if (auto temp_sp = wp.lock()) {
        std::cout << *temp_sp << std::endl;
    } else {
        std::cout << "对象已被释放" << std::endl;  // 输出: 对象已被释放
    }
    return 0;
}

7、malloc是如何工作的?底层调度原理,是系统调用吗?

malloc 的工作原理

  1. 堆内存的分配:

    • malloc 是通过在上分配内存的。堆内存是程序进程在运行时动态管理的区域,由操作系统为进程提供,大小可以根据需要进行增长或缩减。
    • malloc 被调用时,它从堆中寻找一块适当大小的空闲内存,并返回其地址给调用者。分配的内存通常是未初始化的,需要程序员显式初始化或赋值。
  2. 内存池(Memory Pool)管理:

    • 为了提高效率,malloc 不会每次都调用操作系统的内存分配系统,而是使用内存池机制。也就是说,malloc 会维护一块已经向操作系统申请的大块内存区域,并在其中分配小块内存。这样可以避免频繁的系统调用,减少开销。
    • 当内存池中的可用内存不足时,malloc 会向操作系统请求新的大块内存来扩展内存池。
  3. 内存碎片与管理:

    堆内存是连续的,因此多次的内存分配和释放操作可能会造成内存碎片,即可用的空闲内存块不连续,使得大块的内存分配变得困难。为了处理这种情况,malloc 的底层实现通常会使用空闲链表(free list)伙伴系统(buddy system) 来跟踪和管理空闲内存块。

  4. 分配策略:

    • malloc 实现中常见的分配策略有首次适配(first-fit)最佳适配(best-fit)最坏适配(worst-fit)。这些策略决定了如何在堆中找到合适大小的空闲块进行分配。
    • 首次适配策略从头开始遍历空闲链表,找到第一个足够大的块。
    • 最佳适配策略选择最小的能够满足要求的块,以减少碎片。
    • 最坏适配策略选择最大的空闲块,以确保剩下的部分仍然可以使用。

底层调用:系统调用

当程序需要的内存超过当前内存池容量,或者堆空间需要扩展时,malloc 可能会通过系统调用向操作系统申请更多的内存。

在 Unix-like 系统中,malloc 底层常用的系统调用有以下几种:

  1. brksbrk

    • brksbrk 系统调用用于调整**数据段(堆)**的大小。brk 设置堆的末端,而 sbrk 调整堆的末端位置。
    • 这些系统调用通过修改进程的虚拟地址空间来扩大或缩小堆的大小,从而为 malloc 提供更多的内存。

    示例:

    • sbrk(0) 返回当前堆的末端地址。
    • sbrk(n) 将堆扩大 n 字节,返回新的堆顶地址。
  2. mmap

    • 另一种常用的机制是 mmap,它用于映射文件或设备到内存,通常也可以用于直接分配大块的内存。现代 malloc 实现常常使用 mmap 来分配大块内存,特别是当需要的内存大小超过某个阈值时。
    • mmap 提供了更灵活的内存管理机制,可以分配不连续的内存块,适合分配大块内存。

    示例:

    • mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0) 分配一块匿名的(不与文件关联的)可读写内存区域。

malloc 底层机制的实现步骤

  1. 初始化: 当第一次调用 malloc 时,内存分配器会初始化堆的管理结构,比如空闲链表和内存池。
  2. 检查内存池: malloc 会在现有的内存池中寻找足够大的空闲块。如果找到,直接从中分配并返回指针。
  3. 向操作系统请求内存: 如果现有的内存池无法满足需求,malloc 会通过 brk/sbrkmmap 等系统调用向操作系统申请新的内存块。
  4. 更新空闲链表: 分配内存后,malloc 会更新其内部的管理结构(如空闲链表),并将未使用的部分继续保存在链表中以供下次分配使用。
  5. 释放内存: 当通过 free 释放内存时,malloc 会将释放的内存块标记为可用,并尝试合并相邻的空闲块,减少内存碎片。

malloc 的系统调用示意图:

malloc系统调用

8、malloc有哪些缺点?

1. 内存分配效率问题

  • 分配开销大:malloc 在分配和释放内存时需要进行额外的管理操作,比如维护空闲链表或内存块表。这些操作会带来一定的时间开销,尤其是在频繁分配和释放内存时,可能导致性能问题。
  • 碎片化(Fragmentation):由于 malloc 在分配和释放内存时可能产生内存碎片,尤其是在分配不同大小的内存块时,碎片化会增加。内存碎片会导致堆空间的使用效率降低,使得大块内存分配变得困难。

2. 无法自动初始化内存

  • malloc 只是分配内存,不会自动对内存进行初始化。分配的内存内容是未定义的,包含的可能是之前该内存区域的残留数据。因此,程序员必须手动初始化内存,否则可能会导致潜在的未定义行为或安全漏洞(例如读取未初始化的内存)。
  • 相比之下,calloc 可以自动将分配的内存初始化为零。

3. 内存泄漏

  • malloc 分配的内存需要通过 free 函数手动释放。如果程序员忘记调用 free,就会导致内存泄漏。内存泄漏会随着程序运行时间的增加,逐渐占用更多的内存,最终可能导致内存不足或程序崩溃。
  • 现代语言(如 C++11 中的智能指针)和垃圾回收机制可以帮助自动管理内存,减少这种手动释放的风险。

4. 不安全的内存管理

  • 双重释放(double free):如果某块内存被释放多次,会导致程序出现崩溃或不可预测的行为。mallocfree 并不自动检查这一点,程序员需要自己保证不会重复释放同一块内存。
  • 悬空指针(Dangling Pointer):在释放内存之后,指针仍然指向已经被释放的内存地址,如果再对这个悬空指针进行访问,可能会导致崩溃或错误的行为。

5. 内存越界(Buffer Overflow)

使用 malloc 分配内存时,程序员需要自己管理内存的大小和边界。如果程序员误访问超过分配内存范围的地址,可能导致缓冲区溢出。这不仅可能破坏数据,还可能引发严重的安全漏洞,比如在网络程序中容易被攻击者利用。

6. 不可扩展的内存

malloc 一次只能分配一个固定大小的内存块,不能动态扩展。如果需要扩展内存,必须使用 realloc 来重新分配内存块,这可能会涉及到数据的移动和复制,增加额外的开销。

7. 不支持对齐要求

在某些情况下,程序可能需要特定对齐的内存(例如 SIMD 指令要求的 16 字节或 32 字节对齐)。malloc 不能保证对齐要求,程序员需要自己处理对齐问题或者使用 posix_memalign 等专门的对齐分配函数。

8. 线程不安全

在多线程程序中,malloc 并不是线程安全的。多个线程同时调用 mallocfree 可能导致竞态条件(race condition)或内存损坏。在现代系统中,标准库中提供了线程安全的版本,但仍然存在性能瓶颈,因为需要使用锁来保护内存分配的临界区。

9. 可移植性问题

malloc 的行为和实现可能在不同的操作系统或平台上有所不同,特别是在内存分配的效率和策略上。例如,不同的操作系统可能对内存分配和回收的策略有差异,这可能导致相同的程序在不同环境中表现不一致。

9、mmap是什么?

mmap 是内存映射文件(Memory Mapped File)的缩写,它是一种在程序中直接将文件或设备内容映射到进程的虚拟地址空间的机制。mmap 提供了在文件和内存之间创建映射的功能,使得可以将文件或设备的内容当作内存来访问。这个过程避免了使用常规的文件读取和写入系统调用(如 readwrite),从而可以提高性能和简化内存管理。

mmap 的工作原理

mmap 将文件中的某个部分映射到进程的虚拟地址空间中,映射完成后,程序可以像访问普通内存一样访问这个文件的内容。读取或写入映射区域就相当于对文件进行读写,操作系统会负责将这些内存访问自动映射到相应的文件内容或设备。

mmap 的系统调用

在 Linux 或 UNIX 系统中,mmap 是通过一个系统调用实现的。

基本语法如下:

c++
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

  • addr:指定映射区域的起始地址,通常设置为 NULL,表示由内核决定映射的地址。
  • length:映射的字节数,通常设置为文件的大小。
  • prot:内存保护方式,决定映射区域的访问权限。常见的选项有:
    • PROT_READ:可读。
    • PROT_WRITE:可写。
    • PROT_EXEC:可执行。
    • PROT_NONE:不可访问。
  • flags:映射的类型和其他属性。常见的选项有:
    • MAP_SHARED:映射的内存共享,多个进程可以共享映射区域的修改,且修改会同步到文件中。
    • MAP_PRIVATE:映射的内存为私有,修改不会反映到文件中,进程间也不共享。
  • fd:文件描述符,指向要映射的文件,通常通过 open 打开。
  • offset:文件偏移量,表示从文件的哪个位置开始映射。

返回值:

  • 成功时返回映射区域的指针(虚拟地址),失败时返回 MAP_FAILED

一个简单的示例程序,演示如何使用 mmap 将文件映射到内存中:

c++
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    const char *filepath = "example.txt";
    int fd = open(filepath, O_RDONLY); // 打开文件
    if (fd == -1) {
        perror("open");
        return 1;
    }

    struct stat file_stat;
    if (fstat(fd, &file_stat) == -1) { // 获取文件大小
        perror("fstat");
        return 1;
    }

    size_t file_size = file_stat.st_size;

    // 使用 mmap 将文件内容映射到内存
    char *file_content = (char *)mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file_content == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // 关闭文件描述符,文件仍然被映射到内存
    close(fd);

    // 输出文件内容
    for (size_t i = 0; i < file_size; i++) {
        putchar(file_content[i]);
    }

    // 取消映射
    if (munmap(file_content, file_size) == -1) {
        perror("munmap");
        return 1;
    }

    return 0;
}

mmap 的优点

  1. 性能提升:通过 mmap,可以避免在文件读写时频繁的系统调用(如 readwrite),操作系统会自动管理内存的加载和回写,这可以减少 I/O 开销,提高性能,特别是对于大文件。
  2. 内存共享:如果使用 MAP_SHARED 标志,不同进程可以共享同一文件的内存映射区域,允许它们以共享内存的方式进行文件内容的读写,而无需使用进程间通信机制(如管道、消息队列等)。
  3. 文件内容随机访问:通过 mmap,可以像操作内存一样操作文件,尤其适合需要频繁访问文件中的任意部分的场景(如数据库系统),无需每次都进行文件的定位和读取操作。
  4. 节省内存:通过 mmap 映射文件,内存中不需要额外存储文件的副本,直接将文件的内容映射到内存中,减少了内存的消耗。

mmap 的缺点

  1. 可能导致页面错误:mmap 的实际加载是按需的,只有在访问映射区域时才会将文件内容加载到内存中。虽然这可以节省内存,但也可能导致页面错误(page fault),从而引发性能波动。
  2. 不适合频繁小块更新:mmap 更适合大块数据的读写。如果文件内容需要频繁地小块更新,mmap 可能无法提供预期的性能,因为涉及到的页面可能会频繁被写入和刷回磁盘。
  3. 兼容性问题:不同平台对 mmap 的支持和实现有所不同,特别是在嵌入式系统或某些特殊平台上,mmap 可能不完全兼容或表现不同。
  4. 错误处理复杂:由于文件映射的内存和普通内存操作非常类似,因此程序员需要特别小心处理越界访问等问题,可能导致难以追踪的错误(如访问未映射的内存区域)。

10、多线程会有什么问题?

1. 竞态条件

竞态条件是指多个线程同时访问和修改共享数据,而这些访问和修改的操作顺序可能导致程序出现不同的结果。由于线程的调度是由操作系统控制的,多个线程可能会同时对共享资源进行读写操作,导致程序行为不可预测。

示例:两个线程同时增加一个共享的全局变量,结果可能会比预期少增加几次,因为某个线程的更新被另一个线程的操作覆盖了。

2. 死锁

死锁是指两个或多个线程在等待彼此释放资源时进入了无限期的等待状态,导致程序无法继续执行。死锁通常发生在多个线程竞争多种资源时,比如当线程 A 持有资源 1 并等待资源 2,而线程 B 持有资源 2 并等待资源 1 时,就会产生死锁。

避免方法:

  • 避免嵌套锁定,即避免一个线程在持有一个锁的同时试图获取另一个锁。
  • 使用锁的超时机制,使线程在等待锁时能够有机会超时退出。
  • 按照一致的顺序获取锁,避免死锁循环。

3. 资源竞争

当多个线程同时竞争有限的系统资源(如 CPU、内存、I/O 设备等)时,可能会导致资源竞争问题。这会降低程序的性能,甚至可能造成某些线程无法获得足够的资源而无法执行。最常见的竞争资源问题体现在锁和互斥量的过度使用。

性能影响:

  • 频繁加锁解锁会降低程序的并发性,增加上下文切换的开销。
  • 当一个线程长时间持有锁时,其他线程会处于阻塞状态,从而影响系统性能。

4. 活锁

活锁与死锁类似,不同之处在于线程并没有被阻塞,但它们总是在互相让步或者重复进行无效的操作,导致线程无法继续执行实际的工作。尽管活锁的线程依然在执行,但它们无法取得进展。

示例:两个线程反复尝试获取资源并不断地失败和重试,导致程序一直处于等待状态。

5. 饥饿

饥饿是指某个线程由于资源分配不公,长时间得不到执行的机会。通常饥饿是因为优先级较低的线程被优先级较高的线程持续占用资源,而使得它无法获得执行机会。

解决方法:

  • 使用公平锁(fair lock)机制,确保线程能够按顺序获得资源。
  • 控制线程优先级,避免过高或过低的线程优先级设置。

6. 上下文切换开销

多线程程序会涉及线程间的上下文切换。每次线程切换时,操作系统需要保存当前线程的上下文并恢复新线程的上下文。这会消耗 CPU 时间,并影响性能。如果线程过多或线程切换过于频繁,程序的性能可能反而下降。

解决方法:

  • 尽量减少不必要的线程创建。
  • 避免过多的同步操作和锁竞争。

7. 内存可见性问题

在多线程环境下,线程之间对共享变量的更新不一定是立即可见的。这是因为 CPU 会使用缓存和寄存器,线程可能在自己的缓存中修改变量,而未将其写回到主存中,导致其他线程读取到的值是旧的。

解决方法:

  • 使用 volatile 关键字(适用于某些语言,如 Java),强制变量直接从主存读取和写入。
  • 使用适当的同步机制(如互斥锁、条件变量等)确保内存的可见性。

8. 线程安全

如果某些代码在多线程环境下无法保证其正确性,则它们被认为是线程不安全的。这种代码在没有适当的同步机制保护时,会导致不可预测的行为。

示例:同时修改共享数据结构(如链表、数组等),如果没有锁定机制,可能导致数据结构的状态不一致。

解决方法:

  • 使用线程安全的数据结构或库(如 std::atomic、线程安全的队列等)。
  • 对关键代码段加锁以保证线程安全。

9. 顺序性问题(Out-of-Order Execution)

在多线程环境中,CPU 和编译器可能会对指令进行乱序执行,以提高效率。由于线程之间的操作顺序可能不同步,这可能会导致意料之外的结果。

解决方法:使用内存屏障(memory barriers)或同步机制确保操作的顺序一致性。

MySQL

1、Mysql的存储引擎你知道那些?区别是什么

1. InnoDB

  • 特点
    • 事务支持:InnoDB 是 MySQL 默认的存储引擎,支持 ACID(原子性、一致性、隔离性、持久性)事务,具有 事务回滚崩溃恢复能力
    • 行级锁:InnoDB 使用 行级锁,能够在高并发场景下提供更好的性能。
    • 外键支持:InnoDB 支持 外键约束,能够维护数据的完整性。
    • MVCC:InnoDB 使用 多版本并发控制(MVCC) 来提高读取性能,避免了读写锁定冲突。
    • 数据存储:InnoDB 表中的数据按 聚簇索引(Clustered Index) 的方式存储,即数据按主键顺序存放。
  • 适用场景
    • 高并发读写操作。
    • 需要事务支持的应用,如金融、电子商务等。
    • 需要外键约束来保证数据完整性。
  • 缺点
    • 相较于 MyISAM,占用更多的磁盘空间和内存。
    • 某些特定查询场景下,性能可能不如 MyISAM。

2. MyISAM

  • 特点
    • 无事务支持:MyISAM 不支持事务处理,也不支持外键。
    • 表级锁:MyISAM 使用 表级锁,在写操作时会锁定整个表,导致并发性能较差,尤其是频繁的读写操作混合场景。
    • 压缩表:MyISAM 支持压缩表,可以有效节省存储空间。
    • 全文索引:MyISAM 早期支持 全文索引,适合进行快速的全文检索(InnoDB 在 5.6 后也支持了全文索引)。
    • 存储方式:每个 MyISAM 表有三个文件:.frm(表结构文件),.MYD(数据文件),.MYI(索引文件)。
  • 适用场景
    • 读多写少的应用场景,如数据仓库、日志管理等。
    • 不需要事务、外键约束的场景。
  • 缺点
    • 无法支持事务和外键,无法保证数据一致性。
    • 高并发写操作时性能较差,因为表级锁限制了并发写入。

3. Mebmory(Heap)

  • 特点
    • 基于内存:Memory 存储引擎将数据存储在内存中,因而读写非常快速,但服务器重启或崩溃后数据会丢失。
    • 表级锁:与 MyISAM 类似,Memory 引擎使用 表级锁
    • 数据类型限制:不支持 BLOBTEXT 类型字段,因为这些字段可能占用过多内存。
    • 哈希索引:Memory 引擎支持 哈希索引,适用于等值查询。
  • 适用场景
    • 需要快速临时数据访问的应用场景,如缓存、会话存储等。
    • 数据对持久性要求不高的场景。
  • 缺点
    • 数据存储在内存中,断电或重启时会丢失。
    • 表级锁可能会影响并发性能。

4. CSV

  • 特点
    • 纯文本存储:CSV 存储引擎将数据存储为逗号分隔的文本文件。
    • 轻量化:CSV 存储引擎的实现非常简单,适合于处理需要导入和导出 CSV 格式的场景。
    • 不支持索引和事务:CSV 引擎不支持任何索引和事务,因此只能进行简单的数据处理。
  • 适用场景
    • 需要从 CSV 文件导入或导出数据的应用场景。
    • 临时存储或传输少量数据。
  • 缺点
    • 性能较差,不支持索引和事务。
    • 不适合大规模数据处理。

5. ARCHIVE

  • 特点
    • 压缩存储:ARCHIVE 引擎将数据以高压缩格式存储,适合存储大量历史归档数据。
    • 只支持插入和查询:ARCHIVE 只支持 插入(INSERT)查询(SELECT) 操作,不支持 更新(UPDATE)删除(DELETE) 操作。
    • 表级锁:ARCHIVE 引擎使用表级锁,查询操作性能可能较差。
  • 适用场景:适合存储大量的历史数据,如日志、审计记录等。
  • 缺点
    • 只支持插入和查询,不适合需要频繁更新或删除的场景。
    • 查询性能相对较差。

给个表格帮助大家记忆:

存储引擎事务支持锁机制特点适用场景
InnoDB支持行级锁支持事务、外键,适合高并发需要高并发、事务支持的场景
MyISAM不支持表级锁无事务,支持全文索引读多写少的应用
Memory不支持表级锁基于内存,速度快,数据不持久临时数据存储
CSV不支持无锁机制存储为 CSV 文件,简单轻量数据导入导出
ARCHIVE不支持表级锁压缩存储,只支持插入和查询历史数据存储
NDB Cluster支持行级锁分布式存储,支持事务分布式应用
Federated不支持无锁机制跨服务器访问跨数据库服务器操作
TokuDB支持行级锁高效压缩,Fractal Tree 索引大规模写密集型应用

2、索引分别有什么结构?

1. B树(B-Tree)

  • 概念:B树是一种自平衡的树数据结构,广泛用于数据库和文件系统的索引。它的每个节点都可以有多个子节点,从而保持树的平衡。
  • 特性:
    • 自平衡:保持树的平衡,所有叶子节点都在同一层,确保查询时间复杂度为 O(log N)。
    • 多路搜索:每个节点可以有多个子节点,使得搜索操作更高效。
    • 适合范围查询:支持高效的范围查询操作。
  • 应用:适用于需要高效查找、插入、删除和范围查询的场景。

2. B+树(B+-Tree)

  • 概念:B+树是 B树的一个变种,所有的值都存储在叶子节点中,内部节点只存储索引信息。叶子节点通过链表连接,以支持范围查询。
  • 特性:
    • 所有值存储在叶子节点:使得范围查询更加高效。
    • 叶子节点链表:链表结构允许顺序遍历和范围查询操作。
  • 应用:在数据库系统和文件系统中广泛使用,如 MySQL 的 InnoDB 存储引擎。

3. 哈希表(Hash Table)

  • 概念:哈希表通过哈希函数将键映射到表中的位置,以实现快速的查找操作。
  • 特性:
    • 常数时间复杂度:查找操作平均时间复杂度为 O(1)。
    • 适合等值查询:对单个值的查询非常高效,但不适合范围查询。
  • 应用:适用于只需快速查找、插入和删除操作的场景,如数据库中的哈希索引。

4. 位图索引(Bitmap Index)

  • 概念:位图索引使用位图(bitmaps)来表示数据表中每个可能的值,通常用于低基数列(即列的不同值数量较少)。
  • 特性:
    • 压缩:位图可以压缩,适用于具有少量唯一值的列。
    • 高效的逻辑运算:位图索引支持高效的逻辑运算(如 AND、OR、NOT),使得查询更加高效。
  • 应用:适用于具有较少不同值的列,如性别、状态等。

5. 跳表(Skip List)

  • 概念:跳表是一种链表的随机化数据结构,通过增加多层链表来实现快速的查找、插入和删除操作。
  • 特性:
    • 层次化链表:多层链表结构使得查找操作的时间复杂度为 O(log N)。
    • 随机化:跳表利用随机化来实现平衡,提高操作效率。
  • 应用:用于需要支持动态变化的高效查找操作的场景,部分数据库系统和内存数据库(如 Redis)中会使用跳表。

6. Trie(前缀树)

  • 概念:Trie 是一种树状数据结构,用于高效存储和检索字符串,特别适用于前缀匹配。
  • 特性:
    • 前缀查询:支持快速的前缀匹配和词典查询。
    • 空间效率:对于共享前缀的字符串,Trie 具有良好的空间效率。
  • 应用:广泛用于实现字典、自动补全、拼写检查等功能。

7. 全文索引(Full-Text Index)

  • 特点:
    • 结构:全文索引对文本数据进行分词处理,创建一个倒排索引(inverted index),记录每个词在文档中的出现位置。
    • 查询效率:支持快速的文本搜索操作,如单词匹配和短语匹配。
    • 支持:支持对长文本字段进行模糊查询,如 MATCH...AGAINST
  • 应用:
    • 在 MySQL 中,InnoDB 和 MyISAM 存储引擎都支持全文索引。
    • 适用于需要进行复杂的文本搜索的场景,如搜索引擎和内容管理系统。

8. R-Tree 索引(R-Tree Index)

  • 特点:
    • 结构:R-Tree 是一种用于空间数据的树形结构,每个节点表示一个矩形区域,可以包含多个子矩形。
    • 查询效率:适用于空间数据的范围查询和邻近查询,如地理信息系统中的点、线和区域数据。
    • 更新效率:支持对空间数据进行高效的插入、删除和更新操作。
  • 应用:常用于地理信息系统(GIS)和需要空间数据存储和查询的应用。

9. 倒排索引(Inverted Index)

  • 特点:
    • 结构:倒排索引用于存储文本数据的词汇表,其中每个词汇指向包含该词汇的文档列表。
    • 查询效率:用于快速的全文检索,支持词汇匹配和复杂的查询操作。
    • 应用:与全文索引相似,适用于需要进行复杂的文本搜索的场景。
  • 应用:用于搜索引擎和文本分析系统。

3、B+树和B树有什么区别?

1. 数据存储位置

  • B树(B-Tree)
    • 在 B 树中,数据(键值)可以存储在内部节点和叶子节点中。
    • 内部节点同时存储索引和数据,因此每个节点都有可能包含实际的数据。
  • B+树(B+ Tree)
    • 在 B+ 树中,所有数据都存储在叶子节点中,内部节点仅存储索引信息。
    • 内部节点只包含键值和指向子节点的指针,用于导航到叶子节点。

2. 叶子节点的链接

  • B树
    • 叶子节点之间没有直接的链接。
    • 要查找范围查询中的数据,通常需要进行多个遍历。
  • B+树
    • 叶子节点之间通过链表连接(即每个叶子节点有一个指向下一个叶子节点的指针),形成一个有序的链表。
    • 这种链接使得范围查询更加高效,可以通过链表一次性扫描所有相关的数据。

3. 查询效率

  • B树
    • 查找操作需要在树中进行层次遍历,并且每个节点都可能包含数据和索引。
    • 在查询某个具体的值时,查找速度与树的高度有关。
  • B+树
    • 查找操作也涉及到层次遍历,但由于所有数据都在叶子节点中,查找的效率较高。
    • 范围查询的效率更高,因为可以利用叶子节点间的链表进行顺序访问。

4. 插入和删除

  • B树
    • 插入和删除操作会在内部节点和叶子节点上进行,可能会导致内部节点的复杂调整。
    • 可能需要在内部节点上进行合并或分裂操作。
  • B+树
    • 插入和删除操作通常在叶子节点进行,内部节点仅需要更新索引信息。
    • 由于内部节点不存储数据,插入和删除操作相对简单。

5. 使用场景

  • B树
    • 适用于需要频繁进行查找、插入和删除操作的场景,如一般的数据库索引。
    • 适合对所有节点都有数据存储要求的情况。
  • B+树
    • 适用于需要范围查询和顺序访问的场景,如文件系统的目录索引、数据库索引等。
    • 由于叶子节点有链表结构,特别适合范围查询和全表扫描操作。
特性B树(B-Tree)B+树(B+ Tree)
数据存储位置数据存储在内部节点和叶子节点数据存储在叶子节点,内部节点只存储索引
叶子节点链接叶子节点之间没有直接的链表链接叶子节点通过链表链接,形成有序链表
查询效率查找速度与树的高度有关范围查询效率高,通过链表顺序访问
插入删除可能在内部节点和叶子节点上进行调整主要在叶子节点进行,内部节点只更新索引
使用场景一般数据库索引等文件系统、数据库范围查询、顺序访问等

4、索引使用的时候有什么需要注意的?

1. 选择合适的索引类型

根据查询需求选择:

  • 对于范围查询(如 BETWEENLIKE 'abc%')和排序操作,B-Tree 索引是合适的选择。
  • 对于等值查询(如 =),哈希索引可能更高效。
  • 对于复杂的文本搜索,全文索引(Full-Text Index)适合。
  • 对于需要处理空间数据的应用,R-Tree 索引是合适的选择。

2. 索引的覆盖范围

选择适当的列进行索引:

  • 通常将经常用于查询条件(WHERE 子句)、排序(ORDER BY)和连接(JOIN)操作的列进行索引。
  • 避免对经常变化的列(如经常更新的字段)进行索引,因为频繁的更新会影响索引的性能。

3. 索引的数量

避免过多的索引:

  • 虽然索引可以提高查询性能,但每个索引都需要额外的存储空间,并且会增加插入、删除和更新操作的开销。
  • 过多的索引可能会导致性能下降,因此应根据实际需要合理设置索引数量。

4. 索引的维护

定期重建和优化索引:

  • 索引在数据库的使用过程中可能会变得碎片化。定期重建和优化索引可以提升性能。
  • 在 MySQL 中,可以使用 OPTIMIZE TABLE 语句来优化表和索引。

5. 索引的覆盖

利用覆盖索引:

  • 覆盖索引(Covering Index)是指索引包含了查询所需的所有列,这样数据库可以通过索引来满足查询,无需访问表的实际数据。
  • 使用覆盖索引可以显著提高查询性能。

6. 监控和评估

监控索引的性能:

  • 定期使用数据库的性能监控工具来评估索引的使用情况和效率。
  • 使用 EXPLAIN 语句来分析查询执行计划,检查索引的使用情况。

7. 索引的选择性

考虑索引的选择性:

  • 索引的选择性是指索引列的唯一值的比例。选择性高的索引(唯一值多)通常比选择性低的索引(重复值多)更有效。
  • 对选择性低的列建立索引可能不会带来显著的性能提升。

8. 联合索引

合理使用联合索引:

  • 对于多个列的查询条件,联合索引可以显著提高性能。需要注意联合索引的列顺序,通常将选择性高的列放在前面。
  • 在查询中使用的列顺序应与联合索引的列顺序相匹配。

9. 避免覆盖不必要的列

避免创建不必要的索引:

  • 确保索引用于实际需要的列,避免在不经常用于查询的列上创建索引。
  • 检查和移除冗余或无效的索引,以减少维护开销和存储需求。

5、索引的缺点是什么?

1. 增加存储开销

额外的存储空间:

  • 每个索引都需要额外的存储空间。尤其是在索引涉及多个列时,存储开销会显著增加。
  • 对于大表或有多个索引的表,存储开销可能变得相当可观。

2. 影响写操作性能

插入、更新和删除的开销:

  • 索引需要在数据插入、更新和删除时进行维护。这会导致额外的开销,因为数据库必须同时更新索引。
  • 在高并发的环境下,频繁的写操作可能导致性能下降。

3. 可能的维护复杂性

索引的维护:

  • 对索引进行定期维护(如重建、优化)是必要的,以避免碎片化和性能下降。维护索引可能增加管理复杂性和系统开销。
  • 需要监控和分析索引的性能,确保它们在实际应用中仍然有效。

4. 可能的选择性降低

选择性低的索引:

  • 对选择性较低的列(如包含大量重复值的列)建立索引,可能不会显著提高查询性能,甚至可能导致性能下降。
  • 对这些列的索引可能会增加额外的存储开销,但带来的性能提升有限。

5. 影响查询优化

查询优化的挑战:

  • 在某些情况下,数据库优化器可能无法选择最优的索引,导致查询性能下降。
  • 需要定期检查和调整索引策略,以确保查询优化器能够利用正确的索引。

6. 对多表连接的影响

连接性能的影响:

  • 虽然索引可以提高单表查询的性能,但在复杂的多表连接查询中,索引的效果可能会受到限制。
  • 不恰当的索引策略可能导致连接操作的性能问题。

6、使用索引查询一定会变快么?

使用索引通常可以提高查询性能,但并不总是保证查询一定会变快。索引的效果取决于多个因素,包括数据分布、查询类型、索引设计以及数据库的实际执行情况。

1. 数据选择性

  • 选择性高的列:索引在选择性高的列上(即列中包含大量唯一值)通常能显著提高查询性能。例如,使用索引在员工表的员工ID列上进行查找。
  • 选择性低的列:如果索引的列选择性低(即列中包含大量重复值),索引的效果可能有限。例如,在性别列上建立索引,可能不会显著提高查询性能,因为性别列的值重复较多。

2. 查询类型

  • 简单查询:对于简单的等值查询,索引通常能显著提高性能。例如,SELECT * FROM employees WHERE employee_id = 123
  • 复杂查询:对于复杂的查询,如多表连接、大量数据聚合或子查询,索引的效果可能受到限制。索引不能总是优化所有复杂查询,特别是在涉及大量数据处理时。

3. 索引设计

  • 索引选择:选择合适的索引类型(如 B-Tree、哈希索引、全文索引等)以及合理的索引组合对于提高查询性能至关重要。联合索引可能对多列查询更有效。
  • 索引维护:索引需要定期维护以防止碎片化。未优化的索引可能会影响查询性能。

4. 查询计划

  • 数据库优化器会根据查询的执行计划选择是否使用索引。如果优化器判断使用索引不合适,可能会选择全表扫描。
  • 使用 EXPLAIN 语句可以帮助分析查询的执行计划,检查索引是否被有效使用。

5. 数据更新频率

索引会增加数据插入、更新和删除操作的开销。如果表的写操作频繁,索引的维护成本可能会抵消其查询性能的提升。

6. 索引的选择性和覆盖性

  • 覆盖索引:使用覆盖索引(即索引包含查询所需的所有列)可以显著提高性能,因为数据库可以只访问索引而不需要回表查询。
  • 索引优化:确保索引列的顺序和组合适应查询条件,以最大化索引的效率。

7. 表大小

在小表上,索引可能不会有明显的性能提升,因为全表扫描的成本可能与使用索引的成本相当。

7、redis有哪些持久化策略?二者在实际使用上有什么差异呢?

1. RDB(Redis Database Backup)

工作原理
  • 快照持久化:RDB 持久化通过创建数据集的时间点快照来实现。Redis 定期将内存中的数据快照保存到磁盘上。
  • 配置:可以通过配置 redis.conf 文件中的 save 选项来设置快照的触发条件,例如 save 900 1 表示每 900 秒(15 分钟)如果至少有 1 个键发生了变化,则保存一个快照。
优点
  • 性能:RDB 持久化对 Redis 的性能影响较小,因为持久化过程是在后台异步进行的。
  • 恢复速度:由于 RDB 文件是单个文件,数据恢复速度较快,可以更迅速地从磁盘加载数据到内存。
缺点
  • 数据丢失:RDB 只能恢复到最后一次快照的状态。如果在快照之间发生了故障,可能会丢失最近的数据。
  • 持久化开销:创建快照的过程中可能会有一定的性能开销,特别是在数据量较大时。

2. AOF(Append Only File)

工作原理
  • 日志持久化:AOF 持久化通过记录所有写操作的日志来实现。Redis 将每个写操作追加到 AOF 文件中,以记录所有修改数据的操作。
  • 配置:可以通过配置 redis.conf 文件中的 appendonlyappendfsync 选项来启用 AOF 持久化。appendfsync 可以设置为 alwayseverysecno,决定 AOF 文件的同步策略。
优点
  • 数据持久性:AOF 记录了所有写操作,提供了更高的数据持久性。可以通过配置 appendfsync 选项来控制数据持久化的频率,从而在故障恢复时减少数据丢失。
  • 恢复精确:通过重放 AOF 文件中的写操作,Redis 可以恢复到崩溃之前的精确状态。
缺点
  • 性能影响:AOF 写入操作可能会对 Redis 性能产生影响,尤其是在 appendfsync 配置为 always 时,因为每次写操作都会同步到磁盘。
  • 文件体积:AOF 文件可能会变得非常大,需要定期重写以压缩文件。Redis 提供了 BGREWRITEAOF 命令来重写 AOF 文件。

实际使用中的差异

特性RDBAOF
持久化策略定期快照记录所有写操作日志
数据恢复速度快速恢复,因为数据以快照形式存储恢复速度较慢,因为需要重放写操作日志
数据丢失风险最新快照后的数据丢失取决于 appendfsync 的设置,可以减少丢失
性能影响对性能影响小,快照在后台异步进行写操作可能影响性能,特别是在 always 配置时
文件大小快照文件较小文件可能很大,需要定期重写
恢复精确性只能恢复到最后一次快照的状态可以恢复到崩溃之前的精确状态