Skip to content

米哈游C++后端开发一面凉经,体验很好,但还是挂了。。。

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

1、C++面向对象的三个特征?

1、封装

封装是指将数据(属性)和操作数据的方法(函数)封装在一个单元中,这个单元就是类。封装的主要目的是隐藏类的内部实现细节,只暴露必要的接口给外部使用者。

优点:

  • 信息隐藏: 封装可以将类的内部细节隐藏起来,不暴露给外部,提高了安全性和防止误用。
  • 简化接口: 封装通过提供清晰的接口简化了类的使用,使用者只需关注如何使用接口而不需要了解内部实现。
  • 提高可维护性: 内部实现的修改不会影响外部使用者,从而提高了代码的可维护性。
C
#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int age;

public:
    // 构造函数
    Student(const std::string& n, int a) : name(n), age(a) {}

    // 获取姓名
    std::string getName() const {
        return name;
    }

    // 设置年龄
    void setAge(int a) {
        if (a >= 0) {
            age = a;
        }
    }

    // 显示学生信息
    void displayInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    Student student("Alice", 20);
    
    // 使用公有接口获取和设置信息
    student.setAge(21);
    std::cout << "Student Name: " << student.getName() << std::endl;
    student.displayInfo();

    return 0;
}

2、继承

继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,子类可以获得父类的特征,并可以添加新的特征或修改继承的特征。

优点:

  • 代码重用: 继承允许在不重复编写代码的情况下扩展和修改现有类,提高了代码的重用性。
  • 层次结构: 继承可以创建类的层次结构,使得代码更有组织性和可扩展性。
  • 多态性支持: 继承是多态性的基础,通过基类指针或引用调用派生类的方法实现多态行为。
C
#include <iostream>
#include <string>

// 基类
class Animal {
protected:
    std::string name;

public:
    Animal(const std::string& n) : name(n) {}

    void eat() {
        std::cout << name << " is eating." << std::endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}

    void bark() {
        std::cout << name << " is barking." << std::endl;
    }
};

int main() {
    Dog myDog("Buddy");

    myDog.eat();  // 继承自基类
    myDog.bark(); // 派生类自己的方法

    return 0;
}

3、多态

多态性是指同一个操作可以作用于不同类型的对象,并且可以根据对象的类型执行不同的行为。多态性通过虚函数和函数重载实现。

  • 编译时多态性(静态多态性): 通过函数重载实现,编译器在编译时根据函数参数的类型和数量来选择调用合适的函数。这种多态性是在编译时解析的。
  • 运行时多态性(动态多态性): 通过虚函数和继承实现,允许在运行时根据对象的实际类型来调用适当的函数。这种多态性是在运行时解析的。

优点:

  • 灵活性: 多态性允许在不同的情境下以通用的方式处理不同类型的对象,提高了代码的灵活性。
  • 可扩展性: 可以轻松地添加新的派生类而不影响现有的代码,增加了系统的可扩展性。
  • 简化接口: 多态性简化了代码的接口,允许使用者按统一的方式与不同类型的对象交互。
C
#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

class Square : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a square." << std::endl;
    }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle());
    shapes.push_back(new Square());

    for (Shape* shape : shapes) {
        shape->draw(); // 多态性:根据对象的实际类型调用适当的方法
    }

    // 释放内存
    for (Shape* shape : shapes) {
        delete shape;
    }

    return 0;
}

2、介绍STL容器vector、list,vector扩容机制?

1. vector

概述
  • vector 是一个动态数组,能够在运行时自动调整大小。
  • 它提供随机访问功能,允许以常数时间复杂度访问任何元素。
特点
  • 内存连续性:所有元素在内存中是连续存储的,这使得 vector 对缓存友好,提高了访问速度。
  • 动态大小:当添加新元素时,vector 会自动扩展,如果当前容量不足,会分配一个更大的数组并复制原有元素。
  • 迭代器支持:支持随机访问迭代器,允许使用标准算法进行遍历和操作。
优缺点
  • 优点:
    • 快速随机访问(O(1))。
    • 方便地使用 push_backpop_back 方法添加和移除元素。
  • 缺点:
    • 在中间插入或删除元素的性能较差(O(n)),因为需要移动后续元素。
    • 扩容时可能会导致内存重新分配和元素复制。
适用场景

当需要频繁访问元素或在末尾添加/删除元素时,使用 vector 是理想选择。

2. list

概述
  • list 是一个双向链表,允许在任意位置快速插入和删除元素。
特点
  • 节点不连续:每个元素(节点)在内存中可以不连续存储,每个节点包含指向前后节点的指针。
  • 双向访问:可以从任意节点向前或向后遍历,支持常数时间复杂度的插入和删除。
  • 迭代器支持:支持双向迭代器,允许从任意位置遍历。
优缺点
  • 优点:
    • 在中间插入和删除元素效率高(O(1)),只需调整指针。
    • 适合需要频繁插入/删除的场景。
  • 缺点:
    • 随机访问性能差(O(n)),因为必须从头节点或尾节点遍历链表。
    • 内存使用效率低,因每个节点都需要额外存储指针。
适用场景

当需要频繁在任意位置插入和删除元素,而不关心随机访问时,使用 list 是合适的。

vector扩容机制?

1. 初始状态

当创建一个 vector 时,它会有一个初始容量(通常是0或其他小值),并在首次插入元素时分配一定的内存。

2. 添加元素
  • 使用 push_back 或其他插入方法添加元素时,vector 会首先检查当前容量是否足够。
  • 如果当前容量足够,直接在数组末尾添加元素。
3. 扩容触发
  • 当添加的元素超出当前容量时,vector 会进行扩容。
  • 扩容的过程包括:
    1. 分配新内存:通常新容量为当前容量的2倍(或更高),以减少频繁的内存分配。
    2. 复制元素:将现有元素从旧数组复制到新数组。
    3. 释放旧内存:释放旧数组的内存。
4. 性能考虑
  • 扩容是一个耗时的操作,时间复杂度为 O(n),但由于它是按需进行的,平均情况下对 push_back 操作的时间复杂度为 O(1)。
  • 由于扩容的策略(如每次翻倍),大多数情况下,虽然某些插入操作会很慢,但整体插入效率仍然很高。
5. 温馨提示
  • 在扩容过程中,如果使用了自定义对象,可能还会涉及到对象的拷贝构造或移动构造。
  • 用户可以通过 reserve 方法提前指定容量,避免多次扩容。

3、常见的锁有哪些?

1. 互斥锁(Mutex)

  • 描述:互斥锁是最基本的锁,用于确保同一时间只有一个线程可以访问共享资源。
  • 实现:使用 std::mutex 在 C++ 中实现。
  • 特点:可以防止多个线程同时访问共享数据。
    • 如果一个线程持有互斥锁,其他尝试获取该锁的线程会被阻塞,直到锁被释放。

2. 读写锁(Read-Write Lock)

  • 描述:读写锁允许多个线程同时读取资源,但写操作会独占资源。
  • 实现:在 C++ 中可以使用 std::shared_mutex
  • 特点:
    • 多个线程可以同时获得读锁。
    • 当一个线程获取写锁时,所有其他读写操作会被阻塞。

3. 自旋锁(Spinlock)

  • 描述:自旋锁是一种轻量级锁,它会在获取锁时不断检查锁的状态,适用于锁的持有时间非常短的场景。
  • 实现:使用原子变量实现。
  • 特点:
    • 当一个线程尝试获取锁时,如果锁已经被其他线程持有,它会循环检查("自旋")而不是被挂起。
    • 适合于多核系统,因为它减少了上下文切换的开销。

4. 递归锁(Recursive Lock)

  • 描述:递归锁允许同一线程多次获得同一把锁而不会导致死锁。
  • 实现:在 C++ 中可以使用 std::recursive_mutex
  • 特点:
    • 适合需要在同一线程中多次进入临界区的场景。
    • 每次加锁都需要相应的解锁。

5. 条件变量(Condition Variable)

  • 描述:条件变量用于线程间的同步,允许一个线程等待某个条件发生,同时释放锁。
  • 实现:使用 std::condition_variable
  • 特点:
    • 使得一个或多个线程可以在某个条件不满足时阻塞,直到其他线程通知它们条件已满足。
    • 通常与互斥锁配合使用。

6. 偏向锁(Biased Locking)

  • 描述:偏向锁是一种优化策略,允许线程在获取锁时不进行竞争,而是偏向于最常访问该锁的线程。
  • 实现:主要用于 Java 中的 HotSpot JVM。
  • 特点:在某些情况下可以减少锁的获取开销,但可能会导致死锁。

7. 乐观锁(Optimistic Locking)

  • 描述:乐观锁不通过加锁来控制并发,而是通过版本号或时间戳来检测数据冲突。
  • 实现:通常用于数据库中的事务处理。
  • 特点:
    • 在操作开始时不加锁,操作完成后检查是否有冲突,如果没有则提交。
    • 适合读多写少的场景。

4、常见的kv存储,map、set、unorderedmap等等,之间的区别?

1. std::map

  • 定义:std::map 是一个有序的键值对集合。它内部通常实现为红黑树(一种自平衡二叉查找树),这使得插入、删除和查找操作的时间复杂度均为 O(log n)。
  • 特点:
    • 键是唯一的,不能有重复的键。
    • 元素按照键的顺序自动排序。
    • 支持双向迭代器,可以进行正向或反向遍历。
  • 适用场景:当需要根据键的自然顺序来访问元素,或者保证元素总是有序的时候,适合使用 std::map

2. std::unordered_map

  • 定义:std::unordered_map 是一个无序的键值对集合。它内部通过哈希表实现,因此插入、删除和查找操作的平均时间复杂度接近 O(1),但在最坏情况下可能达到 O(n)。
  • 特点:
    • 键也是唯一的,不能有重复的键。
    • 不保证元素的顺序,因为它是基于哈希函数组织的。
    • 支持快速的查找、插入和删除操作。
  • 适用场景:当需要非常快的查找速度,并且不关心元素的顺序时,适合使用 std::unordered_map

3. std::set

  • 定义:std::set 是一个有序的唯一元素集合。它同样使用红黑树实现,确保每个元素都是唯一的,并且元素按排序顺序存储。
  • 特点:
    • 只存储键,没有对应的值。
    • 元素唯一,不允许重复。
    • 元素自动排序。
  • 适用场景:当你需要一个不重复的有序集合时,可以使用 std::set

4. std::unordered_set

  • 定义:std::unordered_set 是一个无序的唯一元素集合。它使用哈希表实现,提供平均常数时间的查找、插入和删除操作。
  • 特点:
    • 只存储键,没有对应的值。
    • 元素唯一,不允许重复。
    • 不保证元素的顺序。
  • 适用场景:当你需要一个不重复的集合,并且对查找速度有较高要求,而不关心元素的顺序时,可以使用 std::unordered_set
特性std::mapstd::unordered_mapstd::setstd::unordered_set
有序性
实现红黑树哈希表红黑树哈希表
查找复杂度O(log n)O(1)(平均情况)O(log n)O(1)(平均情况)
插入复杂度O(log n)O(1)(平均情况)O(log n)O(1)(平均情况)
删除复杂度O(log n)O(1)(平均情况)O(log n)O(1)(平均情况)
键/元素唯一性
适用场景需要保持键的顺序,范围查询快速查找和插入,不关心顺序需要唯一元素的集合,保持有序快速查找的唯一元素集合,不关心顺序

5、红黑树和普通的二叉树区别?

1. 基本性质

普通二叉搜索树 (Binary Search Tree, BST)
  • 节点结构:每个节点包含一个键(key)、一个值(value)、一个指向左子树的引用和一个指向右子树的引用。
  • 搜索性质:对于任意节点,其左子树中的所有节点的键都小于该节点的键,其右子树中的所有节点的键都大于该节点的键。
  • 平衡性:普通二叉搜索树不保证树的平衡性,因此在最坏情况下(例如,连续插入递增或递减的序列),树可能会退化成链表,导致操作时间复杂度退化到 O(n)。
红黑树 (Red-Black Tree)
  • 节点结构:除了普通二叉搜索树的节点属性外,每个节点还包含一个颜色属性(红色或黑色)。
  • 搜索性质:与普通二叉搜索树相同,满足二叉搜索树的性质。
  • 平衡性
    1. 每个节点要么是红色,要么是黑色。
    2. 根节点是黑色。
    3. 每个叶子节点(NIL节点,空节点)是黑色。
    4. 如果一个节点是红色的,则它的两个子节点必须是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
    5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑高相等)。

2. 平衡机制

普通二叉搜索树
  • 插入和删除:插入和删除操作可能导致树失去平衡,但普通二叉搜索树不会自动调整以恢复平衡。
  • 性能:在最坏情况下,树的高度可能达到 O(n),导致插入、删除和查找操作的时间复杂度为 O(n)。
红黑树
  • 插入和删除:插入和删除操作后,红黑树会通过一系列的旋转和重新着色操作来恢复上述的平衡性质。
    • 插入:新节点插入时默认为红色。如果插入后违反了红黑树的性质,通过旋转和重新着色来修复。
    • 删除:删除节点后,如果违反了红黑树的性质,同样通过旋转和重新着色来修复。
  • 性能:由于红黑树的高度始终保持在 O(log n),因此插入、删除和查找操作的时间复杂度均为 O(log n)。

6、redis的底层数据结构?

1. 字符串(String)

  • 底层数据结构:简单动态字符串(Simple Dynamic String, SDS)
  • 特点:
    • SDS 是 Redis 自己实现的动态字符串,类似于 C 语言中的 char*,但增加了长度信息和预分配空间,避免了频繁的内存重新分配。
    • SDS 在每次修改字符串时,会预先分配额外的空间,以减少未来的重新分配次数。
    • 字符串可以存储二进制数据,最大长度为 512 MB。

2. 列表(List)

  • 底层数据结构:双端链表(ziplist 或 linked list)
  • 特点:
    • ziplist:当列表中的元素数量较少且每个元素的大小较小时,Redis 使用压缩列表(ziplist)来存储列表。ziplist 是一个紧凑的数组结构,可以节省内存。
    • linked list:当列表中的元素数量较多或每个元素的大小较大时,Redis 使用双端链表(quicklist)来存储列表。双端链表由多个 ziplist 组成,每个 ziplist 节点称为一个“块”。
    • 列表支持从两端高效地插入和删除元素,时间复杂度为 O(1)。

3. 集合(Set)

  • 底层数据结构:整数集合(intset)或哈希表(hashtable)
  • 特点:
    • intset:当集合中的所有元素都是整数且数量较少时,Redis 使用整数集合(intset)来存储。intset 是一个有序数组,存储整数时非常紧凑。
    • hashtable:当集合中的元素数量较多或包含非整数元素时,Redis 使用哈希表(hashtable)来存储。哈希表提供了平均 O(1) 的插入、删除和查找操作。
    • 集合中的元素是唯一的,不支持重复元素。

4. 有序集合(Sorted Set)

  • 底层数据结构:跳跃表(skiplist)或压缩列表(ziplist)
  • 特点:
    • skiplist:跳跃表是一种多层链表结构,通过增加多个层次的指针来加速查找操作。跳跃表支持 O(log n) 的插入、删除和查找操作。
    • ziplist:当有序集合中的元素数量较少且每个元素的大小较小时,Redis 使用压缩列表(ziplist)来存储。ziplist 是一个紧凑的数组结构,可以节省内存。
    • 有序集合中的每个元素都有一个分数(score),元素按分数从小到大排序。

5. 哈希表(Hash)

  • 底层数据结构:哈希表(hashtable)或压缩列表(ziplist)
  • 特点:
    • hashtable:当哈希表中的字段数量较多或字段值较大时,Redis 使用哈希表来存储。哈希表提供了平均 O(1) 的插入、删除和查找操作。
    • ziplist:当哈希表中的字段数量较少且每个字段值较小时,Redis 使用压缩列表(ziplist)来存储。ziplist 是一个紧凑的数组结构,可以节省内存。
    • 哈希表中的每个字段(field)和值(value)都是字符串。

6. 位图(Bitmap)

  • 底层数据结构:字符串(String)
  • 特点:
    • 位图实际上是字符串的一个特殊应用,每个字符串的每一位可以被视为一个独立的布尔值。
    • 位图支持高效的位操作,如设置、获取和统计位的个数等。

7. 超时队列(Stream)

  • 底层数据结构:径向树(Radix Tree)或压缩列表(ziplist)
  • 特点:
    • Stream 是 Redis 5.0 引入的一种新的数据类型,用于处理消息队列。
    • Stream 内部使用径向树(Radix Tree)来存储消息,支持高效的范围查询和消息消费。
    • Stream 支持消息的持久化和消费组(consumer group)功能。

7、为什么要用跳表不用红黑树?

1. 实现复杂度

红黑树
  • 复杂度:红黑树的实现相对复杂,需要处理多种旋转和重新着色操作来维持平衡性。插入和删除操作需要考虑多种情况,代码逻辑较为复杂。
  • 调试难度:由于红黑树的操作涉及多个步骤,调试和维护相对困难。
跳表
  • 简单性:跳表的实现相对简单,主要通过多层链表结构和随机化来实现平衡。插入和删除操作的逻辑较为直观,容易理解和实现。
  • 调试容易:跳表的调试相对容易,因为其操作逻辑清晰,易于排查错误。

2. 性能特性

红黑树
  • 时间复杂度:
    • 插入、删除和查找操作的时间复杂度均为 O(log n)。
    • 但由于红黑树的平衡操作涉及多次旋转和重新着色,实际性能可能略低于理论值。
  • 内存使用:每个节点需要额外的指针来记录颜色和父节点信息,内存开销相对较大。
跳表
  • 时间复杂度:
    • 插入、删除和查找操作的平均时间复杂度为 O(log n),最坏情况下也是 O(log n)。
    • 跳表通过多层链表结构和随机化来实现平衡,避免了复杂的旋转操作,实际性能通常较好。
  • 内存使用:跳表的每个节点需要多个指针来维护多层链表,但总体来说,内存开销与红黑树相当或略高一些。

3. 并发性能

红黑树
  • 并发性:红黑树的平衡操作涉及多个节点的修改,这在多线程环境下可能导致较高的锁竞争,影响并发性能。
  • 锁定粒度:为了保证树的平衡性,红黑树通常需要对整个树进行锁定,这会导致较低的并发性能。
跳表
  • 并发性:跳表的多层链表结构使得并发操作更容易实现。可以在不同层级上独立进行操作,减少了锁的竞争。
  • 锁定粒度:跳表可以通过细粒度的锁来实现高并发,每个层级上的操作可以独立进行,减少了全局锁的需求。

4. 应用场景

红黑树
  • 适用场景:红黑树适用于对数据结构的平衡性和性能有严格要求的场景,尤其是在单线程环境中。例如,C++ STL 中的 std::mapstd::set 就是基于红黑树实现的。
跳表
  • 适用场景:跳表适用于需要高效并发操作的场景,尤其是在多线程环境中。例如,Redis 使用跳表来实现有序集合(Sorted Set),因为跳表在并发环境下的表现更好。

5. 随机化和可预测性

红黑树
  • 确定性:红黑树的平衡操作是确定性的,每次插入和删除操作后的树结构是固定的。
  • 可预测性:红黑树的性能和行为是可预测的,适合对性能有严格要求的应用。
跳表
  • 随机化:跳表的层数是通过随机化决定的,这使得跳表的结构具有一定的随机性。
  • 可预测性:虽然跳表的结构具有随机性,但其平均性能仍然可以保证为 O(log n),并且在大多数情况下表现良好。

8、缓存与数据库的一致性?

1. 写穿透(Write Through)

  • 定义:在写操作时,先将数据写入缓存,然后再写入数据库。
  • 优点:
    • 确保数据一致性,因为数据在写入缓存后才会写入数据库。
    • 读取操作可以直接从缓存中获取数据,提高了读取性能。
  • 缺点:
    • 写操作的延迟较高,因为需要两次写操作。
    • 如果缓存或数据库写入失败,可能会导致数据不一致。

2. 写回(Write Back)

  • 定义:在写操作时,只将数据写入缓存,然后在某个时机(如缓存满、定时任务)再将数据写入数据库。
  • 优点:
    • 写操作的延迟较低,因为只需要一次写操作。
    • 减少了数据库的写压力。
  • 缺点:
    • 数据一致性问题,因为数据在缓存和数据库之间存在时间差。
    • 如果缓存故障或数据丢失,可能会导致数据丢失。

3. 读修复(Read Repair)

  • 定义:在读操作时,如果发现缓存中的数据与数据库中的数据不一致,立即更新缓存中的数据。
  • 优点:
    • 确保读取到的数据是最新的。
    • 可以逐步修复缓存中的不一致数据。
  • 缺点:
    • 增加了读操作的复杂性。
    • 可能会导致额外的数据库查询,增加数据库的负担。

4. 消息队列(Message Queue)

  • 定义:使用消息队列来异步同步缓存和数据库。
  • 优点:
    • 解耦了缓存和数据库的操作,提高了系统的可扩展性和可靠性。
    • 可以处理高并发写操作,避免了直接写入缓存和数据库带来的性能瓶颈。
  • 缺点:
    • 增加了系统的复杂性。
    • 需要处理消息队列的可靠性和一致性问题。

5. 版本号或时间戳

  • 定义:在数据项中添加版本号或时间戳,用于判断数据的新旧。
  • 优点:
    • 简单易实现,可以有效防止脏读和脏写。
    • 适用于数据更新频率较低的场景。
  • 缺点:
    • 需要在每次写操作时更新版本号或时间戳。
    • 增加了数据项的存储开销。

6. 事件驱动架构(Event-Driven Architecture)

  • 定义:使用事件驱动的方式,当数据库中的数据发生变化时,触发一个事件,通知缓存进行相应的更新。
  • 优点:
    • 实时性强,可以立即更新缓存中的数据。
    • 解耦了缓存和数据库的操作,提高了系统的可扩展性和可靠性。
  • 缺点:
    • 增加了系统的复杂性。
    • 需要处理事件的可靠性和一致性问题。

7. 缓存失效策略

  • 定义:在数据更新时,立即或延时使缓存中的数据失效。
  • 优点:
    • 确保缓存中的数据不会长期不一致。
    • 简单易实现。
  • 缺点:
    • 可能会导致缓存击穿(大量请求直接打到数据库)。
    • 需要处理缓存失效后的数据加载问题。

8. 缓存预热

  • 定义:在系统启动或数据更新后,主动将数据加载到缓存中。
  • 优点:
    • 避免了缓存击穿问题。
    • 提高了系统的响应速度。
  • 缺点:增加了系统启动时间和数据更新的复杂性。

9、redis过期删除的机制?

Redis 的过期删除机制是为了确保过期的键能够及时从内存中移除,从而释放资源。Redis 采用了两种主要的策略来处理过期键:惰性删除(Lazy Deletion)定期删除(Periodic Deletion)。这两种策略相互补充,共同确保过期键的有效管理和内存的高效利用。

1. 惰性删除(Lazy Deletion)

定义:惰性删除是指在客户端尝试访问一个键时,Redis 才检查该键是否已经过期。如果键已过期,则将其删除。

优点:

  • 简单高效:只有在访问键时才进行检查,减少了不必要的检查操作。
  • 实时性强:确保客户端始终访问到最新的数据。

缺点:

  • 内存占用:过期的键在未被访问之前仍然占用内存。
  • 潜在性能问题:如果大量过期键在同一时间被访问,可能会导致短暂的性能下降。

实现:

  • 当客户端请求一个键时,Redis 会检查该键是否存在。
  • 如果键存在,Redis 会进一步检查该键是否已过期。
  • 如果键已过期,Redis 会删除该键并返回一个表示键不存在的响应。
  • 如果键未过期,Redis 返回键的值。

2. 定期删除(Periodic Deletion)

定义:定期删除是指 Redis 定期检查并删除过期的键。这种检查是周期性的,通常在后台执行,不会影响客户端的正常操作。

优点:

  • 主动管理:定期检查和删除过期键,减少内存占用。
  • 均衡负载:通过周期性检查,避免了大量过期键同时被访问时的性能问题。

缺点:

  • 资源消耗:定期检查会消耗一定的 CPU 和内存资源。
  • 延迟性:过期键可能在检查间隔期间仍然存在于内存中。

实现:

  • Redis 在后台运行一个定时任务,定期检查并删除过期的键。
  • 每次检查时,Redis 会从数据库中随机选择一部分键进行检查。
  • 如果发现某个键已过期,Redis 会立即将其删除。
  • 检查的频率和每次检查的键的数量可以根据配置进行调整。

3. 综合机制

Redis 结合了惰性删除和定期删除两种策略,以实现高效的过期键管理。

流程

  1. 客户端请求:当客户端请求一个键时,Redis 会检查该键是否存在。
  2. 惰性删除:如果键存在,Redis 进一步检查该键是否已过期。如果已过期,Redis 删除该键并返回表示键不存在的响应;如果未过期,返回键的值。
  3. 定期删除:在后台,Redis 定期运行一个定时任务,随机选择一部分键进行检查。如果发现某个键已过期,立即删除该键。

4. 配置参数

Redis 提供了一些配置参数来控制过期键的管理和删除行为:

  • hz:控制 Redis 服务器的事件频率。默认值为 10,表示每秒执行 10 次事件循环。增加 hz 的值可以提高定期删除的频率,但会增加 CPU 负载。
  • active-expire-effort:控制定期删除任务的强度。默认值为 10,表示每次定期删除任务最多检查 10 个过期键。增加这个值可以更快地删除过期键,但会增加 CPU 负载。

5. 示例代码

展示Redis 如何在客户端请求时进行惰性删除:

c
// 检查键是否过期
void checkExpiration(redisDb *db, robj *key) {
    long long expiretime = dictGetInteger(db->expires, key);
    if (expiretime > 0 && expiretime < mstime()) {
        // 键已过期,删除键
        dictDelete(db->dict, key);
        dictDelete(db->expires, key);
    }
}

// 客户端请求键
robj *get(redisDb *db, robj *key) {
    if (dictFind(db->dict, key)) {
        checkExpiration(db, key); // 检查键是否过期
        if (!dictFind(db->dict, key)) {
            // 键已过期并被删除
            return NULL;
        }
        return dictFetchValue(db->dict, key);
    }
    return NULL;
}

10、tcp与udp的区别?

1. 连接性

  • TCP:面向连接的协议。在发送数据之前,必须先建立连接(通过三次握手),确保通信的可靠性。
  • UDP:无连接的协议。在发送数据之前不需要建立连接,数据可以直接发送。

2. 可靠性

  • TCP:提供可靠的数据传输服务。通过序列号、确认应答(ACK)、重传机制等手段,确保数据的正确到达。
  • UDP:不提供可靠性保证。数据包可能会丢失、重复或乱序,接收方无法确认数据是否正确到达。

3. 数据传输方式

  • TCP:以字节流的形式传输数据,数据的边界不明确。应用层需要根据实际需求进行数据分割。
  • UDP:以数据报的形式传输,保留了应用层的消息边界。每个UDP数据报是独立的。

4. 流量控制和拥塞控制

  • TCP:实现流量控制和拥塞控制,能够自动调节数据的发送速度,以防止网络拥塞。
  • UDP:没有流量控制和拥塞控制机制,发送数据的速度不受限制,可能导致网络拥塞。

5. 头部开销

  • TCP:头部开销较大,通常为20字节,包含序列号、确认号、标志位、窗口大小等信息。
  • UDP:头部开销较小,只有8字节,包含源端口、目的端口、长度和校验和。

6. 传输速度

  • TCP:由于其可靠性和流量控制机制,传输速度相对较慢。
  • UDP:由于缺少连接建立和确认机制,传输速度较快,适合对速度要求高的应用。

7. 适用场景

  • TCP:
    • 文件传输(如FTP)
    • 电子邮件(如SMTP、POP3)
    • 网页浏览(HTTP、HTTPS)
    • 任何需要可靠传输的场景
  • UDP:
    • 视频会议、直播(如RTSP、RTP)
    • 在线游戏
    • DNS查询
    • 实时语音通话(VoIP)
    • 适合对速度和时延要求高但对丢包不敏感的场景

8. 流量控制机制

  • TCP:采用滑动窗口机制,动态调节数据的发送量。
  • UDP:没有流量控制,数据包按需发送。

9. 拥塞控制机制

  • TCP:使用慢启动、拥塞避免、快速重传等算法,动态调整发送速度。
  • UDP:不考虑拥塞,不做任何限制。

11、https与http的区别,https的加密机制,公钥、私钥?

1. 安全性

HTTP
  • 不安全:HTTP 是明文传输协议,数据在传输过程中不进行加密。这意味着数据可以被中间人截获、篡改或窃听,存在安全风险。
  • 数据完整性:HTTP 不提供数据完整性保护,数据在传输过程中可能会被篡改。
HTTPS
  • 安全:HTTPS 是基于 SSL/TLS(安全套接层/传输层安全)协议的加密传输协议,确保数据在传输过程中的安全性和隐私性。
  • 数据加密:HTTPS 使用对称加密和非对称加密相结合的方式,确保数据在传输过程中不被窃听或篡改。
  • 数据完整性:HTTPS 提供数据完整性保护,确保数据在传输过程中不被篡改。

2. 加密机制

HTTP

无加密:HTTP 不使用任何加密机制,数据以明文形式传输。

HTTPS

SSL/TLS 加密:

  • 握手过程:HTTPS 在建立连接时会进行 SSL/TLS 握手,协商加密算法和密钥。
  • 非对称加密:在握手过程中,客户端和服务器使用非对称加密(公钥和私钥)来交换会话密钥。
  • 对称加密:握手完成后,客户端和服务器使用对称加密(共享密钥)来加密和解密数据,提高传输效率。
  • 证书验证:服务器提供数字证书,客户端验证证书的有效性,确保与合法服务器通信。

3. URL 前缀

HTTP

前缀:HTTP URL 以 http:// 开头。

HTTPS

前缀:HTTPS URL 以 https:// 开头。

4. 端口号

HTTP

默认端口:HTTP 使用 80 端口。

HTTPS

默认端口:HTTPS 使用 443 端口。

5. 性能

HTTP
  • 较低延迟:HTTP 不进行加密和解密操作,因此在传输速度上通常比 HTTPS 快。
  • 资源消耗:HTTP 不需要额外的计算资源来处理加密和解密操作。
HTTPS
  • 较高延迟:HTTPS 需要进行 SSL/TLS 握手和数据的加密解密操作,增加了初始连接的延迟。
  • 资源消耗:HTTPS 需要额外的计算资源来处理加密和解密操作,但现代硬件和优化算法已经大大降低了这一开销。

6. 适用场景

HTTP
  • 非敏感数据:适用于传输非敏感数据,如公开的网页内容、静态资源等。
  • 低延迟要求:适用于对延迟敏感的应用,如实时视频流传输。
HTTPS
  • 敏感数据:适用于传输敏感数据,如登录凭据、支付信息、个人隐私等。
  • 安全要求高:适用于需要高度安全性的应用,如电子商务网站、银行系统、政府网站等。

7. 浏览器支持

HTTP

所有浏览器都支持 HTTP 协议。

HTTPS

所有现代浏览器都支持 HTTPS 协议,并且越来越多的浏览器开始标记 HTTP 网站为“不安全”。

8. 搜索引擎排名

HTTP

无影响:搜索引擎对 HTTP 网站的排名没有特别的偏好。

HTTPS

排名提升:Google 等搜索引擎倾向于优先推荐 HTTPS 网站,因为它们更安全。

12、写sql,一个用户表,一个小组表,一个关系表,查找年龄大于 20 岁,小组名称为 a 的,男性的用户的名字?

表结构

  1. users 表
    • id (主键,用户 ID)
    • name (用户名)
    • age (年龄)
    • gender (性别,假设用 'M' 表示男性,'F' 表示女性)
  2. groups 表
    • id (主键,小组 ID)
    • name (小组名称)
  3. user_group_relations 表
    • user_id (外键,用户 ID)
    • group_id (外键,小组 ID)

SQL 查询

我们需要通过以下步骤来构建查询:

  1. 连接用户表和关系表:通过 user_idusers 表和 user_group_relations 表连接起来。
  2. 连接关系表和小组表:通过 group_iduser_group_relations 表和 groups 表连接起来。
  3. 过滤条件:
    • 年龄大于 20 岁
    • 小组名称为 "a"
    • 性别为男性

SQL 查询语句

sql
SELECT u.name
FROM users u
JOIN user_group_relations ugr ON u.id = ugr.user_id
JOIN groups g ON ugr.group_id = g.id
WHERE u.age > 20
  AND g.name = 'a'
  AND u.gender = 'M';

13、lc三数之和

思路

  1. 排序:首先对数组进行排序,这样可以方便地使用双指针法来查找三元组。
  2. 遍历数组:使用一个指针 i 遍历数组,固定一个数 nums[i]
  3. 双指针:对于每个固定的 nums[i],使用两个指针 leftright 分别从 i+1 和数组末尾向中间移动,寻找满足 nums[i] + nums[left] + nums[right] == 0 的三元组。
  4. 去重:
    • 跳过相同的 nums[i],避免重复的三元组。
    • 在找到一个三元组后,跳过相同的 nums[left]nums[right],避免重复的三元组。

参考代码

c++
#include <vector>
#include <algorithm>
#include <iostream>

class Solution {
public:
    std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
        std::vector<std::vector<int>> result;
        int n = nums.size();
        
        // 1. 排序
        std::sort(nums.begin(), nums.end());
        
        // 2. 遍历数组
        for (int i = 0; i < n - 2; ++i) {
            // 3. 跳过相同的 nums[i],避免重复的三元组
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            // 4. 双指针
            int left = i + 1, right = n - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                
                if (sum == 0) {
                    // 5. 找到一个三元组,加入结果
                    result.push_back({nums[i], nums[left], nums[right]});
                    
                    // 6. 跳过相同的 nums[left] 和 nums[right],避免重复的三元组
                    while (left < right && nums[left] == nums[left + 1]) {
                        ++left;
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        --right;
                    }
                    
                    // 7. 移动指针
                    ++left;
                    --right;
                } else if (sum < 0) {
                    // 8. 如果和小于 0,移动左指针
                    ++left;
                } else {
                    // 9. 如果和大于 0,移动右指针
                    --right;
                }
            }
        }
        
        return result;
    }
};

int main() {
    // 读取输入
    int n;
    cin >> n;
    vector<int> nums(n);
    for (int i = 0; i < n; ++i) {
        cin >> nums[i];
    }

    // 创建 Solution 对象并调用 threeSum 方法
    Solution sol;
    vector<vector<int>> result = sol.threeSum(nums);

    // 输出结果
    for (const auto& triplet : result) {
        cout << '[';
        for (size_t j = 0; j < triplet.size(); ++j) {
            cout << triplet[j];
            if (j < triplet.size() - 1) {
                cout << ',';
            }
        }
        cout << ']';
        if (&triplet != &result.back()) {
            cout << ' ';
        }
    }
    cout << endl;

    return 0;
}