c++游戏后台面试总结

基础项:

  1. c++基础:

    深拷贝和浅拷贝:

    • 深拷贝:增加一个指针指向申请的一块新的内存,并把原数据复制到新内存中,修改原数据时新数据不会改变;

    • 浅拷贝:增加一个指针指向已有对象的内存,修改原数据时该指针指向的(内存中的)数据也会变;

    智能指针:

    • shared_ptr:采用引用计数方式管理所指向的对象。用途:观察者模式;

    • weaked_ptr:配合shared_ptr使用,指向shared_ptr指向的对象,但不增加引用计数,用lock()函数获取原指针;

    • unique_ptr:独占所指向的对象;

    • auto_ptr:管理权限的转移;

    智能指针是怎么实现的

    • 采用引用计数的方式,构造+1,析构-1,赋值时左边的-1,右边的+1;可以引入辅助类进行实现智能指针;

    多态实现及虚函数:

    • 函数调用表现多态的条件:

      存在继承关系;

      基类指针指向子类对象;

      继承关系中必须有同名的虚函数,且是覆盖关系;

    • 虚函数设置:

      虚函数只能是普通(对象)成员函数;

      静态成员函数不能是虚函数,因为没有this指针;

      类的构造函数不能是虚函数,因为构造时还未生成对象;

      类的虚函数在类外定义时不能有virtual关键字;

      析构函数通常定义为虚函数,防止基类指针指向子类对象析构时子类无法调用析构函数;

      构造函数不能是虚函数,因为虚函数使用必须通过虚函数指针指向的虚表进行调用,但是虚函数指针需要在构造函数中进行初始化,这就是个悖论。

      类的普通成员函数根据指针类型来选择调用,类的虚函数根据虚表中的类型调用。

    • 虚表和虚基类表:

      虚表属于类,而不属于任何对象,对象只是保存一个指向虚表的指针。

      虚基类表在虚继承时产生,首项存储虚基类指针的偏移量,接下来依次存储虚基类的偏移量(偏移量是相对于虚基类表指针的存储地址)。这样菱形继承关系的基类A就只有一份,B和C的虚基类指针都指向同一个A,所以就解决了菱形继承的二义性。

    • 菱形继承(类D同时继承B和C,B和C又继承自A):

      菱形继承会存在二义性问题,需要使用虚继承的方式。此时子类将不会使用基类的虚函数表,而是自己重新创建一个虚函数表,另外还有一个虚基类表。所以会有三个虚表指针和一个虚地址指针,普通继承只会有两个虚表指针,不会有虚地址指针。

      解决方案:采用虚继承,新增虚基类指针,指向虚基类表。

      class Ca //会创建一个虚表
      {
      public:
      virtual void test4(int a,int b) { cout << "test ca" << endl; }
      virtual void test1() { cout << "test1 ca" << endl; }
      public:
      int count;//Cc中的变量count和Cb中的变量count在Cd中不会覆盖,而是由自己的独立地址
      };

      class Cb:virtual public Ca   //采用虚继承 会创建一个虚表和虚基类表
      {
      public:
      virtual void test(int a ,int b) { cout << "test cb" << endl; }
      virtual void test1() { cout << "test1 cb" << endl; }
      int m;
      };

      class Cc :virtual public Ca  //采用虚继承 会创建一个虚表和虚基类表
      {
      public:
      virtual void test(int a, int b) { cout << "test cb" << endl; }
      virtual void test1() { cout << "test1 cb" << endl; }
      int m; //Cc中的变量m和Cb中的变量m在Cd中不会覆盖,而是由自己的独立地址
      };

      class Cd :public Cb,public Cc //和Cb共用一个虚表,因为不是虚继承,也没有虚基类表
      {
      public:
      virtual void test(int a, int b) { cout << "test cb" << endl; }
      virtual void test1() { cout << "test1 cb" << endl; }
      };
    • 重载,重写与覆盖:

      重载:

      • 必须在同一个类中;

      • 函数名称相同;

      • 函数参数列表不同;

      • virtual关键字可有可无;

      • 返回值可以不同;

      重写:多态

      • 必须在子类和基类两个作用域中;

      • 函数名必须相同;

      • 函数参数列表必须相同;

      • 函数必须有virtual关键字,且不是静态函数;

      • 返回值必须相同;

      覆盖:

      • 必须在子类和基类两个作用域;

      • 函数名必须相同;

      • 返回值可以不同;

      • 参数列表不同时,无论有无virtual关键字,基类的函数都将被隐藏;

      • 参数列表相同时,如果基类无virtual关键字,则基类函数被隐藏;

    内联函数:inline

    • 定义:不在调用时发生控制转移,而是在编译时将代码嵌入(复制到)每一个调用处,适用于功能简单,规模小又频繁调用的函数。

    • 与宏的区别:

      宏是在预处理阶段进行的替代,且不进行严格的参数检查,返回值不能被强制转换成可转换的合适类型;

      内联函数是在编译阶段进行的替换,取消了函数的参数压栈,减少了调用开销;

    • 注意事项:

      1.内联函数中不能有递归和switch语句;

      2.内联函数的规模不能太大,函数调用的时间开销如果大于执行函数本身时间,效率收获就很少,且代码总量增大,消耗更多内存空间;

    Lua与c++的交互,闭包:

    • lua与c++通过虚拟栈进行交互。栈中正数1永远是栈底,倒数-1永远是栈顶;

    • c++中压入栈的数据最后都存入lua中的TValue中,TValue结构对应lua所有数据类型;

    • lua中,number,boolean,nil,light userdata四种类型的值是直接存在栈上元素里,和垃圾回收无关;

    • lua中,string,table,closure,userdata,thread存在栈上元素的只有指针,会在生命周期结束后被垃圾回收;

    • 凡是lua中的变量,lua负责其生命周期和垃圾回收,c++只需要api来进行调用,lua也不知道c++中的值;

    闭包:通过调用含有一个内部函数加上该外部函数持有的外部局部变量(upvalue)的外部函数产生的一个实例;

    闭包组成:外部函数 + 外部函数创建的upvalue + 内部函数(闭包函数)

    function test()//外部函数
     local i=0   //upvalue
     return function()//内部函数
       i++
      ...
     end
    end
  2. linux基础及分布式:

    零拷贝:

    作用:

    • 减少用户态和内核态的转换;

    • 减少数据的拷贝;

    方法:

    • 1.mmap():系统调用导致文件被DMA引擎拷贝到内核缓存中,这个缓存是和用户进程共享的,在内核和用户内存空间中没有任何拷贝;

    • 2.sendfile():系统调用导致文件被DMA引擎拷贝到内核缓存中,没有数据被拷贝到socket缓存中,只会把文件数据的位置和长度信息的描述符复制到套接字缓冲区,DMA引擎直接将数据从内核缓存传输到协议引擎,消除了最后的拷贝。

    写时拷贝:

    只有在修改时才进行拷贝。

    • redis中备份rdb数据时fork一个子进程就是用了写时拷贝,此时为了节省内存,主进程和子进程共用内存中的快照的数据,只有当数据被更新时,主进程将修改的页面复制出来进行修改。

    • string的实现就使用的写时拷贝。其拷贝构造和赋值都不会新申请内存,而是和原数据使用一块内存,只有当原数据需要修改,如 [] 操作或resize()时才会复制到一块新的内存中。

    协程:

    • 一个线程中可以创建多个协程,且是串行执行的,不能有效利用多cpu;

    • 协程是由程序控制(用户态执行),不会像线程和进程一样由操作系统控制,所以不涉及用户态到内核态的转变,所以开销更小,效率更高;

    • 协程更加轻量,创建成并小,降低了内存消耗;

    • 协程因为是串行所以没有同步锁,提高了性能;

    • 协程中不能由阻塞操作,否则整个线程都会被阻塞;

    • 协程使用场景:IO密集型应用中;

    进程和线程之间的通讯:

    • 进程间通讯方式:

      1. socket:可用于不同设备及其间的进程通讯;

      2. 消息队列:放在内核中由消息队列标识符标识,克服了信号传递信息少,管道只能承载无格式字节流及缓冲区大小限制的缺点。

      3. 信号量:是一种计时器,常作为一种锁机制防止进程访问共享资源时的竞争问题。主要作为进程间以及同一进程内不同线程之间的同步手段。

      4. 信号:通知接收进程某个事件发生。

      5. 共享内存(IPC):映射一段能被其他进程所访问的内存。最快的IPC方式是针对其他进程通讯方式运行效率低而专门设计的,往往与其他通讯机制共同使用。

      6. (匿名)管道:半双工通讯方式,数据只能单向流动,只能在有亲缘关系的进程(父子进程)间使用。

      7. 命名管道:半双工通讯方式,数据只能单向流动,可以在无亲缘关系的进程间使用。

    • 线程间同步(通讯):

      • 临界区:读写锁

      • 互斥对象:互斥锁

      • 信号量:以通知的方式进行控制

      • 事件对象:条件变量(condition_variable),以通知的方式进行控制

    多进程和多线程的好处:

    • 多进程:

      • 优点:

        相互独立,子进程崩溃也没关系,不影响主进程稳定;

        通过增加cpu来扩充性能;

        减少线程加锁/解锁对性能的影响;

        每个子进程都有2GB地址空间和相关资源,总体能达到的性能上限非常大;

      • 缺点:

        逻辑控制复杂,需要与主程序交互;

        需要跨进程边界,大量数据传输费力;

        多进程调度开销比较大;

    • 多线程:

      • 优点:

        不需要跨进程边界;

        逻辑和控制方式简单;

        可以直接共享内存和变量等;

        消耗的总资源相对较少;

      • 缺点:

        每个线程与主程序共用地址空间,受限于2GB地址空间大小;

        线程间同步和加锁控制比较麻烦;

        线程的崩溃会导致整个程序崩溃;

        每个程序的线程数量有上限,即使增加cpu也无法提高性能;

        线程数量多了调度会消耗较多的cpu;

    自己实现定时器:

    • 最小堆:采用优先队列

    • 时间轮:时间轮一共有N个槽,每个槽上有一个链表,每个tick转动一格。

      代码链接

      ts = (cs + (ti / si)) % N; //cs为当前时间轮的格数,ti为过期时间,si为每个tick时间,N为时间轮槽数量

    多线程编程,具体服务器调用的api:

    • 创建套接字--socket()系统调用;

    • 绑定套接字--bind()系统调用;

    • 监听套接字--listen()系统调用;

    • 接收连接--accept()系统调用;

    • 请求连接--connect()系统调用;

    • 关闭连接--close()系统调用;

    cas机制:(compare and set)----比较和替换

    • 适用场景:并发不高时

    • 缺点:

      • 1.cpu开销大:采用死循环一直尝试更新某个变量,会给cpu带来很大压力;

      • 2.不能保证代码块的原子性:只能保证单个变量的原子性,不能保证整个代码块的原子性;

      • 3.ABA问题:一个变量的值从A变为B又变为A,这样需要加版本号来区别;

    CAP:分布式理论

    • Consistency(一致性):写操作后的读操作必须返回该值;

    • Availability(可用性):只要收到用户请求,服务器必须做出响应;

    • Partition tolerance(分区容错性):区间通信可能失败;

    三者关系:需要p总是成立,但分区容错性无法避免,所以C和A无法同时满足。

    跳表:

    定义:在有序链表上进行查询时,仿照二分法查询数组在链表上做多组索引;

    查询,删除的时间复杂度:O(logn)

    使用范例:redis

    垃圾回收:

    A.引用计数算法:

    定义:每个对象计算指向其的指针数量,有指针指向时加1,删除一个指向自己的指针时减1,当引用计数为0时回收内存;

    优点:

    • 1.内存管理开销分布在整个程序运行期间,非常平滑,无需挂起程序进行垃圾回收;

    • 2.引用局部性比较好,引用计数为0时,系统无需访问其他页面单元;

    • 3.废弃即回收,不会存在废弃后还存活一段时间的问题;

    缺点:

    • 1.时间开销;

    • 2.空间开销;

    • 3.无法解决环形引用问题;

    B.标记-清除算法:Lua采用的gc方法

    定义:标记阶段,从根集对象开始标记,对整个对象层级都标记完,被标记的都是可到达对象;清除阶段,遍历对象链表(所有动态分配的对象连成的多个链表),如果对象被标记过则擦除标记并跳过,如果未标记则说明其不可达,回收其内存;

    Lua5.0之前的回收时stop all world,GC一次性完成,但这样对于大规模程序效率有影响,所以5.0之后采用分步GC;

    三色标记:

    • 白色1:表示未被标记,在GC周期开始之前对象为白色1,如果到清除阶段还是白色则说明需要被清除;

    • 白色2:表示在标记后但未清除前新建的对象,这些对象被标记为白色2将不会被清除,要在下一个GC周期做判断

    • 灰色:表示对象本身已标记,但它引用的对象未被标记,是一个中间状态;

    • 黑色:表示对象和其引用对象都被标记,在清除阶段黑色对象为可达对象;

    优点:

    • 可以处理循环引用问题;

    • 减少了创建和消耗对象时操作引用计数的开销;

    缺点:

    • 是一种“停止-启动”算法,在GC时应用程序必须停止;

    • 标记阶段要遍历所有存活的对象,有一定开销;

    • 清除阶段,清除垃圾对象后有大量内存碎片;

    如何解决循环引用:弱引用

    • 弱引用就是会被GC忽视的对象引用,如果一个对象的引用是弱引用,则这个对象可以被回收;

    • 弱引用table分为三种:

      • 具有弱引用key的table

      • 具有弱引用value的table

      • 具有两种弱引用的table

    C.标记-缩并算法:

    定义:是为了解决内存碎片问题而产生的一种算法。过程:标记所有存活对象-->重新调整存活对象位置来缩并对象图-->更新指向被移动了位置的对象的指针;

    最大的难点在于压缩算法的选择:

    • 任意:移动对象时不考虑他们原来的次序,也不考虑他们之间是否存在引用关系;

    • 线性:尽可能把原来的对象和它所指向的对象放在相邻的位置上,这样可以达到更好的空间局部性;

    • 滑动:将对象“滑动”到堆的一端,把存活对象之间的自由单元“挤出去”,从而维持了分配时的原始次序;

    D.节点拷贝算法:

    定义:把整个堆分为两个半区(From,To),GC的过程其实就是把存活对象从一个半区From拷贝到另一个半区To的过程,而在下一次回收时,两个半区再互换角色。移动结束后,再更新对象的指针引用;

    优点:

    • 在拷贝过程中就可以进行内存整理,不会存在内存碎片问题;

    • 不需要专门做一次内存压缩;

    缺点:

    • 需要双倍的空间;

    Python垃圾回收:

    • 1.引用计数:主要通过引用计数进行垃圾回收

    • 2.标记-清除:解决容器对象可能的循环引用问题;

    • 3.分代回收:以空间换时间的方法提高垃圾回收效率;

    分代回收:

    • 思想:对象存在时间越长越可能不是垃圾,应该减少去收集。这样执行标记-清除算法可以减少遍历的对象数量,提高垃圾回收速度;

    • 方法:python中的所有对象分为三代。

      • 刚创建的对象是0代;

      • 经过一次垃圾回收后依然存在的对象,便会依次从上一代移到下一代;

      • 每一代启动自动垃圾回收的阈值可以单独指定,当垃圾回收器中新增对象减去删除对象达到相应的阈值时启动自动回收;

      • 高代数启动垃圾回收时比其低的代数自动启动垃圾回收。如1代启动垃圾回收,则0代也会启动垃圾回收;

    内存屏障:

    定义:因为编译器的优化或cpu对寄存器和cache的使用,及cpu乱序执行,导致对内存的操作不能及时反映出来,比如cpu写入后,读出来的值仍是旧内容;

    分为三类:

    • 编译器优化

    • 缓存优化

    • cpu乱序执行

    解决方案:

    • 锁机制

    • MESI协议:缓存一致性协议

      • 修改:M。一个处理器对该缓存行进行过修改,需要写回内存,并通知其他拥有者该缓存失效;

      • 独占:E。仅该处理器拥有该缓存行,但并未进行修改,是最新的值

      • 共享:S。多个处理器拥有该缓存行,每个处理器都没有修改过,是最新的值;

      • 失效:I。缓存行被其他处理器修改过,该值已经不是最新的值,需要从内存上读取;

      • 相关优化:

        • 1.store buffers:存储缓存。处理器将缓存行修改后需要通知其他拥有该缓存行对应内存所映射的处理器进行失效处理,只有当其他处理器都失效返回时才能写入内存,这段时间该处理器只能阻塞。所以优化后将缓存的修改放到存储缓存队列中,处理器直接返回做其他事,剩下的通知和等待失效返回及写内存都由存储缓存来执行;

        • 2.失效队列:处理器收到其他处理器的存储缓存发来的缓存失效信息时,直接将该信息放入失效队列,并返回失效返回信息。等合适的机会才去处理失效队列,将缓存标记为失效,从内存中获取最新值。因为这块需要读取内存,并且当发送失效信息的处理器的存储缓存满了还是得阻塞处理器等待失效返回信息,所以这块采用失效队列进行优化。

    • cpu乱序执行:用cpu内存屏障

      • 1.up架构(单处理器)不需要内存屏障就可以保证顺序执行;

      • 2.SMP架构(多处理器)需要使用内存屏障

        _asm_ _volatile_("mfence":::"memory"); //cpu内存屏障
        • 1.sfence:指令前后的写入(store/release)指令,按照sfence前后的指令顺序执行。是将数据写回内存。即处理stroe buffers中的内容;

        • 2.lfecne:指令前后的读出 (load/acquire)指令,按照lfence前后的指令顺序执行。是将数据从高速缓存中抹去,从内存中直接读取。即处理失效队列中的内容;

        • 3.mfecne:结合以上两个指令的操作。

      • 3.linux中的内存屏障

        • 1.单核使用编译器屏障:barrier()和ACESS_ONCE();

        • 2.多核使用cpu屏障:

          • 通用屏障,保证读写操作,包括mb()和smp_mb();

          • 写操作屏障,仅保证写操作,包括wmb()和smp_wmb();

          • 读操作屏障,仅保证读操作,包括rmb()和smp_rmb();

    • volatile关键字:禁止编译器优化,每次读都从内存而不是从寄存器中进行;

      使用场景:

      • 1.中断服务程序中修改的供其他程序检测的变量需要加volatile关键字;

      • 2.多任务环境下各任务共享的标志应该加volatile;

      • 存储器映射的硬件寄存器通常也要加volatile,因为每次对它的读写可能意义不同;

      注意事项:

      • 1.一个参数可以同时用const和volatile修饰;

      • 2.一个指针也可以被volatile修饰;

      • 3.以下取平方函数:

        int square(volatile int *ptr)
        {
           return *ptr * *ptr;  
        }
        //因为函数参数是volatile修饰的,所以编译器产生以下类似代码:
        int square(volatile int *ptr)
        {
           int a = *ptr;
           int b = *ptr;  //因为ptr指向的值可能被意外改变,所以a和b可能不相等,所以这个函数有问题
           return a*b;
        }
        //正确的如下
        int square(volatile int *ptr)
        {
           int a = *ptr;
           return a*a;
        }

    惊群效应:

    • accept函数引起的惊群效应:

      原因:主进程/线程创建监听socket,子进程/线程分别进行监听,当有连接到来时所有监听的子进程/线程都会被唤醒,引起惊群效应;

      方案:linux2.6以后的内核已经修复该问题,在内核中增加一个互斥等待变量WQ_FLAG_EXCLUSEVE,,当wake_up被在一个等待队列上调用时,它唤醒第一个有该标记的的子进程/线程后停止,队列中其他则等待下一次事件发生,这样避免惊群问题;

    • epoll引起的惊群效应:

      • 1.fork()之前调用epoll_create():

        原因:主进程/线程创建listenfd和epollfd,子进程/线程将listenfd加到epollfd中,引起的惊群效应类似于accept函数;

        方案:和accept函数引起的惊群解决方案一样,增加一个互斥等待变量;

      • 2.fork()之后调用epoll_create():

        原因:主进程/线程创建listenfd,子进程/线程各自创建epollfd,并将同一个listenfd加到各自的epollfd中,当有消息来时唤醒了所有子进程/线程;

        方案:目前内核并未解决该问题,nginx中给出了解决方案;

    • nginx惊群效应:

      原因:同epoll中fork()之后调用epoll_create();

      方案:

      • 1.采用互斥锁,只有拿到锁的子进程/线程才能将listenfd加到epollfd中进行监听,当有消息来时也只有这个子进程/线程accept;

      • 2.采用主动的方法在获得锁的子进程/线程将listenfd加到epollfd,同时将未获得锁的子进程/线程将listenfd移出epollfd;

      • 根据自身负载决定是否抢锁,做了简单的负载平衡;

    • 线程池惊群:

      pthread_cond_signal函数时发送给一个信号到另一个正在处于阻塞等待状态的线程,使其继续执行。调用此函数后,系统会唤醒在相同条件变量上等待的一个或多个线程。所以当多个消费者线程使用同一个条件变量时都会被唤醒,造成惊群效应;

      方案:每个消费者线程同样使用一个互斥锁,但是每个消费者线程拥有自己的条件变量,这样pthread_cond_singal函数只会唤醒其中一个线程;

    gdb命令,shell命令:

    • gdb命令:

      bt:查看堆栈

      p:输出

      b:断点

    • shell命令:

      cp:复制

      ps:显示当前系统用户进程列表 eg: ps -ef | grep redis 查找redis进程

      grep:在文件里查找内容

    Hash冲突解决方法:

    • 1.开放地址法;

    • 2.拉链法;

    如何检测内存泄漏

    • 对象计数法。构造加1,析构减1,每隔一段时间打印对象数量。没有性能开销,但是是侵入性方法需要修改代码,对库等不友好;

    • 重载new/delete。侵入式方法,需要将头文件加到大量的源文件头部,记录分配点如果是多线程还要加锁,记录占用大量内存;

    • Hook windows api。使用微软的detours库,hook分配内存的系统api,记录分配点,定期打印。非侵入式。无需修改文件,检测全面,对第三方库等都可以统计。但要占用大量内存,多线程需要加锁;

    • 使用DIagLeak检测。原理同hookapi。有缺点类似;

    • 总结:Linux使用valgrind,windows下建议大对象用计数法,定位快速准确,开销小,对外测试阶段使用leakdiag,并发压力不大,性能开销可以承受。线上大规模时使用hookapi,结合gm指令控制时间段检测,减小对玩家影响。

    如何设计一个日志库

    • 1.生产日志。给日志加上必要的数据,如时间,线程id等

    • 2.过滤日志。给日志分级,可以控制记录不同的日志级别

    • 3.格式化日志。针对不同的输出途径进行不同的格式化;

    • 4.输出日志。可以输出到控制台,文件记录和网络传输等

  3. 网络:

    tcp,kcp和udp的区别,如何使用udp实现可靠传输

    tcp控制拥塞的四种算法:
    • 1.慢开始:发送放的拥塞窗口(cwnd)大小小于拥塞阈值(ssthresh)(初始值为16)时,cwnd从1开始采用指数增长

    • 2.拥塞避免:cwnd>=ssthresh时,cwnd大小采用线性增长,每次加1,直到网络拥塞;网络拥塞时cwnd直接从1开始,ssthresh的值变为网络拥塞时的一半,采用慢开始策略继续发送数据;

    • 3.快重传:接收方收到失序的数据时立刻返回确认,而不是等到自己发送数据时捎带确认。当发送方收到三次相同的确认时即认为数据丢失,立即重传丢失的数据,而不是等待重传定时器到时再重传;

    • 4.快恢复:在快重传发生时,因为还能收到确认消息,发送方不认为网络拥塞,此时会将cwnd和ssthresh的值都降为重传时ssthresh的一半,并开始拥塞避免算法。此处不会用慢开始;

    拥塞控制和滑动窗口(流量控制):
    • 共同点:

      • 1.现象都是丢包;

      • 2.实现机制都是让发送方发的慢一点,少一点;

    • 不同点:

      • 1.丢包位置不同:

        • 拥塞控制丢包是在网络中;

        • 流量控制丢包是在接收端;

      • 2.适用对象不同:

        • 1.拥塞控制的对象是网络,怕网络中的数据太多造成网络拥塞;‘

        • 2.流量控制的对象是接收端,怕接收端缓存不够来不及接收;

    Tcp BBR协议
    • 不再使用丢包作为拥塞的信号,也不使用“加性增,乘性减”来维护发送窗口大小,而是分别估计极大带宽和极小延迟,把它们的乘积作为发送窗口的大小。

    • BBR连接开始阶段由慢启动,排空两阶段组成。为了解决带宽和延迟不易同时测准的问题,BBR在连接稳定后交替探测带宽和延迟,其中探测带宽阶段占绝大部分时间,通过正反馈和周期性的带宽增益尝试来快速响应可用带宽变化,偶尔的探测延迟阶段发包速率很慢,用于测准延迟。

    • BBR解决了两个问题:

        1. 在有一定丢包率的网络链路上充分利用带宽。非常适合高延迟,高带宽的网路链路;

        1. 降低网络链路上的buffer占用率,从而降低延迟。非常适合慢速接入网络的用户;

    • TCP拥塞控制算法是数据的发送端决定发送窗口,因此在那边部署,就对那边发送的数据有效。下载部署在服务器端,上传部署在客户端。

     

    用udp实现可靠传输-kcp:

    kcp是纯算法实现,以浪费10%--20%的带宽来降低30%--40%的平均延迟,内部不会做系统调用,且不负责底层的数据发送(如udp),而是采用回调的方式调用udp的sendto函数进行发送,连时钟都需要外部传递;

    kcp在发送数据时会有类似tcp滑动窗口的设置,这个窗口大小取 min(发送方缓存值,接收方缓存值,网络拥塞窗口值),并且每个kcp的数据包都有序列号,采用tcp哪种发送确认机制,根据不同策略选择超时重传,快速重传和选择重传;

    tcp的三次握手和四次挥手:

    • 握手:

      三次握手不能改成两次握手,可能会造成死锁。因为网络不可靠,导致服务器发送的FIN+ACK消息没有到达客户端,此时客户端也未收到ack所以会不断发送SYN消息到服务器建立连接,但服务器在第一次返回FIN+ACK时已经建立连接,开始向客户端发送数据,客户端因为未建立连接会忽略这些数据导致服务器超时重传。这样造成死锁。

    • 挥手:

      • 客户端:FIN_WAIT1 | FIN_WAIT2 | TIME_WAIT(2*MSL)

      • 服务器:CLOSE_WAIT | LAST_ACK

      客户端最后收到服务器FIN返回ACK后还有TIME_WAIT等待2个最长报文时间的原因是害怕因为网络原因导致最后的ACK没有到达服务器,用这段等待时间进行重传,当然重传时会继续等待2*MSL。

    • 大量TIME_WAIT问题:

      原因:服务器存在大量tcp短链接时,服务器主动断开连接则socket就会处于TIME_WAIT状态,此状态下的socket不能被其他连接使用,而系统的socket的数量是有限的,所以如果在高并发的情况下这个是很大的问题;

      解决:打开系统的TIME_WAIT重用和回收

      netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'  #查看tcp连接状态

      #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
      net.ipv4.tcp_syncookies = 1  
      #表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭  
      net.ipv4.tcp_tw_reuse = 1  
      #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭  
      net.ipv4.tcp_tw_recycle = 1
      #表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间  
      net.ipv4.tcp_fin_timeout=30
    • 大量CLOSE_WAIT问题:

      原因:在被动关闭tcp连接时,接收到主动方发送的Fin后由于自身代码等原因导致没有关闭链接;

      方案:检查代码。判断socket,一旦读到0,断开连接,read返回负值,检查errno,如果不是AGAIN就断开连接

    • tcp的拆包与粘包:

      原因:

      • 1.发送缓冲区不够,发生拆包;

      • 2.数据包超过MSS(最大报文长度),发生拆包;

      • 3.发送的数据包小于发送缓冲区大小,tcp发送将多个数据一次性发送出去,发生粘包;

      • 4.接收端没有及时读取数据,导致粘包;

      解决方案:

      • 1.发送端给每个数据包头部添加包长度信息;

      • 2.固定每个数据包大小;

      • 3.用特定的符号来做包的分隔符;

    • 心跳机制设计:

      1.应用层自己设计,每隔固定时间发送消息;

      2.使用TCP的keepalive设置,单独配置探测时间。这个设置会为每个socket开启,会造成带宽浪费,且与应用层不能很好的交互;

    • 断线重连:需要考虑安全性,超时处理和数据包缓存;

      在通讯协议中,每个包头都包含一个pkgid的字段,客户端的每个上行请求包都有服务器与之对应的下行回复包,两者的pkgid相同;

      客户端上行包:客户端有一个队列,记录已经发送的request包,收到服务器的response后再删除对应的request。如果超时没有收到response,可以认为发生了断线,重新发送缓存中的request,将pkgid设置为负值标记为重传包。重试有次数限制,超过次数仍未收到回复就提示断线;

      服务器下行包:服务器开一个缓存池,记录近期一定数量的response包和notify推送包。当有重传包时,通过id+pkgid,从缓冲池中检测是否有该记录,如果有则返回进行重连的数据发送,如果因为超时等该数据已经被删除了,则表示重传失败,服务器做踢下线处理;

    • 服务器优雅关闭:

      原因:我们在关闭服务器时,希望将目前正在执行的任务做完之后再关闭;

      方案:若需要平滑停止服务,我们一般可以通过ShutdownHook和linux提供的Signal来实现。ShutdownHook一般比较难保证关闭任务的执行顺序,这个时候可以考虑使用Signal机制来完全托管我们关闭服务的执行顺序。

    • 如果A机器与B机器网络 connect 成功后从未互发过数据,此时其中一机器突然断电,则另外一台机器与断电的机器之间的网络连接处于哪种状态?

      答:半开连接状态。客户端网线断开,电源掉电,系统崩溃这些,服务器进程select/epoll监测不到断开和错误,表现为半连接状态,浪费服务器可用的文件描述符。

    epoll和IOCP:

    • epoll:同步非阻塞IO。

      • linux系统独有;

      • Reactor,内核有数据来时通知应用程序,应用程序自己将数据从内核复制到socket缓冲区;

      • 文件描述符在内核中用红黑树保存,因为红黑树接近平衡的查找,删除,可以高效管理文件描述符;

      • 就绪文件描述符存储在一个双向链表redlist中,有数据来时内核的中断回调函数将文件描述符加到就绪链表中;

    • IOCP:异步非阻塞IO。

      • windows系统独有;

      • Proactor,内核将数据复制到socket缓冲区后才通知应用程序;

    epoll 水平触发(LT)和边缘触发(ET):

    • 水平触发:内核在一个文件描述符就绪时通知程序进行IO操作,如果程序未做操作或操作后内核中还有数据,则继续通知程序进行IO操作。同时支持block和non-block,poll和select都用该模式;

      • 优点:在进行socket通信时可以保证数据完整输出,进行IO操作时如果还有数据,则一直通知你;

      • 缺点:只要还有数据,内核就会在内核态和用户态中不停转换进行通知,占用了大量内核资源,特别是当大部分就绪的文件描述符数据并不需要读取时,此时内核会一直通知;

    • 边缘触发:内核在一个文件描述符就绪时只会通知一次程序,之后无论程序未操作或是操作后还有数据,内核都不会再继续通知,知道该文件描述符有新的数据到来才会再通知程序。只支持no-block。

      • 优点:只会通知一次,大大减少了内核资源的浪费,提高效率;

      • 缺点:不能保证数据完整输出,不能及时取出所有数据;

    Nagle:

    • 算法原由:为了减少网络上的小包,减少网络拥堵。将小包合并到一个分组一次性发送出去,要求一个tcp连接上最多只有一个未确认的消息分组,只有等到ACK后才能发送下一个分组。

    • 优点:自适应很强,确定到达越快,数据就发送越快。在希望减少分组的广域网上发送更少的分组;

    • 延迟ACK:tcp对数据包的确认如果单独的数据包为了发送一个ACK确认代价太高,所以tcp会延迟一段事件,如果这段事件内有数据包发送则可以捎带上发送ACK,如果在延迟ACK定时器触发时还未发送则单独使用数据包发送

      • 延迟ACK的好处:

        1.避免糊涂窗口综合症;

        2.发送数据时捎带ACK,不用单独发送,节约网络资源;

        3.如果延迟时间内有多个数据到达,允许协议栈发送一个ack确认多个报文段;

    • 延迟ACK与Nagle的问题:

      如果向对端发送长度小于MSS的数据包,第一次发时对端延迟ACK,而本端因为数据包长度小于MSS所以使用Nagle算法,数据不会立即发送,这就导致了要等待对端ACK超时返回后,本端才能发送数据,白白浪费了一个ACK超时时间;

    • 解决方案:

      1.对端不向本端发送数据,并且对延迟敏感,无法捎带ack,需要关闭Nagle算法

      2.如上延迟ACK与Nagle的问题:

      • 1.使用writev函数,将多个非连续缓冲区的数据一次性写出去,这样tcp输出一次,只产生一个tcp分组,对端收到完整数据处理完后返回ack;

      • 2.把多次的写操作的数据复制到单个缓冲区,然后对缓冲区调用write;

      • 3.关闭Nagle算法;

    • 关闭Nagle算法:TCP_NODELAY关键字

    糊涂窗口综合症:

    起因:网络中小包太多,如果发送端和接收端速率差别较大,会出现比较蠢的现象:发送方发送的数据,一个很大的头部带着很少的数据;

    解决方法:

    • 接收端:

      1.应用程序处理了一部分消息后不急着向发送方通告,而是等到接收窗口大小大于MSS或最大窗口的一半时再向发送方发送通告;

      2.延迟ACK;

      3.累计ACK,并不是每个数据都回复ACK,可以多个数据段一起回复;

    • 发送方:

      1.使用Nagle算法;

    Libevent:

    • 优点:

      事件驱动,高性能;

      轻量级,专注于网络;

      支持IO多路复用技术;

      支持IO,定时器和信号事件;

    • bufferevent:负责IO的管理和调度

    • evbuffer:IO数据缓冲

    ZeroMQ:

     

    特点:

    • 1.处理了网络异常,包括连接异常,重连;

    • 2.处理了网络粘包,拆包,以msg为单位收发数据,结合protocolBuffers,可以对应用层彻底屏蔽网络通讯;

    • 3.对大数据童工SENDMORE/RECVMORE提供分包机制;

    • 4.通过线程间数据流动来保证同一时刻任何数据都只会被一个线程持有,以此实现多线程“去锁化”;

    • 5.服务器端和客户端的启动没有先后顺序;

    流程:

    • 主线程与IO线程通过管道(pipe_t)收发数据(msg_t),通过mailbox_t收发命令(command_t);

    无锁队列:

    • mailbox_t底层存储命令用的是无锁队列,多个线程同时写入,一个线程读取;

    Muduo:

    特点:

    遵循one thread one loop,在主线程和工作线程中都有一个多路IO复用Epoll,主线程处理监听accept,然后交给工作线程进行消息处理。主线程在接收到新连接后通过负载均衡获得一个io线程,将新建的socket交给io线程做通信处理。

    缓冲区设计:

    Muduo的缓存采用vector,申请一块连续的内存,不像libevent采用的是链表。用两个int类型的readindex和writeindex分别指向读和写,vector会自适应数据大小进行扩增,且其首部还预留了8字节大小的空间存储数据大小。

     

  4. 内存相关:

    内存管理算法:

    • buddy system(伙伴算法):

      linux底层内存分配算法。以页为单位,将内存按2的幂次方大小进行划分,相当于分离出若干个块大小一致的链表,搜索链表给出需求最佳匹配大小。优点时快速搜索合并和低外部碎片,缺点是高内部碎片。分配时会将大的链表单元平均分为两块链接到对应的链表上,然后在其中一块上进行内存分配。释放时会合并连续的碎片内存,链接到对应大小的链表上;

    • slab算法:

      1.动机:内核通常依赖小对象的分配,它们在系统生命周期内进行无数次分配,但其初始化消耗时间超过了其分配和释放所需时间,所以需要一个缓存分配器来对类似大小的对象提供缓存功能,避免其多次初始化;

      2.方法:将buddy算法获得的页进行二次分配,在高速缓存上创建一个slab链表,包含三种形态的slab:已经分配完,部分分配,完全空闲。每个slab可以在链表上根据目前的形态变化移动,这样分配在slab上的对象可以一直作为缓存,下次使用时不用再初始化;

      3.优点:提供了缓存功能,避免了碎片问题和同一个对象的反复初始化;

    • malloc的底层调用:

      • brk:当malloc分配的内存小于128k时采用brk从低地址开始向高地址分配虚拟内存,但是brk分配的不会初始化,只有在第一次使用分配的内存时才会发生缺页中断,分配物理内存。且brk分配的内存不能单独释放,只有高地址内存释放后才能释放,但是可以可以重复使用。

      • mmap:当malloc分配的内存大于等于128k时采用mmap进行虚拟地址分配,且会初始化,只有在第一次使用分配的内存时才会发生缺页中断,分配物理内存。mmap从高地址开始向地址分配,可以单独释放。

    • windows和linux进程空间:

      系统总内存空间内核空间用户空间栈空间(默认)最大线程数量
      windows 32位 4G 2G 2G 1M 小于2048
      linux 32位 4G 1G 3G 10M 小于(3*1024/10)
      • linux下设置栈默认大小:ulimit -s

      • linux下查看电脑最大进程数:ulimit -a

    缓存调度算法:

    • FIFO:先进先出调度。用双向链表实现,新来的数据加到链表尾;

    • LRU:最久未使用调度。用链表保存数据,当缓存命中时将其移到链表头;

    TCmalloc和Jemalloc:

    tcmalloc:thread-cache-malloc;

    • 1.每个线程都有一个cache,存储小对象空闲链表;

    • 2.有一个共用的centercache保存着其他小对象的空间,如果某个线程空间不足则可以去向centercache申请;

    • 3.如果centercache空间不足,则可以向操作系统申请,使用sbrk()和mmap();

  5. mysql:

    常用命令:

    • show processlist 查看状态

    乐观锁:

    定义:默认对数据库的操作不会引起冲突,在操作时并不会进行任何特殊操作(不加锁),而是在更新之后再去判断冲突。
    实现方式:给数据加上version版本号,当读取数据时,将version一并读出,提交更新时,判断数据库表对应记录的当前版本信息与第一次取出来的version值,如果相等则给与更新,数据更新时对version进行加一,否则认为是过期数据。
    eg:
    update table_name set value=1,version=version+1 where id=2 and version = #{version};

    悲观锁:

    定义:在执行数据库操作时,认为本次操作一定会冲突,所以要获得锁后才对数据库进行本次操作。分为共享锁和排他锁。

    共享锁:又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事物都不能对数据进行更改。加共享锁的目的就是怕在读是被其他线程加上排他锁(写锁)。
    select * from table_name where id="1" lock in share mode;
    排他锁:又称写锁,是写操作创建的锁。加上后其他事物不能对该数据加任何锁,会阻塞所有的共享锁和排他锁。
    select status from table_name where id="1" for update;
    行锁:分为共享锁和排他锁,对某一行加锁。行锁是基于索引的,如果一条SQL语句用不到索引,则就不会用到行锁,直接用表锁。缺点是由于申请大量的锁资源,所以速度慢,内存消耗大。
    表锁:在没有索引的情况下,都是表锁。

    死锁的解决方案:

    第一种:
    1.查询是否锁表
    show OPEN TABLES where In_use>0;
    2.查询进程
    show processlist;
    3.杀死进程 id
    kill -9 id

    第二种:
    1.查看当前事务
    select * from INFORMATION_SCHEMA.INNODB_TRX;
    2.查看当前锁定的事物:
    select * from INFORMATION_SCHEMA.INNODB_LOCKS;
    3.查看当前等锁的事物:
    select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

    自旋锁:

    MyISAM和InnoDB:

    • MyISAM:为非聚簇索引。B+树叶子节点key存储的是数据物理地址,其索引表和数据表是分开的。其数据是根据插入顺序进行保存,因此更适合单个数据查询,插入顺序不受键值影响。

    • InnoDB:为聚簇索引。主索引B+树叶子节点存储键值对应的数据本身,辅助索引B+树叶子节点存放的是主键键值。聚簇索引的数据和主键索引存放在一起,根据主键的顺序保存,适合按主键索引的区间查找,可以减少磁盘IO,所以其插入顺序最好按照主键的单调顺序插入,否则会频繁引起页分裂,影响性能。

    聚簇索引在插入新数据时比非聚簇索引慢的多,因为插入新数据时需要检测主键是否重复,需要遍历主索引所有叶子节点,而非聚簇索引的叶子节点保存的时数据地址,占用空间少,分布集中,查询的时候IO更少。聚簇索引的主索引中存储的是数据本身,数据占用空间大,分布范围广,可能占用很多扇区,需要更多的IO才能遍历完。

    • MyIsam和InnoDb区别:

    • 索引分类:

      • 主键索引:

      • 辅助索引:

      • 唯一索引:

      • 全文索引:

      • 组合索引:

    • 回表:innodb中有聚簇索引和辅助(普通)索引。聚簇索引存储着键值和数据本身,而辅助(普通)索引中只保存键值。这样从辅助(普通)索引进行查询时就时先获得主键值,然后去聚簇索引中查找真实数据。这就是回表。

    • 索引覆盖:通过联合索引index()将被查询的字段加到联合索引中,这样就可以通过普通索引树获得需要的字段数据,无需回表,符合索引覆盖,效率较高;

      • 全表count查询;

      • 列查询回表优化;

      • 分页查询;

    • 最左匹配:对于联合索引,其构建B+树时只能根据一个值来构建,因此数据库依据联合索引最左边的字段来构建B+树。最左优先,以最左边的为起点任何连续的索引都能匹配上。遇到范围查询时就停止匹配。注意:联合索引多个字段中,只有当查询条件包含联合索引的最左边那个时,查询才能使用该索引。

    • 索引下推:在联合索引中,如果多条件查询,需要先返回回表再根据其他条件在回表上筛选,这样返回的回表数据量大,索引下推就是在所有条件都筛选完之后再返回回表。

      • innodb引擎的表,索引下推只用于二级索引。

      • 索引下推一半可用于查询字段(select列)不是/不全是联合索引的字段,查询条件为多条件查询且查询条件子句(where/order/by)字段全是联合索引。

    幻读,脏读和不可重复读:

    • 脏读:第一个事务在修改了数据但并未提交时,第二个事务使用了该数据。

      解决方案:将数据库事务隔离级别调整到READ_COMMITTED(读已提交)。

    • 不可重复读:同一事务内,两个相同的查询返回不同的结果。

      解决方案:把数据库事务隔离级别调整到REPEATABLE_READ(可重复读)。只有在修改事务完成提交后才能查询。

    • 幻读:事务不是独立执行时的一种现象。重点在于新增和删除。当第一个事物对一个表中的数据进行了修改,涉及到表中的全部数据行,同时第二个事务也修改了表中的数据,向表中插入或删除了一行,这样第一个事务发现表中还有没有修改的数据行,好像发生幻觉一样。

      解决方案:将数据库的事务隔离级别调整到SERIALIZABLE_READ(串行化读)。 如果在操作事务完成数据处理之前,其他事务不能再修改数据;

    数据库事务隔离级别:

    • READ_UNCOMMITED:读未提交。

      事务A不放置共享锁,也不放独占锁。那么并发的事务B可以改写A事务读取的数据。脏读,不可重复读和幻读都会出现。

    • READ_COMMITED:读已提交。

      事务A对数据Data放置共享锁,Data数据不会被其他事务改写,避免了脏读。但A没有提交之前可以对Data改写,那么B事务读取的值前后可能不一致,所以不可重复读和幻读都会出现。

    • REPEATABLE_READ:可重复读。

      事务A对数据Data添加一致性非锁定读锁,并发的事务B在读取Data时并不需要等待A释放锁,而是读取Data的快照,Data的快照存放的时事务A开始时Data的原始数据,所以事务B可以多次读取Data数据而不用担心数据更改,直到事务B提交后才会读取事务A更改的Data数据。但幻读还是会出现。

      注:事务是在第一次查询语句执行时才会建立快照点。

      START TRANSACTION WITH consistent snapshot;替代 START TRANSACTION; 来开启事务
      相当于 START TRANSACTION;之后执行了查询select语句,直接建立的快照点。
    • SERIALIZABLE_READ:串行化读。

      在数据库表上放置了写锁,降低了数据库的并发性。事务A在未提交之前,事务B对Data的查询和修改等都会被阻塞。这样就可以保证不会出现脏读,不可重复读和幻读了。

    Redo和Undo log:

    • Redo log:物理日志,记录数据页的物理修改,而不是某一行或几行的修改,它用来恢复提交后的物理数据页,只能恢复最后一次提交的位置。

      • redolog属于innodb引擎,二进制日志无论什么引擎,对数据库做了修改都会产生二进制日志。二进制日志优先于redo日志被记录。

      • 二进制日志记录操作的方法是逻辑的语句,redolog记录的是每个页的修改;

      • 二进制日志是每次事务提交时一次性写入缓存中的日志文件。redolog是在数据准备修改前写入缓存中,然后才对缓存中的数据进行修改,并且保证在事务提交之前就将缓存中的redolog写入日志,写完后才执行提交。

      • 二进制日志旨在提交的时候一次性写入,所以记录方式和提交顺序有关,一次提交对应一次记录。redolog文件中同一个事物可能记录多次,最后一次提交的事务记录会覆盖所有未提交的事务记录。

      • redolog具有幂等性,多次操作前后状态一致。二进制日志记录的是所有影响数据的操作,记录的内容比较多。

      • Mysql支持用户自定义在commit时如何将log buffer的日志刷log file中,通过变量innodb_flush_log_at_trx_commit来决定。这个变量的值为:

        • 0:事务提交时不会将log buffer中的日志写入os buffer,而是每秒写入os buffer并调用发sync()写入log file on disk中,系统崩溃时损失1秒数据;

        • 1:默认设置。每次事务提交都将log buffer中的日志写入os buffer并调用sync()写入log file on disk中。这种方式不会丢失数据,但是io性能差。

        • 2:事务每次提交都日志都从log buffer写到os buffer,每秒都调用fsync()写入到log file on disk中。

      • 主从复制结构中,要保证事务的持久性和一致性,需要对日志相关设置如下:

        • 如果启用了二进制文件,则设置sync_binlog=1,即事务每次提交都同步写到磁盘;

        • 总是设置inndb_flush_log_at_trx_commit=1,即每次提交事务都写到磁盘。如果数据量大则不要每次循环都提交,而是等所有数据都修改完后再统一提交。

    • Undo log:逻辑日志,用来回滚行记录到某个版本,根据每行记录进行记录。

      • 可用于版本控制mvcc中,当读取的某一行被其他事务锁定时,它可以从undolog中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现一致性非锁定读。

      • undolog采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。目前有128个rollback segment ,每个rollback segment中有1024个undo log segment;

      • undolog也会产生redolog,因为undolog也要实现持久化保护;

      • undolog删除:delete时并不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作时purge线程完成的。并且在提交事务时还会判断undolog分配的页是否可以复用,如果可以,则会分配给后面来的事务,避免为每个独立的事务分配独立的undolog页而浪费存储空间和性能;

      • update分为两种情况:update的列是否是主键列

        • 如果不是主键列,在undolog中直接反向记录是入股update的。即update是直接进行的;

        • 如果是主键列,updtae分为两步执行:先删除改行,再插入一行目标行;

    CRUD:

    • create:创建

    • read:读取

    • update:更新

    • delete:删除

    索引的概念、索引的原理、索引的创建技巧:

    • 索引是存储引擎中用于快速查询的一种数据结构

    • 原理:B树和B+树

    • 创建技巧:

    MySql优化技巧:参考资料

    • 如果mysql估计使用索引比全表扫描还慢,则不会使用索引;

    • 前导模糊查询不能命中索引;

    • 数据类型出现隐式转换的时候不能命中索引,特别是当列类型是字符串,一定要将字符串常量值用引号引起来;

    • 复合索引的情况下,查询条件不包含索引最左边部分(不满足最左原则),不会命中复合索引;

    • union,in,or都能命中索引,建议使用in;

    • 用or分割开的条件,如果or前的条件中有索引,而后面的列中没有索引,那么涉及的索引都不会被用到;

    • 负向条件查询不能使用索引,可以优化为in查询。如 !=,<>,not in ,not exists,not like等;

    • 范围条件查询可以命中索引。例如 <,<=,>,>=,between;

    • 数据库执行计算不会命中索引;

    • 利用覆盖索引进行查询,避免回表;

    • 建立索引的列,不允许为null;

    • 更新十分频繁的字段不宜建立索引;

    • 区分度不大的字段不宜建立索引;

    • 业务上具有唯一性的字段,即使是多个字段组合,也必须建立唯一索引;

    • 多表关联时,要保证关联字段上一定有索引;

    • 创建索引时避免以下观念:

      • 1.索引越多越好,认为一个查询就需要创建一个索引;

      • 2.宁缺勿滥,认为索引会消耗空间,严重拖慢更新和新增速度;

      • 3.抵制唯一索引,认为业务的唯一性一律需要应用层通过“先查后插”方式解决;

      • 4.过早优化,在不了解系统的情况下就开始优化;

    性能对比:count(可空字段)<count(非空字段)=count(id)<count(1)=count(*)

    • count(可空字段):进行全表扫描,读到server层,判断字段可空,拿出该字段所有值,判断每一个值是否为空,不为空则累加;

    • count(不可空字段):进行全表扫描,读到server层,判断字段不可空,按行累加;

    • count(1):遍历全表,但是不返回任何字段值,server层收到的每一行都是1,判断不是null,按值累加;

    • count(*):mysql对该语句进行了优化,因为其返回的行一定不为空,遍历全表,但是不取值,按行累加;

  6. redis:

    • 1.redis为什么是key,value的,为什么不支持SQL的?

    • 2.reids是多线程还是单线程?

      redis在网络请求模块采用了单线程,避免了不必要的上下文切换和竟态条件。其他模块,如备份等还是采用多线程处理。采用了线程封闭,把任务封闭在一个线程,避免了线程安全问题,不过对于依赖多个redis操作的复合操作来说,还是需要锁。

    • 3.redis的持久化开启RDB和AOF下重启服务是如何加载的?

      同时开启后redis会优先载入AOF文件来恢复数据,一般情况下aof存储的文件数据比rdb形式存储的文件完整;

    • 4.redis如何做集群如何规划?AKF/CAP如何实现和设计?

       

    • 5.10万用户一年365天的登陆情况如何用redis存储,并快速检索任意时间窗内的活跃用户?

      使用bitmap存储

    • 6.redis的5种value类型你用过几个,能举例吗?

      string:

      • 字符串:指令

      • 数值计算:限流,秒杀 二进制安全 string

      • bitmap:统计用户任意时间窗口内登陆的天数 / 统计时间窗口活跃用户数量

      List:双向链表。

      • 模拟栈:

      • 模拟队列:

      • 模拟数组:

      • LTrim命令:帖子评论

      Hash:

      • 详情页

      • 分库结果聚合

      Set:底层结构 无序 去重

      • SRANDMEMBER:随机事件,用来抽奖

      • 集合操作:推荐系统

        • SUNION:并集,共同好友

        • SDIFF:差集,推荐可能认识的人;

      ZSet:有序集合 去重 排序 动态 默认从小到达

      • 排行榜;

      • ZREVRANGE:反向取值

      • 分页动态;

      • 排序是怎么实现的:跳表

    • 7.100万并发4G数据,10万并发400G数据,如何设计redis存储?

       

    • 8.redis和memcached的区别:

      • 数据类型:memcached只支持key-value,redis支持5种数据类型;

      • 性能:redis只能使用单核,存储小数据性能比memcached高。memcached多核,大于100k的数据性能比redis高;

      • 内存空间:memcached可以修改最大内存,采用LRU算法。redis增加了VM的特性,突破了物理内存的限制;

      • 操作便利性:Memcached的数据结构单一,仅用来缓存数据,redis支持丰富的数据类型,也可以在服务器端对数据进行操作,减少了IO次数;

      • 可靠性:memecached不支持持久化,断电后数据会丢失。redis支持aof和rdb两种持久化方式,允许单点故障;

      • 应用场景:

        memcached:动态系统中减轻数据库负载,提高性能;做缓存,适合读多写少,大数据 量的情况;

        redis:适用于对读写效率要求都很高,数据处理业务繁杂和对安全性要求较高的系统;

    • 9.RESP协议:客户端与服务器之间的通信协议。

      • 命令格式:

        *     //返回结果格式
        <参数数量>
        <第一个参数字节数>
        <第一个参数>
        <第二个参数字节数>
        <第二个参数>
        ···
      • 返回结果格式:

        • 状态回复:第一个字节为‘+’;

        • 错误回复:第一个字节为‘-’;

        • 整数回复:第一个字节为‘:’;

        • 字符串回复:第一个字节为‘$’;

        • 多字符串回复:第一个字符为'*';

    • redis为什么快?

      • 1.纯内存操作;

      • 2.单线程,避免了不必要的上下文切换;

      • 3.采用多路IO复用技术;

    • redis过期策略:定时删除+惰性删除

      每过固定时间(100s)对所有key进行抽样检查,如果过期就删除,之所以选择抽样是因为全部检查耗时太长,redis可能卡死。之所以不用定时器及时删除是因为太消耗cpu;剩下的其他key,在用到时redis会检查其是否过期,如果过期则删除;

      如果这两种策略都失效,则redis的内存会越来越高,需要采用内存淘汰策略:

      • volatile_lru;

      • volatile_ttl;

    • redis性能问题:

      • 1.master最好不要做持久化工作;

      • 2.如果数据比较重要,某个slave开启aof备份数据,策略为每秒一次;

      • 3.为了主从复制的速度,最好放在一个局域网内;

      • 4.尽量避免在压力大的主库增加从库;

      • 5.主从复制不要用图状结构,用单向链表更稳定;

    • redis的事务:不支持回滚,如果执行失败,剩下的命令继续执行。如果事务内的命令出现错误,所有命令立即停止执行,如果出现运行错误,那么正确的命令会被执行。

      • multi命令:开启一个事务,总是返回ok。执行后客户端向服务器发送的多条命令不会立即执行,而是放到一个队列种,当exec调用时才被执行;

      • exec命令:执行所有事务内的命令。返回事务块内的所有命令的返回值,按命令执行的先后顺序。被打断时返回nil;

      • discard命令:客户端调用情况事务队列,并放弃事务,并且客户端会从事务状态退出;

      • watch命令:为redis事务提供CAS行为。监控一个或多个键,一旦其中一个键被修改(删除除外),之后的事务都不会被执行,监控一直持续到exec命令;

    • redis实现分布式锁:

      使用setnx命令加锁:setnx lock-key value 解锁:del key

      解决死锁:

      • 1.通过redis的expire()给锁设定超时时间;

      • 2.使用setnx key "超时时间" 和 getset key "超时时间"组合命令实现;

    • 布隆过滤器:

    • 缓存穿透:

      原因:查询缓存中没有的数据,访问底层存储系统进行查询;

      方案:1.使用bitmap或布隆过滤器,将已经存在的缓存中的key记录下来,这样就禁止了没有在缓存中的查询;

      2.允许第一次去底层存储系统进行查询,然后将空结果缓存到redis,加一个短的过期时间;

    • 缓存雪崩:

      原因:大量缓存同一时间失效;

      方案:1.使用锁或队列防止多个线程同一时间对数据库进行读写,避免大量并发请求落在底层存储上;

      2.对缓存的失效时间进行随机处理,尽可能分布均匀;

    • 缓存预热:

      原因:避免在用户请求时先查数据库再将数据缓存的问题,用户可以直接查询事先预热的数据;

      方案:

      • 1.直接写个缓存页面,上线时手工操作;

      • 2.数据量不大,可以再项目启动的时候自动加载;

      • 2.定时刷新缓存;

    • 缓存降级:

      原因:当访问量剧增,需要保证核心业务,对非核心业务进行服务降级,防止redis崩溃导致的底层存储系统崩溃;

      方案:redis出现问题时,如果有查询到来,不去数据库查询,直接返回一个默认值;

    • 一致性Hash算法:

      原因:在redis集群中,如果要查询一个数据在那台redis缓存中,最好的方式就是对数据key进行hash,得到具体的redis服务器编号,然后进行读取数据,这样就不用遍历redis服务器了。但hash也有问题,使用取模hash算法,如果新加一台服务器或减少一台服务器会造成缓存全部失效,引起缓存雪崩;

      方案:一致性Hash算法就是解决普通Hash算法在增加或减少模的值时导致的缓存失效问题。一致性Hash算法采用对2^32进行取模,组成一个虚拟的环,然后将redis服务器的ip和端口的Hash值分布到这个环上,同时将缓存数据的key也进行Hash并分布到环上。按顺时针来看,数据第一个遇到的redis服务器就是其存放的服务器。这样就算增加和减少服务器,缓存中也只需要更改上个redis节点到新加的redis节点之间的数据,其他都不用变。

      其他:一致性Hash算法可能因为redis服务器的Hash值分布不均匀引起数据倾斜,解决方案就是对redis服务器进行多次Hash,在环上分布多个虚拟节点,这样就能解决数据倾斜的问题。

       

  7. 其他算法等:

    单链表反转,递归和循环:

    struct ListNode
    {
    int node;
    struct ListNode* next;
    };

    //递归 利用堆栈从链表尾进行出栈操作
    ListNode* Reverse(ListNode* node)
    {
    if(!node || !node->next)
    return node;
    ListNode* newnode = Reverse(node->next);
    node->next->next = node;
    node->next = NULL;
    return newnode;
    }

    //循环 定义三个指针分别指向当前节点,前驱节点和后驱节点
    ListNode* Reverse(ListNode* node)
    {
    if(!node || !node->next)
    return node;
    ListNode* prev = NULL;//前驱节点,初始化为NULL
    ListNode* pnext = NULL;//后驱节点;
    ListNode* curnode = node;//目前的节点
    while(CurNode != NULL)
    {
           pnext = CurNode->next;
           curnode->next=prev;
           prev = curnode;
           curnode = pnext;  
    }
    return prev;
    }

    单链表判断:

    1.判断单链表是否有环:用快慢指针;
    2.判断两条单链表是否相交:直接判断两条单链表的最后一个节点是否相同,如果相同则相交;
    3.判断两条相交的单链表的交点:用长链表减去短链表得到len = |len1 - len2|,长链表先从头向后移动len次,然后两个链表逐个对比;
    4.判断有环单链表的入环点:用快慢指针,当他们第一次相遇时,将slow指针指向链表头,然后让fast指针和slow指针同时每次移动一格,其第一次的相遇点就是链表入环点。 a = (n-1)*r + c;

    如何判断一个整数的二进制中有多少个1:

    n&(n-1)循环直到为0

     

    AVL(平衡树),红黑树,B树,B+树:

    • AVL:

      1.如果有左子树,则左子树的值必然小于跟节点;

      2.如果有右子树,则右子树的值必然大于根节点;

      3.左子树与右子树的高度差不能大于1;

      4.无键值相等的节点;

      使用于查询多,但删除和修改操作比较少的场景,维护成本高;

      用途:windows NT内核中大量使用

    • 红黑树:

      1.根节点为黑色;

      2.所有叶子节点为黑色(叶子节点即树尾端的NULL节点);

      3.红色节点的两个子节点必然为黑色;

      4.从根节点到每个叶子节点的路径上的黑色节点数量一致;

      5.查找,删除和插入的时间复杂度都为O(logn);

      因为其查找效率略逊于AVL树,但其删除和插入效率比AVL高,所以其维护成本低;

      用途:epoll的内核中文件描述符的管理就是红黑树;linux中进程虚拟内存管理;主要用于内存中,STL的map和set底层实现;

    • B树:

      1.对于在内部节点的数据,可以直接得到,无需根据叶子节点定位;

      用途:主要用于文件系统

    • B+树:

      1.B+树只有叶子节点会带有指向记录的指针,而B树则所有的节点都带有;

      2.B+树中所有的叶子节点都是通过指针连接在一起,可以横向遍历,B树只能中序遍历;

      3.对磁盘友好:

      • 1.磁盘读写代价更低;

  • 2.查询效率更加稳定;

    • 3.遍历所有的数据更方便;

    用途:mysql数据库索引。B+树就是为文件而生,数据库文件存在磁盘,每次定位请求意味着一次IO操作,减少IO操作次数时提高性能的关键,而IO的查询次数就是索引树的高度,高度越低查询的次数越少,所以B+树比红黑树更适合;而不用B树的原因时B树在解决IO性能的同时并没有解决元素遍历的低效率问题,数据库中基于范围的查询非常频繁,B+树可以横向遍历,B树不支持这样的操作;

posted @ 2021-05-03 21:05  skylifer  阅读(91)  评论(0编辑  收藏  举报