米哈游C++后端开发一面凉经,体验很好,但还是挂了。。。
来源:https://www.nowcoder.com/feed/main/detail/ff8e136f24c24a3e85a227c6098f8973
1、C++面向对象的三个特征?
1、封装
封装是指将数据(属性)和操作数据的方法(函数)封装在一个单元中,这个单元就是类。封装的主要目的是隐藏类的内部实现细节,只暴露必要的接口给外部使用者。
优点:
- 信息隐藏: 封装可以将类的内部细节隐藏起来,不暴露给外部,提高了安全性和防止误用。
- 简化接口: 封装通过提供清晰的接口简化了类的使用,使用者只需关注如何使用接口而不需要了解内部实现。
- 提高可维护性: 内部实现的修改不会影响外部使用者,从而提高了代码的可维护性。
#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、继承
继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,子类可以获得父类的特征,并可以添加新的特征或修改继承的特征。
优点:
- 代码重用: 继承允许在不重复编写代码的情况下扩展和修改现有类,提高了代码的重用性。
- 层次结构: 继承可以创建类的层次结构,使得代码更有组织性和可扩展性。
- 多态性支持: 继承是多态性的基础,通过基类指针或引用调用派生类的方法实现多态行为。
#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、多态
多态性是指同一个操作可以作用于不同类型的对象,并且可以根据对象的类型执行不同的行为。多态性通过虚函数和函数重载实现。
- 编译时多态性(静态多态性): 通过函数重载实现,编译器在编译时根据函数参数的类型和数量来选择调用合适的函数。这种多态性是在编译时解析的。
- 运行时多态性(动态多态性): 通过虚函数和继承实现,允许在运行时根据对象的实际类型来调用适当的函数。这种多态性是在运行时解析的。
优点:
- 灵活性: 多态性允许在不同的情境下以通用的方式处理不同类型的对象,提高了代码的灵活性。
- 可扩展性: 可以轻松地添加新的派生类而不影响现有的代码,增加了系统的可扩展性。
- 简化接口: 多态性简化了代码的接口,允许使用者按统一的方式与不同类型的对象交互。
#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_back和pop_back方法添加和移除元素。
- 缺点:
- 在中间插入或删除元素的性能较差(O(n)),因为需要移动后续元素。
- 扩容时可能会导致内存重新分配和元素复制。
适用场景
当需要频繁访问元素或在末尾添加/删除元素时,使用 vector 是理想选择。
2. list
概述
list是一个双向链表,允许在任意位置快速插入和删除元素。
特点
- 节点不连续:每个元素(节点)在内存中可以不连续存储,每个节点包含指向前后节点的指针。
- 双向访问:可以从任意节点向前或向后遍历,支持常数时间复杂度的插入和删除。
- 迭代器支持:支持双向迭代器,允许从任意位置遍历。
优缺点
- 优点:
- 在中间插入和删除元素效率高(O(1)),只需调整指针。
- 适合需要频繁插入/删除的场景。
- 缺点:
- 随机访问性能差(O(n)),因为必须从头节点或尾节点遍历链表。
- 内存使用效率低,因每个节点都需要额外存储指针。
适用场景
当需要频繁在任意位置插入和删除元素,而不关心随机访问时,使用 list 是合适的。
vector扩容机制?
1. 初始状态
当创建一个 vector 时,它会有一个初始容量(通常是0或其他小值),并在首次插入元素时分配一定的内存。
2. 添加元素
- 使用
push_back或其他插入方法添加元素时,vector会首先检查当前容量是否足够。 - 如果当前容量足够,直接在数组末尾添加元素。
3. 扩容触发
- 当添加的元素超出当前容量时,
vector会进行扩容。 - 扩容的过程包括:
- 分配新内存:通常新容量为当前容量的2倍(或更高),以减少频繁的内存分配。
- 复制元素:将现有元素从旧数组复制到新数组。
- 释放旧内存:释放旧数组的内存。
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::map | std::unordered_map | std::set | std::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)
- 节点结构:除了普通二叉搜索树的节点属性外,每个节点还包含一个颜色属性(红色或黑色)。
- 搜索性质:与普通二叉搜索树相同,满足二叉搜索树的性质。
- 平衡性:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,空节点)是黑色。
- 如果一个节点是红色的,则它的两个子节点必须是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑高相等)。
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。
- SDS 是 Redis 自己实现的动态字符串,类似于 C 语言中的
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::map和std::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 结合了惰性删除和定期删除两种策略,以实现高效的过期键管理。
流程:
- 客户端请求:当客户端请求一个键时,Redis 会检查该键是否存在。
- 惰性删除:如果键存在,Redis 进一步检查该键是否已过期。如果已过期,Redis 删除该键并返回表示键不存在的响应;如果未过期,返回键的值。
- 定期删除:在后台,Redis 定期运行一个定时任务,随机选择一部分键进行检查。如果发现某个键已过期,立即删除该键。
4. 配置参数
Redis 提供了一些配置参数来控制过期键的管理和删除行为:
hz:控制 Redis 服务器的事件频率。默认值为 10,表示每秒执行 10 次事件循环。增加hz的值可以提高定期删除的频率,但会增加 CPU 负载。active-expire-effort:控制定期删除任务的强度。默认值为 10,表示每次定期删除任务最多检查 10 个过期键。增加这个值可以更快地删除过期键,但会增加 CPU 负载。
5. 示例代码
展示Redis 如何在客户端请求时进行惰性删除:
// 检查键是否过期
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 的,男性的用户的名字?
表结构
- users 表
id(主键,用户 ID)name(用户名)age(年龄)gender(性别,假设用 'M' 表示男性,'F' 表示女性)
- groups 表
id(主键,小组 ID)name(小组名称)
- user_group_relations 表
user_id(外键,用户 ID)group_id(外键,小组 ID)
SQL 查询
我们需要通过以下步骤来构建查询:
- 连接用户表和关系表:通过
user_id将users表和user_group_relations表连接起来。 - 连接关系表和小组表:通过
group_id将user_group_relations表和groups表连接起来。 - 过滤条件:
- 年龄大于 20 岁
- 小组名称为 "a"
- 性别为男性
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三数之和
思路
- 排序:首先对数组进行排序,这样可以方便地使用双指针法来查找三元组。
- 遍历数组:使用一个指针
i遍历数组,固定一个数nums[i]。 - 双指针:对于每个固定的
nums[i],使用两个指针left和right分别从i+1和数组末尾向中间移动,寻找满足nums[i] + nums[left] + nums[right] == 0的三元组。 - 去重:
- 跳过相同的
nums[i],避免重复的三元组。 - 在找到一个三元组后,跳过相同的
nums[left]和nums[right],避免重复的三元组。
- 跳过相同的
参考代码
#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;
}