Skip to content

腾讯WXG一面挂

来源:https://www.nowcoder.com/discuss/524584485535432704?sourceSSR=users

算法

1、找到链表环的入口,不存在返回nullptr(找到相交点,怎么计算相交点到入口的距离)(只找到了相交点)

思路:

  1. 定义两个指针,一个快指针 fast,一个慢指针 slow,初始都指向链表的头节点 head
  2. 使用循环,每次循环中快指针 fast 前进两步,慢指针 slow 前进一步,直到两个指针相遇。如果快指针 fast 遇到了空节点(链表无环),则说明链表无环,返回 nullptr
  3. 如果快慢指针相遇,说明链表中存在环。此时,将快指针 fast 指向链表头节点 head,然后快慢指针同时以每次前进一步的速度移动,直到两个指针再次相遇。
  4. 当两个指针再次相遇时,相遇的节点即为环的入口节点。

参考代码:

c++
#include <iostream>

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

ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head;
    ListNode *fast = head;

    // 判断是否存在环
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            break;
        }
    }

    // 如果没有环,返回 nullptr
    if (fast == nullptr || fast->next == nullptr) {
        return nullptr;
    }

    // 找到环的入口节点
    fast = head;
    while (fast != slow) {
        fast = fast->next;
        slow = slow->next;
    }

    return fast;
}

int main() {
    // 测试示例
    ListNode *head = new ListNode(3);
    head->next = new ListNode(2);
    head->next->next = new ListNode(0);
    head->next->next->next = new ListNode(-4);
    head->next->next->next->next = head->next;

    ListNode *result = detectCycle(head);
    if (result != nullptr) {
        std::cout << "Cycle starts at node with value: " << result->val << std::endl;
    } else {
        std::cout << "No cycle found." << std::endl;
    }

    return 0;
}

2、最近公共父节点

题主没注明是哪个父节点,这里就说一下二叉搜索树的吧。

思路:

  1. 从根节点开始遍历二叉搜索树。
  2. 对于当前节点 root,比较其值与节点 p 和 q 的值的大小关系。
    • 如果 p 和 q 的值都小于 root 的值,说明 p 和 q 都在 root 的左子树中,此时继续搜索 root 的左子树。
    • 如果 p 和 q 的值都大于 root 的值,说明 p 和 q 都在 root 的右子树中,此时继续搜索 root 的右子树。
    • 如果 p 和 q 的值分别位于 root 的左右子树中,或者其中一个节点的值就是 root 的值,说明 root 就是最近公共祖先。
  3. 重复上述步骤,直到找到最近公共祖先为止。

参考代码:

c++
#include <iostream>

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

class Solution {
public:
    int lowestCommonAncestor(TreeNode* root, int p, int q) {
        while (root) {
            if (p < root->val && q < root->val) {
                root = root->left;
            } else if (p > root->val && q > root->val) {
                root = root->right;
            } else {
                return root->val;
            }
        }
        return -1; // 如果树为空,或者 p、q 不在树中,返回 -1
    }
};

int main() {
    // 创建一个二叉搜索树
    TreeNode* root = new TreeNode(6);
    root->left = new TreeNode(2);
    root->right = new TreeNode(8);
    root->left->left = new TreeNode(0);
    root->left->right = new TreeNode(4);
    root->right->left = new TreeNode(7);
    root->right->right = new TreeNode(9);
    root->left->right->left = new TreeNode(3);
    root->left->right->right = new TreeNode(5);

    // 创建解决方案对象
    Solution solution;

    // 寻找最近公共祖先
    int p = 2;
    int q = 8;
    int ancestor = solution.lowestCommonAncestor(root, p, q);
    if (ancestor != -1) {
        std::cout << "Lowest Common Ancestor: " << ancestor << std::endl;
    } else {
        std::cout << "No Common Ancestor Found" << std::endl;
    }

    return 0;
}

3、最长回文子串

思路:

  1. 初始化 dp[i][i] = true,表示单个字符一定是回文子串。
  2. 初始化 maxLen = 1,用于记录最长回文子串的长度。
  3. 从长度为 2 的子串开始,遍历字符串 A,计算 dp[i][j]
    • 如果 A[i] == A[j]j - i + 1 > maxLen,则更新 maxLen = j - i + 1
    • 如果 A[i] == A[j]dp[i+1][j-1] 为真,则 dp[i][j] = true,否则为假。
  4. 最终返回 maxLen

参考代码:

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

class Solution {
public:
    int longestPalindromeSubseq(std::string A) {
        int n = A.size();
        std::vector<std::vector<bool>> dp(n, std::vector<bool>(n, false));
        int maxLen = 1;

        // 单个字符一定是回文子串
        for (int i = 0; i < n; ++i) {
            dp[i][i] = true;
        }

        // 计算长度为 2 到 n 的子串
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i < n - len + 1; ++i) {
                int j = i + len - 1;
                if (A[i] == A[j]) {
                    if (len == 2 || dp[i + 1][j - 1]) {
                        dp[i][j] = true;
                        maxLen = std::max(maxLen, len);
                    }
                }
            }
        }

        return maxLen;
    }
};

int main() {
    Solution solution;
    std::string A = "babad";
    std::cout << solution.longestPalindromeSubseq(A) << std::endl;
    return 0;
}

八股

1、MySQL引擎的区别(Mylsam,Innodb)

  1. 事务支持:InnoDB 支持事务,而 MyISAM 不支持事务。因此,如果你的应用需要使用事务来确保数据的一致性和完整性,应该选择 InnoDB。
  2. 锁级别:InnoDB 支持行级锁和表级锁,而 MyISAM 只支持表级锁。这意味着在高并发情况下,InnoDB 能够提供更好的并发性能和更低的锁冲突。
  3. 外键约束:InnoDB 支持外键约束,而 MyISAM 不支持。这意味着使用 InnoDB 可以更容易地维护数据的完整性和一致性。
  4. 并发性能:由于 InnoDB 支持行级锁,因此在高并发环境下通常具有更好的性能表现。而 MyISAM 在大多数情况下只能通过表级锁来控制并发访问,因此并发性能相对较差。
  5. 备份和恢复:InnoDB 支持在线备份和恢复,而 MyISAM 不支持。这意味着在使用 InnoDB 时可以更方便地进行备份和恢复操作。
  6. 全文搜索:MyISAM 支持全文搜索功能,而 InnoDB 不支持。如果你需要在 MySQL 中进行全文搜索,可能需要考虑使用 MyISAM 引擎。

2、频繁的插入时Innodb的性能更好么(一次可以读到内存里更多)

InnoDB 在处理频繁插入时通常比 MyISAM 更好,尤其在高并发的情况下。这是因为 InnoDB 支持行级锁和事务,这使得它能够更好地处理并发插入操作,避免了对整个表的锁定,提高了并发性能。另外,InnoDB 的行级锁定机制也使得它在处理大量并发插入时能够更有效地处理锁冲突,从而提高了整体的性能表现。

然而,性能优势并不是绝对的,它还取决于具体的使用场景和配置。在一些特定的情况下,例如需要频繁执行大量的全表扫描或者只进行单纯的插入操作时,MyISAM 也可能表现得更好一些。

3、Innodb写入时一定是往后追加么,在中间插入效率会更高么,为什么不用其他引擎

InnoDB 存储引擎在处理插入时并不总是往后追加,它使用了一种称为聚集索引(clustered index)的技术,这种索引会将数据按照主键的顺序存储在磁盘上。当你在一个有聚集索引的表中插入新的记录时,InnoDB 会根据主键的顺序来确定新记录应该存储的位置。

在中间插入数据时,如果插入的位置恰好是已有数据的中间位置,由于 InnoDB 的聚集索引会对数据进行排序存储,可能需要进行页面的拆分和合并操作,这可能会影响插入的性能。不过,在实际的生产环境中,通常会根据具体的应用需求来选择合适的存储引擎。

除了 InnoDB,MySQL 还有其他的存储引擎,比如 MyISAM、Memory 等。每种存储引擎都有其适用的场景和特点。MyISAM 在处理大量的插入操作时可能会比 InnoDB 更快,因为它不支持事务和行级锁,而且在表级别上进行锁定。但是,MyISAM 不支持事务和崩溃恢复,因此在需要事务支持和数据完整性的场景下,还是推荐使用 InnoDB 存储引擎。

4、为什么要用B+树(和B树、线性存储对比了一下,内存能读更多)

  1. 范围查询效率高: B+树的叶子节点形成了一个有序链表,因此对于范围查询,只需要遍历这个链表即可,效率较高。
  2. 支持多种查询: B+树可以高效地支持等值查询、范围查询等多种查询操作。
  3. 磁盘IO少: B+树的高度相对较低,因此检索数据时需要的磁盘IO次数相对较少,性能较好。
  4. 顺序访问性能好: B+树的叶子节点形成了有序链表,支持顺序访问,对于需要按顺序访问大量数据的查询,性能较好。

5、unordered_map底层实现,怎么解决hash冲突,stl中使用的哪个,扩容机制(一次探测、二次探测、链表法)

std::unordered_map 是 C++ 标准库提供的一种哈希表实现的关联容器,用于存储键值对。它基于哈希表的数据结构,通过哈希函数将键映射到哈希表的位置,以实现快速的数据查找、插入和删除操作。以下是 std::unordered_map 底层实现的一般步骤:

  1. 哈希函数:std::unordered_map 使用哈希函数将键映射到哈希表的位置。哈希函数应该尽可能均匀地将不同的键映射到哈希表的不同位置,以减少冲突的发生。C++ 标准库提供了默认的哈希函数,也允许用户自定义哈希函数。
  2. 哈希表:std::unordered_map 内部使用哈希表来存储键值对。哈希表通常是一个数组,每个元素称为一个桶(bucket)。当插入或查找一个键值对时,哈希函数计算键的哈希值,并将其映射到哈希表的一个桶中。
  3. 冲突处理:由于哈希函数的映射不一定是唯一的,可能会导致不同的键映射到同一个桶中,即发生了冲突。std::unordered_map 通常使用链地址法(Chaining)来处理冲突。在发生冲突时,新的键值对会被插入到该桶的链表中。因此,每个桶实际上是一个链表的头指针,指向一个链表,存储了映射到同一桶的多个键值对。
  4. 负载因子和重新哈希:哈希表的负载因子是指哈希表中存储的键值对数量与桶的数量之比。当负载因子超过某个阈值时,std::unordered_map 会触发重新哈希(rehashing)操作,即重新分配更大的存储空间,并重新将所有的键值对插入到新的哈希表中。这样可以保持哈希表的性能,避免出现过多的冲突。
  5. 扩容和缩容:在重新哈希时,std::unordered_map 会根据需要动态调整哈希表的大小,以适应存储的键值对数量的变化。当哈希表需要扩容时,会分配更大的内存空间,并重新计算每个键的哈希值,将键值对重新插入到新的哈希表中。反之,当负载因子较小时,也可以考虑缩小哈希表的大小,以节省内存空间。

6、你理解的右值

右值(rvalue)是 C++ 中的一个术语,指的是表达式结束后产生的临时数值或对象,通常是无法被修改的临时值。在 C++ 中,表达式可以分为左值(lvalue)和右值(rvalue)。

  1. 左值(lvalue):指的是可以被取地址的表达式,通常是具有持久性的对象。例如,变量名、数组元素、返回引用的函数调用等都是左值。
  2. 右值(rvalue):指的是不能被取地址的表达式,通常是临时的、一次性的值。例如,常量、字面量、表达式的计算结果等都是右值。

在 C++11 中引入了右值引用(rvalue reference)的概念,用于标识一个对象是右值。右值引用可以通过 && 符号声明,通常用于移动语义和完美转发等场景,例如移动构造函数和移动赋值运算符中使用。

7、为什么会引入完美转发

完美转发(Perfect Forwarding)是 C++11 中引入的一个特性,用于在函数模板中保持传递参数的原始类型及其引用类型。这个特性的引入主要是为了解决函数模板和泛型编程中的参数传递问题。

在 C++ 中,通常情况下,函数参数的传递会发生拷贝,这可能会导致性能上的损失,特别是对于大型对象或者需要频繁传递的对象。为了解决这个问题,C++11 引入了右值引用和移动语义,但在一些情况下,我们希望能够保持传递参数的原始类型及其引用类型,这就是完美转发所解决的问题。

完美转发的主要目的是解决模板函数在接受参数时的参数类型保持问题。在函数模板中,我们可能会希望将参数原封不动地传递给其他函数,而不希望发生额外的拷贝或者参数类型的改变。通过完美转发,可以将参数以原始类型及其引用类型进行传递,保持了参数的原始特性。

完美转发通常与 std::forward 结合使用,std::forward 是一个用于转发参数的模板函数,能够将参数以原始类型及其引用类型进行转发。在实现模板函数时,通过使用 std::forward 可以保持参数的原始特性,实现了完美转发的效果。

8、为什么redis很快,怎么实现的(单进程+epoll)

  1. 内存存储:Redis 是基于内存存储的键值数据库,数据都存储在内存中,这使得它具有非常高的读写速度。内存的访问速度远远快于磁盘,因此 Redis 能够快速地读取和写入数据。
  2. 单线程模型:Redis 采用单线程模型,通过使用非阻塞 I/O 和事件驱动等技术,能够在单线程下处理大量并发请求。这种设计能够避免多线程之间的上下文切换和锁竞争,提高了系统的并发能力。
  3. 简单的数据结构:Redis 提供了丰富的数据结构,如字符串、哈希表、列表、集合、有序集合等,这些数据结构都经过优化,使得其操作在时间复杂度上都能够达到 O(1) 或者接近 O(1),这样就能够快速地进行数据操作。
  4. 持久化选项:Redis 提供了多种持久化选项,可以将数据持久化到磁盘中,以防止数据丢失。在持久化方面,Redis 有两种主要的方式:RDB(快照)和 AOF(追加写入文件),它们都有各自的优势,可以根据实际需求选择合适的方式。
  5. 优化的网络通信:Redis 使用了高效的网络通信协议,如 RESP(Redis Serialization Protocol),它是一种文本协议,能够减少网络传输的数据量,提高了网络通信的效率。
  6. 丰富的功能:Redis 不仅提供了基本的键值存储功能,还支持发布订阅、事务、Lua 脚本等高级功能,这些功能使得 Redis 在处理各种应用场景时都能够保持高效。

9、redis是线程安全的么

Redis 是单线程的,这意味着 Redis 在任何时候都只会执行一个操作,不会发生多个线程同时访问共享数据的情况。因此,从并发访问的角度来说,Redis 是线程安全的。Redis 通过使用事件驱动模型和非阻塞 I/O 来处理并发请求,从而实现了高效的并发处理能力。虽然 Redis 是单线程的,但是它可以通过多路复用技术同时处理多个连接,因此可以支持大量的并发请求。

10、怎么实现的分布式锁

11、Redis的数据结构,底层分别是怎么实现的

Redis是一个基于内存的键值存储系统,它支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。这些数据结构在底层都是通过数据结构来实现的,不同的数据结构有不同的底层实现方式。

  1. 字符串(String):

    Redis的字符串是简单的动态字符串,底层实现是使用C语言的char*来存储字符串内容,并且可以动态扩展内存以存储变长字符串。

  2. 哈希(Hash):

    Redis的哈希是一个键值对集合,底层实现是使用哈希表(hash table),它将键映射到值,通过哈希函数计算键的哈希值,然后将键值对存储在哈希表中。在哈希冲突时,通常使用链表或者红黑树来解决。

  3. 列表(List):

    Redis的列表是一个有序的字符串列表,底层实现是使用双向链表(doubly linked list),它允许在列表的两端进行快速插入和删除操作。

  4. 集合(Set):

    Redis的集合是一个无序的字符串集合,底层实现是使用哈希表(hash table),它利用哈希函数将元素映射到哈希表中,实现了快速的查找和插入操作。

  5. 有序集合(Sorted Set):

    Redis的有序集合是一个有序的字符串集合,底层实现是使用跳跃表(skip list)和哈希表(hash table)的混合结构,跳跃表用来维护元素的有序性,哈希表用来实现快速的查找操作。

12、zlist的底层原理,数据是怎么压缩的

在 Redis 中,ZSET 或称为有序集合(Sorted Set)是一种特殊的数据结构,它类似于集合(Set),但每个成员都关联了一个分数(score),通过分数对成员进行排序。有序集合的底层实现通常采用跳跃表(Skip List)和哈希表(Hash Table)的结合。

  1. 跳跃表(Skip List):

    跳跃表是一种有序数据结构,它通过多级索引来加速查找。每个节点包含一个元素和若干个指向其他节点的指针,这些指针构成了多级索引。在有序集合中,跳跃表可以根据成员的分数快速定位到对应的节点,从而实现快速的插入、删除和查找操作。

  2. 哈希表(Hash Table):

    哈希表用于存储成员和分数的映射关系,它将成员作为键,分数作为值,通过哈希函数计算键的哈希值,并将键值对存储在哈希表中。哈希表可以实现快速的查找和更新操作,用于支持有序集合的元素唯一性和快速查找。

在有序集合中,跳跃表和哈希表相结合,跳跃表用于维护成员的有序性,哈希表用于存储成员和分数的映射关系。这样的设计使得有序集合既能够保持元素的有序性,又能够实现快速的查找和更新操作。

关于数据的压缩,Redis 在内部对数据进行了一定程度的压缩和优化,以减少内存占用和提高性能。具体的压缩方式取决于数据的类型和存储引擎的实现细节。例如,在有序集合中,可以通过对成员和分数进行编码来减少存储空间,或者使用特定的数据结构来存储稀疏的有序集合,以节省内存空间。Redis 的存储引擎会根据具体的数据特点和配置参数来选择合适的压缩方式和优化策略。

13、进程间通讯方式(管道、共享内存、消息队列、信号)

  1. 管道(Pipe):

    管道是一种半双工的通讯方式,它可以在具有亲缘关系的进程之间进行通讯。管道有两种类型:匿名管道和命名管道。匿名管道只能用于父子进程之间的通讯,而命名管道可以用于任意两个进程之间的通讯。

  2. 共享内存(Shared Memory):

    共享内存是一种高效的进程间通讯方式,它允许多个进程共享同一块内存区域,从而实现快速的数据交换。共享内存通常需要配合信号量等同步机制来确保数据的一致性和安全性。

  3. 消息队列(Message Queue):

    消息队列是一种消息传递机制,它允许进程通过向队列发送消息来进行通讯。消息队列可以实现进程间的异步通讯,不同进程之间通过读写消息队列来进行数据交换。

  4. 信号(Signal):

    信号是一种异步通知机制,它用于通知进程发生了某种事件。进程可以通过系统调用来发送和接收信号,从而实现进程间的通讯和同步。

  5. 套接字(Socket):

    套接字是一种网络编程接口,它可以用于在不同主机上的进程之间进行通讯。套接字通常用于实现跨网络的进程间通讯,它可以在不同主机之间传输数据。

  6. 信号量(Semaphore):

    信号量是一种计数器,它用于控制多个进程对共享资源的访问。信号量通常用于解决进程间的同步和互斥问题,确保多个进程能够安全地访问共享资源。

14、管道怎么用,数据写到哪里(内核)

管道(Pipe)是一种进程间通信的方式,它可以在具有亲缘关系的进程之间进行通信。管道有两种类型:匿名管道和命名管道。这里我将介绍匿名管道的使用方法。

匿名管道是一种单向通道,数据只能从一个方向流动,通常用于父子进程之间的通信。在Unix/Linux系统中,可以使用pipe系统调用创建匿名管道,它返回两个文件描述符,一个用于读取,一个用于写入。在创建管道后,父子进程可以通过文件描述符进行数据的读取和写入。

给个简单的例子:

c++
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buf[1024];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();

    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {  // 子进程
        // 关闭管道的写入端
        close(pipefd[1]);

        // 从管道中读取数据
        read(pipefd[0], buf, sizeof(buf));
        printf("Child process received: %s\n", buf);

        // 关闭管道的读取端
        close(pipefd[0]);
    } else {  // 父进程
        // 关闭管道的读取端
        close(pipefd[0]);

        // 向管道中写入数据
        write(pipefd[1], "Hello, pipe!", 12);

        // 关闭管道的写入端
        close(pipefd[1]);
    }

    return 0;
}

15、进程切换

进程切换是操作系统在多任务环境下进行进程调度的过程。当操作系统需要切换到另一个进程时,它会暂停当前进程的执行,并保存当前进程的状态(比如程序计数器、寄存器状态、堆栈指针等)到进程控制块(Process Control Block,PCB)中,然后选择下一个要执行的进程,并加载其状态,使其成为当前正在执行的进程。进程切换是操作系统实现多任务的重要机制,它可以使得多个进程共享CPU资源,从而实现并发执行。

进程切换涉及到以下几个步骤:

  1. 保存当前进程的状态:

    当操作系统决定切换到另一个进程时,首先需要保存当前进程的状态,包括程序计数器、寄存器状态、堆栈指针等。这些状态信息通常保存在进程控制块(PCB)中。

  2. 选择下一个要执行的进程:

    在保存当前进程的状态后,操作系统会根据调度算法选择下一个要执行的进程。调度算法的选择可能基于优先级、时间片轮转、多级反馈队列等策略。

  3. 加载下一个进程的状态:

    一旦确定了下一个要执行的进程,操作系统会从其对应的进程控制块中加载其状态信息,包括程序计数器、寄存器状态、堆栈指针等。这样,CPU就会开始执行新进程的代码。

  4. 切换到用户态:

    在完成状态的加载后,操作系统将CPU的执行模式从内核态切换到用户态,使得新进程可以开始执行用户态的代码。

16、电脑上多个app如何保证内存够用(虚拟内存、页表换入换出)

  1. 虚拟内存:

    虚拟内存是一种将硬盘空间用作内存扩展的技术。当物理内存不足时,操作系统会将部分不常用的内存数据暂时存储到硬盘上,从而释放物理内存。这样可以扩展可用内存的容量,但需要注意虚拟内存的性能会比物理内存差很多。

  2. 页表换入换出:

    在使用虚拟内存的系统中,操作系统会维护一个页表来管理虚拟地址和物理地址之间的映射关系。当内存不足时,操作系统会根据一定的算法将部分物理内存中不常用的页面(Page)换出到硬盘上,从而释放物理内存空间;而当需要访问已经换出的页面时,操作系统会根据需要将其换入到物理内存中。这个过程称为页表换入换出(Page In/Page Out)。

  3. 内存管理策略:

    操作系统会根据内存管理策略来决定哪些页面需要被换出,以及何时将其换出。常见的内存管理策略包括页面置换算法(如最近最少使用算法 LRU)、工作集算法等,它们可以根据页面的访问频率和时间来决定哪些页面是“热点数据”需要保留在物理内存中,哪些页面是“冷数据”可以换出到硬盘上。

17、介绍项目(略)

18、协程和线程的区别

  1. 调度方式:
    • 线程是由操作系统内核进行调度的,它使用操作系统提供的线程调度器来实现并发执行。线程的切换由操作系统负责,切换时会涉及到内核态和用户态之间的切换,因此线程的切换成本相对较高。
    • 协程是由程序员自行控制调度的,它在用户空间实现,不依赖于操作系统的线程调度器。协程的切换由程序员显式地控制,切换时不涉及内核态和用户态之间的切换,因此协程的切换成本相对较低。
  2. 并发性:
    • 线程是操作系统提供的最小执行单元,它可以在多核处理器上并发执行,由操作系统调度线程的执行顺序。
    • 协程是在单个线程内部实现的,并不能利用多核处理器的并行性。协程的并发性是通过在单个线程内部进行切换来实现的,因此它适用于处理大量的 I/O 操作和高并发的网络编程。
  3. 内存消耗:
    • 线程在操作系统中需要维护较大的内核数据结构(如线程控制块),因此创建和销毁线程的开销比较大,而且线程的数量受到操作系统的限制。
    • 协程是在用户空间实现的,不需要操作系统维护线程的内核数据结构,因此创建和销毁协程的开销较小,并且可以创建大量的协程。
  4. 状态切换:
    • 线程的状态切换由操作系统负责,切换时会保存和恢复线程的上下文,因此线程的状态切换比较耗时。
    • 协程的状态切换由程序员控制,切换时只需要保存和恢复协程的上下文,因此协程的状态切换成本比较低。

19、什么情况适合用协程池,什么情况适合用线程池(IO密集协程,资源多线程)

  1. 协程池适用场景
    • I/O 密集型任务:当程序中有大量的 I/O 操作(如文件读写、网络请求等)时,使用协程池可以提高程序的并发性能。因为协程能够在单线程内部高效地切换执行,适合处理大量的 I/O 操作,能够充分利用 CPU 时间,减少等待时间。
    • 高并发的网络编程:在网络编程中,协程池可以很好地支持高并发的网络请求处理,能够有效地管理大量的网络连接和请求。
  2. 线程池适用场景
    • CPU 密集型任务:当程序中有大量的 CPU 计算密集型任务时,使用线程池可以提高程序的并发性能。因为线程池能够利用多核处理器的并行性,同时执行多个 CPU 密集型任务,提高 CPU 的利用率。
    • 长时间运行的任务:对于需要长时间运行的任务,使用线程池可以避免阻塞主线程,保持系统的响应性。

20、定时器怎么做的(epoll_wait)

21、会发生大量epoll在等待么,测试过吞吐量么

22、序列化和反序列化怎么做的(protobuf)

23、如何实现一个高可靠、低延迟的定时器(给出的是发布订阅模式+redis+过期时间)

  1. 使用 Redis 存储定时任务信息:

    将定时任务的信息(比如任务 ID、执行时间等)存储在 Redis 中,可以使用有序集合(Sorted Set)来存储,其中成员是任务的执行时间,分数是任务的唯一标识符(ID)。这样可以利用有序集合的特性,快速地获取最早要执行的任务。

  2. 使用 Redis 过期时间设置任务的执行时间:

    在将任务信息存储到 Redis 中时,可以设置任务的过期时间,让 Redis 在任务执行时间到达时自动删除任务信息。这样可以避免定时器过期后一直占用 Redis 的内存。

  3. 使用发布订阅模式实现任务触发:

    定义一个定时任务触发的频道(Channel),当任务的执行时间到达时,发布一个消息到该频道,订阅该频道的消费者(Worker)会收到消息并执行相应的任务。

  4. Worker 处理任务:

    Worker 订阅了定时任务触发的频道,当收到消息时,会根据消息中的任务 ID 从 Redis 中获取任务信息,并执行相应的任务逻辑。

  5. 保证高可靠性:

    • 使用 Redis 的持久化功能,可以将定时任务信息持久化到磁盘上,以防止数据丢失。
    • 使用 Redis 的主从复制或者集群模式,提高系统的可用性和容错性。

24、怎么做到高可靠(redis分布式)

  1. 数据持久化:

    使用 Redis 的持久化功能,将数据存储到硬盘上,以防止数据丢失。Redis 提供了两种持久化方式:RDB(快照)和 AOF(追加写入文件),可以根据实际需求选择合适的持久化方式。

  2. 主从复制:

    使用 Redis 的主从复制功能,将主节点的数据复制到多个从节点上,以实现数据的备份和容灾。在主节点发生故障时,可以快速切换到从节点提供服务,提高系统的可用性。

  3. 哨兵模式:

    对于 Redis 的高可用性要求较高的场景,可以使用 Redis Sentinel(哨兵)来监控 Redis 实例的健康状态,并在主节点故障时自动进行故障转移。哨兵可以自动选举新的主节点,并通知客户端进行更新。

  4. 集群模式:

    如果单个 Redis 实例无法满足需求,可以使用 Redis Cluster(集群模式)来横向扩展。Redis Cluster 可以将数据分片存储到多个节点上,并提供自动的数据重分配和故障转移功能,以实现高可用性和扩展性。

  5. 错误处理和重试机制:

    在使用 Redis 时,需要考虑网络异常、连接超时等错误情况,并实现相应的错误处理和重试机制,以保证系统的稳定性和可靠性。

  6. 监控和报警:

    在生产环境中,需要建立完善的监控系统,监控 Redis 的运行状态、性能指标和故障情况,并设置相应的报警机制,及时发现并解决问题。

  7. 容灾演练和备份恢复:

    定期进行容灾演练,验证系统的容灾能力,并建立完善的备份和恢复机制,以应对意外情况和灾难恢复。

25、redis的高可靠性怎么保证,宕机时怎么保证任务不会丢失(选举机制)

要保证 Redis 的高可靠性,并在宕机时保证任务不会丢失,可以使用 Redis Sentinel 或 Redis Cluster 中的选举机制来实现故障转移和数据不丢失的目标。

  1. Redis Sentinel 的选举机制:

    Redis Sentinel 是 Redis 官方提供的高可用性解决方案,它可以监控 Redis 实例的健康状态,并在主节点宕机时自动进行故障转移。当主节点宕机时,Sentinel 会选择一个从节点升级为新的主节点,并通知其他从节点和客户端进行更新。

  2. Redis Cluster 的选举机制:

    Redis Cluster 是 Redis 提供的分布式集群解决方案,它可以将数据分片存储到多个节点上,并提供自动的数据重分配和故障转移功能。在 Redis Cluster 中,每个节点都知道整个集群的状态,并通过选举机制选择一个新的主节点来接管失效节点的工作。

在 Redis Sentinel 或 Redis Cluster 中,选举机制通常包括以下几个步骤:

  1. 监控节点状态:

    Sentinel 或 Cluster 中的每个节点都会监控其他节点的健康状态,包括主节点和从节点的状态。如果发现某个节点失效(如主节点宕机),则会触发选举过程。

  2. 选举新的主节点:

    当发生主节点宕机时,Sentinel 或 Cluster 中的节点会启动选举过程,选择一个从节点升级为新的主节点。选举过程通常会考虑节点的健康状态、复制进度、负载等因素。

  3. 更新配置信息:

    一旦选举出新的主节点,Sentinel 或 Cluster 会更新集群的配置信息,通知其他节点和客户端更新连接信息。

  4. 数据同步和恢复:

    在新的主节点选举完成后,从节点会重新连接新的主节点进行数据同步,确保数据的一致性。同时,客户端也会重新连接新的主节点,继续执行任务。

26、redis还没同步时就宕机了怎么办

  1. 持久化策略:

    使用 Redis 的持久化功能,将数据定期持久化到磁盘上。通过设置合适的持久化策略(如每隔一定时间或在有一定数量的写操作后触发持久化),可以将数据及时地写入到磁盘,避免数据丢失。

  2. 持久化方式选择:

    根据业务需求选择合适的持久化方式。RDB(快照)方式可以在指定的时间间隔内生成数据快照,AOF(追加写入文件)方式可以记录每条写命令,两者结合使用可以提高数据的可靠性。

  3. 持久化文件备份:

    定期备份持久化文件,以防止持久化文件损坏或丢失。可以将备份文件存储到多个位置或多个存储介质上,提高备份的可靠性。

  4. 监控和报警:

    建立监控系统,监控 Redis 的持久化状态和健康状态。设置相应的报警机制,及时发现持久化异常或 Redis 宕机的情况。

  5. 故障恢复策略:

    在发生宕机后,及时启动故障恢复策略,如重新启动主节点、手动执行数据同步等,以尽快恢复服务并尽可能减少数据损失。

  6. 数据冗余:

    在主从复制或集群模式下,可以配置多个从节点来提高数据的冗余性,当主节点宕机时,可以选择一个从节点升级为新的主节点,提供服务并尽可能减少数据丢失。

27、redis发生内存宕机时怎么保证高可靠(写日志,仿redo log)

当 Redis 发生内存宕机时,为了保证数据的高可靠性,可以借鉴数据库的日志(Redo Log)机制,将写入的数据先记录到日志中,然后在内存恢复后,通过回放日志的方式来恢复数据。

实现这样的机制可以按照以下步骤进行:

  1. 写日志:

    在写入数据到 Redis 内存时,同时将写入的数据操作记录到日志文件中,记录包括操作类型(如写入、更新、删除)、操作的键值对等信息。

  2. 内存宕机后的恢复:

    当 Redis 内存发生宕机后,可以通过读取日志文件来恢复数据。Redis 在启动时可以检查是否存在未应用的日志,并进行相应的恢复操作。

  3. 日志格式设计:

    设计合适的日志格式,包括记录操作类型、键值对等信息,并考虑日志的持久化方式(如定期刷盘到磁盘)以及日志的压缩和归档策略,以确保日志的可靠性和高效性。

  4. 数据一致性保证:

    在写入数据时,确保日志和内存数据的一致性。可以采用类似数据库的 WAL(Write-Ahead Logging)机制,在写入内存之前先将操作写入日志,然后再写入内存,确保数据操作的原子性。

  5. 日志的管理和维护:

    定期清理和归档日志文件,避免日志文件过大影响性能。可以设置日志的滚动策略,定期删除旧的日志文件或者将旧的日志文件归档到其他存储介质。

28、redis的持久化(RDB,AOF)

  1. RDB 持久化:
    • RDB 持久化是通过定期将 Redis 内存中的数据快照写入磁盘文件来实现持久化的。当启用 RDB 持久化时,Redis 会周期性地在指定的时间间隔内生成数据快照文件(默认为 900 秒),并将数据写入到一个临时文件中,然后将临时文件替换为最终的 RDB 文件。RDB 文件是一个二进制文件,包含了 Redis 在生成快照时的数据状态,可以通过加载 RDB 文件来恢复 Redis 的数据。
    • RDB 持久化的优点是生成的文件比较小,适合用于备份和恢复整个数据集,且对 Redis 的性能影响较小。但是缺点是如果发生宕机,可能会丢失最后一次快照之后的数据。
  2. AOF 持久化:
    • AOF 持久化是通过将 Redis 所有的写命令追加到一个文件(Append-Only File)中来实现持久化的。当启用 AOF 持久化时,Redis 会将每条写命令追加到 AOF 文件末尾,以保证数据的持久化。AOF 文件是一个文本文件,可以通过读取 AOF 文件中的命令来恢复 Redis 的数据状态。
    • AOF 持久化的优点是可以保证每条写命令都能持久化,不会丢失数据。同时,AOF 文件以文本格式存储,易于人类阅读和手动修改。但是缺点是 AOF 文件通常比 RDB 文件大,且在恢复数据时需要重新执行所有的写命令,可能会影响恢复的速度。

29、定时任务用redis怎么实现任务消费(发布订阅,绑定设置的键。轮询或回调。能不能用消息队列实现)

要使用 Redis 实现定时任务的消费,可以借助 Redis 的有序集合(Sorted Set)和发布/订阅(Pub/Sub)功能。

下面是一个基本的实现思路:

  1. 将定时任务存储到有序集合中:

    将定时任务的执行时间作为分数,任务内容作为成员存储到 Redis 的有序集合中。有序集合会自动按照分数进行排序,因此可以方便地获取最早要执行的任务。

  2. 定时检查任务:

    使用定时器或者定时任务,定期从有序集合中获取当前时间之前的任务,并将其取出来进行处理。

  3. 处理任务:

    从有序集合中取出任务后,进行相应的处理。这里可以根据具体业务逻辑来执行任务,比如调用相应的接口、执行相应的代码逻辑等。

  4. 任务完成后更新或删除任务:

    处理完成后,根据业务需求决定是否将任务从有序集合中删除,或者更新任务的执行时间。

  5. 发布/订阅模式:

    如果需要实时通知任务的状态变化或者结果,可以考虑使用 Redis 的发布/订阅模式。当任务状态发生变化时,可以发布一个消息到相应的频道,订阅该频道的消费者可以收到消息并进行相应的处理。