暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

滴滴,已oc!(信息量超大!!!)

阿Q正砖 2023-09-19
223
大家好,我是阿Q。
今天给大家分享一位同学在拿到滴滴offer后分享的一面面经,后面还有二面、三面、HR面,我看了之后感觉信息量非常大,对考察的问题也是逐步深入。
后面二三面基本上都是项目经历之类的,所以对大家的帮助不是很大,所以就整理了一下一面的一些问题,给出了我一些自己的理解,希望对大家有用处。
还得是中大厂,问的问题又广又深入。要是平时不留意,虽然说也能答,但是说出来让面试官满意的答案也不是很容易的。
我们一起来看看吧~
1、内存泄漏怎么定位?
这个在开发过程中也难免会遇到
  1. 使用内存调试工具:许多编程语言和开发环境提供了内存调试工具,例如Valgrind(用于C/C++)、Memory Profiler(用于Python)、Xcode Instruments(用于iOS/macOS开发)等。这些工具可以帮助检测内存泄漏并提供详细的报告,包括泄漏的位置和分配堆栈。
  2. 手动代码审查:仔细检查程序中的内存分配和释放代码。确保每个malloc
    new
    calloc
    或类似的函数都有对应的释放操作(free
    delete
    free
    等)。
  3. 记录内存分配和释放:在程序中记录每个内存分配和释放操作,以便跟踪资源的生命周期。一旦发现未释放的资源,您可以确定在何处分配了该资源。
  4. 使用智能指针:如果您使用支持智能指针的编程语言(如C++的std::shared_ptr
    std::unique_ptr
    ),可以使用它们来管理内存,以减少手动内存管理的错误。
  5. 静态代码分析工具:某些静态代码分析工具(例如Clang的scan-build
    )可以检测到潜在的内存泄漏问题,并提供警告。
  6. 内存分析工具:使用专门的内存分析工具,例如Heap Profiler,可以可视化地查看程序的内存使用情况,从而找出泄漏。
  7. 测试用例:编写专门的测试用例来模拟程序的使用情况,以查找内存泄漏。这些测试用例可以捕获内存分配和释放的问题。
  8. 增量检查:您可以在程序的不同阶段进行内存检查,以查看哪个阶段导致了内存泄漏。
  9. 记录内存使用情况:记录程序的内存使用情况,以便可以比较不同版本之间的内存占用情况,以找出是否存在泄漏。
2、内存过高会怎么样?
  1. 性能下降:当程序占用大量内存时,操作系统可能不得不频繁地进行内存页面交换,将内存中的数据写入磁盘并重新加载数据,这会导致程序的性能大幅下降。系统变得响应迟钝,运行速度减慢,用户体验变差。
  2. 程序崩溃:当系统的物理内存耗尽,无法满足程序的内存需求时,程序可能会崩溃或被操作系统强制终止。这通常是因为操作系统无法为程序分配所需的内存空间。
  3. 资源争夺:如果多个进程或线程同时竞争有限的内存资源,可能会导致资源争夺。这可能会导致死锁或无法预测的行为。
  4. 系统稳定性问题:内存过高可能会导致操作系统本身出现问题。操作系统需要维护内核数据结构和进程管理,如果内存不足,操作系统可能无法正常工作,导致整个系统不稳定。
3、malloc实现原理?
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
  1. 空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。)
  2. 当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。
  3. 如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。
  4. 释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。
因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。
4、tcmalloc了解吗?
tcmalloc 是由Google开发的一种高效的、线程安全的内存分配器(malloc/free实现)。它主要是减少多线程应用程序中的内存分配锁竞争,从而提高性能。
tcmalloc 的主要特点和优点:
  1. 线程局部缓存:tcmalloc 使用了一种线程局部缓存的机制,每个线程都有自己的内存缓存,这减少了多线程应用程序中的锁竞争,从而提高了性能。
  2. 高性能:tcmalloc 专注于提供高性能的内存分配和释放操作。它的设计目标是降低内存管理开销,减少内存分配的时间,特别是在多线程环境中。
  3. 内存利用率:tcmalloc 通过多种技术,如大小分类、中心缓存等,来提高内存利用率。它可以有效地减少内存碎片问题。
  4. 调试支持:tcmalloc 提供了一些调试工具和环境变量,用于帮助诊断和解决内存分配问题。
  5. 跨平台性:tcmalloc 可以在多种操作系统上使用,并且与各种编译器兼容。
5、vector扩容机制,实现原理?
vector 扩容的原理是在内部创建一个新的更大的数组,并将原始数组中的元素复制到新的数组中,最后释放原始数组的内存。在新的数组中添加新元素之后,vector 的容量将增加到新数组的大小。
vector 扩容的过程可能会导致性能问题,特别是当 vector 中包含大量元素时。因此,我们可以通过在创建 vector 时指定初始容量来减少 vector 扩容的次数。
我们可以使用以下代码创建一个具有 100 个元素的 vector:
    std::vector<int> vec(100);
    创建一个包含 100 个 int 类型元素的 vector,并将 vector 的容量设置为 100,这样在添加元素时,vector 可能不需要扩容。如果我们知道 vector 中的元素数量可能会很大,我们也可以使用 reserve() 方法来手动预留足够的内存空间,以避免不必要的扩容操作。
      std::vector<int> vec;
      vec.reserve(1000); // 预留容量为 1000
      在 vector 中预留 1000 个元素的内存空间,以减少在添加元素时可能发生的扩容操作。
      需要注意的是,当使用 push_back() 方法向 vector 中添加元素时,如果 vector 中的元素数量已经达到了容量的极限,vector 将自动扩容。此时,添加新元素的时间可能会比较长,因为需要分配新的内存空间并复制原始数组中的所有元素。如果我们需要向 vector 中添加大量元素,可以先使用 reserve() 方法预留足够的内存空间,然后使用 insert() 方法一次性添加多个元素,以减少扩容的次数和复制的开销。
      6、vector扩容怎么查看扩容了多少?
      std::vector
      的扩容行为并不直接暴露给用户,所以你不能直接查看它的扩容次数或扩容了多少。
      可以通过下面的方法来大致了解 std::vector
      扩容的情况。
      1. 容量信息:你可以使用 capacity()
        成员函数来查看当前 std::vector
        的容量,即分配的内存空间大小。容量不等于元素数量,它表示在不需要重新分配内存的情况下,可以存储的元素数量。如果容量小于元素数量,那么 std::vector
        可能已经进行了一次或多次扩容。
      2. 实验性分析:你可以通过编写一些测试代码来观察 std::vector
        的扩容行为。例如,你可以在循环中不断地添加元素,并在每次添加后检查容量的变化。这样可以粗略地估计扩容的规模。
        #include <iostream>
        #include <vector>


        int main() {
        std::vector<int> vec;
        size_t prevCapacity = vec.capacity();


        for (int i = 0; i < 1000; ++i) {
        vec.push_back(i);


        if (vec.capacity() != prevCapacity) {
        std::cout << "Capacity increased from " << prevCapacity << " to " << vec.capacity() << std::endl;
        prevCapacity = vec.capacity();
        }
        }


        return 0;
        }
        上面这个代码可作参考,它每次容量发生变化时都会打印出新的容量。我们可以通过观察输出,看到扩容的情况。
        7、HashMap用过吗?说说原理?
        HashMap由数组(键值对entry组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个entry组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)进行数据的存储。
        HashMap的工作原理(key-value对在HashMap中是如何存储的):HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。
        当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
        8、C++智能指针?
        智能指针之前有非常详细的文章去讲解,请点击:C++智能指针详解(最全)
        9、右值引用?
        C++的右值引用(Rvalue Reference)是C++11标准引入的一个重要特性,它允许我们对右值进行引用,为C++语言引入了新的语义,主要用于实现移动语义和完美转发。右值引用使用一个特殊的符号 && 来声明,例如:
          int&& rvalue_ref = 42; // 右值引用

          特点及用途:
          1. 移动语义(Move Semantics):右值引用最常见的应用是在容器类如std::vector和std::string等的内部实现中,它们可以利用移动语义来高效地管理内存资源。通过将资源的所有权从一个对象(右值)转移到另一个对象,可以避免不必要的内存拷贝,提高性能。
            std::vector<int> source = getLargeData(); // 获取一个大型数据
            std::vector<int> destination = std::move(source); // 使用移动语义,避免拷贝
            1. 完美转发(Perfect Forwarding):右值引用还用于实现完美转发,这是一种允许函数将参数原封不动地传递给其他函数的机制。这对于编写泛型代码和函数包装器非常有用。
              template <typename Func, typename... Args>
              auto wrapper(Func&& func, Args&&... args) {
              // 对参数进行处理或日志记录等操作
              return std::forward<Func>(func)(std::forward<Args>(args)...);
              }

              1. 临时对象(Temporary Objects):右值引用通常用于引用临时对象,即那些不具备持久性的、一次性的对象。这些对象通常产生于表达式求值过程中,如函数返回值、类型转换等。
              2. std::move() 函数:std::move() 是一个用于将左值强制转换为右值引用的函数,它并不实际移动数据,只是改变了对数据的引用方式。这对于告诉编译器我们希望执行移动操作非常有用。
                std::vector<int> source = getLargeData(); // 获取一个大型数据
                std::vector<int> destination = std::move(source); // 使用std::move()告诉编译器执行移动操作
                10、协程用过吗 (C++协程用过吗)?
                C++协程(Coroutines)是C++20标准引入的重要特性之一,它提供了一种更加高效和方便的多线程编程方式。协程可以看作是一种轻量级的线程,它允许在函数内部保存状态,并在需要时暂停和恢复执行,而不需要创建线程和管理线程的开销。
                协程的概念
                协程是一种控制流的抽象,它允许在函数执行中暂停和恢复执行,而无需阻塞线程。C++的协程通过co_await
                co_yield
                等关键字来实现。
                1. 协作式多任务:协程是协作式多任务的一种实现方式,不同于线程的抢占式多任务,它需要程序员显式地控制协程的切换。
                2. 状态保存:协程可以在函数中保存状态,包括局部变量、指令指针等,以便在恢复执行时继续之前的状态。
                3. 异步编程:协程非常适合异步编程,通过co_await
                  关键字可以等待异步操作完成而不会阻塞线程。
                协程关键字
                • co_await
                  :在协程中等待异步操作完成,暂停协程执行。
                • co_yield
                  :将值传递给协程的调用者,并临时暂停协程执行,直到调用者请求继续。
                • co_return
                  :返回值并结束协程的执行。
                • co_begin
                  :标记协程的起始点。
                • co_end
                  :标记协程的结束点。
                用法
                以下demo仅供参考
                  #include <iostream>
                  #include <experimental/coroutine>
                  #include <chrono>


                  using namespace std::chrono_literals;


                  // 定义一个协程类型
                  struct MyCoroutine {
                  struct promise_type {
                  MyCoroutine get_return_object() {
                  return {};
                  }
                  std::experimental::suspend_never initial_suspend() {
                  return {};
                  }
                  std::experimental::suspend_never final_suspend() noexcept {
                  return {};
                  }
                  void unhandled_exception() {}
                  void return_void() {}
                  };


                  // 协程函数
                  void operator()() {
                  std::cout << "Coroutine started." << std::endl;
                  std::this_thread::sleep_for(2s);
                  std::cout << "Coroutine resumed." << std::endl;
                  }
                  };


                  // 启动协程
                  MyCoroutine createCoroutine() {
                  co_await std::experimental::suspend_never{};
                  }


                  int main() {
                  MyCoroutine coroutine = createCoroutine();
                  coroutine();

                  std::cout << "Main thread continues." << std::endl;

                  return 0;
                  }

                  11、进程线程协程区别?
                  1. 进程(Process):
                  • 独立性:进程是操作系统分配资源的最小单位。每个进程都有自己独立的内存空间和系统资源,进程之间的通信需要额外的机制(例如管道、消息队列、共享内存等)。
                  • 开销:创建和销毁进程的开销比较大,因为需要分配和回收独立的内存空间和资源。
                  • 并发性:进程之间的并发性较高,可以在多核处理器上并行执行,但进程间的切换开销也较高。
                  1. 线程(Thread):
                  • 共享资源:线程是进程的一部分,多个线程共享同一进程的内存空间和资源。因此,线程之间的通信和数据共享相对容易。
                  • 开销:相对于进程,线程的创建和销毁开销较小,因为它们共享相同的地址空间。
                  • 并发性:线程之间的并发性较高,可以在多核处理器上并行执行,线程的切换开销相对较低。
                  1. 协程(Coroutine)
                  • 独立性:协程是用户级别的执行单元,与线程相比,协程更加轻量级。多个协程运行在同一个线程内,共享线程的内存空间。
                  • 协作式多任务:协程是协作式多任务的一种实现,需要显式地让出执行权给其他协程,切换开销非常小。
                  • 状态保存:协程可以保存自己的状态,包括局部变量、指令指针等,以便在恢复执行时继续之前的状态。
                  • 通信:协程之间的通信相对简单,可以通过函数调用和消息传递来实现。
                  区别总结:
                  • 进程是操作系统分配资源的最小单位,线程是进程的一部分,而协程是用户级别的执行单元。
                  • 进程之间的通信需要额外的机制,线程可以共享进程内的资源,协程共享线程内的资源。
                  • 进程切换开销最高,线程次之,协程最低。
                  • 线程和协程更适合多核处理器上的并行执行,进程通常用于更大规模的并发。
                  12、线程怎么调度的?
                  线程调度是多任务操作系统的核心功能之一,确保各个线程以公平和有效的方式分享CPU资源。
                  线程调度的基本原理和步骤
                  1. 线程状态:每个线程都有一个状态,常见的状态包括就绪态、运行态和阻塞态。
                    1. 就绪态:表示线程已经准备好运行,只等待分配CPU时间。
                    2. 运行态:表示线程正在CPU上执行指令。
                    3. 阻塞态:表示线程被阻塞,等待某个事件(如I/O操作完成)。
                  2. 调度队列:操作系统维护一个或多个调度队列,每个队列中包含了不同状态的线程。例如,就绪队列包含了所有处于就绪态的线程。
                  3. 调度器:调度器是操作系统内核的一部分,它负责从就绪队列中选择一个线程分配CPU时间。
                  4. 调度算法:调度器使用不同的调度算法来选择下一个运行的线程。常见的调度算法包括:
                    1. 抢占式调度:在任何时刻,操作系统可以中断当前运行的线程,并将CPU分配给另一个线程。这种方式下,线程可以被强制挂起,以便其他线程执行。常见的抢占式调度算法包括优先级调度和时间片轮转调度。
                    2. 非抢占式调度:线程只有在主动放弃CPU时,才会让出CPU给其他线程执行。这种方式下,线程自身控制着线程的切换时机。常见的非抢占式调度算法包括协作式多任务。
                  5. 上下文切换:当调度器选择了下一个线程后,需要进行上下文切换。上下文切换是保存当前线程的执行状态,包括寄存器、程序计数器、栈指针等,然后加载下一个线程的执行状态。
                  6. 时间片:某些调度算法(如时间片轮转调度)会为每个线程分配一个时间片,即一段固定的时间。线程在自己的时间片用完后,让出CPU给下一个线程。
                  7. 优先级:线程可以分配不同的优先级,高优先级的线程会被优先执行。但要注意不要滥用线程优先级,以免导致低优先级线程饥饿问题。
                  13、线程调度算法?
                  1. 先来先服务
                  在所有调度算法中,最简单的是非抢占式的先先来先服务(Firis-Come Fist-Severed,FCFS)算法。使用该算法,进程按照它们请求CPU的顺序使用CPU。基本上,有一个就绪进程的单一队列。早上,当第一个作业从外部进入系统,就立即开始并允许运行它所期望的时间。不会中断该作业,因为它需要很长的时间运行。当其他作业进入时,它们就被安排到队列的尾部。当正在运行的进程被阻塞时,队列中的第一个进 程就接着运行。当被阻塞的进程变为就绪时,就像一个新来到的作业样,排到队列的末尾。
                  主要优点是易于理解并且便于在程序中运用。在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可;要添加一个新的作业或阻塞一个进程,只要把该作业或进程附加在相应队列的末尾即可。
                  1. 最短作业优先
                  现在来看一种适用于运行时可以预知的另一个非抢占式的批处理调度算法。当输人队列中有若干个同等重要的作业被启动时,调度程序应使用最短作业优先(Shortest Job First,SJF)算法。
                  1. 最短剩余时间优先
                  最短作业优先的抢占式版本是最短剩余时间优先( Shortest Remaining Time Next,SRTN)算法。使用这个算法,调度程序总是选择其剩余运行时间最短的那个进程运行。有关的运行时间必须提前掌握。当一个新的作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式可以使新的短作业获得良好的服务。
                  1. 最高响应比优先算法
                  在批处理系统中,最高响应比优先算法(Highest Response Rate First,HRRF)的性能是介于先来先服务和最短作业优先算法之间的折中算法。先来先服务算法在调度中最为公平,但是一旦出现计算密集型的长作业则会对其他进程造成较长时间的等待;最短作业优先算法又偏好短作业,当短作业源源不断进入后备池时,长作业将会长时间滞留在后备池中,其运行将得不到保证,出现这种现象我们称为长作业处于“饥饿( starvation)”状态。
                  如果能每个作业引人响应比,情况就会有所改善。响应比的计算式为:
                  响应比 Rp= (等待时间+预计运行时间)/预计运行时间=周转时间/预计运行时间

                  每个作业随着在后备池等待时间的增长其响应比也不断增长,而且,预计运行时间越短的作业响应比增长越快。最高响应比优先算法在每次调度时选择响应比最高的作业投入运行,这种算法较好地适应了长短作业混合的系统,使得调度的性能指标趋于合理。

                  最高响应比优先算法在一定程度上改善了调度的公平性和调度的效率,响应比在每次调度前进行计算,作业运行期间不计算。计算需要消耗系统的资源,存在一定的系统开销。
                  1. 轮转法
                  轮转(Round-Robin,RR)算法最早来自分时系统。轮转法的基本思想是,将CPU的处理时间划分成一个个时间片,就绪队列中的诸进程轮流运行一个时间片。当时间片结束时,就强迫运行进程让出CPU,该进程进入就绪队列,等待下一-次调度。同时,进程调度又去选择就绪队列中的一个进程,分配给它一一个时间片,以投人运行。如此轮流调度,使得就绪队列中的所有进程在一个有限的时间T内都可以依次轮流获得一个时间片的处理机时间,从而满足了系统对用户分时响应的要求。
                  1. 最高优先级算法
                  最高优先级(Highest Priority First,HPF)进程(线程)调度每次将处理机分配给具有最高优先级的就绪进程(线程)。进程(线程)的优先级由进程(线程)优先数决定。
                  1. 多级反馈队列算法
                  在实际的计算机系统中,进程(线程)的调度模式往往是几种调度算法的结合。例如,可以以最高优先级算法作为主要的调度模式,但对于具有相同优先级的进程((线程)则按先进先出算法法处理。又如,可以将时间片轮转算法和最高优先级算法结合,对于具有相同优先级的进程(线程)按时间片轮转调度算法处理。多级队列反馈法就是综合了先进先出调度算法、时间片轮转算法和可抢占式最高优先级算法的一种进程(线程 )调度算法。
                  1. 最短进程优先
                  对于批处理系统而言,由于最短作业优先常常伴随着最短响应时间,所以如果能够把它用于交互进程,那将是非常好的。交互进程通常遵循下列模式:等待命令,执行命令,等待命令,执行命令,如此反复。如果将每一条命令的执行看作一个独立的“作业”,则可以通过首先运行最短的作业来使用响应时间最短。这里唯一的问题是如何从当前可运行进程中找出最短的那一个进程。
                  1. 实时系统中的调度算法
                  实时系统是一种时间起着主导作用的系统,即系统的正确性不仅取决于计算的逻辑结果,   而且还依赖于产生结果的时间。典型的,外部物理设备给计算机发送了一个信号,则计算机必须在一个确定的时间范围内恰当地做出反应。
                  14、协程怎么调度的?
                  协程调度是指在多个协程之间切换执行的过程。协程调度可以是由操作系统内核调度,也可以是由用户空间的协程库进行管理。
                  流程:
                  1. 创建协程:首先,需要创建多个协程任务。每个协程通常表示一个独立的执行单元,可以在其中执行特定的任务。
                  2. 初始化调度器:如果是由用户空间的协程库进行管理,需要初始化协程调度器。这通常包括创建协程队列、分配资源等。
                  3. 选择要执行的协程:协程调度器会选择一个协程来执行。选择的策略可以是先进先出(FIFO)、优先级、循环调度等,具体根据需求而定。
                  4. 切换协程上下文:当一个协程被选中执行时,需要进行上下文切换,保存当前协程的状态(寄存器、栈等),并加载目标协程的状态。
                  5. 执行协程任务:协程开始执行其任务。协程可以在任务执行中主动挂起(yield)或等待其他协程。
                  6. 挂起协程:协程在执行过程中可以选择挂起,将控制权交还给调度器。挂起的协程的状态会被保存,以便稍后恢复执行。
                  7. 恢复协程:当挂起的协程再次被调度时,调度器会恢复其状态,并从上次挂起的地方继续执行。
                  8. 重复选择和切换:调度器会根据选定的调度策略,重复选择和切换不同的协程,直到所有协程都执行完毕或达到终止条件。
                  15、redis的用途?
                  1. 缓存系统:Redis最常见的用途之一是作为缓存系统。由于其数据存储在内存中,读取速度非常快,适用于缓存经常访问的数据,从而减轻后端数据库的负载。
                  2. 会话存储:Redis可以用于存储用户会话数据,尤其对于需要快速访问和更新的应用程序非常有用,如Web应用程序的用户会话管理。
                  3. 消息队列:Redis支持发布/订阅模式(Pub/Sub),可以用于构建消息队列系统。生产者将消息发布到频道,而消费者则通过订阅频道来接收消息。这对于构建实时应用、日志处理和任务队列非常有用。
                  4. 计数器和排行榜:Redis支持原子操作,因此可以用于实现计数器,例如,统计网站上的页面访问次数。它还可以用于构建排行榜,例如,跟踪分数最高的用户。
                  5. 地理位置服务:Redis的GEO
                    数据结构支持地理位置数据的存储和查询。这对于构建地理位置服务和附近搜索非常有用。
                  6. 分布式锁:Redis可以用于实现分布式锁,帮助控制多个进程或服务对共享资源的访问,以避免竞态条件。
                  7. 持久性数据存储:虽然Redis的主要数据存储是内存中的,但它支持将数据持久化到磁盘,以防止数据丢失。
                  8. 实时统计和监控:Redis可以用于实时统计和监控,例如,跟踪在线用户数、查看服务器性能指标等。
                  16、消息队列了解吗?
                  Redis消息队列,也称为Redis Pub/Sub(发布/订阅),是Redis的一种特性,用于实现消息发布和订阅模式。它是一种简单而高效的消息传递机制,通常用于构建实时通信、事件处理、消息广播和实时数据更新等应用。
                  1. 发布者(Publisher):发布者是向Redis消息队列发送消息的客户端。发布者使用PUBLISH
                    命令将消息发布到一个或多个频道(Channels)。消息可以是任何字符串,通常表示一个事件或数据更新。
                  2. 频道(Channel):频道是消息的发布和订阅的主题或分类。发布者将消息发布到一个或多个频道,而订阅者则通过指定频道来接收消息。频道的名称是字符串,例如:"news"、"chatroom"等。
                  3. 订阅者(Subscriber):订阅者是等待接收消息的客户端。它们使用SUBSCRIBE
                    命令来订阅一个或多个频道,然后开始等待消息的到达。订阅者可以随时取消订阅。
                  4. 消息传递:当发布者发布一条消息到某个频道时,所有订阅了该频道的订阅者都将收到相同的消息。这是一对多的消息传递方式,消息传递是异步的,即发布者不等待订阅者接收消息。
                  5. 模式匹配:Redis支持通配符订阅,允许订阅者使用通配符来匹配多个频道。例如,可以使用SUBSCRIBE news.*
                    来订阅以"news."开头的所有频道。
                  6. 消息持久性:默认情况下,Redis Pub/Sub是瞬态的,如果没有订阅者在监听消息,消息将被丢弃。但Redis提供了一种方式来将消息持久化,称为"消息订阅历史"(Message Retention)。通过配置,可以保留最近发布到频道的N条消息,以便新订阅者可以获取最新的消息历史。
                  7. 消息订阅历史:通过配置Redis,可以启用消息订阅历史,这样新订阅者可以在订阅频道后立即获取一定数量的历史消息。这对于确保新加入的订阅者能够获取到关键的起始数据非常有用。
                  8. 取消订阅:订阅者可以随时取消对频道的订阅,通过UNSUBSCRIBE
                    命令取消对特定频道的订阅,或使用UNSUBSCRIBE *
                    取消对所有频道的订阅。
                  Redis消息队列通常用于实现实时事件处理、实时聊天、实时通知、日志分发、数据更新通知等场景。它是一种简单且高性能的消息传递机制,非常适合处理需要实时数据传递的应用程序。需要注意的是,Redis消息队列并不适用于需要高级消息队列功能(如消息持久性、消息优先级、事务性和复杂路由规则)的高级消息队列用例,对于这些用例,可以考虑使用专门的消息队列系统,如RabbitMQ、Apache Kafka等。
                  17、grpc原理?
                  gRPC(gRPC Remote Procedure Call)是一个开源的远程过程调用框架,由Google开发,基于HTTP/2协议。它允许客户端和服务器之间通过定义方法和消息类型的接口规范来通信。
                  基本原理:
                  1. IDL(接口定义语言): gRPC使用IDL来定义服务的接口规范。IDL文件可以使用Protocol Buffers(protobufs)编写,它是一种轻量级且高效的二进制序列化格式。IDL文件定义了服务方法、消息类型和消息字段。
                  2. 多语言支持: gRPC支持多种编程语言,包括C++, Java, Python, Go, Ruby等,这意味着您可以使用不同的语言来开发客户端和服务器,并它们可以相互通信。
                  3. HTTP/2协议: gRPC使用HTTP/2作为底层传输协议。HTTP/2相对于HTTP/1.1具有更好的性能,支持双向流、多路复用、头部压缩等特性,这些特性有助于减少延迟和提高效率。
                  4. 双向流: gRPC允许客户端和服务器之间建立双向流,这意味着它们可以同时发送和接收数据。这对于实现实时通信非常有用。
                  5. 基于消息: gRPC通信是基于消息的,客户端和服务器使用protobufs消息进行通信。这些消息可以包含请求和响应数据,它们具有严格定义的结构。
                  6. 自动化的序列化和反序列化: gRPC框架提供自动生成的代码来处理消息的序列化和反序列化,使得在不同语言之间通信变得更加容易。
                  7. 支持多种认证和安全性: gRPC支持各种认证和安全性机制,包括SSL/TLS、OAuth2等,以确保通信的安全性。
                  8. 拦截器(Interceptors): gRPC允许您在客户端和服务器上定义拦截器,以添加各种功能,如身份验证、日志记录、错误处理等。
                  9. 流控制: gRPC支持流控制,以避免过多的数据传输导致内存问题。
                  18、http 2.0了解吗?
                  HTTP/2(Hypertext Transfer Protocol 2)是HTTP/1.1的继任者,旨在提供更高的性能和效率。
                  工作原理:
                  1. 二进制分帧(Binary Framing):
                    1. HTTP/2使用二进制格式来传输数据,而不是HTTP/1.1中的文本格式。所有的消息(请求和响应)都被拆分成多个帧(frames),每个帧都有特定的用途,如头部帧、数据帧等。
                    2. 使用二进制格式带来了多方面的好处,包括更高效的解析和传输,以及更好的错误检测和纠正。
                  2. 多路复用(Multiplexing):
                    1. HTTP/2引入了多路复用机制,允许多个请求和响应在单个TCP连接上同时传输,而无需按顺序等待。
                    2. 这消除了HTTP/1.1中的队头堵塞问题,可以更充分地利用网络带宽,从而提高页面加载速度。
                  3. 头部压缩(Header Compression):
                    1. 为了减小消息头的传输大小,HTTP/2使用了头部压缩算法(通常为HPACK)。
                    2. 服务器和客户端会维护一个动态的头部表,用于存储已发送或接收的头部字段,减少重复的传输。
                  4. (Stream):
                    1. HTTP/2引入了流的概念,每个请求和响应都与一个唯一的流相关联。
                    2. 流可以并行传输,通过帧的标识符关联,而不会相互干扰。这使得多个请求可以同时在单个连接上处理,提高了效率。
                  5. 服务器推送(Server Push):
                    1. HTTP/2允许服务器在客户端请求之前主动推送资源。服务器可以发送与请求相关的其他资源,以减少客户端的等待时间。
                    2. 这特别有助于提高网页加载速度,因为服务器可以预测客户端需要哪些资源。
                  6. 流量控制(Flow Control):
                    1. HTTP/2包括流量控制机制,使得客户端和服务器可以控制对方发送数据的速率。
                    2. 这有助于防止过多的数据传输导致网络拥塞,并提高了性能。
                  7. 优先级(Priority):
                    1. HTTP/2允许为流设置优先级,确保关键资源的加载优先级更高。客户端可以向服务器发送优先级信息,以指导服务器资源的传输顺序。
                  总的来说,HTTP/2通过引入二进制分帧、多路复用、头部压缩、流、服务器推送、流量控制和优先级等一系列新特性,显著提高了Web应用的性能和效率。这些特性使得HTTP/2在高延迟和高带宽网络环境下表现出色,并已经广泛应用于现代Web服务器和浏览器中,以提供更好的用户体验。
                  19、算法(多线程)
                  给你两个线程,一个线程打印字母(从A到Z),一个线程打印数组(从1到26),要求交替打印,如:A1B2C3?
                  参考代码:
                    #include <iostream>
                    #include <thread>
                    #include <mutex>
                    #include <condition_variable>


                    std::mutex mtx;
                    std::condition_variable cv;
                    bool printAlphabet = true;


                    void printAlphabets() {
                    for (char c = 'A'; c <= 'Z'; ++c) {
                    std::unique_lock<std::mutex> lock(mtx);
                    cv.wait(lock, [] { return printAlphabet; });
                    std::cout << c;
                    printAlphabet = false;
                    cv.notify_one();
                    }
                    }


                    void printNumbers() {
                    for (int i = 1; i <= 26; ++i) {
                    std::unique_lock<std::mutex> lock(mtx);
                    cv.wait(lock, [] { return !printAlphabet; });
                    std::cout << i;
                    printAlphabet = true;
                    cv.notify_one();
                    }
                    }


                    int main() {
                    std::thread t1(printAlphabets);
                    std::thread t2(printNumbers);


                    t1.join();
                    t2.join();


                    std::cout << std::endl;


                    return 0;
                    }

                    结果输出:
                    我们使用一个条件变量 cv
                    和一个布尔变量 printAlphabet
                    用来表示当前是打印字母还是数字。每个线程在打印完一个字符或数字后会等待条件满足,然后唤醒另一个线程并切换 printAlphabet
                    的状态,以实现交替打印。
                    这种方式避免了不必要的互斥锁竞争,线程只会在需要时等待条件满足,从而提高了效率。

                    以上便是我对这些面经的整理以及问题的一些理解,都仅供参考。希望大家有收获,但别盲目记忆这些问题答案,更多地理解其背后原理,面试就不会发挥的差到哪儿去。

                    如果说,有哪方面不明白,或者有质疑,可以随时私我,也烦请您看看书,一切以书本为准。

                    文章转载自阿Q正砖,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论