qq面经

目录


作者:CJL1995
链接:https://www.nowcoder.com/discuss/504100?type=post&order=create&pos=&page=1&channel=1009&source_id=search_post
来源:牛客网

操作系统有哪几种锁?

1、信号量(Semaphore)

信号量分为二元信号量和多元信号量,所谓二元信号量就是指该信号量只有两个状态,要么被占用,要么空闲;而多元信号量则允许同时被N个线程占有,超出N个外的占用请求将被阻塞。信号量是“系统级别”的,即同一个信号量可以被不同的进程访问。

2、互斥量 (Mutex)

和二元信号量类似, 唯一不同的是,互斥量的获取和释放必须是在同一个线程中进行的。如果一个线程去释放一个并不是它所占有的互斥量是无效的。而信号量是可以由其它线程进行释放的。

3、临界区(Critical Section)

术语中,把临界区的锁的获取称为进入临界区,而把锁的释放称为离开临界区。临界区是“进程级别”的,即它只在本进程的所有线程中可见,其它性质与互斥量相同(即谁获取,谁释放)

4、读写锁(Read-Write Lock)

适 用于一个特定的场合。比如对于一段线程间访问的数据,如果程序大部分时间都是在读取,而只有很少的时间才会写入,那么使用前面几种锁时,每次读取也是同样 要申请锁的,而这时其它的线程就无法再对此段数据进行读取。可是,多个线程同时对一段数据进行读取时,是不存在同步问题的,那么这些读取时设置的锁就影响 了程序的性能。读写锁的出现就是为了解决这个问题的。

对于一个读写锁,有两种获取方式:共享(Shared)或独占 (Exclusive)。如果当前读写锁处于空闲状态,那么当多个线程同时以共享方式访问该读写锁时,都可以成功;而此时如果一个线程以独占的方式访问该 读写锁,那么它会等待所有共享访问都结束后才可以成功。在读写锁被独占访问的过程中,再次共享和独占请求访问该锁,都会进行等待状态。

5、条件变量(Condition Variable)

条件变量相当于一种通知机制。多个线程可以设置等待该条件变量,而一旦另外的线程设置了该条件变量(相当于唤醒条件变量)后,多个等待的线程就可以继续执行了。
死锁一般什么条件会发生?

用户态和内核态

有什么区别?

系统态(也称为管态或核心态),操作系统在系统态运行——运行操作系统程序
用户态(也称为目态),应用程序只能在用户态运行——运行用户程序

内核态:系统程序在内核态运行。该状态下,CPU可以访问内存的所有数据,包括外围设备,例如硬盘、网卡等。

用户态:应用程序只在用户态运行。该状态下只能受限的访问内存且不允许访问外设,占用CPU的能力被剥夺,导致CPU资源可以被其他程序获得。

这两种状态的主要差别在于:

处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所占有的处理机是可被抢占的;
而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占用的处理机是不允许被抢占的。

用户态和内核态有哪些方法可以切换?

用户态切换到内核态的唯一途径——>中断/异常/陷入(系统调用)
内核态切换到用户态的途径——>设置程序状态字

注意一条特殊的指令——陷入指令(又称为访管指令,因为内核态也被称为管理态,访管就是访问管理态)该指令给用户提供接口,用于调用操作系统的服务。

切换时涉及到堆栈切换

当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。

进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。

那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢

关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内 核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以 在进程陷入内核的时候**,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了**。

线程有哪几种状态?

Java中的线程的状态分为6种。

初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。

**阻塞(**BLOCKED):表线程阻塞于锁。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

在这里插入图片描述

STL比较熟悉的概念?

  1. 容器(Containers):各种数据结构,如:vector、list、deque、set、map。用来存放数据。从实现的角度来看,STL容器是一种class template。
    2. 算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。
    3. 迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
    4. 仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看**,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。**
    5. 适配器(配接器 adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。
    6. 配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

Map和set的区别?

set是一种关联式容器,其特性如下:

set以RBTree作为底层容器
所得元素的只有key没有value,value就是key
不允许出现键值重复
所有的元素都会被自动排序
不能通过迭代器来改变set的值,因为set的值就是键
map和set一样是关联式容器,它们的底层容器都是红黑树,区别就在于map的值不作为键,键和值是分开的。它的特性如下:

map以RBTree作为底层容器
所有元素都是键+值存在
不允许键重复
所有元素是通过键进行自动排序的
map的键是不能修改的,但是其键对应的值是可以修改的

你知道长连接怎么实现吗?

在HTTP1.0和HTTP1.1协议中都有对长连接的支持。其中HTTP1.0需要在request中增加”Connection: keep-alive“ header才能够支持,而HTTP1.1默认支持.

http1.0请求与服务端的交互过程:

  a)客户端发出带有包含一个header:”Connection: keep-alive“的请求

  b)服务端接收到这个请求后,根据http1.0和”Connection: keep-alive“判断出这是一个长连接,就会在response的header中也增加”Connection: keep-alive“,同是不会关闭已建立的tcp连接.

  c)客户端收到服务端的response后,发现其中包含”Connection: keep-alive“,就认为是一个长连接,不关闭这个连接。

http1.1请求与服务端的交互过程:

a)客户端发出http1.1的请求

b)服务端收到http1.1后就认为这是一个长连接,会在返回的response设置Connection: keep-alive,同时不会关闭已建立的连接.

c)客户端收到服务端的response后,发现其中包含”Connection: keep-alive“,就认为是一个长连接,不关闭这个连接。

管道化(管线化)知道吗?(长连接里的管道化)

参考链接
不管是http短连接还是长连接,它们的请求和响应都有有序的,都是等上一次请求响应后,才接着下一个请求的,那能不能不等第一次请求回来,我就开始发第二次请求呢?这就引出http管道化了

在长连接的基础上,HTTP1.1进一步地支持在持久连接上使用管道化(pipelining)特性,这是相对于keep-alive连接的又一性能优化。**在相应到达之前,可以将多条请求放入队列,当第一条请求发往服务器的时候,第二第三条请求也可以开始发送了,**不用等到第一条请求响应回来,在高延时网络条件下,这样做可以降低网络的环回时间,提高性能。

http队首阻塞: 请求可以并行发出,但是响应必须串行返回

简单理解就是需要排队,队首的事情没有处理完的时候,后面的人都要等着。
队头阻塞”与短连接和长连接无关,而是由 HTTP 基本的“请求 - 应答”机制所导致的。因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。
而http的队首阻塞,在管道化和非管道化下,表现是不同的

http1.0的队首阻塞 ( 非管道化下 ) :

         对于同一个tcp连接,所有的http1.0请求放入队列中,只有前一个请求的响应收到了,然后才能发送下一个请求,由下图可以看到,如果前一个请求卡着了,那么队列中后续的http就会阻塞

http1.1的队首阻塞 ( 管道化下 )

对于同一个tcp连接,开启管道化后,http1.1允许一次发送多个http1.1请求,也就是说,不必等前一个响应收到,就可以发送下一个请求,这样就解决了http1.0的客户端的队首阻塞。但是,http1.1规定,服务器端的响应的发送要根据请求被接收的顺序排队,也就是说,先接收到的请求需要先响应回来(如下图所示)。这样造成的问题是,如果最先收到的请求的处理时间长的话,响应生成也慢,就会阻塞已经生成了的响应的发送。也会造成队首阻塞,响应的阻塞。
内存泄漏如何检查?

HTTP队头阻塞 的解决方法

解决http队头阻塞的方法:

1. 并发TCP连接(浏览器一个域名采用6-8个TCP连接,并发HTTP请求)

2. 域名分片(多个域名,可以建立更多的TCP连接,从而提高HTTP请求的并发)

2. HTTP2方式(多路复用特性,二进制分帧)

http2中将多个请求复用同一个tcp通道中,通过二进制分帧并且给每个帧打上流的 ID 去避免依次响应的问题,对方接收到帧之后根据 ID 拼接出流,这样就可以做到乱序响应从而避免请求时的队首阻塞问题,

内存泄漏如何检查?

大致方法

静态分析技术

对源程序进行词法和语法分析,进行数据类型的检查以及一些优化的分析

LCLink重点分析两类内存释放错误:
试图释放某内存块,该内存块有两个或两个以上的有效指针指向它。
试图释放某内存块,该内存块没有任何有效指针指向它

插装技术

为了获得被测程序的动态执行信息,需要对其进行跟踪,一般使用插装方法。所谓插装就是在保持被测程序的逻辑完整性的基础上,在被测程序的特定部位插入一段检测程序又称探针函数,通过探针的执行抛出程序的运行特征数据。

源代码插装技术

目标代码插装技术

被测代码预处理
测试执行阶段
数据统计与结果汇总

工具:LeakTracer

在使用LeakTracer时,通过提供的LeakCheck脚本运行你的程序,它使用LD_PRELOAD在你的函数上层进行“重写”。如果你的平台不支持LD_PRELOAD,则需要将LeakTracer.o 对象文件加入到Makefile文件中,然后运行你的应用程序
 LeakTracer利用gdb去输出发生内存泄露所发生的位置,它是通过override operator new和operator delete来实现检测。

碰到的内存泄露无非有以下几种:

(1) 堆内存泄漏(Heap leak)。堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak. 这是最常见的内存泄露。

(2)系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定

内存分区大概分几个区?

在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
堆:堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配内容容量有限。

自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。

全局/静态存储区:这块内存是在程序编译的时候就已经分配好的,在程序整个运行期间都存在。例如全局变量,静态变量。

常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。
Const和define有什么区别

堆和栈有什么区别?

栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。

区别

2.1申请方式,栈由系统自动分配,堆需要程序员自己申请

stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。

2.2 申请后系统的响应,栈只要剩余空间大直接分配,堆需要遍历记录空闲内存的链表

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3 申请大小的限制,栈由高地址向低地址扩展,最大容量预先规定。堆由低向高,不连续,受限于虚拟内存。

栈:在Windows下**,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的**,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4 申请效率的比较:栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5 堆和栈中的存储内容

栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6 存取效率的比较,为什么栈比堆快,栈有专门寄存器,直接寻址;堆是间接寻址
char s1[] = “aaaaaaaaaaaaaaa”;
char *s2 = “bbbbbbbbbbbbbbbbb”;
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:

2.6 碎片问题:栈先进后出的队列,不会有碎片,堆会有

结构体的内存对齐有什么规则?

  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
  • 结构体总大小为最大对齐数(每个成员变量除了第一个成员,都有一个对齐数)的整数倍。
  • 果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  • 结构体的对齐数为结构体当中所有对齐数中的最大对齐数。

位域听过没?

C语言又提供了一种数据结构,叫做“位域”或“位段”。

位域是操控位的一种方法(操控位的另一种方法是使用按位运算符,按位运算符将在之后的笔记中做介绍)。

位域通过一个结构声明来建立:该结构声明为每个字段提供标签,并确定该字段的宽度。例如,下面的声明建立了个4个1位的字段:

struct 
{
   unsigned int autfd:1;
   unsigned int bldfc:1;
   unsigned int undin:1;
   unsigned int itals:1;
}prnt;

C语言又提供了一种数据结构,称为"位域"或"位段"。所谓"位域"是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
使用位域的好处是:
1.有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。这样节省存储空间,而且处理简便。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
2.可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间。

内联函数一般有什么优缺点,在什么场景下适用?

内联函数的执行过程与带参数宏定义很相似,但参数的处理不同。带参数的宏定义并不对参数进行运算,而是直接替换;内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数。

关系型数据库和非关系型数据库有什么区别?

1、数据存储方式不同。

关系bai型du和非关系型数据库的主要差异是数据存储的方式。关zhi系型数据天dao然就是表格式的,因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据。

与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数据存储和提取方式的首要影响因素。
2、扩展方式不同。

SQL和NoSQL数据库最大的差别可能是在扩展方式上,要支持日益增长的需求当然要扩展。

要支持更多并发量,SQL数据库是纵向扩展,也就是说提高处理能力,使用速度更快速的计算机,这样处理相同的数据集就更快了。

因为数据存储在关系表中,操作的性能瓶颈可能涉及很多个表,这都需要通过提高计算机性能来客服。虽然SQL数据库有很大扩展空间,但最终肯定会达到纵向扩展的上限。而NoSQL数据库是横向扩展的。

而非关系型数据存储天然就是分布式的,NoSQL数据库的扩展可以通过给资源池添加更多普通的数据库服务器(节点)来分担负载。

3、对事务性的支持不同。

如果数据操作需要高事务性或者复杂数据查询需要控制执行计划,那么传统的SQL数据库从性能和稳定性方面考虑是你的最佳选择。SQL数据库支持对事务原子性细粒度控制,并且易于回滚事务。

关系型数据库
关系型数据库:指采用了关系模型来组织数据的数据库。
关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。

关系型数据库的优点:
1.容易理解:二维表结构是非常贴近逻辑世界的一个概念,关系模型相对网状、层次等其他模型来说更容易理解
2**.使用方便**:通用的SQL语言使得操作关系型数据库非常方便
3.易于维护:丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率

关系型数据库存在的问题
1.网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈
2.网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的
3.在基于web的结构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。当需要对数据库系统进行升级和扩展时,往往需要停机维护和数据迁移。
4.性能欠佳:在关系型数据库中,导致性能欠佳的最主要原因是多表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询。为了保证数据库的ACID特性,必须尽量按照其要求的范式进行设计,关系型数据库中的表都是存储一个格式化的数据结构。

非关系型数据库
非关系型数据库:指非关系型的,分布式的,且一般不保证遵循ACID原则的数据存储系统。

非关系型数据库结构
非关系型数据库以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,不局限于固定的结构,可以减少一些时间和空间的开销。

优点
1.用户可以根据需要去添加自己需要的字段,为了获取用户的不同信息,不像关系型数据库中,要对多表进行关联查询。仅需要根据id取出相应的value就可以完成查询。
2.适用于SNS(Social Networking Services)中,例如facebook,微博。系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库难以应付,需要新的结构化数据存储。由于不可能用一种数据结构化存储应付所有的新的需求,因此,非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。

不足:
只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,关系型数据库显的更为合适。不适合持久存储海量数据

关系型与非关系型数据库的比较

1.成本:Nosql数据库简单易部署,基本都是开源软件,不需要像使用Oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。
2.查询速度:Nosql数据库将数据存储于缓存之中,而且不需要经过SQL层的解析,关系型数据库将数据存储在硬盘中,自然查询速度远不及Nosql数据库
3.存储数据的格式:Nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。
4.扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。Nosql基于键值对,数据之间没有耦合性,所以非常容易水平扩展。
5.持久存储:Nosql不使用于持久存储,海量数据的持久存储,还是需要关系型数据库
6.数据一致性:非关系型数据库一般强调的是数据最终一致性,不像关系型数据库一样强调数据的强一致性,从非关系型数据库中读到的有可能还是处于一个中间态的数据,
Nosql不提供对事务的处理。

CAP理论

CAP理论:一个分布式系统不可能同时满足C(一致性)、A(可用性)、P(分区容错性)三个基本需求,并且最多只能满足其中的两项**。对于一个分布式系统来说,分区容错是基本需求,否则不能称之为分布式系统,因此需要在C和A之间寻求平衡**

C(Consistency)一致性
一致性是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。与ACID的C完全不同
A(Availability)可用性
可用性是指服务一直可用,而且是正常响应时间。
P(Partition tolerance)分区容错性
分区容错性是指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

野指针遇到过吗?什么情况下会遇到野指针?

野指针:访问一个已销毁或者访问受限的内存区域的指针,野指针不能判断是否为NULL来避免

指针指向了一块随机的空间,不受程序控制。
3.野指针产生的原因:
1.指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会随机指向一个区域,因为任意指针变量(出了static修饰的指针)它的默认值都是随机的
2.指针被释放时没有置空:我们在用malloc()开辟空间的时候,要检查返回值是否为空,如果为空,则开辟失败;如果不为空,则指针指向的是开辟的内存空间的首地址。指针指向的内存空间在用free()和delete释放后,如果程序员没有对其进行置空或者其他赋值操作的话,就会成为一个野指针
3.指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。

5.规避方法:
1.初始化指针的时候将其置为nullptr,之后对其操作。
2.释放指针的时候将其置为nullptr。

指针和引用有什么区别?

2.指针和引用的区别
2.1本质:
引用是别名,指针是地址

2.2具体:
2.2.1 从现象上看:
指针在运行时可以改变所指向的值,而引用一旦与某个对象绑定后就不再改变。意思是:指针可以被重新赋值以指向另一个对象,但是引用则总是在初始化时被指定的对象,以后不能改变,但是指向的内容可以改变。(下面是这个规则的理解)
string str1 = “a”;
string str3 = “b”;
string &str2 = str1;
str2 = str3;
我们看到了“str2 = str3”之后,str2指向的还是str1的地址,str2映射的地址没有发生改变,只是指向的内容发生了改变。 如果用&str2作为左值,编译器会报错,提示“表达式必须是可以修改的值”,这也就是而引用一旦与某个对象绑定后就不再改变的意思,也就是str2是str1的别名,已经映射了str1的地址了,它不能再映射其他的地址,他不能作为其他变量的别名了。

2.2.2内存分配:
程序为指针变量分配区域,而不为引用分配内存区域。因为引用生命时必须初始化,从而指向一个已经存在的对象,引用不能为空值。
2.2.3编译:
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针。
2.2.4级数:
从理论上来说,对于指针没有级数限制,但是引用只有一级。
int** p1; // 合法。指向指针的指针
int*& p2; // 合法。指向指针的引用
int&* p3; // 非法。指向引用的指针是非法的
int&& p4; // 非法。指向引用的引用是非法的
2.2.5在效率上:
其实两者的效率是一致的,因为在底层中,指针和引用的参数都指向同一个地址。
在高级编程语言中,因为用*传参可能会指向空的地址或者错误的地址,所以要时时判断参数是否为空,导致降低效率。

写一下strcpy的实现?

#include<stdio.h>
#include<string.h>
#include<assert.h>

char* my_strcpy(char* str1, char* str2)
{
    assert(str2);//判断源地址是否为空;
    char* cur = str1;
    while (*str2 != '\0')
    {
        *str1 = *str2;
        str1++;
        str2++;
    }
    *str1 = '\0';
    return cur;
}

int main()
{
    char str1[10];
    char str2[10] = "China";
    printf("%s\n", my_strcpy(str1, str2));
    return 0;
}

C++里的多态是一个什么概念,虚表指针是放在类还是对象中的什么地方的?多少字节?

4个字节放在最前面

C++中父类和子类的同名函数是怎么通过编译的?父类子类

子类继承关系: 子类复制父类全部成员

首先,理解父子类的继承关系是怎样发生的。在此基础上就很容易理解它们之间的关系和区别。  
  每一个类有它自己的成员变量和成员函数,是一个独立的空间整体。当子类继承父类时,会将父类的全部成员全部复制一份,作为子类的成员,但是,同时也会标记这些成员是从父类中继承的,与子类本身的成员,还是有区别的。这里认为将子类本身的成员存在子类域,从父类复制过来的存在父类域。

隐藏:子类对象优先考虑子类域自身成员(成员变量和成员函数)

隐藏发生的主要原因,就是当子类有父类的同名成员时,子类对象访问该成员时,会发生冲突。所以编译器的处理方式是,优先考虑子类域中的自身成员。

即,子类对象访问某成员时,如ch.m_m 或者ch.f(),成员变量和成员函数都一样。编译器首先在子类域中检索,如果在子类域中找到该成员,则检索结束,返回该成员进行访问。如果在子类域中找不到该成员,则去父类域中检索。如果父类域中存在,则返回该成员进行访问,如果父类域中也不存在,则编译错误,该成员无效。

当父子类域都存在同一成员时,编译器优先在子类中检索,就算父类域中也存在该同名成员,也不会被检索到。因此,父类域中的该成员被子类域中的该同名成员隐藏,即访问时完全以为该成员不存在,如果想访问父类域中的该成员,只能通过显示调用的方式,即:ch.Father::m_m;

标准模板库是静态还是动态的?回答了静态

以.h头文件与.lib静态库形式提供的

标准模板库里经常用到的仿函数,仿函数是用来做什么的?

仿函数(functors)在C++标准中采用的名称是函数对象(function objects)。仿函数主要用于STL中的算法中,虽然函数指针虽然也可以作为算法的参数,但是函数指针不能满足STL对抽象性的要求,也不能满足软件积木的要求–函数指针无法和STL其他组件搭配,产生更灵活变化。仿函数本质就是类重载了一个operator(),创建一个行为类似函数的对象。

仿函数(functor),就是使一个类的使用看上去象一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了

迭代器失效一般有什么场景,迭代器会失效?

总结:迭代器失效分三种情况考虑,也是非三种数据结构考虑,分别为数组型,链表型,树型数据结构。

数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,*erase(iter)会返回下一个有效迭代器的值,或者erase(iter++).

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.**erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)**的方式删除迭代器。

迭代器失效去操作的话会报什么错误? vector iterator not incrementable.

在一个线程使用weak_ptr,在另一个线程中已经释放了,会出现什么问题?报什么错?

原话:线程安全一方面要防止内存泄漏,另一方面也要保证程序是否稳定,不要出现闪退,不用智能指针的话,访问一块被析构的内存,那程序不就挂掉了吗?
如何判断weak_ptr的对象是否失效?
A:1、expired():检查被引用的对象是否已删除。
2、**lock()**会返回shared指针,判断该指针是否为空。
3、**use_count()**也可以得到shared引用的个数,但速度较慢。

HTTP的GET和POST有什么区别?

1.GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头<request-line>中),以?分割URL和传输数据,多个参数用&连接;例如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。

POST提交:把提交的数据放置在是HTTP包的包体<request-body>中。上文示例中红色字体标明的就是实际的传输数据

因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变

2.传输数据的大小:

首先声明,HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。 而在实际开发中存在的限制主要有:

GET:特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。

因此对于GET提交时,传输数据就会受到URL长度的限制。

POST:由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。

3.安全性:

POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存, (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了。

POST的两次请求能说一下吗?需不需要服务器确认啊?为什么不一次就可以了?为什么要分两次呢(查询跨域)?

是跨域引起的。

浏览器发起跨域请求分两种:一种是simple,另一种需要preflight。如果进行preflight了,你就会看到两次请求,一个OPTION,另一个就是你的跨域请求。

总的来说就是你的请求浏览器不知道是否被允许,需要发送OPTION请求提前查看下

都会默认发送两次请求,第一次是预检请求,查询是否支持跨域,第二次才是真正的post提交

JS中出现这个现象原因在于你发送了一个非简单请求。

简单请求与非简单请求:

* 请求方式:HEAD,GET,POST
* 请求头信息:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type 对应的值是以下三个中的任意一个
                        application/x-www-form-urlencoded
                        multipart/form-data
                        text/plain

只有同时满足以上两个条件时,才是简单请求,否则为非简单请求

解决方法:
由上面定义可以看出,把你的post提交改成get提交,就不会两次请求,或者将post的header改成application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种

HTTPS了解吗?可靠性传输,安全传输是怎么保证的?

https参考链接
HTTPS工作原理
首先服务端给客户端传输证书,这个证书就是公钥,只是包含了很多的信息,比如说证书的办法机构,证书的过期时间
客户端进行证书的解析,比如说验证办法机构,过期时间,如果发现没有任何问题,就生成一个随机值(私钥),然后用证书对这个私钥进行加密,并发送给服务端
服务端使用私钥将这个信息进行解密,得到客户端的私钥,然后客户端和服务端就可以通过这个私钥进行通信了
服务端将消息进行对称加密(简单来说就是讲消息和私钥进行混合,除非知道私钥否则服务进行解密),私钥正好只有客户端和服务端知道,所以信息就比较安全了
服务端将进行对称加密后的消息进行传送
客户端使用私钥进行信息的解密

验证哪些信息能够证明说不是从伪造的服务器发送过来的?,ca权威机构用私钥加密

数字证书内容

包括了加密后服务器的公钥、权威机构的信息、服务器域名,还有经过CA私钥签名之后的证书内容(经过先通过Hash函数计算得到证书数字摘要,然后用权威机构私钥加密数字摘要得到数字签名),签名计算方法以及证书对应的域名。

验证证书安全性过程

当客户端收到这个证书之后,使用本地配置的权威机构的公钥对证书进行解密得到服务端的公钥和证书的数字签名数字签名经过CA公钥解密得到证书信息摘要
然后证书签名的方法计算一下当前证书的信息摘要,与收到的信息摘要作对比,如果一样,表示证书一定是服务器下发的,没有被中间人篡改过。因为中间人虽然有权威机构的公钥,能够解析证书内容并篡改,但是篡改完成之后中间人需要将证书重新加密,但是中间人没有权威机构的私钥,无法加密,强行加密只会导致客户端无法解密,如果中间人强行乱修改证书,就会导致证书内容和证书签名不匹配。

那第三方攻击者能否让自己的证书显示出来的信息也是服务端呢?(伪装服务端一样的配置)

显然这个是不行的,因为当第三方攻击者去CA那边寻求认证的时候CA会要求其提供例如域名的whois信息、域名管理邮箱等证明你是服务端域名的拥有者,而第三方攻击者是无法提供这些信息所以他就是无法骗CA他拥有属于服务端的域名。

假如说一个从QQ的域名来的,一个从TENCENT来的?会不会验证域名?

有些开发者,把代码拿走了,那么它是不是可以获取腾讯的一些数据?认证是如何纠察出哪些恶意的客户呢?

DNS劫持是一个什么概念?

DNS劫持就是指用户访问一个被标记的地址时,DNS服务器故意将此地址指向一个错误的IP地址的行为。范例,网通、电信、铁通的某些用户有时候会发现自己打算访问一个地址,却被转向了各种推送广告等网站,这就是DNS劫持。

解决DNS劫持有什么好办法?(回答了HTTPDNS)

可以采用使用国外免费公用的DNS服务器解决

单点登录原理?

简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。
用户只需要登录一次就可以访问所有相互信任的应用系统。

每次请求都要验证、无法保存会话状态,促使session的生成。因为HTTP请求是无状态的(session cookie维护状态),服务端向客户端发送session id来做区分;

session缺陷:服务器要保存sessio,占内存,影响扩展能力;促使token的生成。

Cookie

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中。session通常在cookie中,但不是绝对的,可以放header,自定义header字段放session就可以了啊,而且这种方式比用cookie安全,是防止CSRF的一种手段(跨站请求伪造(英语:Cross-site request forgery)

session 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

cookie与session区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。而且session只存在本次会话中,cookie本次会话结束仍保存在本地客户端。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
    小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来。token区别session id的方式是利用密钥加密数据作为签名返回给客户端,客户端下次把签名发送过来,服务端再次对数据加密,通过计算对比验证。本质上是通过计算而不做存储。 token无状态,可扩展。

作者:一个玩劫的主播
链接:https://www.nowcoder.com/discuss/483850?source_id=profile_create&channel=1009
来源:牛客网

接雨水那道,我先用单调栈解出来了。

class Solution {
public:
    int trap(vector<int>& height) {
        int size = height.size();
        int ans = 0;
        int l = 0, r = size - 1;
        int max_left = 0, max_right = 0;
        while(l < r)
        {
            max_left = max(max_left, height[l]);
            max_right = max(max_right, height[r]);
            if(max_left < max_right)
                ans += max_left - height[l ++];
            else
                ans +=  max_right - height[r --];
        }
        return ans;
    }
};

本地ide写代码,单链表每k个一翻转,剩余不足k个,保持原位置,自己定义数据结构,自己写测试用例。

static修饰的代码块什么时候执行 初始化

装载
连接
初始化
static块的执行发生在“初始化”的阶段。初始化阶段,jvm主要完成对静态变量的初始化,静态块执行等工作。
类加载过程

网络:MTU了解吗?ping是用到什么协议(应用层,使用ICMP网络层,没有使用传输层)

Maximum Transmission Unit,缩写MTU,中文名是:最大传输单元。 MTU是数据链路层的概念。MTU限制的是数据链路层的payload,也就是上层协议的大小,例如IP,ICMP等。

ping命令本身相当于一个应用程序,位于应用层,虽然它使用的是ICMP协议,就好像HTTP位于应用层,但是也是使用的TCP协议


作者:马思琪
链接:https://www.nowcoder.com/discuss/473944?type=post&order=create&pos=&page=2&channel=1009&source_id=search_post
来源:牛客网

TCP 和 UDP 区别(答案烂大街了,略)

UDP 和 TCP 的数据包有最大长度限制吗?

——UDP包分为包头和正文, 包头共有64位(8字节),分别是16位源端口,16位目的端口,16位UDP包长度和16位校验和.因此UDP包正文的 最大长度是2^16 - 1=65535字节.【以太网帧的数据区最大长度为1500字节】

TCP 分段 和 IP 分段,UDP 会分段吗

——TCP分段的原因是MSS,IP分片的原因是MTU,由于一直有MSS<=MTU,很明显,分段后的每一段TCP报文段再加上IP首部后的长度不可能超过MTU,因此也就不需要在网络层进行IP分片了。因此TCP报文段很少会发生IP分片的情况。

由于UDP数据报不会自己进行分段,因此当长度超过了MTU时,会在网络层进行IP分片。同样,ICMP(在网络层中)同样会出现IP分片情况。

总结**:UDP不会分段,就由IP来分。TCP会分段,当然就不用IP来分了!**(https://www.jianshu.com/p/f9a5b07d99a2)

TCP TIME_WAIT 状态

——通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT 状态。

如下图:客户端主动关闭连接时,会发送最后一个 ack 后,然后会进入TIME_WAIT状态,再停留 2 个 MSL 时间,进入 CLOSED 状态。

【为什么叫 FIN 包呢?因为是 finish 的 abbr】

MSL:maximum segment lifetime (最大分节生命期),这是一个 IP 数据包能在互联网上生存的最长时间,超过这个时间 IP 数据包将在网络中消失 。MSL 在 RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒。

TIME_WAIT:状态维持时间,是两个 MSL 时间长度,也就是在 1-4 分钟。Windows 操作系统就是 4 分钟。

为什么建立连接协议是三次握手,而关闭连接却是四次握手?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

【建议:看看 TCP 的各个状态,参考 https://www.cnblogs.com/softidea/p/5741192.html】

TCP 的半连接队列是什么?

——https://www.cnblogs.com/sidesky/p/6844228.html

C++中的容器了解多少 —— https://blog.csdn.net/tju_fengbo/article/details/81978595

C++ vector 是在头还是尾插入的?如果 C++ 要删除一个列表中的元素,会影响头指针还是尾指针

C++ 中重载、重写(虚函数)的区别

重载是指参数列表不同,函数名字相同,在同一类内的;
重写是指派生类继承基类的虚函数,参数列表一定相同;
覆盖是指派生类的方法与基类的方法名称相同,基类方法被隐藏。;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。

C++ 进程内存空间分布

代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

数据段:存放程序运行的时候就能确定的数据,可读可写。存储程序中已初始化的全局变量和静态变量

BBS段:定义而没有初始化的全局变量和静态变量,以及所有被初始化为0的全局变量和静态变量。

堆:堆的内存地址由低到高。调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

映射区:存储动态链接库以及调用mmap函数进行的文件映射

栈:栈。编译器自动分配和释放。内存地址由高到低。 使用栈空间存储函数的返回地址、参数、局部变量、返回值

mysql 怎样保障数据一致性

通过预写式日志,undo log保证原子性,redo log保证持久性,设置隔离级别,保证并发事务进行的时候,保证数据一致性。

对于一个基本有序的数组进行排序用什么最快?

—— 插入排序?求指正

红黑树中已经有n个数据,寻找某个key是否存在的时间复杂度为 logN (是一种平衡二叉树)。
进程间通信(IPC:Inter-Process Communication)的几种方式

—— 管道、信号量(semophore)、消息队列、信号(signal)、共享内存、套接字

Linux 对 IPC 的管理的命令  ipsc -m 查看共享内存

——https://www.cnblogs.com/wt645631686/p/9151029.html

socket 编程的 epoll、select、poll 什么意思?

——https://blog.csdn.net/jyy305/article/details/73012706 、 https://blog.csdn.net/wk_bjut_edu_cn/article/details/80669310

附:无意间看到的文章

看到一个讲排序的 blog ,动图很可
topK 、BFPRT、KMP

QQ 是怎么实现通信的?

C10K问题,即单机1万个并发连接问题.

早期的腾讯QQ也同样面临C10K问题,只不过他们是用了UDP这种原始的包交换协议来实现的,绕开了这个难题,当然过程肯定是痛苦的。如果当时有epoll技术,他们肯定会用TCP。众所周之,后来的手机QQ、微信都采用TCP协议。

C10K问题的本质:操作系统的问题,传统的同步阻塞IO模型都是一样的,处理的方式都是requests per second。并发10K和100的区别关键在于CPU

线程过多,导致数据拷贝频繁,进程/线程切换消耗大,导致os崩溃。

C10K问题的解决方案:

1.每个进程/线程处理一个连接,但占用资源很大

2.每个进程/线程同时处理多个连接,IO多路复用,epoll

1.分布式系统架构。保证系统没有单点故障,发布版本不会宕机,所有的节点至少要有主备机。相关的技术:负载均衡,nginx。

其中负载均衡的实现包括:

非自适应的负载平衡算法:随机,和轮转

自适应的负载平衡算法:最小负载算法和平均负载算法。

2.数据的持久化与缓存。海量的数据和文件的读写,关系型数据库是绝对不够的,热点数据一般放在缓存中,分布在多个存储节点上。

3.网络传输的优化。DNS,CDN加速

4.主流开发技术与框架。比如Java, php,python等。

5.开源或自制的中间件,比如消息,异步通信等。

6.减少TIME_WAIT状态的连接数量,通过调整内核参数解决。

7.每个进程/线程处理多个连接,IO多路复用,epoll。

8.创建线程池,减少线程创建和销毁的时间。

9.确保每个线程都要detach

qq通信


作者:三七是只小学鸡
链接:https://www.nowcoder.com/discuss/446471?type=post&order=create&pos=&page=1&channel=1009&source_id=search_post
来源:牛客网

计算机网络
七层模型和四层模型
tcp在哪一层
四次挥手为什么多一次
udp和tcp的区别
udp的应用场景
拥塞控制

操作系统
堆栈的区别
临时变量写在哪

数据库
索引结构
索引位置
事务的工作原理


作者:三七是只小学鸡
链接:https://www.nowcoder.com/discuss/445825?type=post&order=create&pos=&page=1&channel=1009&source_id=search_post
来源:牛客网

计算机网络
1.tcp三次握手过程
2.为啥是三次握手不是两次握手
防止已失效的连接请求又传送到服务器端,因而产生错误

3.tcp如何可靠
应答机制/拥塞控制/流量控制/重复丢弃/失序重排/超时重传
4.tcp如何判断信息流结束
上层协议做的事,通过加包长度,特殊字符做判断.
我回答的是序列号 但不太确定
5.一个网页的打开过程
DNS解析(本地缓存 递归查询 根服务器) —— TCP三次握手建立连接 —— 发起HTTP请求(get/post) ——HTML解析渲染 加载资源 ——断开连接
操作系统 我没学过…欢迎指正
进程与线程
进程是资源共享的最小单位 一个进程可以有多个线程 地址空间独立
线程是资源调度的最小单位 共享数据和地址空间 多线程不够健壮 一个线程死了整个进程都死了
线程有没有自己的独立数据
我觉得有 不然干嘛要通信…
进程间通信
信号量/管道/消息队列/共享内存 共享内存是效率最高的

数据结构与算法
判断链表相交
头尾相连 判断是否有环

开放式智力题
一百本书 两个人拿 每次拿1-5本 先拿的人能否保证拿到最后一本
第一次拿4本 之后两个人只和保证为6即可

从底层谈谈map数据结构的设计。如果容量不够了怎么办,扩容过程中可能会耗费比较多的时间,如果在扩容时要访问怎么办

无序容器unordered_map存储为一组桶,各元素通过hash函数映射到各个桶中。心血来潮,来看一下桶的增长规律
bucket[n] = 2*bucket[n-1] + 奇数 (1, 3, 5, 9 …)

守护进程了解过吗

护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。

守护进程(daemon),是一种运行在后台 的特殊进程,它独立与控制终端 ,并 周期性地执行某项任务或等待处理某些发生的事件。 。守护进程可以由一个普通的进程按照守护进程的特性改造而来。

在这里插入图片描述

创建守护进程

在这里插入图片描述

创建守护进程的步骤:

1、fork()创建子进程,父进程exit()退出;

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,完成这一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

由于父进程先于子进程退出,子进程就变为孤儿进程,并由 init 进程作为其父进程收养

2、在子进程调用setsid()创建新会话;

在调用了 fork() 函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变。这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

setsid()创建一个新会话,调用进程担任新会话的首进程,其作用有:

使当前进程脱离原会话的控制
使当前进程脱离原进程组的控制
使当前进程脱离原控制终端的控制
这样,当前进程才能实现真正意义上完全独立出来,摆脱其他进程的控制。

3、再次 fork() 一个子进程,父进程exit退出;

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。

也就是说通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。

4、在子进程中调用chdir()让根目录“/”成为子进程的工作目录;

这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。(避免原父进程当前目录带来的一些麻烦)

5、在子进程中调用umask()重设文件权限掩码为0;

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限(就是说可读可执行权限均变为7)。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此把文件权限掩码重设为0即清除掩码(权限为777),这样可以大大增强该守护进程的灵活性。通常的使用方法为umask(0)。(相当于把权限开发)

6、在子进程中close()不需要的文件描述符;

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。其实在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。(关闭失去价值的输入、输出、报错等对应的文件描述符)

for (i=0; i < MAXFILE; i++)
close(i);

7、守护进程退出处理,kill

当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

在这里插入图片描述

孤儿进程:init进程收养

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程完成对它们状态收集工作。

孤儿进程:孤儿进程因为在其父进程退出时被init进程所收养,所以init进程会wait()孤儿进程,所有孤儿进程并没有什么危害。

僵尸进程:有危害,需要释放掉真凶父进程

一个进程使用 fork创建子进程,如**果子进程退出,而父进程没有调用wait或waitpid函数获取子进程的状态信息,那么子进程的进程描述符仍然会保存在系统中。**这种进程称为僵尸进程。


作者:大黑熊
链接:https://www.nowcoder.com/discuss/391599?type=post&order=create&pos=&page=0&channel=1009&source_id=search_post
来源:牛客网

问题一:现在所有的qq号共40亿个,假设有1000亿条登录记录,怎么样找到出现过的qq号,内存消耗多大。
参考答案:哈希表,把40亿个放进哈希表中,遍历100亿条就好了。消耗大概是 40亿4Byte=160亿Byte=16010^8Byte<16G

问题二:现在只有8个G的内存,怎么实现这个操作。
参考答案:位图,用一位来代表一个数据是否出现。开一个40亿/8 Byte+1Byte=5亿Byte<500M空间的数组(char NUMS[]),40亿个数字映射对应的num 1~40亿,对应的数组项为NUMS[num/8],需要修改的位是num%8。这样消耗的空间只需要500M不到。

问题三:如果需要统计每个qq号出现过的次数,该怎么解决(开放)。
参考答案:把每个数映射到每个char类型,每个char类型可以统计256个登录记录,那么共需要40亿Byte=4G空间。因为实际应用场景中,登录记录应该符合正态分布,所以假设超过256次登录的qq号不多,把多出来的qq号用hash_map来存储大于登录次数大于256的qq号次数。

给大量qq号(亿为单位)如何排序(我讲添加链接描述了一
下数据库的外排序,不知道他是不是要这个答案,但是听我讲了挺多的),问我这个算法的时间复杂度。

海量数据

海量数据面试

一、Bloom filter

适用范围:可以用来实现数据字典,进行数据的判重,或者集合求交集

基本原理及要点:
  对于原理来说很简单,位数组+k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是 counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。

还有一个比较重要的问题,如何根据输入元素个数n,确定位数组m的大小及hash函数个数。当hash函数个数k=(ln2)(m/n)时错误率最小。在错误率不大于E的情况下,m至少要等于nlg(1/E)才能表示任意n个元素的集合。但m还应该更大些,因为还要保证bit数组里至少一半为0,则m应该>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2为底的对数)。

举个例子我们假设错误率为0.01,则此时m应大概是n的13倍。这样k大概是8个。

注意这里m与n的单位不同,m是bit为单位,而n则是以元素个数为单位(准确的说是不同元素的个数)。通常单个元素的长度都是有很多bit的。所以使用bloom filter内存上通常都是节省的。

扩展:
  Bloom filter将集合中的元素映射到位数组中,用k(k为哈希函数个数)个映射位是否全1表示元素在不在这个集合中。Counting bloom filter(CBF)将位数组中的每一位扩展为一个counter,从而支持了元素的删除操作。Spectral Bloom Filter(SBF)将其与集合元素的出现次数关联。SBF采用counter中的最小值来近似表示元素的出现频率。

问题实例:给你A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A,B文件共同的URL。如果是三个乃至n个文件呢?

根据这个问题我们来计算下内存的占用,4G=2^32大概是40亿*8大概是340亿,n=50亿,如果按出错率0.01算需要的大概是650亿个bit。现在可用的是340亿,相差并不多,这样可能会使出错率上升些。另外如果这些urlip是一一对应的,就可以转换成ip,则大大简单了。

二、Hashing

适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存

基本原理及要点:
  hash函数选择,针对字符串,整数,排列,具体相应的hash方法。
  碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。

  扩展:

d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。

问题实例:
  1).海量日志数据,提取出某日访问百度次数最多的那个IP。
  IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。

三、bit-map

适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下

基本原理及要点:使用bit数组来表示某些元素是否存在,比如8位电话号码

扩展:bloom filter可以看做是对bit-map的扩展

问题实例:
  1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
  8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。
  2)2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。

将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map。

四、堆

适用范围:海量数据前n大,并且n比较小,堆可以放入内存

基本原理及要点:最大堆求前n小,最小堆求前n大。方法,比如求前n小,我们比较当前元素与最大堆里的最大元素,如果它小于最大元素,则应该替换那个最大元素。这样最后得到的n个元素就是最小的n个。适合大数据量,求前n小,n的大小比较小的情况,这样可以扫描一遍即可得到所有的前n元素,效率很高。

扩展:双堆,一个最大堆与一个最小堆结合,可以用来维护中位数。

问题实例:
  1)100w个数中找最大的前100个数。
  用一个100个元素大小的最小堆即可。

五、双层桶划分----其实本质上就是【分而治之】的思想,重在“分”的技巧上!

适用范围:第k大,中位数,不重复或重复的数字
  基本原理及要点:因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。可以通过多次缩小,双层只是一个例子。

扩展:
  问题实例:
  1).2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
  有点像鸽巢原理,整数个数为232,也就是,我们可以将这232个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。

2).5亿个int找它们的中位数。
  这个例子比上面那个更明显。首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。

实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成224个区域,然后确定区域的第几大数,在将该区域分成220个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了。

六、数据库索引

适用范围:大数据量的增删改查

基本原理及要点:利用数据的设计实现方法,对海量数据的增删改查进行处理。

七、倒排索引(Inverted index)

适用范围:搜索引擎,关键字查询

基本原理及要点:为何叫倒排索引?一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。

以英文为例,下面是要被索引的文本:
T0 = “it is what it is”
T1 = “what is it”
T2 = “it is a banana”

我们就能得到下面的反向文件索引:

"a":      {2}
"banana": {2}
"is":     {0, 1, 2}
"it":     {0, 1, 2}
"what":   {0, 1}

检索的条件"what","is"和"it"将对应集合的交集。

正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样的查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。

扩展:
  问题实例:文档检索系统,查询那些文件包含了某单词,比如常见的学术论文的关键字搜索。

八、外排序

适用范围:大数据的排序,去重

基本原理及要点:外排序的归并方法,置换选择败者树原理,最优归并树

扩展:

问题实例:
  1).有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16个字节,内存限制大小是1M。返回频数最高的100个词。

这个数据具有很明显的特点,词的大小为16个字节,但是内存只有1m做hash有些不够,所以可以用来排序。内存可以当输入缓冲区使用。

九、trie树

适用范围:数据量大,重复多,但是数据种类小可以放入内存

基本原理及要点:实现方式,节点孩子的表示方式

扩展:压缩实现。

问题实例:
  1).有10个文件,每个文件1G,每个文件的每一行都存放的是用户的query,每个文件的query都可能重复。要你按照query的频度排序。
  2).1000万字符串,其中有些是相同的(重复),需要把重复的全部去掉,保留没有重复的字符串。请问怎么设计和实现?
  3).寻找热门查询:查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个,每个不超过255字节。

十、分布式处理 mapreduce

适用范围:数据量大,但是数据种类小可以放入内存

基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。

扩展:
  问题实例:
  1).The canonical example application of MapReduce is a process to count the appearances of
each different word in a set of documents:
  2).海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。
  3).一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?

十个经典问题

1、海量日志数据,提取出某日访问百度次数最多的那个IP。 分而治之+Hash

首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有个2^32个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。

或者如下阐述(雪域之鹰):
算法思想:分而治之+Hash
1.IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
2.可以考虑采用“分而治之”的思想,按照IP地址的Hash(IP)%1024值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
3.对于每一个小文件,可以构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址;
4.可以得到1024个小文件中的出现次数最多的IP,再依据常规的排序算法得到总体上出现次数最多的IP;

2、搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。

典型的Top K算法,还是在这篇文章里头有所阐述,详情请参见:十一、从头到尾彻底解析Hash表算法。

文中,给出的最终算法是:
第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计(之前写成了排序,特此订正。July、2011.04.27);
第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。
    即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)。ok,更多,详情,请参考原文。

或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。

3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

方案:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为x0,x1,...x4999)中。这样每个文件大概是200k左右。

如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。
对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点的最小堆),并把100个词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。

4、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

还是典型的TOP K算法,解决方案如下:
方案1:
顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。

找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为)。

对这10个文件进行归并排序(内排序与外排序相结合)。

方案2:
 一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。这样,我们就可以采用trie树/hash_map等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。

方案3:
与方案1类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。

5、 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

方案1:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。

遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。

遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。

求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。

方案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。

Bloom filter日后会在本BLOG内详细阐述。

6、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。

方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。

方案2:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。

7、腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

与上第6题类似,我的第一反应时快速排序+二分查找。以下是其它更好的方法:
方案1:oo,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。

dizengrong:
方案2:这个问题在《编程珠玑》里有很好的描述,大家可以参考下面的思路,探讨一下:

又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;
这里我们把40亿个数中的每一个用32位的二进制来表示
假设这40亿个数开始放在一个文件中。

然后将这40亿个数分成两类:
  1.最高位为0
  2.最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);

与要查找的数的最高位比较并接着进入相应的文件再查找

再然后把这个文件为又分成两类:
  1.次最高位为0
  2.次最高位为1

并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);
与要查找的数的次最高位比较并接着进入相应的文件再查找。
.......
以此类推,就可以找到了,而且时间复杂度为O(logn),方案2完。

附:这里,再简单介绍下,位图方法:
使用位图法判断整形数组是否存在重复
判断集合中存在重复是常见编程任务之一,当集合中数据量比较大时我们通常希望少进行几次扫描,这时双重循环法就不可取了。

位图法比较适合于这种情况,它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。这种给新数组初始化时置零其后置一的做法类似于位图的处理方法故称位图法。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。

欢迎,有更好的思路,或方法,共同交流。

8、怎么在海量数据中找出重复次数最多的一个?

方案1:先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。

9、上千万或上亿数据(有重复),统计其中出现次数最多的钱N个数据。

方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第2题提到的堆机制完成。

10、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。

方案1:这题是考虑时间效率。用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度)。然后是找出出现最频繁的前10个词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是O(n*lg10)。所以总的时间复杂度,是O(n*le)与O(n*lg10)中较大的哪一个。

附、100w个数中找出最大的100个数。

方案1:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。

方案2:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。

方案3:采用局部淘汰法。选取前100个元素,并排序,记为序列L。然后一次扫描剩余的元素x,与排好序的100个元素中最小的元素比,如果比这个最小的要大,那么把这个最小的元素删除,并把x利用插入排序的思想,插入到序列L中。依次循环,知道扫描了所有的元素。复杂度为O(100w*100)。

致谢:http://www.cnblogs.com/youwang/。


如何追踪运行期间进程的堆栈?

gdb调试

1、backtrace、bt

(gdb) bt
#0 func (n=250) at tst.c:6
#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

bt
n是一个正整数,表示只打印栈顶上n层的栈信息。

backtrace <-n>
bt <-n>
-n表一个负整数,表示只打印栈底下n层的栈信息。
如果你要查看某一层的信息,你需要在切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。

2、frame、f

n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame 1,表示栈的第二层。
查看当前栈层的信息,你可以用以下GDB命令:
frame 或 f

3、info frame、info f

这个命令会打印出更为详细的当前栈层的信息,只不过,大多数都是运行时的内存地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。如:

4、info args

打印出当前函数的参数名及其值。

5、info locals

打印出当前函数中所有局部变量及其值。

6、info catch

打印出当前的函数中的异常处理信息。

gdb 多线程

利用gdb查看线程信息

将进程附加到gdb调试器当中,查看是否创建了新线程

gdb attach 主线程ID

查看线程信息(很重要)info inferiors/threads bt thread n

//1.查看进程:info inferiors
//2.查看线程:info threads
//3.查看线程栈结构:bt
//4.切换线程:thread n(n代表第几个线程)

利用gdb调试多线程

设置断点

//1. 设置断点:break 行号/函数名
//2. 查看断点:info b

执行线程2的函数,指行完毕继续运行到断点处

1. 继续使某一线程运行:thread apply 1-n(第几个线程) n
2. 重新启动程序运行到断点处:r

只运行当前线程

1. 设置:set scheduler-locking on
2. 运行:n

所有线程并发执行

    1. 设置:set scheduler-locking off
    2. 运行:n

命令 用法
info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程
thread ID(1,2,3…) 切换当前调试的线程为指定ID的线程
break thread_test.c:123 thread all(例:在相应函数的位置设置断点break pthread_run1) 在所有线程中相应的行上设置断点
thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command
thread apply all command 让所有被调试线程执行GDB命令command
set scheduler-locking 选项 command 设置线程是以什么方式来执行命令
set scheduler-locking off 不锁定任何线程,也就是所有线程都执行,这是默认值
set scheduler-locking on 只有当前被调试程序会执行
set scheduler-locking on step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行

gdb的原理有了解吗? ptrace系统调用来实现的 ,父进程观察控制

ptrace系统调用提供了一种方法,让父进程可以观察和控制其它进程的执行,检查和改变其核心映像及寄存器。主要用来实现断点调试和系统调用跟踪

goroutine原理了解吗 通过goroutine和channel

gpm
Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。

Go的CSP并发模型,是通过goroutine和channel来实现的

goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

生成一个goroutine的方式非常的简单:Go一下,就生成了。

go f();
通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。

在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。

M个用户线程对应N个系统线程,缺点增加了调度器的实现难度。

Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)

Go线程实现模型MPG

M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。

P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列

均衡的分配工作 分配一半

按照以上的说法,上下文P会定期的检查全局的goroutine 队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。

每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。
该如何解决呢?

Go的做法倒也直接,从其他P中偷一半!

CDN的概念有了解吗?

Content Delivery Network,即内容分发网络。

其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上

1.用户向浏览器输入www.web.com这个域名,浏览器第一次发现本地没有dns缓存,则向网站的DNS服务器请求;
2.网站的DNS域名解析器设置了CNAME,指向了www.web.51cdn.com,请求指向了CDN网络中的智能DNS负载均衡系统;
3.智能DNS负载均衡系统解析域名,把对用户响应速度最快的IP节点返回给用户;
4.用户向该IP节点(CDN服务器)发出请求;
5.由于是第一次访问,CDN服务器会向原web站点请求,并缓存内容;
6.请求结果发给用户。


CAP

P(partition tolerance)
p代表分区容错,它的意思是区间通信可能失败,比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

分区容错性指在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

C(consistency)
c代表一致性,表示写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

A(availability)
Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。

实时证明,大多数都是牺牲了一致性。像12306还有淘宝网,就好比是你买火车票,本来你看到的是还有一张票,其实在这个时刻已经被买走了,你填好了信息准备买的时候发现系统提示你没票了。这就是牺牲了一致性。

但是不是说牺牲一致性一定是最好的。就好比mysql中的事务机制,张三给李四转了100块钱,这时候必须保证张三的账户上少了100,李四的账户多了100。因此需要数据的一致性,而且什么时候转钱都可以,也需要可用性。但是可以转钱失败是可以允许的。

分布式缓存策略

分布式缓存首先也是缓存,一种性能很好但是相对稀缺的资源,涉及到并发和网络的问题

缓存的更新模式

Cache Aside模式

读取失效:cache数据没有命中,查询DB,成功后把数据写入缓存
读取命中:读取cache数据
更新:把数据更新到DB,失效缓存

在这里插入图片描述

为什么更新不直接写缓存?

为了降低并发情况下的数据不一致发生概率(cache aside无法完全避免数据不一致,只能降低发生的概率,如果需要数据强一直可以考虑使用分布式锁)

Read/Write Through模式,缓存看成唯一存储

缓存代理了DB读取、写入的逻辑,可以把缓存看成唯一的存储。

Write Behind Caching(Write Back)模式

这种模式下所有的操作都走缓存,缓存里的数据再通过异步的方式同步到数据库里面。所以系统的写性能能够大大提升了。

缓存淘汰策略

FIFO

LRU

LFU
最近最少使用(Least Frequently Used),这种策略根据最近访问的频率来进行淘汰,如果空间不足,会释放最近访问频率最低的对象。这个算法也是用优先队列实现的比较常见。

分布式缓存的常见问题

缓存穿透
DB中不存在数据,每次都穿过缓存查DB,造成DB的压力。一般是网络攻击
解决方案:放入一个特殊对象(比如特定的无效对象,当然比较好的方式是使用包装对象)

如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

缓存击穿

描述:

  缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:

设置热点数据永远不过期。
加互斥锁
,互斥锁参考代码如下:

缓存雪崩

  描述:

  缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,        缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

 解决方案:

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中
设置热点数据永远不过期。


作者:廿半
链接:https://www.nowcoder.com/discuss/378224?type=post&order=time&pos=&page=2&channel=1009&source_id=search_post&subType=2
来源:牛客网

共享内存的用法

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量

一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

共享内存是在物理内存上开辟一块内存空间,然后将这个空间映射到虚拟地址空间上,如果存在多个进程都映射到了该区域的虚拟地址空间,就会发生一个进程写入共享内存的信息,也可以被其它使用这个共享内存的进程通过简单的内存读取读出信息,从而实现了进程间的通信

共享内存的操作步骤:

  1. 创建共享内存
  2. 将共享内存映射到虚拟地址空间上
  3. 通过虚拟地址操作共享内存
strcpy(shmat返回值,"data");//也可以利用memcpy对共享内存进行操作
  1. 解除共享内存在虚拟地址空间上的映射关系
  2. 控制删除共享内存

1)获取共享内存地址:在内核态空间开辟内存空间,返回该内存区域的唯一标识

2)映射到进程地址空间:映射开辟的kernel空间到用户态内存空间(虚拟内存)

3)操作空间: 操作用户态的内存空间

4)关闭映射

5)删除内核态内存空间

服务端:(1.)创建共享内存区域 (2.)内存映射到当前进程 (3.)写入数据

客户端:(1.)打开共享内存区域 (2.)内存映射到当前进程 (4.)读出数据

允许多个进程(运行在同一计算机上)使用内存映射文件来共享数据

这种数据共享是让两个或多个进程映射同一文件映射对象的视图,即它们在共享同一物理存储页。这样,当一个进程向内存映射文件的一个视图写入数据时,其他的进程立即在自己的视图中看到变化。但要注意,对文件映射对象要使用同一名字。

在这里插入图片描述

为什么共享内存速度最快

对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件

借助上图说明:Proc A 进程给内存中写数据, Proc B 进程从内存中读取数据,在此期间一共发生了两次复制

(1)Proc A 到共享内存 (2)共享内存到 Proc B

因为直接在内存上操作,所以共享内存的速度也就提高了

Linux如何实现共享内存 mmap()系统调用,Posix共享内存,以及系统V共享内存。

共享内存的通信方式

Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。
linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,

系统调用mmap()通过映射一个普通文件实现共享内存系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的)

mmap()系统调用实现原理:
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

说出四个端口及作用

查看进程状态的指令 ps top

   ps -l   列出与本次登录有关的进程信息;
   ps -aux   查询内存中进程信息;
   ps -aux | grep ***   查询***进程的详细信息;
   top   查看内存中进程的动态信息;
   kill -9 pid   杀死进程。

C++中include时如何保证不重复加载头文件

  • #pragma once ( 比较常用)
  • 条件编译
#ifndef _HEADERNAME_H
#define _HEADERNAME_H

...//(头文件内容)

#endif
  • 类的前置声明有许多的好处的。
#include "B.h" 

class B; //前置声明

class A  

{
}

编译一下A.cpp,不通过。再编译B.cpp,还是不通过。编译器去编译A.h,发现包含了B.h,就去编译B.h。编译B.h的时候发现包含了A.h,但是A.h已经编译过了(其实没有编译完成,可能编译器做了记录,A.h已经被编译了,这样可以避免陷入死循环。编译出错总比死循环强点),就没有再次编译A.h就继续编译。后面发现用到了A的定义,这下好了,A的定义并没有编译完成,所以找不到A的定义,就编译出错了。

我们使用前置声明的一个好处是,从上面看到,当我们在类A使用类B的前置声明时,我们修改类B时,只需要重新编译类B,而不需要重新编译a.h的(当然,在真正使用类B时,必须包含b.h)。

另外一个好处是减小类A的大小,上面的代码没有体现,那么我们来看下:
如果我们在类A中包含的是B的对象,那么类A的大小就是12(假设没有其它成员变量和虚函数)。如果包含的是类B的指针*b变量,那么类A的大小就是4,所以这样是可以减少类A的大小的,特别是对于在STL的容器里包含的是类的对象而不是指针的时候,这个就特别有用了。
在前置声明时,我们只能使用的就是类的指针和引用(因为引用也是居于指针的实现的)。

数据库中delete和drop的区别

drop主要用于删除结构,数据库,表,字段

例如删除数据库:drop database XX,删除表 drop table XX。字段也是结构的一种,也可以使用drop了?对的,但是我们改变了表结构要先alter方法。例如,我们要删除student表上的age字段的信息,可以这样写:alter table student drop age

delete主要用于删除数据

举个例子,要删除 student表上名字为‘张三’的所有信息:delete from student where name=‘张三’。这种情况下用delete,由此可见delete常用于删除数据。

操作对象不同,drop的操作对象可以是数据库,也可以是数据库中的数据表,delete只能是数据表

1、drop:drop的操作对象可以是数据库,也可以是数据库中的数据表。

2、delete:delete的操作对象只能是数据库中的数据表。

数据库中视图的应用场景,数据库数据改变视图中的数据是否会改变(会)

视图是一个虚表, 
视图就是封装了一条复杂查询的sql语句集,
它与真实表保持同步,也就是修改视图会影响真实表,修改真实表也会影响视图

视图能够对机密数据提供安全保护,适当的利用视图可以更清晰地表达查询

有了视图机制,就可以在设计数据库应用系统时,对不同的用户定义不同的视图,使机密数据不出现在不应该看到这些数据的用户视图上

对于一个读多写少的大表,如果要增加一个字段,可以怎么做

多读少写的场景 如何提高性能

引入 CopyOnWrite 思想解决问题!

写数据的时候利用拷贝的副本来执行”。

你在读数据的时候,其实不加锁也没关系,大家左右都是一个读罢了,互相没影响。


作者:廿半
链接:https://www.nowcoder.com/discuss/378224?type=post&order=time&pos=&page=2&channel=1009&source_id=search_post&subType=2
来源:牛客网

数据库的隔离级别?底层怎么实现的可重复读?(mvcc版本号)

一个事务在执行过程中可以看到其他事务已经提交的新插入的记录(读已经提交的,其实是读早于本事务开始且已经提交的),但是不能看到其他事务对已有记录的更新(即晚于本事务开始的),并且,该事务不要求与其他事务是“可串行化”的。

使用MVCC(多版本并发控制)。InnoDB为每行记录添加了一个版本号(系统版本号),每当修改数据时,版本号加一。

如何使用数据库实现分布式锁?具体的sql语句?for update怎么区分是行级锁还是表级锁?(看是否有索引)

分布式锁是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
目前比较常见的分布式锁实现方案有以下几种:

基于数据库,如MySQL
基于缓存,如Redis
基于Zookeeper、etcd等

基于数据库(MySQL)的方案:基于表记录、乐观锁和悲观锁。

基于表记录:最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。

INSERT INTO database_lock(resource, description) VALUES (1, 'lock');

DELETE FROM database_lock WHERE resource=1;

乐观锁

INSERT INTO optimistic_lock(resource, version, created_at, updated_at) VALUES(20, 1, CURTIME(), CURTIME());

借助更新时间戳(updated_at)也可以实现乐观锁,和采用version字段的方式相似:更新操作执行前线获取记录当前的更新时间,在提交更新时,检测当前更新时间是否与更新开始时获取的更新时间戳相等。

悲观锁

我们还可以借助数据库中自带的锁来实现分布式锁。在查询语句后面增加FOR UPDATE,数据库会在查询过程中给数据库表增加悲观锁,也称排他锁。当某条记录被加上悲观锁之后,其它线程也就无法再改行上增加悲观锁。

for update 判断行锁表锁

取决于是否能使用索引(例如主键,unique字段),能则为行锁,否则为表锁;未查到数据则无锁。而 使用’<>’,'like’等操作时,索引会失效,自然进行的是table lock

进程通信方式?共享内存怎么实现的?共享内存读取的是虚拟内存吗?通过读取相同的虚拟地址不行吗?为什么?

零拷贝机制说一下( DMA 直接内存访问 )?零拷贝的底层怎么实现的?Kafka在哪个阶段用的零拷贝?

零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。
零拷贝是用于对不变的数据做传输,如果应用程序需要修改数据那势必就不能用到零拷贝了,所以零拷贝可不是万能的

DMA(Direct Memory Access) ———— 直接内存访问DMA是允许外设组件将I/O数据直接传送到主存储器中并且传输不需要CPU的参与,以此将CPU解放出来去完成其他的事情。

通过mmap实现的零拷贝I/O

直接IO:应用程序直接访问硬件存储,内核只辅助数据传输,不进行页缓存。

写时复制技术:当应用程序不需要修改数据时只保存在内核缓冲区,不复制到用户空间。(这类方法没有dma,需要cpu的全程参与

线程池底层怎么实现的(非Java层面)?它具体是怎么管理的线程?
在这里插入图片描述
在这里插入图片描述

作者:廿半
链接:https://www.nowcoder.com/discuss/378224?type=post&order=time&pos=&page=2&channel=1009&source_id=search_post&subType=2
来源:牛客网

Kafka中的partition是怎样读取的 分区

Topic

Topic 是一个存储消息的逻辑概念,可以理解为是一个消息的集合。每条发送到 Kafka 集群的消息都会自带一个类别,表明要将消息发送到哪个 Topic 上。在存储方面,不同的 Topic 的消息是分开存储的,每个 Topic 可以有多个生产者向他发送消息,也可以有多个消费者去消费同一个Topic中的消息
在这里插入图片描述

Partition

Partition,在 Kafka 中是分区的意思。即:Kafka 中每个 Topic 可以划分多个分区(每个 Topic 至少有一个分区),同一个 Topic 下的不同分区包含的消息是不同的(分区可以间接理解成数据库的分表操作)。
每个消息在被添加到分区的时候,都会被分配一个 offset (偏移量),它是消息在当前分区中的唯一编号。Kafka 通过 offset 可以保证消息在分区中的顺序性,但是跨分区是无序的,即 Kafka 只保证在同一个分区内的消息是有序的。
在这里插入图片描述

producer消息分发策略

消息是 Kafka 中最基本的数据单元在 Kafka 中,一条消息由 key 和 value 两部分组成,key 和 value 值都可以为空。

这里的 key 有什么用呢?当我们在发送一条消息时,我们可以指定这个 key ,那么 producer 则会根据 key 和 partition 机制,来判断当前这条消息应该发送并存储到哪个 partition 中。(此时问题1便得到了解决)

如果 Kafka 中的 key 为 null 该怎么办?默认情况下,Kafka 采用的是 hash 取模的分区算法如果 key 为 null 的话,则会随机的分配一个分区。这个随机是在这个参数 "metadata.max.age.ms"的时间范围内随机选择一个。对于这个时间段内,如果 key 为 null,则只会发送到唯一的分区,这个值默认情况下是 10 分钟更新一次

此外,Kafka 也为我们提供了自定义消息分发策略的入口,我们可以根据自身业务的情况,来自定义消息分发策略。那么如何来实现我们自己的分区策略呢?我们只需要定义一个类,实现 Partitioner 接口,重写它的 partition 方法即可。然后在配置 kafka 的时候,设置使用我们自定义的消息分发策略即可。如何自定义消息分发策略,请参照 4.1 自定义消息分发策略Demo

Kafka性能高的原因(架构方面考虑)

利用Partition实现并行处理(partition在物理上对应本地文件夹)

Kafka中的每个Topic都包含一个或多个Partition,且它们位于不同节点。同时,Partition在物理上对应一个本地文件夹,每个Partition包含一个或多个Segment,其中包含一个数据文件与一个索引文件。Partition像一个数组,可以通过索引(offset)去访问其数据。

Kafka不仅可以实现机器间的并行处理,因为其Partition物理上对应一个文件夹,可以通过配置让同一节点的不同Partition置于不同的disk drive上,从而实现磁盘间的并行处理。具体方法:将不同磁盘mount到不同目录,在server.properties中,将log.dirs设置为多目录(逗号分隔),Kafka会自动将所有Partition均匀分配到不同disk上。
一个Partition只能被一个Consumer消费,如果Consumer的个数多于Partition的个数,会有部分Consumer无法消费该Topic的数据,所以,Partition的个数决定了最大并行度。
如果用Spark消费Kafka数据,如果Topic的Partition数为N,则有效的Spark最大并行度也为N,即使Spark的Executor数为N+M,最多也只有N个Executor可以同时处理该Topic数据。

实现CAP中可用性与数据一致性的动态平衡

Kafka的数据复制方案接近于Master-Slave,不同的是**,Kafka既不是完全的同步复制,也不是完全的一步复制,而是基于ISR的动态复制方案**。
ISR,In-Sync Replica,每个Partition的Leader都会维护这样一个列表,其中包含了所有与之同步的Replica。每次写入数据时,只有ISR中的所有Replica都复制完,Leader才会将这条数据置为Commit,它才能被Consumer消费。
与同步复制不同的是,这个ISR是由Leader动态维护的,如果Follower不能紧跟上Leader,它将被Leader从ISR中移除,待它重新“跟上”Leader后,会被Leader再次加入ISR中。每次改变ISR后,Leader会将最新ISR持久化到Zookeeper中。
那么如何判断Follower是否“跟上”Leader?

如果Follower在replica.lag.time.max.ms时间内未向Leader发送Fetch请求(数据复制请求),则Leader将其从ISR中移除。

分库分表分区简述

分区

就是把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的
数据分区是一种物理数据库的设计技术,它的目的是为了在特定的SQL操作中减少数据读写的总量以缩减响应时间。
分区并不是生成新的数据表,而是将表的数据均衡分摊到不同的硬盘,系统或是不同服务器存储介子中,实际上还是一张表。另外,分区可以做到将表的数据均衡到不同的地方,提高数据检索的效率,降低数据库的频繁IO压力值,分区的优点如下:

分表

就是把一张表按一定的规则分解成N个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的字表明,然后操作它。

分库

一旦分表,一个库中的表会越来越多
分库解决的问题
其主要目的是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展性问题。

垂直拆分
将系统中不存在关联关系或者需要join的表可以放在不同的数据库不同的服务器中。

按照业务垂直划分。比如:可以按照业务分为资金、会员、订单三个数据库。

需要解决的问题:跨数据库的事务、jion查询等问题。

水平拆分
例如,大部分的站点。数据都是和用户有关,那么可以根据用户,将数据按照用户水平拆分。

按照规则划分,一般水平分库是在垂直分库之后的。比如每天处理的订单数量是海量的,可以按照一定的规则水平划分。需要解决的问题:数据路由、组装。

读写分离
对于时效性不高的数据,可以通过读写分离缓解数据库压力。需要解决的问题:在业务上区分哪些业务上是允许一定时间延迟的,以及数据同步问题。
分区和分表的区别与联系
分区和分表的目的都是减少数据库的负担,提高表的增删改查效率。

分区只是一张表中的数据的存储位置发生改变,分表是将一张表分成多张表。
当访问量大,且表数据比较大时,两种方式可以互相配合使用。
当访问量不大,但表数据比较多时,可以只进行分区。

常见分区分表的规则策略(类似)
Range(范围)
Hash(哈希)

按照时间拆分
Hash之后按照分表个数取模
在认证库中保存数据库配置,就是建立一个DB,这个DB单独保存user_id到DB的映射关系

MR和Spark的区别


作者:woeM
链接:https://www.nowcoder.com/discuss/90723?type=post&order=time&pos=&page=1&channel=1009&source_id=search_post&subType=2
来源:牛客网

一面
1、三次握手
2、流控和拥塞控制
3、vector和list

strcpy和memcpy的区别

strcpy和memcpy主要有以下3方面的区别。

复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,如果空间不够,就会引起内存溢出。memcpy则是根据其第3个参数决定复制的长度
用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy,由于字符串是以“\0”结尾的,所以对于在数据中包含“\0”的数据只能用memcpy。
5、int几个字节,long几个字节,指针几个字节
int 4 4
long 4 4
long long 8 8
char 1 1
char* 4 4
float 4 4
double 8 8

4、nginx、apache执行cgi的过程
CGI其实本质上就是一个普通的二进制程序
因为apache本身可以运行cgi程序。但随着 nginx服务器的大规模应用,而恰好nginx又没有cgi模块,所以我们不得不采用一些变通的手段来解决它。
5、ipc、哪种最好

TCP在建立连接过程中会交换哪些信息

Windows size(滑动窗口):告诉对端我这边能接收多少数据,从而达到一定的流控的效果。
MSS(最大报文长度):每一个TCP报文段中数据字段的最大长度,注意:只是数据部分的字段,不包括TCP的头部。TCP在三次握手中,每一方都会通告其期望收到的MSS(MSS只出现在SYN数据包中)如果一方不接受另一方的MSS值则定位默认值536byte
WS(窗口因子 window scale):TCP发送端在发送一个满窗口长度(最大65535字节)的数据后必须等待对端的ACK更新窗口后才能继续发送数据。在广域网中传输数据时,由于往返时间较长,发送端等待的时间也会较长,这样会使得TCP数据交互的速度大大降低(长肥管道现象)。使用窗口扩大选项可以使得发送端得到更大的通告窗口,这样就可以在ACK到来前发送更多的数据,减少了等待的时间,提高了数据传输效率。
SACK_PERM:TCP收到乱序数据后,会将其放入乱序队列中,然后发送重复ACK给对端。对端收到多个重复的ACK后,就会推测到可能发生了数据丢失,再重传数据。如果乱序的数据比较零散,则这种机制的效率会很低。使用SACK选项可以告知发包方收到了哪些数据,发包方收到这些信息后就会知道哪些数据丢失,然后立即重传缺失部分即可。这就大大提高了数据重传的速度。

C++中extern的用法 外部声明

extern是一种“外部声明”的关键字,字面意思就是在此处声明某种变量或函数,在外部定义。
extern关键字的主要作用是扩大变量/函数的作用域,使得其它源文件和头文件可以复用同样的变量/函数,也起到类似“分块储存”的作用,划分代码。如图所示,在一个头文件里做了外部声明,就能把变量的定义部分和函数体的实现部分转移到其它地方了。
在这里插入图片描述
1.主函数include的box_manu.h已经include了Extra.h,如果再include一遍Extra.h。会报大量不兼容和重定义等错,应尽力避免重复引用头文件。

2.在extra.h里面声明的长宽高变量必须要在其它哪里给出定义,哪怕没有初值(例子中写到了box_make.cpp里面)。如果没有这么做,编译时会报LNK200“无法解析外部命令”的错

3.多个头文件或源文件使用同一些变量,尝试把extern去掉后编译就会报“重定义”的错

14、hash_map如何解决冲突,以及扩容
15、1亿个数字求前100个
16、课外还看过哪些东西
17、对腾讯音乐有什么了解吗

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值