字节跳动C++客户端开发实习生一面总结(2021-3-14)

C++

(1)C++编译的过程

① 预编译:预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令,包括宏定义指令、条件编译指令和头文件包含指令。
宏定义指令,如#define Name TokenString等。 预编译所要做的是将程序中的所有Name用TokenString替换。(宏替换)
条件编译指令,#ifdef,#ifndef,#else,#elif,#endif等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。
头文件包含指令,如#include <stdio.h>,#include "animal.h"等。 将所有的 include 语句替换为头文件的实际内容。
同时预处理过程还会删除注释行,添加行号和文件名标识,便于编译时编译器产生编译错误或警告时能够显示行号。(.i文件)

② 编译:编译是将文本文件转换成汇编代码的过程。(.s文件)

③ 汇编:汇编阶段是把编译阶段生成的汇编语言代码翻译成二进制目标代码(也就是二进制机器指令)。(.o文件)

④ 链接:将 .o文件生成 .exe执行文件。

  • 静态链接:在程序执行之前就完成链接工作,也就是等链接完成后文件才能执行。但是这有一个明显的缺点,比如说库函数,如果文件A 和文件B都需要用到某个库函数,链接完成后他们连接后的文件中都有这个库函数,当A和B同时执行时,内存中就存在该库函数的两份拷贝,这无疑浪费了存储空间。当规模扩大的时候,这种浪费尤为明显。静态链接还有不容易升级等缺点。为了解决这些问题,现在的很多程序都用动态链接。
  • 动态链接:和静态链接不一样,动态链接是在程序执行的时候才进行链接。也就是当程序加载执行的时候。还是上面的例子 ,如果A和B都用到了库函数 fun(),A和B执行的时候内存中就只需要有 fun() 的一个拷贝。

(2)#include <file.h>和 #include “file.h” 的区别

<>:表示系统自带的头文件,编译器先从系统目录下开始搜索,然后再搜索PATH环境变量所列出的目录。

" ": 表示用户自定义的头文件,编译器从当前目录搜索,然后再搜索系统目录和PATH环境变量所列出的目录。

(3)C++程序的内存分为哪几个区?

① 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

② 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

③ 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放 。

④ 文字常量区:存放字符串常量。 程序结束后由系统释放 。

⑤ 程序代码区:存放函数体的二进制代码。

【内存的分配方式有几种? 】
一、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量。

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

三、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

【堆和栈的区别】
在函数体中定义的变量通常是在栈上,用malloc, calloc, realloc等分配内存的函数以及运算符new分配得到的就是在堆上。
当一个函数调用完返回后它会释放该函数中所有的栈空间。栈是由编译器自动管理的。堆是动态分配内存的,并且你可以分配使用很大的内存。但是用不好会产生内存泄漏。并且频繁地malloc和free会产生内存碎片,因为分配动态内存是寻找匹配内存的。而用栈则不会产生碎片。
在栈上存取数据比通过指针在堆上存取数据快些。

(4)内存泄漏怎么产生的?

内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。

由于栈上的内存的分配和回收都是由编译器控制的,所以在栈上是不会发生内存泄露的,只会发生栈溢出(Stack Overflow),也就是分配的空间超过了规定的栈大小。

(5)如何避免内存泄漏?

① 不要手动管理内存,可以尝试在适用的情况下使用智能指针。

② 使用string而不是char* 。string类在内部处理所有内存管理,而且它速度快且优化得很好。

③ 除非要用旧的lib接口,否则不要使用原始指针。

④ 在C++中避免内存泄漏的最好方法是尽可能少地在程序级别上进行new和delete调用。任何需要动态内存的东西都应该隐藏在一个RAII对象中,当它超出范围时释放内存。RAII在构造函数中分配内存并在析构函数中释放内存,这样当变量离开当前范围时,内存就可以被释放。
(注:RAII资源获取即初始化,也就是说在构造函数中申请分配资源,在析构函数中释放资源)

⑤ 使用了内存分配的函数,要记得使用其想用的函数释放掉内存。可以始终在new和delete之间编写代码,通过new关键字分配内存,通过delete关键字取消分配内存。

⑥ 培养良好的编码习惯,在涉及内存的程序段中,检测内存是否发生泄漏。

(6)智能指针如何解决内存泄漏?

解决内存泄漏最有效的办法就是使用智能指针(Smart Pointer)。使用智能指针就不用担心这个问题了,因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放,这样就不用担心内存泄漏的问题了。

C++里面的四个智能指针: ① auto_ptr;② unique_ptr;③ shared_ptr;④ weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用,包含在头文件 <memory> 中

智能指针指向NULL,那么保存的东西就会释放,因为它是智能的,指向了一个新对象、新东西,会释放原来的内存去存新的东西,那么指向空就相当于析构了,普通指针不行,不会自己析构指向的东西(即保存的东西)则可能会导致内存泄漏。
智能指针则可以在退出作用域时,不管是正常离开或是因异常离开,总调用delete来析构在堆栈上动态分配的对象。

智能指针(smart pointer)的通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。每次创建类的新对象时,初始化指针就将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。

传指针相当于新建了一个指针,但是仍然指向那个地址,那个内存。传引用相当于把内存直接传过去。传指针引用,相当于指针没变,仍然指向那个地址,那个内存。

(7) 野指针

野指针,也就是指向不可用内存区域的指针。如果对野指针进行操作,将会使程序发生不可预知的错误,甚至可能直接引起崩溃。 野指针不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是野指针是很危险的,也具有很强的掩蔽性,if语句对它不起作用。

造成野指针的常见原因有三种:

1、指针变量没有被初始化。

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

char *p; //此时p为野指针

2、指针指向的内存被释放了,而指针本身没有置NULL。

对于堆内存操作,我们分配了一些空间(使用malloc函数、calloc函数或new操作符),使用完后释放(使用free函数或delete操作符)。指针指向的内存被释放了,而指针本身没有置NULL。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用。因为即便p不是NULL指针,它也不指向合法的内存块。所以在指针指向的内存被释放后,应该将指针置为NULL。

char *p=new char[10];  //指向堆中分配的内存首地址,p存储在栈区
cin>> p;
delete []p; //p重新变为野指针

3 、指针超过了变量的作用范围。

即在变量的作用范围之外使用了指向变量地址的指针。这一般发生在将调用函数中的局部变量的地址传出来引起的。这点容易被忽略,虽然代码是很可能可以执行无误,然而却是极其危险的。局部变量的作用范围虽然已经结束,内存已经被释放,然而地址值仍是可用的,不过随时都可能被内存管理分配给其他变量。

char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
cout<<*(p+10); //可能输出未知数据

代码题

(1)不调用C++/C的字符串库函数,请编写函数 strcpy

char *strcpy(char *strDest, const char *strSrc)
{
	assert((strDest != NULL) && (strSrc != NULL));
	char *address = strDest;
	while((*strDest++=*strSrc++)!='\0')
	NULL;
	return address;
}

在这里插入图片描述
参考自《程序员面试宝典(第5版)》

assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。

#include <iostream>
#include <malloc.h>
#include <assert.h>
#include <string>
using namespace std;

void stringcpy(char* des, const char* src) {
	assert(des != NULL && src != NULL); //若des和src为空则结束程序
	while (*src != '\0') {
		*des++ = *src++; //先赋值再各自自增
	}
	*des = '\0'; //补上结束符,否则会乱码
}

int main() {
	char* src;
	char* des = new char[15]; //此时指针变量src和des为野指针,因为初始化时没有赋值或设置为NULL,但此处不需要
	src = (char*)malloc(15); //使用malloc向堆动态申请内存
	 //由于malloc返回类型为(void*),故用(char*)强制转换类型

	stringcpy(src, "abcdefg");
	stringcpy(des, src);
	cout << des << ends << sizeof(des) << ends << strlen(des) << endl;
	//sizeof()是一种内存容量度量函数,功能是返回一个变量或者类型的大小(以字节为单位)
	//strlen()截取'\0'之前的字符串长度
	free(src); //若已不再使用,需要手动释放由malloc申请的内存,防止内存泄漏
	src = NULL; //指针指向的内存被释放了,需要将该指针设置为NULL,否则会成为野指针
	delete[]des;
	des = NULL;

	system("pause");
	return 0;
}

调试结果为:
在这里插入图片描述
(2)给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例1:
输入:[10, 9, 2, 5, 3, 7, 101, 18]
输出:4
解释:最长的上升子序列是 [2, 3, 7, 101],它的长度是4。
static int lengthOfLIS(int[ ] nums)

示例2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

LeetCode #300. 最长递增子序列
https://leetcode-cn.com/problems/longest-increasing-subsequence/

动态规划
在这里插入图片描述

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) {
            return 0;
        }
        vector<int> dp(n, 0);
//创建额外的数组空间,用于记录每个数组元素对应最长递增子序列的长度
        for (int i = 0; i < n; ++i) {
            dp[i] = 1; //对元素自身而言,序列长度为1
            //往前回溯
            for (int j = 0; j < i; ++j) {
                if (nums[j] < nums[i]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};

复杂度分析
在这里插入图片描述

计算机网络原理

(1)http协议

HTTP(超文本传输协议,应用层,定义浏览器如何向万维网服务器请求万维网文档,使用 TCP)
HTML(超文本标记语言,制作万维网页面的标准语言)

协议功能

HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传输协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。

HTTP是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。在Internet上的Web服务器上存放的都是超文本信息,客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成。

我们在浏览器的地址栏里输入的网站地址叫做 URL (Uniform Resource Locator,统一资源定位符)。就像每家每户都有一个门牌地址一样,每个网页也都有一个Internet地址。当你在浏览器的地址框中输入一个URL或是单击一个超级链接时,URL就确定了要浏览的地址。浏览器通过超文本传输协议(HTTP),将Web服务器上站点的网页代码提取出来,并翻译成漂亮的网页。

工作过程(重点)

① HTTP客户端发起请求,创建端口。
② HTTP服务器在端口监听客户端请求。
③ HTTP服务器向客户端返回状态和内容。

→ 以谷歌浏览器,访问博客(www.mshanzi.com)为例

1、输入地址后回车,首先执行域名解析。

① Chrome搜索自身的DNS缓存,看有没有对应该域名的IP地址,这个缓存的时间只有一分钟。
② 如果在浏览器没有找到缓存或者缓存已经失效,则搜索操作系统自身的DNS缓存。
③ 如果在操作系统中也没有找到缓存或者缓存已经失效,则读取本地的host文件。
④ 如果在host文件中找不到对应的配置,浏览器则发起一个DNS的系统调用。
⑤ 主机向本地域名服务器(宽带运营商服务器)发出查询(主机向本地域名服务器的查询一般是递归查询)。
( 域名系统(Domain Name System,DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。)

2、连接并使用TCP进行通信。

① 浏览器获得域名对应的IP地址后,发起HTTP”三次握手”。主机发送TCP连接请求,这个请求经过层层路由,TCP/IP协议栈,最后到达web服务器端。
② TCP/IP连接建立起来后,浏览器就可以向服务器发送http请求了,例如请求 mshanzi.com 的资源。
③ 服务器接收到这个请求,根据路径参数,经过后端的一些处理之后,把处理后的一个结果的数据返回给浏览器,如果是博客页面,就会把完整的html页面代码返回给浏览器。
④ 浏览器拿到了博客的完整html页面代码,在解析和渲染这个页面的时候,里面的 js,css,图片静态资源,他们同样是一个个http请求都需要经过商民的主要步骤。

3、浏览器渲染页面。

① 拿到页面的资源后,浏览器首先进行HTML解析,创建DOM tree,树的根部是“document”对象。
② 解析CSS,创建CSSOM。
③ 基于DOM和CSSOM执行脚本。
④ 合并DOM和CSSOM创建render tree。
⑤ 然后进行布局(为每个render tree中的每个节点分配一个应该出现在屏幕上的确切坐标)、绘制(遍历render tree,将每个节点绘制出来)和合并图层。

http协议的版本

HTTP/1.0,发送请求,创建一次连接,获得一个web资源,连接断开
HTTP/1.1,发送请求,创建一次连接,获得多个web资源,连接断开

参考:
Http协议详解(深入理解)
HTTP工作过程

(2)网址的构成

网址,即URL

① 网络文件地址

<协议>://<服务器名称>.<域名>/<目录>/<文件名>

如:http://www.cnnic.net.cn/develst/cnnic200101.shtml
其中,“: //”之前部分指的是协议,常用的协议有http(www协议)、ftp(文件传输协议)、telnet(远程传输协议)、news(新闻组协议)、file(用户计算机中的文件)等;“www"是指服务器类型;“ cnnic. net. cn” 为域名;” revels"为文件的目录路径,如有多层路径,则分别用“ / ”分隔;“cnic200101.shtml”为文件名。

② 电子邮件地址

<用户名>@<电子邮件服务器域名>

如:zzz @yahoo.com.cn表示为一个电子邮件地址。其中:zzz是电子邮件用户名,用户名是由用户在申请电子邮箱时自己确定的。yahoo. com. cn是电子邮件服务器地址,邮件服务器地址通常是采用该主机的域名地址。

③ 通用网址

(3)osi模型以及传输层有哪些协议?

OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。

每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。
在这里插入图片描述
在这里插入图片描述
(4)TCP的传输及拥塞控制

① TCP三次握手建立连接

连接建立(三报文握手):客户、服务器处于 CLOSED(关闭)→服务器被动打开,进入 LISTEN(收听)→客户主动打开,发送连接请求报文段(SYN=1,选择初始序号 seq=x;不可携带数据,但消耗一个序号),进入 SYN-SENT(同步已发送)→服务器发送确认报文段(SYN=ACK=1,ack=x+1,seq=y;不可携带数据,但消耗一个序号),进入 SYN-RCVD(同步确认)→客户发送确认报文段(ACK=1,ack=y+1,seq=x+1;可以携带数据,携带则消耗序号),进入 ESTABLISHED(已建立连接)→服务器进入 ESTABLISHED。

最后一次确认可防止“已失效的连接请求报文段”突然传送到服务器,从而产生错误。

② TCP四次挥手释放连接

连接释放(四报文握手):客户、服务器处于 ESTABLISHED→客户发送连接释放报文段(FIN=1,seq=u,为已传送数据的最后一个字节的序号+1;可携带数据,必定消耗一个序号),进入 FIN-WAIT-1(终止等待 1)→服务器发送确认报文段(ACK=1,ack=u+1,seq=v,为已传送数据的最后一个字节的序号+1),进入 CLOSE-WAIT(关闭等待)(客户→服务器方向的连接释放,TCP 连接半关闭)→客户进入 FIN-WAIT-2(终止等待 2)→……(服务器→客户方向数据传送)→服务器发送连接释放报文段(FIN=ACK=1,ack=u+1,seq=w),进入 LAST-ACK(最后确认)→客户发送确认报文段(ACK=1,ack=w+1,seq=u+1),进入 TIME-WAIT(时间等待)→服务器进入CLOSED(关闭),客户经过时间等待计时器设置时间 2MSL(最长报文段寿命)后进入 CLOSED(关闭)。

等待 2MSL 是为了保证最后一个确认报文段能够到达服务器,以及防止“已失效的连接请求报文段”出现在下一个连接中。
在这里插入图片描述
③ TCP可靠传输

TCP的可靠性依靠以下7种机制实现:

(1)校验和:检验范围包括TCP首部和数据部分,发送方将整个报文段以字(16位)为单位分开,并将所有段进行反码相加,结果存放在校验和字段中,接收方用同样的方法计算,若最终结果全为1则正确,否则存在错误。

(2)序列号:TCP对每个字节的数据都进行编号。序列号作用有:
a.保证数据不丢失
b.保证数据按序到达
c.提高效率,累计确认
d.去除重复数据

(3)确认应答机制(ACK):TCP首部有标志位ACK,当ACK=1时表示确认号有效。接收方对于按序到达的数据会进行确认,发送方如果收到已发送数据的确认报文,则继续传输下一部分数据,否则等待一段时间后启动重传机制。

(4)超时重传机制:当数据在发送后一定时间内未收到接收方的确认,发送方就会进行重传。报文段发出到收到应答中间有一个报文段的往返时间RTT,超时重传时间RTO会略大于RTT,TCP会根据网络情况动态计算RTT,因此RTO也是不断变化的,Linux中RTO呈指数递增,重传一定次数后关闭连接。

(5)连接管理机制:即上面提到的三次握手和四次挥手。

(6)流量控制:根据接收端的处理能力,来决定发送端的发送速度。TCP报文段首部有一个16位的窗口大小字段,该字段大小随着传输情况而变化,如果缓冲区满,就将窗口置为0,发送方收到后就不在发送数据,但定期会发送一个窗口探测报文,使接收方把窗口大小告诉发送方。

(7)拥塞控制:为了防止过多数据注入网络中造成网络拥堵。主要有四种算法:慢开始、拥塞避免、快重传、快恢复。

a.慢开始和拥塞避免
发送方维护一个拥塞窗口cwnd,大小等于发送窗口,慢开始的思想是一开始发送方发送一个字节,在收到接收方的确认后,将拥塞窗口大小翻倍,直到拥塞窗口大小达到慢开始门限ssthresh,停止慢开始算法,采用拥塞避免算法,拥塞窗口由指数增长变为线性增长,当网络拥塞,发生超时重传时,慢开始门限变为当前拥塞窗口大小的一半,拥塞窗口大小置1,重新开始慢启动算法。

b.快重传和快恢复
当发送方收到3个连续的重复确认,可能不是拥塞而是报文丢失,发送方执行快重传算法,对相应报文立即重传而不是等待超时重传,同时执行快恢复算法,发送方将慢开始门限和拥塞窗口调整为当前拥塞窗口大小的一半,然后执行拥塞避免算法。

数据结构

(1)B树和B+树的区别

B+树相对于B树的优势:

  • 单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了。
  • 所有的查询都要查找到叶子节点,查询性能是稳定的,而B树,每个节点都可以查找到数据,所以不稳定。
  • 所有的叶子节点形成了一个有序链表,更加便于查找。

(2)B树和B+树查找的时间复杂度

B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)

B+树内节点不存储数据,所有数据存储在叶节点导致查询时间复杂度固定为O(log n)

数据库

(1)为什么数据库要用到B+树?

① B+树的磁盘读写代价更低
B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

② B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

③ 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

(2) 数据库索引B+树结点大小一般设计为几个字节?

  • B+树的中间节点存的是索引,不存储数据,数据都保存在叶子节点中,而B树的所有节点都能存放数据。所以B+树磁盘读写的代价比B树低,因为中间节点不放数据,所以相同的磁盘块能存放更多的节点,一次性读入内存的节点数量也就越多,所以IO读写次数就降低了。
  • B+树的叶子节点位于同一层,数据也都位于叶子节点中,所以每次查找都是从根节点找到叶子节点,效率很稳定。而B树在查到关键字后就停止查找了,效率不够稳定。
  • B+树的叶子节点还按照大小,通过链表有序的串联在一起,在进行遍历查询时,只需要遍历这个链表即可,而且还支持范围查询,查到范围的开始节点,然后往后遍历即可实现。而B树没有这样的链表,只能通过中序遍历来查找数据,不支持范围查询。

B+树一个节点的大小设为一页或页的倍数最为合适。因为如果一个节点的大小 < 1页,那么读取这个节点的时候其实读取的还是一页,这样就造成了资源的浪费。

在 MySQL 中 B+ 树的一个节点大小为“1页”,也就是16k(1KB=1024字节,1字节=8位)。之所以设置为一页,是因为对于大部分业务,一页就足够了。

在InnoDB的B+树中,非叶子节点存的是key + 指针,叶子节点存的是数据行。
对于叶子节点,如果一行数据大小为1k,那么一页就能存16条数据;对于非叶子节点,如果key使用的是bigint,则为8字节,指针在mysql中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170 个索引指针。于是可以算出,对于一颗高度为2的B+树,根节点存储索引指针节点,那么它有1170个叶子节点存储数据,每个叶子节点可以存储16条数据,一共 1170 x 16 = 18720 条数据。而对于高度为3的B+树,就可以存放 1170 x 1170 x 16 = 21902400 条数据(两千多万条数据),也就是对于两千多万条的数据,我们只需要高度为3的B+树就可以完成,通过主键查询只需要3次IO操作就能查到对应数据。所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储,所以一个节点为1页,也就是16k是比较合理的。

(3)数据库事务

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

性质:
1、原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
2、一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。
3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

事务的ACID特性是由关系数据库系统(DBMS)来实现的,DBMS采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所作的更新,如果某个事务在执行过程中发生错误,就可以根据日志撤销事务对数据库已做的更新,使得数据库回滚到执行事务前的初始状态。
对于事务的隔离性,DBMS是采用锁机制来实现的。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。

作用:
一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:
1、为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
2、当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

数据库事务模型:
1、显式事务
显式事务又称自定义事务,是指用显式的方式定义其开始和结束的事务,当使用start transaction和 commit语句时则表示发生显式事务。
2、隐式事务
隐式事务是指每一条数据操作语句都自动地成为一个事务,事务的开始是隐式的,事务的结束有明确的标记。即当用户进行数据操作时,系统自动开启一个事务,事务的结束则需手动调用 commit或 rollback语句来结束当前事务,在当前事务结束后又自动开启一个新事务。
3、自动事务
自动事务是指能够自动开启事务并且能够自动结束事务。在事务执行过程中,如果没有出现异常,事务则自动提交;当执行过程产生错误时,则事务自动回滚。

优点:
以事务的方式对数据库进行访问,有如下的优点:
1、把逻辑相关的操作分成了一个组;
2、在数据永久改变前,可以预览数据变化;
3、能够保证数据的读一致性。

操作系统原理

虚拟内存
虚拟内存与物理内存的对应

详见:
虚拟内存与物理内存的联系与区别
虚拟内存与物理内存的对应

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值