Skip to content

腾讯一面

来源:https://www.nowcoder.com/discuss/560939306211098624

两道题

1、合并两个有序数组

思路:

  1. 初始化指针: 设定三个指针,分别指向 nums1 的有效元素的末尾(m-1)、nums2 的末尾(n-1),以及合并后的数组的末尾(m+n-1)。
  2. 比较并插入: 从后向前遍历,比较 nums1 和 nums2 对应位置的元素,将较大的元素插入到合并后的数组的末尾,并将相应指针向前移动。
  3. 处理剩余元素: 如果 nums2 中还有剩余的元素,需要将其依次插入到 nums1 的前面。

参考代码:

C++
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        // 初始化指针,分别指向 nums1、nums2 和合并后数组的末尾
        int p1 = m - 1;
        int p2 = n - 1;
        int p = m + n - 1;

        // 比较并插入,从后向前遍历
        while (p1 >= 0 && p2 >= 0) {
            if (nums1[p1] > nums2[p2]) {
                nums1[p--] = nums1[p1--];
            } else {
                nums1[p--] = nums2[p2--];
            }
        }

        // 处理 nums2 中剩余的元素
        while (p2 >= 0) {
            nums1[p--] = nums2[p2--];
        }
    }
};

// 输出数组的辅助函数
void printArray(const vector<int>& arr) {
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
}

int main() {
    Solution solution;

    // 示例输入
    vector<int> nums1 = {1, 2, 3, 0, 0, 0};
    vector<int> nums2 = {2, 5, 6};
    int m = 3, n = 3;

    // 调用合并函数
    solution.merge(nums1, m, nums2, n);

    // 输出合并后的数组
    printArray(nums1);

    return 0;
}

2、使用快排的方式,知道第K大的数(代码逻辑没问题,结果没有做出来) 快排最快时间复杂度是多少,最差时间复杂度是多少

思路:

  1. 对数组进行快速排序,以保证数组中的元素按照非递增顺序排列。
  2. 返回排序后数组的第 K 个元素即为第 K 大的数。

参考代码:

C++
#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 对数组进行快速排序
        quickSort(nums, 0, nums.size() - 1);
        
        // 返回第 K 大的数
        return nums[nums.size() - k];
    }

    // 快速排序的递归函数
    void quickSort(vector<int>& nums, int left, int right) {
        if (left < right) {
            // 划分区域
            int pivotIndex = partition(nums, left, right);
            
            // 对左右两个区域分别递归排序
            quickSort(nums, left, pivotIndex - 1);
            quickSort(nums, pivotIndex + 1, right);
        }
    }

    // 划分区域的函数
    int partition(vector<int>& nums, int left, int right) {
        // 选择右边界的元素作为基准
        int pivot = nums[right];
        int i = left - 1; // i 是小于等于基准的元素的最右索引

        // 遍历数组,将小于等于基准的元素交换到左侧
        for (int j = left; j < right; ++j) {
            if (nums[j] <= pivot) {
                i++;
                swap(nums[i], nums[j]);
            }
        }

        // 将基准元素放到正确的位置
        swap(nums[i + 1], nums[right]);

        // 返回基准元素的索引
        return i + 1;
    }
};

int main() {
    Solution solution;

    // 示例输入
    vector<int> nums = {3, 2, 1, 5, 4};
    int k = 2;

    // 调用查找函数
    int result = solution.findKthLargest(nums, k);

    // 输出结果
    cout << "第 " << k << " 大的数是:" << result << endl;

    return 0;
}
  1. 最优情况:

    最优情况下,快速排序的时间复杂度为O(n log n)。这种情况发生在每次划分都能够均匀地将数组分成两部分的情况下。即每次选择的基准元素都刚好将数组分成大小相等的两个子数组。

  2. 最差情况:

    • 最差情况下,快速排序的时间复杂度为O(n^2)。这种情况通常在每次划分时,选择的基准元素都是数组中的最小或最大元素,导致划分非常不均衡。
    • 最差情况可以通过随机选择基准元素、使用中位数作为基准元素或者三数取中法等方式来避免,但这些方法会增加算法的实现复杂度。

项目

  1. 你说一下你负责的项目
  2. 如何保障写入的数据不会冲突,这个没有思考过(空气宁静了半分钟,拉跨)
  3. 有没有想过你这个项目,如何提高写入磁盘的效率?这里没有答上来,说没有思考过

C++

3、int a[10]; int *a = new int[10]; 的差别

  1. int a[10];

定义了一个包含 10 个整数的数组 a,它是一个栈上的数组。数组的大小在编译时确定,而且它的生命周期与定义它的作用域相同。在这种情况下,数组 a 的大小是固定的,不能改变。

  1. int *a = new int[10];

通过动态内存分配,使用 new 关键字在堆上创建了一个包含 10 个整数的数组,并将其地址赋给了指针 a。这是一个堆上的数组,它的大小是动态确定的,可以在运行时改变。需要注意的是,使用 new 创建的动态数组需要在使用完毕后使用 delete[] 进行释放,以防止内存泄漏。

4、C++11新特性你了解过那些,用过哪些

  1. 自动类型推导(Auto): 允许编译器推导变量的类型,使代码更加简洁。
C++
auto x = 5; // x的类型将被推导为int
  1. 范围-based for 循环: 简化了对容器元素的遍历。
C++
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
    // 使用num
}
  1. 智能指针: 引入了std::shared_ptrstd::unique_ptr等智能指针,用于管理动态分配的内存,帮助防止内存泄漏。
C++
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
  1. Lambda 表达式: 允许在函数内部定义匿名函数,提高代码可读性和灵活性。
C++
auto add = [](int a, int b) { return a + b; };
  1. nullptr: 引入了空指针常量nullptr,用于替代传统的空指针NULL
C++
int* ptr = nullptr;
  1. 强制类型转换(Type Casting): 引入了static_castdynamic_castconst_castreinterpret_cast等更安全和灵活的类型转换操作符。
C++
double x = 3.14;
int y = static_cast<int>(x);
  1. 右值引用和移动语义: 支持通过右值引用实现移动语义,提高了对临时对象的处理效率。
C++
std::vector<int> getVector() {
    // 返回一个临时vector
    return std::vector<int>{1, 2, 3};
}

std::vector<int> numbers = getVector(); // 使用移动语义
  1. 新的容器和算法: 引入了新的容器,如std::unordered_mapstd::unordered_set,以及一些新的算法。
C++
std::unordered_map<int, std::string> myMap = {{1, "one"}, {2, "two"}};
  1. 线程支持(std::thread): 提供了原生的多线程支持,使得并发编程更加方便。
C++
#include <thread>

void myFunction() {
    // 线程执行的代码
}

int main() {
    std::thread t(myFunction);
    t.join(); // 等待线程结束
    return 0;
}

5、C++容器使用过那些(说自己实现过,加分)

  1. std::vector: 动态数组,提供动态大小的数组,支持快速随机访问,可以动态增长或收缩。
C++
#include <vector>
std::vector<int> myVector = {1, 2, 3, 4, 5};
  1. std::list: 双向链表,支持在列表的任意位置进行插入和删除操作。
C++
#include <list>
std::list<int> myList = {1, 2, 3, 4, 5};
  1. std::deque: 双端队列,支持在两端进行快速插入和删除操作。
C++
#include <deque>
std::deque<int> myDeque = {1, 2, 3, 4, 5};
  1. std::queue: 队列,先进先出(FIFO)的数据结构。
C++
#include <queue>
std::queue<int> myQueue;
  1. std::stack: 栈,后进先出(LIFO)的数据结构。
C++
#include <stack>
std::stack<int> myStack;
  1. std::set: 集合,有序且不包含重复元素的容器。
C++
#include <set>
std::set<int> mySet = {1, 2, 3, 4, 5};
  1. std::map: 映射,键值对的容器,键是唯一的。
C++
#include <map>
std::map<std::string, int> myMap = {{"one", 1}, {"two", 2}, {"three", 3}};
  1. std::unordered_set: 无序集合,无序且不包含重复元素的容器。
C++
#include <unordered_set>
std::unordered_set<int> myUnorderedSet = {1, 2, 3, 4, 5};
  1. std::unordered_map: 无序映射,键值对的容器,键是唯一的,无序。
C++
#include <unordered_map>
std::unordered_map<std::string, int> myUnorderedMap = {{"one", 1}, {"two", 2}, {"three", 3}};

6、说一下静态多态和动态多态

  1. 静态多态

静态多态是在编译时发生的多态性。它主要通过函数重载和运算符重载来实现。编译器在编译阶段确定调用哪个函数或运算符,具体的函数或运算符重载版本是根据函数或运算符的签名(参数类型和数量)来确定的。

示例 - 函数重载:

C++
class StaticPolymorphism {
public:
    void print(int value) {
        std::cout << "Integer: " << value << std::endl;
    }

    void print(double value) {
        std::cout << "Double: " << value << std::endl;
    }
};

int main() {
    StaticPolymorphism obj;
    obj.print(42);       // 调用 print(int)
    obj.print(3.14);     // 调用 print(double)

    return 0;
}

示例 - 运算符重载:

C++
class Complex {
public:
    double real;
    double imag;

    Complex operator+(const Complex& other) const {
        Complex result;
        result.real = real + other.real;
        result.imag = imag + other.imag;
        return result;
    }
};

int main() {
    Complex a{1.0, 2.0};
    Complex b{2.0, 3.0};
    Complex c = a + b;   // 调用 operator+()

    return 0;
}
  1. 动态多态(Runtime Polymorphism):

动态多态是在运行时发生的多态性,主要通过虚函数和继承来实现。动态多态允许在运行时选择调用哪个函数,具体的函数实现由对象的类型决定。

示例 - 虚函数和继承:

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

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

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

int main() {
    Shape* shapePtr;

    Circle circle;
    Square square;

    shapePtr = &circle;
    shapePtr->draw();    // 调用 Circle 类的 draw()

    shapePtr = &square;
    shapePtr->draw();    // 调用 Square 类的 draw()

    return 0;
}

在动态多态中,函数的调用是通过虚函数表(vtable)来实现的,允许在运行时选择正确的函数实现。通过基类指针或引用指向派生类对象,可以实现多态性,即相同的调用可以根据对象的实际类型调用不同的函数实现。

7、指针和引用的区别是什么

  1. 定义和声明:
  • 指针: 指针是一个变量,其值是另一个变量的地址。指针需要通过 * 运算符来声明和使用。
C++
int x = 10;
int *ptr = &x;  // 指针的声明和初始化
  • 引用: 引用是一个别名,即某个变量的别名。引用在声明时使用 & 运算符。
C++
int x = 10;
int &ref = x;  // 引用的声明和初始化
  1. 语法和操作符:
  • 指针: 指针使用 * 运算符来访问所指向的变量。
C++
int x = 10;
int *ptr = &x;
*ptr = 20;  // 通过指针修改 x 的值
  • 引用: 引用在声明时使用 & 运算符,但在使用时不需要。
C++
int x = 10;
int &ref = x;
ref = 20;  // 直接修改 x 的值,无需使用运算符
  1. 空值(Nullability):
  • 指针: 指针可以是空指针(nullptrNULL),即不指向任何有效的地址。
C++
int *ptr = nullptr;  // 空指针
  • 引用: 引用必须在声明时初始化,并且一旦引用被初始化,它将一直引用同一个变量,不存在空引用的概念。
C++
int x = 10;
int &ref = x;  // 引用初始化
  1. 多重引用和指针:
  • 指针: 指针可以重新赋值指向其他变量或空。
C++
int x = 10;
int y = 20;
int *ptr = &x;
ptr = &y;  // 指针重新指向 y
  • 引用: 引用在初始化后不能再引用其他变量。
C++
int x = 10;
int y = 20;
int &ref = x;
ref = y;  // 修改了 x 的值,但 ref 仍然引用 x
  1. 数组和函数参数:
  • 指针: 指针可以用于遍历数组和作为函数参数传递。
C++
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // 指向数组的第一个元素
void foo(int *ptr) {
    // 函数接受指针参数
}
  • 引用: 引用不直接用于遍历数组,但可以作为函数参数传递。
C++
void foo(int &ref) {
    // 函数接受引用参数
}

8、说一下TIME_WAIT的作用(卡了一下,说的第一个点不感兴趣,第二个点说让对端正常关闭——态度好些)

  1. 确保最后的 ACK 被接收: 在四次挥手中,主动关闭方最后发送的 ACK 报文是为了确认被动关闭方已经接收到了 FIN 报文。TIME_WAIT 状态的存在确保了主动关闭方有足够的时间等待被动关闭方对最后的 ACK 进行确认,防止可能的超时重传。
  2. 处理滞留的数据报文: 在网络中,有可能存在滞留的数据报文,它们可能在 TIME_WAIT 状态的时间段内到达。TIME_WAIT 的存在允许网络中的这些滞留数据得以处理,防止它们影响后续的连接。
  3. 防止旧连接的数据影响新连接: 如果没有 TIME_WAIT,新连接可能会受到之前连接的残留数据的影响。TIME_WAIT 状态的持续时间确保了在这段时间内,网络中的旧连接的数据都被处理完毕。
  4. 避免连接复用时的问题: 如果不留出 TIME_WAIT 时间,而是立即释放连接,可能导致之前连接的序列号在新连接中被重用,从而引发数据混淆和错误。TIME_WAIT 状态确保了足够的时间,使得之前连接的所有数据都能够在网络中被清理。

9、说一下HTTPS和HTTP的差异

  1. 安全性:
    • HTTP: 是一种明文传输协议,数据在传输过程中是未加密的,容易被中间人截获和窃听。因此,对于敏感信息(如登录凭证、个人数据等)的传输,HTTP 不安全。
    • HTTPS: 是在HTTP的基础上通过使用SSL/TLS协议添加了加密层的安全协议。通过使用公钥和私钥加密和解密数据,HTTPS保护了数据在传输过程中的安全性,有效防止了中间人攻击。
  2. 加密协议:
    • HTTP: 不提供数据加密和身份验证机制。
    • HTTPS: 使用SSL(Secure Sockets Layer)或其升级版TLS(Transport Layer Security)协议,通过加密算法保护数据的隐私性和完整性。
  3. 端口:
    • HTTP: 默认使用80端口。
    • HTTPS: 默认使用443端口。
  4. URL协议标识:
    • HTTP: URL以"http://"开头。
    • HTTPS: URL以"https://"开头。
  5. 证书:
    • HTTP: 不需要使用数字证书。
    • HTTPS: 需要服务器端使用数字证书,由可信任的证书颁发机构(CA)签发,用于验证服务器的身份。
  6. 性能:
    • HTTP: 由于不涉及加密解密等过程,相对于HTTPS,性能上更高效。
    • HTTPS: 加密和解密过程会引入一定的计算和网络开销,因此相对于HTTP,性能上略低一些。
  7. 搜索引擎排名:
    • HTTP: 搜索引擎可能会对使用HTTP的网站进行降权处理,因为它不安全。
    • HTTPS: 谷歌等搜索引擎更倾向于为使用HTTPS的网站提高排名,因为它提供更安全的用户体验。

10、说一下TCP的拥塞控制

TCP的拥塞控制是为了在网络中防止过多的数据注入,从而导致网络拥塞和性能下降。拥塞控制主要通过一些算法和机制来确保网络中的数据流量在合理范围内,防止网络过载。主要的TCP拥塞控制算法包括慢启动、拥塞避免、快速重传和快速恢复。

  1. 慢启动(Slow Start):
    • TCP连接刚开始时,发送方会将拥有拥塞窗口(cwnd)的大小设置为一个较小的值,例如1或2。
    • 对于每个传输的数据段的确认,发送方将cwnd加倍,以指数增长的方式逐渐增加发送速率,直到达到一个阈值(慢启动阈值)。
  2. 拥塞避免(Congestion Avoidance):
    • 一旦cwnd达到慢启动阈值,TCP就进入拥塞避免阶段。
    • 在拥塞避免阶段,cwnd每次只增加1个MSS(最大报文段长度),采用线性增长。
  3. 快速重传(Fast Retransmit):
    • 如果发送方连续收到三个相同的ACK,说明有报文段丢失,触发快速重传。
    • 发送方不等待超时,而是立即重传丢失的报文段。
  4. 快速恢复(Fast Recovery):
    • 在快速重传的基础上,TCP进入快速恢复阶段。
    • 在快速恢复中,cwnd的增加不再是线性增加,而是每收到一个重复的ACK,cwnd增加一个MSS,从而维持一种较高的发送速率。

11、select poll epoll的差别是什么

  1. 数据结构的区别:
    • select: 使用一个fd_set数据结构来存放文件描述符,并且该集合有一定的大小限制,通常为1024。
    • poll: 使用一个pollfd结构体数组来存放文件描述符,没有集合大小的限制。
    • epoll: 使用一个事件表(event table)来存放事件,它不限制文件描述符的数量。
  2. 效率差异:
    • select: 在文件描述符比较多的情况下,效率下降明显,因为它需要遍历整个fd_set集合。
    • poll: 在文件描述符比较多的情况下,性能相对于select有一些提升,但仍然需要遍历整个pollfd数组。
    • epoll: 通过事件通知的方式,效率更高,因为它只会通知发生了事件的文件描述符,而不需要遍历所有的文件描述符。
  3. 支持的事件类型:
    • selectpoll: 可以支持读、写和异常等事件。
    • epoll: 支持的事件更多,包括ET(边缘触发)和LT(水平触发),并且可以同时处理大量的文件描述符。
  4. 触发方式的区别:
    • selectpoll: 采用轮询的方式,需要遍历所有的文件描述符。
    • epoll: 采用事件通知的方式,只通知发生了事件的文件描述符。
  5. 接口调用的参数不同:
    • select: 修改fd_set
    • poll: 修改pollfd数组。
    • epoll: 通过epoll_ctl向事件表中添加、修改或删除事件,并通过epoll_wait等待事件的发生。

12、说一下乐观锁和悲观锁

  1. 悲观锁
    • 基本思想: 悲观锁认为在并发环境中,总是假设最坏的情况,即认为会发生冲突,因此在访问共享资源之前先加锁,确保同一时刻只有一个线程或进程可以访问。
    • 实现方式: 典型的实现方式是使用传统的互斥锁,如Mutex或Semaphore。当一个线程获得锁之后,其他线程就必须等待,直到获得锁的线程释放锁。
  2. 乐观锁
    • 基本思想: 乐观锁认为在并发环境中,冲突是比较少见的,因此在访问共享资源之前不加锁,而是在更新时检查是否有其他线程进行了修改。如果没有发现冲突,则提交更新;如果发现冲突,则进行回滚或执行一些冲突处理策略。
    • 实现方式: 典型的实现方式是使用版本号(Versioning)或时间戳(Timestamping)。每个数据项都有一个版本号或时间戳,当要更新数据时,先检查版本号或时间戳,如果匹配则进行更新,否则认为发生了冲突。

比较:

  • 悲观锁:
    • 适用于对共享资源的访问冲突比较频繁的情况。
    • 加锁的开销相对较大,可能会导致性能下降。
    • 提供了强的一致性,保证了数据的完整性。
  • 乐观锁:
    • 适用于对共享资源的访问冲突比较少见的情况。
    • 不需要显式加锁,减小了锁的开销,提高了性能。
    • 提供了弱一致性,需要额外的机制来处理冲突。

13、项目中用过索引优化吗

这里简单说一下索引优化。

  1. 选择合适的索引类型:
    • 根据查询的特性选择适当的索引类型,如普通索引、唯一索引、全文索引等。
    • 考虑到查询条件的情况,决定是单列索引还是组合索引。
  2. 避免过多的索引:
    • 过多的索引不仅增加了存储空间,还会导致更新、插入和删除操作的性能下降。
    • 评估业务需求,只创建必要的索引,删除不再使用的索引。
  3. 使用索引覆盖:
    • 确保查询可以使用覆盖索引,即只通过索引就能够获取所需的数据,而不必访问表的实际数据行。
    • 减少了对表的访问,提高了查询性能。
  4. 定期分析和重建索引:
    • 定期执行索引的分析(ANALYZE)和重建(REBUILD/REORGANIZE)操作,以保持索引的统计信息和碎片度的良好状态。
    • 确保索引的最新统计信息,以便数据库优化器做出更好的执行计划。
  5. 使用覆盖索引:
    • 当查询的列都包含在索引中时,数据库可以直接使用索引而无需访问表的数据行。
    • 这可以减少I/O操作,提高查询性能。
  6. 了解查询优化器的行为:
    • 了解数据库查询优化器的工作原理,分析查询执行计划,确保数据库选择了最优的索引。
    • 使用查询提示(hints)来引导优化器选择合适的索引。
  7. 分区表:
    • 对于大型表,可以考虑使用分区表来加速查询。
    • 分区表将大表划分成较小的物理部分,可以减少查询时需要扫描的数据量。
  8. 避免使用通配符前缀:
    • 避免在查询条件中使用通配符前缀,如LIKE '%abc',因为这样的查询无法充分利用索引。
  9. 了解数据库引擎特性:
    • 不同的数据库引擎对索引的实现和优化有不同的特性,例如,InnoDB和MyISAM在索引处理上有一些区别。
    • 根据具体数据库引擎的特性来选择适当的优化策略。
  10. 使用合适的数据类型:
    • 使用合适大小的数据类型,避免使用过大或不必要的数据类型,以减小索引大小,提高查询性能。

14、数据库主从同步了解过吗

  1. 主从同步的工作流程:
    • 主数据库记录所有的写操作(INSERT、UPDATE、DELETE)。
    • 从数据库定期连接主数据库,并复制主数据库的写操作,使得从数据库的数据保持与主数据库一致。
  2. 数据复制方式:
    • 基于日志文件: 主数据库将写操作记录到二进制日志文件(binlog),从数据库通过解析和执行这些日志文件来实现数据复制。
    • 基于快照: 主数据库定期生成数据快照,从数据库通过获取快照并应用到自己的数据库中来实现数据同步。
  3. 读写分离:
    • 主数据库处理写操作,从数据库处理读操作,这样可以分散数据库负载,提高系统的读写性能。
  4. 故障转移和高可用性:
    • 如果主数据库发生故障,可以将其中一个从数据库切换为主数据库,确保系统的高可用性。
    • 通过自动或手动方式进行主从切换,减少系统的停机时间。
  5. 延迟复制:
    • 由于主从同步过程中存在网络传输、解析等耗时操作,从数据库的数据可能会略有滞后于主数据库。
    • 可以通过设置合适的同步延迟来平衡实时性和性能。
  6. 数据备份:
    • 从数据库可以被用作主数据库的实时备份,当主数据库发生故障时,可以快速切换到从数据库以继续提供服务。
  7. 同步策略:
    • 异步同步:主数据库执行写操作后立即返回,从数据库异步地复制主数据库的数据。
    • 同步同步:主数据库执行写操作后,等待从数据库完成复制后才返回,确保数据的同步性。

15、redis 的 缓存击穿、缓存穿透、缓存雪崩

  1. 缓存击穿(Cache Breakdown):
    • 定义: 缓存击穿指的是一个热点数据在缓存中过期失效或不存在时,大量的请求直接穿透到数据库或后端存储系统,导致数据库负载激增。
    • 原因: 当一个热点数据过期或被删除,此时有大量的并发请求同时访问这个数据,导致缓存未命中,请求直接落到数据库,引起数据库压力骤增。
  2. 缓存穿透(Cache Penetration):
    • 定义: 缓存穿透是指请求的数据在缓存中不存在,而且也不存在于后端存储系统中,导致大量的无效请求直接到达后端,对后端造成压力。
    • 原因: 恶意请求、非法输入或者一些根本不存在的数据被请求,这些请求在缓存和后端存储中都找不到对应的数据,导致系统处理大量无效请求。
  3. 缓存雪崩(Cache Avalanche):
    • 定义: 缓存雪崩是指缓存中的大量数据同时失效,导致大量的请求直接落到后端存储系统,引发瞬时的系统压力激增。
    • 原因: 缓存中的数据设置了相同的过期时间,而且在同一时刻全部过期,导致大量请求同时访问后端,造成后端负载过大。

缓解策略:

  1. 缓存击穿:

    • 使用互斥锁或分布式锁,保证只有一个请求能够去加载数据,其他请求等待加载完成。
    • 在缓存失效时,通过异步线程或定时任务去更新缓存,避免大量请求同时访问。
  2. 缓存穿透:

    • 使用布隆过滤器等技术,对请求的 key 进行判断,将不存在的 key 提前拦截。
    • 对于查询数据库为空的请求,可以在缓存中设置一个默认值,防止对后端的频繁请求。
  3. 缓存雪崩:

    • 为缓存数据设置不同的过期时间,防止在同一时刻大量数据同时失效。

    • 使用分布式缓存,将缓存数据分散到不同的节点,降低同时失效的概率。

    • 设置热点数据永不过期,保证关键数据不会在同一时刻失效。

16、你知道你使用redis的版本吗?

17、有没有思考过redis为什么更快

  1. 内存存储: Redis是基于内存存储的键值对数据库,数据存储在内存中,读写速度非常快。相比于传统数据库需要频繁的磁盘 I/O 操作,Redis能够直接在内存中进行数据读写。
  2. 单线程模型: Redis采用单线程模型,避免了多线程之间的锁竞争和上下文切换的开销。这使得Redis能够更好地利用CPU的缓存,提高了性能。
  3. 非阻塞 I/O: Redis使用了非阻塞 I/O 操作,可以处理大量的并发连接。在网络请求时,Redis会采用非阻塞方式,不会因为等待某个操作完成而停止处理其他请求。
  4. 数据结构多样性: Redis支持丰富的数据结构,包括字符串、哈希表、列表、集合等。不同的数据结构适用于不同的场景,提高了灵活性和性能。
  5. 持久化可选: Redis提供了多种持久化方式,包括快照(Snapshot)和只追加文件(Append-Only File)两种。用户可以根据实际需求选择适合的持久化方式,而不是强制使用某一种方式。
  6. 原子操作: Redis提供了一些原子操作,例如对字符串进行追加、对集合进行交并补运算等。这些原子操作保证了在多个并发请求中的一致性。
  7. 支持分布式: Redis支持分布式架构,可以通过主从复制、哨兵模式和集群模式等方式进行横向扩展。这使得Redis能够处理大规模的数据和请求。

18、操作系统进程和线程的关系,最大区别是什么

  1. 定义:
    • 进程(Process): 进程是操作系统中的一个独立的执行单元,拥有自己的地址空间、文件描述符、资源和独立的内存空间。进程之间是相互独立的。
    • 线程(Thread): 线程是进程中的一个执行单元,共享进程的地址空间和资源,但拥有独立的执行流程。
  2. 资源开销:
    • 进程: 进程之间的切换开销较大,因为切换进程需要保存和恢复整个进程的上下文信息。
    • 线程: 线程之间的切换开销较小,因为切换线程只需要保存和恢复线程的上下文信息,共享进程的资源。
  3. 通信和同步:
    • 进程: 进程之间通信复杂,需要使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。
    • 线程: 线程之间共享进程的地址空间,直接访问共享数据,因此通信相对简单。但也需要注意同步问题,需要使用同步机制来避免竞态条件。
  4. 独立性:
    • 进程: 进程是独立的执行单元,一个进程的崩溃不会影响其他进程。
    • 线程: 线程是进程的一部分,一个线程的崩溃可能会影响整个进程,因为线程共享进程的资源。
  5. 创建和销毁:
    • 进程: 创建和销毁进程的开销相对较大,涉及到独立的地址空间的分配和释放。
    • 线程: 创建和销毁线程的开销相对较小,因为线程共享进程的资源,只需要分配和释放线程的执行上下文。
  6. 并发性:
    • 进程: 进程之间是并发执行的,相互独立。
    • 线程: 线程之间是并发执行的,但是它们共享同一地址空间,可以更方便地共享数据。

19、git和linux使用过吗?熟悉那些命令

20、公司使用过find ps vim,说到top的时候(面试官很激动,说这个项目还是有实操经验的)

反问