腾讯面试题总结

1. 在有继承关系的父子类中,构建和析构一个子类对象时候,父子构造函数和析构函数的执行顺序分别是怎怎样的?

答:构造函数的调用分为以下两种情况。

1)单继承机制下,先调用基类构造函数,然后在调用派生类中定义的子对象的构造函数(子对象的调用顺序按照其在派生类中的声明顺序),最后调用派生类的构造函数。

2)多继承机制下,先调用多个基类的构造函数(基类构造函数的调用顺序按照派生类定义时候锁指定的顺序),然后在调用派生类中定义的子对象的构造函数(子对象的调用顺序按照其在派生类中的声明顺序), 最后在调用派生类的构造函数。

3)菱形继承机制下,如果派生类有一个虚拟类作为祖先类,先调用虚基类构造函数,在调用非虚基类的构造函数,然后在调用子对象的构造函数,然后在调用派生类的构造函数。

析构函数的调用过程如下:先调用派生类的析构函数->在调用派生类中定义的子对象的析构函数->在调用非基类析构函数->在调用虚基类的析构函数

2. 在有继承关系的父子类中,父类的构造函数和析构函数一定要申请为virtual吗?如果不申请为virtual会怎么呢?

答:1)构造函数不能申明为virtual。从存储空间角度看,虚函数是通过vtable,可是这个vtable实际上是存储在对象空间上的,如果构造函数是虚函数,也就是说需要通过vtable访问构造函数,可是构造函数调用之前对象还未实例化,vtable还不存在,所已无法找到构造函数,所以规定构造函数是非virtual。

2)析构函数一定要声明为virtual:是因为如果类函数不定义为virtual,那么就会发生静态绑定,当基类指针指向派生类对象的时候,调用析构函数的时候,就会根据指针的类型调用基类的析构函数,那么派生类析构函数就无法调用,会造成内存泄露等问题,所以析构函数要定义为virtual。

3. 什么是C++多态?C++多态的实现原理是什么?

答:1)C++中的多态是指向不同的对象发送同一个消息,不同的对象产生的不相同的行为,即“一个接口复用,多种方法实现”。

2)C++中的多态分为静态多态和动态多态,其中静态的多态依靠的是函数重载和函数模板实现,动态多态依赖虚函数实现。

3)运行期多态实现原理:对于一个继承体系来说,如果基类定义了virtual函数,派生类中重写该函数,那么运行时候会根据对象的实际类型来调用相应的函数。如果对象是基类对象,则调用的就是基类函数,如果对象是派生类对象,则调用派生类对象中的函数。运行期多态是通过虚函数和虚函数表来实现的。vtable中存放的是虚函数的地址,子类没有覆写父类的虚函数,那么子类对象的虚表指针中的虚函数地址和父类中是相同的,如果子类覆写了虚函数,那么子类的虚表中对应的函数地址就会是子类覆写后的,就是因为这种关系才可以实现运行期动态绑定。

4. 什么是虚函数?虚函数的实现原理是什么/

答:直观上来说,虚函数就是类中使用 virtual 关键字描述的函数。虚函数的作用主要是实现了多态的机制,基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法,否则将会形成覆盖。虚函数的底层实现机制基于虚函数表+虚表指针。 编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中,虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。

5. 什么是虚表?虚表的内存布局结构布局如何?虚表的第一项是什么?

答:1)对于每个存在虚函数的类来说,其实例化后都存在一个虚表和一个虚表指针,虚表用于存在虚函数的地址,虚表指针指向虚表。2)多重继承时候,每个父类都有自己的虚表和虚表指针,且子类新增的虚拟成员函数被放到了第一个父类的表中。

3)多重继承机制下,父类虚函数有被覆写的时候,父类虚表中对应的位子将被设置为覆写后的函数地址,子类新加的虚函数地址将被添加到第一个父类的虚函数表的末尾。

6. 菱形继承体系下(类D继承B和C, B和C继承A),虚表在各个类中的部署如何?如果类B和C同时有一个成员变了m,m如何在D的对象的内存地址上分布的?是否会相互覆盖?

答:虚表会同时存在两个A,内存分布与多继承一致,即存在两个虚指针指向两个父类虚表(B, C),B和C的虚表中又同时存在A的虚表。虚表内存模型如下。

7. 统一的类成员初始化语法与std::initializer_list

答:1)在C++标准中,可以使用花括号对任意类型的变量进行初始化,

class A{

public:

bool ma{true};

int mb{3};

string mc{"gggg"};

};

8. 注解标签

答:c++17提供了三个使用的注解标签

1)[[fallthrough]]:用于 switch-case语句中,在某个case执行完毕后如果没有break语句,则编译器可能会给出警告,但这可能是开发者有意为之的,为了让编译器知道开发者的意图,可以在需要某个case分支被“贯穿”的地方显式设置[[fallthrough]]标记。

2)[[nodiscard]]:[[nodiscard]]一般用于修饰函数,告诉函数调用者必须关注该函数的返回值(即不能丢弃该函数的返回值)。如果函数调用者未将该函数的返回值赋给一个变量,则编译器会给出一个警告。

3)[[maybe_unused]]:用于描述暂时没有被使用的函数或变量,以避免编译器对此发出告警。

9. final/oeverride/=default/=delete语法

答:1)final、override/=default/=delete是C++11添加的一组非常重要的语法。2)final修饰类,写在类定义的后面,表示该类不能在被继承了。3))virtual用在类函数的定义中,是在函数定义的前面,表明当前函数是虚函数,子类可以覆写。不管子类是不是覆写,在子类中函数前面都可以不加virtual,但实际上函数是virtual。4)override修饰子类覆写父类的方法,放在方法的后面,表示该方式是覆写父类的。5)=default用于修饰构造函数,析构函数,拷贝构造/赋值函数,用于高速编译器使用默认生成的函数体。6)=delete关键字用于修饰构造函数、析构函数、拷贝构造函数、operator =的语法,禁止编译器为这些函数生成默认函数体。

10. auto关键字

答:auto关键字用于修饰变量,并且说明变量的类型是自动推导出来的。主要用于替代类型冗长,变量使用范围专一的变量声明。

11. Range-based循环语法

答:C++11引入了基于范围的循环编译写法,如下所示。基于范围的for循环遍历是只读遍历,除非将变量的类型声明为引用类型。

std::vector<int> vec{1,2,3,4,5,6,7,8,9};

for (auto n : vec) {

std::cout << n;

}

12. 结构化绑定

答:C++ 17的新特性,适当的使用结构化绑定可以及大地提升编程体验。结构化绑定将引入的标识符绑定到对象的元素/成员上。

1)绑定到数组的元素

int arr[3] = {1,2,3};

auto [a, b, c] = arr;

2)绑定答数据成员

B b(114,514);

auto [x, y] = b;

13.STL容器新增的实用方法

答:1)C++11中对于std::list和vector的“原位构造元素(Emplace Constructible)”

push/insert

emplace

在容器指定位置原位构造元素

push_front

emplace_front

在容器首部原位构造元素

push_back

emplace_back

在容器尾部原位构造元素

2)std::map的try_emplace和inert_or_assign方法,其中insert_or_assign方法不存在插入,存在更新的意思。

14. std::thread

答:1)C++ 11引入了std::thread,用于多线程编程。#include <thread>。std::thread库可以看作是对不同平台的多线程API的一层包装类,因此使用std::thread编写的代码都是跨平台的。

2)C++11 有两种方式等待线程结束:

11)detach方式,启动的线程自主在后台运行,当前的代码继续往西爱之心,不等待新线程结束,调用detach之后,表示新建的线程与创建的线程分离,分离之后的线程不在受约束和管制,会单独执行,知道执行完毕释放资源,可以看作是一个daemon线程。

22)join方式,等待启动的线程完成,才会继续往下执行,只有处于活动状态的线程才能调用join,可以通过joinable()来检查,joinable()==true表示当前线程是活动线程,才可以调用join函数,默认构造函数创建的对象是joinable() == false;join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;调用 ternimate()的线程必须是 joinable() == false;如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

3)线程是可以move的但是不可以是拷贝的。

4)可以通过thread的实例调用get_id()获取线程编号。

5)在当前线程上通过this_thread::get_id()获取线程编号。

15. 线程局部存储thread_local

答:thread_local是C++ 11引入的一个关键词,它会影响变量的存储周期,表示变量拥有线程存储周期,也就是对象的存储在线程开始时候分配,而在线程结束时候分解。

16. 线程同步原语std::mutex(互斥锁),std::condition_variable(条件变量)和semaphone(信号量)

答:1)std::mutex是最简单的线程同步原语,用来防止多个线程并发的访问临界区资源。如果应用中一个资源需要并发的被多个线程访问,那么就使用mutex来保护这些资源。

2)std::condition_variable条件变量用于实现多线程间的条件变量等待和通知机制,条件变量的发生于某个变量的状态改变有关,在多线程编程中,条件变量通常和胡钗所一起使用,以避免死锁问题。

3)semaphone(信号量)同条件变量。

17. 原子操作类

答:原子操作类是指在多线程程序中最小的不可并行的操作,意味着多个线程访问统一资源时候,有且仅有一个线程能够对资源进行操作。

原子类型名称 对应内置类型

atomic_bool bool

atomic_char atomic_char

atomic_char signed char

atomic_uchar unsigned char

atomic_short short

atomic_ushort unsigned short

atomic_int int

atomic_uint unsigned int

atomic_long long

atomic_ulong unsigned long

atomic_llong long long

atomic_ullong unsigned long long

atomic_ullong unsigned long long

atomic_char16_t char16_t

atomic_char32_t char32_t

atomic_wchar_t wchar_t

18. 智能指针类

答:1)智能指针是帮助我们协助管理动态分配的内存的,在智能指针析构的时候会自动释放new的内存空间,从而避免内存泄露。

2)C++98提出的auto_ptr智能指针,C++11 提出的unique_ptr<T>, shared_ptr<T>,weak_ptr<T>,智能指针模板类重载了*和->云算法,所以智能指针可以像普通指针一样的操作。

3)指针指针的常用函数有get()获取托管的普通指针值,release()取消指针指针对普通指针的托管,reset()重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉。

4)auto_ptr<T>智能指针已经被废弃掉了,是因为该指针在赋值或者复制的时候会发生所有权的转移。

5)unique_ptr<T>无法进行左值赋值或者复制操作,但允许临时右值赋值构造或者赋值。并且在STL容器中使用unique_ptr,不允许使用赋值操作,支持数组对象的内存管理

19. std::bind/std::function

答:1)std::bind和std::function是C++ 11的特性。

2)C++中可调用对象有以下几种定义:是一个函数指针,是一个具有operator()成员函数的类对象,可被转换成函数指针的类对象,一个类的成员函数指针。C++中可调用对象虽然都有一个比较 统一的操作形式,但是定义是五花八门的,这样就导致使用统一的形式保存可调用对象或者传递可调用对象时候,会十分繁琐,std::bind和std::function就是对可调用对象的操作。

3)std::function是一个可调用对象的包装器,是一个类模板,可以包装除了函数指针以外的所有可调用对象,他们可以用统一的方式处理函数、函数对象。特别适合作为回调函数,比普通指针更加灵活和便利。

4)std::bind可以看作是一个通用的函数适配器,可接受一个可调用对象,生成一个新的可调用对象来适用原对象的参数列表。

20. 快速排序、冒泡排序、归并排序、桶排序

答:1)快速排序:是一种基于分支策略的排序算法,运行高效,应用广泛。快速排序的核心思想是“哨兵划分”,其目标是,选择数组中的某个元素作为哨兵,将所有小于基准元素的元素移动到左值,大于基准元素的移到到右侧。时间复杂度为o(nlogn),空间复杂度为o(n)。2)冒泡排序是基于相邻元素的比较,通过连续地交换相邻元素实现排序,这个过程就像气泡从底部升到顶部的过程。时间复杂度是o(n2),空间复杂度是o(1),是稳定的排序算法。3)归并排序是一种分支的策略,先通过划分,然后在合并阶段。时间复杂度o(nlogn),空间复杂度是o(n),是稳定排序算法。4)桶排序:它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。时间复杂度为o(n+k),空间复杂度为o(n)。

https://www.hello-algo.com/chapter_hello_algo/

21. 二分查找

答:二分查找(binary search)是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。

int binarySearch(vector<int> &sums, int tatget)

{

int i = 0; j = nums.size() - 1;

while (i <= j) {

int m = (i + j) / 2;

if (nums[m] == tatget) {

return m;

} else if (nums[m] < target) {

i = m + 1;

} else {

j = m - 1;

}

}

return -1;

}

时间复杂度是o(logn),空间复杂度是o(1)

22. 链表

答:链表是一种线性的数据结构,其中的每个元素除了放置自身的数据外,还需要额外保存一个指针,用于指向下一个元素。。链表的设计为了利用不连续的内存空间。

23. 队列与栈

答:1)队列是一种遵循先进先出规则的线性数据结构,模拟排队现象,即新来的不断加入队列的尾部,而位于队列头部的人总是先被服务。

2)栈是一种遵循先进先出的线性数据结构,

24. 红黑树的特征、左旋和右旋以及染色

答:红黑树是一种自平衡的二叉查找树,红黑树的特点。1)根节点必须是黑色。2)叶子节点必须全部是黑色。3)从根节点到叶子节点,红节点和黑节点交替。4)从根节点到叶子节点路上上的所有黑色节点个数相同。5)每个节点要么是红色要么是黑色。

25.  mutex、semphore、condition_variable、read-write-lock 等操作系统API

答:Linux下提供了多种方式来处理线程同步,最常用的是互斥锁、信号变量和信号量。

1)互斥锁:锁机制是允许一个时刻只有一个线程执行一个关键部分的代码。普通锁、嵌套锁、检错锁、适应锁。

2)条件变量是利用线程间贡献变量进行同步的一种机制,条件变量上的基本操作有,触发条件、等待条件,挂起线程直到其他线程触发条件。

3)信号量semphone,如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。

26. 熟悉基本 SQL 操作 包括增删改查(insert、delete、update、select语句),排序 order,条件查询(where 子语句),限制查询结果数量(LIMIT语句)等

答:1)内连接 inner join,它返回的是两个表中都存在的数据。,比如查询学生表和课程表中所有学生的成绩。

select students.name, scores.subject, scores.score from students inner join scores on students.id = scores.student_id;

2)左连接返回左表中的所有行,即使左边在右表中没有对应的行,那么相关字段置位null。

3)右连接右连接返回右表中的所有行,即使在左表中没有匹配的行。如果左表中没有匹配的行,结果集中对应的列将包含NULL值。

4)想要查询成绩高于平均分的学生姓名和成绩。

select students.name, scores.subject, scores.score from students inner join scores on students.id = scores.students.id where score > (select AVG(score) from scores);

5)假设表inoutinfo是车辆进出记录,查询同辆车的进出记录

select number as 车牌号,count(*) as 数量 from inoutinfo group by number

6)查询同辆车不同状态下的进出记录

select number as 车牌号,stutas as 状态,SUM(spend) as 速度 from inoutinfo group by number, status

27. 稍微高级一点的 SQL 操作(如 Group by,in,join,left join,多表联合查询,别名的使用,select 子语句等)

答:1)窗口函数支持在与当前行的一组上执行计算,可以按照指定窗口的定义进行聚合,排序和分析操作,这种计算方式可以提供更加灵活和精确的数据分析能力。

2)公共表达式

3)聚合函数

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

答:1)如何知道插叙语句使用了哪个索引,可通过explain select* from product where id = 1;这样就会输出这条语句的SQL执行计划,然后执行计划中的key就表示使用了哪个索引。如果key为null,就说明没有使用索引,那就是全表扫描,这种情况下的查询效率是最低的。

2)表空间文件是怎样组成的呢?通常是由段(Segment),区(extent),页(page),行(row)组成。InnoDB的数据是按照页为单位从磁盘读取的,也就是以页为单位,将其整体读入内存中。默认每个页的大小是16KB。每个区的大小是1MB,也就是相当于64个页了,在数据表数据量大的时候,为了减少随机I/O,为某个索引分配空间的时候可以按照区为单位,而不是页为单位了,这样就可以保证所有的索引在同一个区了。段可以分为索引段、数据段、回滚段。

3)InnoDB的行格式有哪些,分别是Redundant、Compact、Dynamic和 Compressed 行格式。其中Compact格式如下所示

记录的额外信息 + 记录的真实数据

4)索引就是数据的目录,是存储引擎我了快速获取数据的一种数据结构。InnoDB在创建数据库表时候,会根据不同的场景选择不同的列作为索引,如果有主键,使用主键作为聚簇索引的索引建,没有主键索引,就选择第一个没有NULL的列做为聚簇索引的索引建,上面两个都没有的情况下,自动生成一个递增的列做为聚簇索引的索引建。其他索引否属于辅助索引,也称为二级索引或者非聚簇索引,创建的主键索引和二级索引默认使用的都是B+Tree索引。

5)B+Tree的特点:是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引不存放数据,而且每个节点上的数据是按照主键的顺序存放的,每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包含了所有的索引值信息,并且叶子节点有两个指针,指向前一个节点和下一个叶子节点,形成一个双向链表。

6)B+Tree树的有点是可以将上万数据量的查询语句只需要3~4次查询就可以满足。

7)主键索引和二级索引的差别:主键索引的叶子节点存放的是完整的用户记录,而二级索引的叶子节点存放的主键的信息,不是实际数据。

8)什么时候需要建立索引:字段有唯一性限制的,比如商品编码;经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

9)什么时候不需要建立索引:WHERE 条件,GROUP BYORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。表数据太少的时候,不需要创建索引;经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。

10)有什么优化索引的方法:前缀索引优化;覆盖索引优化;主键索引最好是自增的;索引最好设置为 NOT NULL;防止索引失效

11)索引失效有哪些:对索引使用左或者左右模糊匹配;对索引使用函数;对索引进行表达式计算;对索引隐式类型转换;联合索引非最左匹配;WHERE 子句中的 OR。

29. 数据库本身的操作,建库建表,数据的导入导出

答:show variables like '%secure%'; 查询mysql默认数据导出目录,之后数据就导出到giant目录下,否则报错没有权限

select * into OUTFILE “C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\ data.sql” from h_dbf5963556c211e9a02a23106b3948fb_201908 limit 1000;

load data infile “C:\ProgramData\MySQL\MySQL Server 8.0\Uploads\ data.sql” into table h_dbf5963556c211e9a02a23106b3948fb_201908;

30. 数据库用户权限控制(权限机制)

答:主要的权限表有user db host table_priv columns_priv procs_priv

1)user表是在mysql库中,表中配置的所有权限都是全局的,适用所有数据库,该表有很多字段,大致可以分为4类(用户字段、权限字段、安全字段、资源控制字段)。

2)db表是MySQL当中的另一个非常重要的数据库表,用来存储某个用户对哪些数据库表存在哪些操作权限,决定用户能在哪个主机上操作哪个数据库表。

3)table_priv是单表权限设置,用来对单个表进行权限设置。tables_priv共有8个字段:host/db、user/table_name,grantor/timestamp/table_priv以及column_priv。

4)columns_priv 表用来对单个数据列进行权限设置。该表共有 7 个字段:hostdbusertable_namecolumn_nametimestamp 以及 column_priv。其中,column_name 用来指定对哪一列进行权限设置,其它字段和 tables_priv 表字段含义相同。

5)存储过程和存储函数权限procs_priv表:procs_priv 表可以对存储过程和存储函数进行权限设置。该表共有 8 个字段:host、db、user、routine_name、routine_type、grantor、proc_priv 以及 timestamp。

31. MySQL的两种数据库引擎的区别

答:1)InnoDB支持事务,MyISAM不支持;2)InnoDB 支持外键,MyISAM不支持;3)InnoDB 是聚集索引,MyISAM 是非聚集索引;4)InnoDB 不保存表的具体行数,MyISAM保存表的具体行数;5)InnoDB 最小的锁粒度是行锁,MyISAM是表锁

事务的特点:

1)原子性:一个事务中所做的操作是一体的,要么成功,要么失败,不能一半成功一半失败。->undo log

2)一致性:事务操作前后满组一致性条件,比如总量保持不变。->原子性+持久性+隔离性

3)隔离性:多个事务之前是相互隔离的,不影响的。->MVCC或者锁

4)持久性:事务所做的操作结构是持久化的,即便系统故障也不可丢失。->redo log

并行事务会引发什么问题?

1)脏读:如果一个事务读取到了两一个事务未提交但已修改的数据,这就意味着发生了脏读。

2)不可重复读:一个事务内,前后两次读取同一个数据,如果两次读取的结果不一致,就意味着发生了不可重读的现象。

3)幻读:在一个事务内多次查询某个条件的数据记录,如果出现前后两次查询的记录数量不一致的情况,就意味着发生了幻读。

SQL标准提出了四种隔离级别来避免事务并发所诱发的问题

1)读未提交:指一个事务还没提交时候,其所作的操作需要被其他事务看到。

2)读已提交:指一个事务只有提交后,其所作的操作才能被看到。

3)可重复读:指一个事务在操作期间看到的数据,一直跟这个事务启动时候看到的数据是一致的,MySQL InnoDB的默认隔离机制。

4)串行化:会对记录加上读写锁,在多个事务对这条记录进行读写操作时候,如果发生了写冲突的时候,后访问的事务就会被阻塞,需要等待前一个事务完成之后,后续的事务才能继续执行。

Read View实现可重复和读已提交的事务隔离级别:

1)Read View相当于一个数据快照,就像照相机那样,定格某一刻的风景。

2)读提交隔离级别:是在每个语句执行前生成一个 Read View,而可重复读是在事务启动时候生成的一个Read View,然后整个事务期间都在用这个Read View.

3)Read View:m_ids min_trx_id max_trx_id creator_trx_id。如下图所示,聚簇索引中的记录中包含两个隐藏列,trx_id(当一个事务对该聚簇索引记录修改时候,就会把该事务id放入到trx_id隐藏列中), roll_pointer是undo日志的指针

如下所示,trx_id中的数据可以分为以下

已提交事务(min_trx_id)已启动但未提交的事务(m_ids)max_trx_id(还没有开始的事务)。

当一个事务去访问记录的时候,除了自己的更新记录总是可见外,还有这几种情况:

1)如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见

2)如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见

3)如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:如果记录的 trx_id  m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见

通过这种版本链来控制并发事务访问同一个记录的行为就叫 MVCC(多版本并发控制)。

M有SQ锁机制

1)全局锁:

2)表级锁:表锁,元数据锁(自动添加),意向锁的目的是为了快速判断表里是否有记录被加锁。AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。

3)行级锁:Record Lock、Gap Lock Next-key Lock

MySQL为什么需要Buffer Pool?

1)InnoDB引擎设计了一个缓冲池,来提高数据库的读写性能。

2)有了Buffer Pool之后,当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

3)在M有SQL启动的时候,InnoDB会为BufferPool申请一块连续的内存空间,然后按照16KB大小分割成页,与数据库磁盘数据块大小相等,Buffer Pool中的页就叫做缓存页。之后随着程序的运行,才会从磁盘上的页加载数据到缓存页中。所以,MySQL 刚启动的时候,你会观察到使用的虚拟内存空间很大,而使用到的物理内存空间却很小,这是因为只有这些虚拟内存被访问后,操作系统才会触发缺页中断,申请物理内存,接着将虚拟地址和物理地址建立映射关系。Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。

为什么需要redo log?

1)为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了

后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上

2)redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。

  • redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
  • undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;

3)缓存在 redo log buffer 里的 redo log 还是在内存中,它什么时候刷新到磁盘?

主要有下面几个时机:

  • MySQL 正常关闭时;
  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制,下面会说)。

4)binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

32. SQL 优化技巧

答:1)减少数据访问:设置合理的字段类型,启用压缩,通过索引访问等减少磁盘 IO。2)返回更少的数据:只返回需要的字段和数据分页处理,减少磁盘 IO 及网络 IO。3)减少交互次数:批量 DML 操作,函数存储等减少数据连接次数。4)减少服务器 CPU 开销:尽量减少数据库排序操作以及全表查询,减少 CPU 内存占用。5)利用更多资源:使用表分区,可以增加并行操作,更大限度利用 CPU 资源。

33. 高可用 MySQL、主从同步、读写分离、分表分库

答:

34. nagle 算法;

答:

35. keepalive 选项;

答:

36. Linger 选项

答:

37. 对于某一端出现大量 CLOSE_WAIT 或者 TIME_WAIT 如何解决;

答:

38. 通讯协议如何设计或如何解决数据包的粘包与分片问题;

答:

39. 心跳机制如何设计;(可能不会直接问问题本身,如问如何检查死链)

答:

40. 断线重连机制如何设计;

答:

41. 对 IO Multiplexing 技术的理解;

答:

42. 收发数据包正确的方式,收发缓冲区如何设计;

答:

43. 优雅关闭;

答:

44. 定时器如何设计;

答:

45. epoll 的实现原理。

答:

46. Redis 支持的基础数据类型、Redis的数据持久化、事务

答:1)Redis是一种基于内存的数据库,对数据的读写操作都是在内存中完成的,因此读写速度非常快的。

2)Redis提供了多种数据类型来支持不同的业务场景,比如字符串,hash,List,Set,Zset(有序集合),Bitmaps(位图),HyperLogLog(基数统计),GEO(地理信息),Stream(流),并且堆数据类型的操作都是原子性的,因为执行命令由单线程负责完成的,不存在并发竞争的问题。

3)此外,Redis还支持多种集群方法(主从复制模式,哨兵模式,切片模式,发布/订阅模式,内存淘汰机制,过期删除机制等待)。

4)Redis中数据类型底层的实现:

11)String:底层的数据结构是SDS(简单动态字符串)

22)List:底层数据结构是双向链表或者压缩列表实现的。

33)Hash:底层数据结构是由压缩列表或哈希表实现的

44)Set:底层数据结构是由哈希表或整数集合实现的。

55)ZSet:类型的底层数据结构是由压缩列表或跳表实现的。

Redis采用单线程为什么还那么快?

1)Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;

2)Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

3)Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

47. Redis 源码层面上,如 Redis 的网络通信模型、Redis 各种数据结构的实现等等。

答:

48. Redis 高可用技术、cluster、哨兵策略等

答:

49.

全部评论

相关推荐

点赞 评论 收藏
转发
7 29 评论
分享
牛客网
牛客企业服务