腾讯面试——血与泪的教训(二)
没想到啊,这个标题下的内容还能成为连续剧。
我终于明白我为什么找不到实习了,给我机会我不中用,我哭死,腾讯不愧是大公司,还给了我第四次面试机会,结果我还是没有把握住。有三分之一的问题没有回答出来,结果几乎全是之前就已经看过但是只是扫了一眼或者压根就只是躺在我的收藏夹里没动过。
还是借此机会,拾起来重新学吧。
今天的面试问题主要集中在两个方向,Go 语言基础的知识和数据库的一些底层内容。
Golang中哪些不能作为map类型的key?
在 Go 语言中,map
的 key
可以是任意使用 ==
或 !=
运算符进行比较的类型。这意味着一下类型可以作为 map
的键:
- 基本类型:
int
、float
、bool
、string
- 接口类型
- 指针类型
- 数组类型(数组中的元素必须是能作为键的类型)
然而,以下类型不能作为 map
的键:
slice
map
function
- 包含上述类型的结构体
这是因为 slice
、map
和 function
等类型的值不是固定的(它们在内存中的表示可能会改变),因此不能用于比较。例如,两个包含相同元素的 slice
在使用 ==
运算符进行比较时会产生编译错误,因此 slice
不能作为 map
的键。
这就是我被问得满头大汗的起始点,从这个问题开始,我从语无伦次变得胡言乱语。
Golang如何实现继承?
我们在学习 Go 语言的时候就知道,它并不是一个常规的面向对象的编程的语言,所以在学习 Go 语言以及看一些底层实现的时候,我都没有认真地看过任何相关的内容。就在前两天我还看到了一个相关的内容,但是就扫了一眼,没有任何意外,我没记住,然后开始胡言乱语。
对于 go 语言的继承,之前总是模模糊糊的分不清是什么。不知道如何通过何种方式来继承的。
那我们就先看看 Java 是怎么实现继承的吧,然后使用 Go 来完成同样的继承操作。
Java 操作
1 | // 动物类 |
1 | 输出结果入下: |
Go 语言实现继承
1 | package main |
1 | 输出结果: |
是不是有点懂了,但是这只是最基础的封装,现在,在其他语言中,我们都知道,大多数都是以 interface 的方式来进行封装的。那么我们用 interface ,以一种高级的方式改造上面的例子如下:
使用接口的方式进行封装一个方法
1 | package main |
输出结果:
1 | 我叫咪咪, 我能睡8分钟 |
通过如上的代码可以看到,
- 在 go 语言中,
type name struct{}
结构体 就相当于其他语言中的class
类的概念。 - 在其他语言中,方法是直接写在在 类 里面的,而在 go 语言中,我们对于该结构体,如果存在方法,比如猫咪存在睡觉的方法那么是以
func (结构体名) 方法名{}
,即func(c Cat) sleep{}
的方式来声明方法。 - 在 java 中, string + int = string,int 类型的值不需要类型转换,而在 go 语言中,string + int,如果想要一个字符串,则需要对 int 类型的值转换为 string 类型,然后才能拼接。
下面着这两句话一定要熟记:
- 结构体解决的是基本数据类型不能满足我们日常需要的问题。再简单点理解就是一个结构体就是一个 json 类型的 object。
- 接口是一种类型。是一种特殊的类型,它规定了不同结构体有哪些相同的行为,只是制定标准,而不实现标准。就好比自来水厂只规定水龙头的半径大小,而不去考虑谁生产水龙头,生产水龙头的厂家不管用什么材料,只需要按照自来水厂的标准制定就好。
其实我并不觉得这是继承相关的内容,在语法上我们没有显示地去声明某个类要去继承另外一个类,也不能使用该类去调用父类的函数,Go 语言继承说白了就是结构体内嵌,而且设计者的想法也是通过组合来代替掉复杂的继承机制。
Golang中两个变量值的交换方式?
先来看看各种方式的实现吧。
在 Go 语言中,可以使用以下四种方法来交换两个变量的值:
使用临时变量:这是最传统的方法,我们创建一个临时变量来保存一个变量的值,然后将另一个变量的值赋给第一个变量,最后将临时变量的值赋给第二个变量。
1
2
3
4
5
6var a int = 100
var b int = 200
var temp int
temp = a
a = b
b = temp使用多重赋值:Go 语言支持多重赋值,这使得我们可以在一行代码中交换两个变量的值。
1
a, b = b, a
使用指针:我们也可以使用指针来交换两个变量的值。在这种方法中,我们创建两个指针,分别指向两个变量,然后通过这两个指针来交换两个变量的值。
1
2
3
4
5
6func swap(a *int, b *int) {
*a, *b = *b, *a
}
a := 100
b := 200
swap(&a, &b)使用算术运算:我们还可以使用加法和减法(或者异或运算)来交换两个变量的值。但是这种方法可能会因为数值过大而导致溢出,所以在实际应用中并不常用。
1
2
3a = a + b
b = a - b // b = (a+b) - b = a
a = a - b // a = (a+b) - a = b
上面的四种方法虽然都能是实现我们想要的功能,但是我们实际上会经常应用的就是第二种方法。下面我们来看一下多重赋值的底层实现。
如何实现多重赋值?
我们来做一个小实验,看看四值交换的 golang 代码的汇编代码。
1 | func main() { |
汇编代码如下:
1 | $>dlv debug main.go |
很好理解了,就是编译器帮我们在栈上创建了一个临时变量 temp, 然后按顺序交换其他各个变量的值。
那么下面这种情况,会发生什么呢?
1 | a := 1 |
a 和 b 最终的值是多少?
看一下汇编代码就清楚了
1 | main.go:5 0x454b9b 48c744241801000000 mov qword ptr [rsp+0x18], 0x1 // a:=1 |
相当于
1 | aTemp = a |
这里两个值交换的操作的原理是将两个被赋值的变量的值,都存储在临时变量里,然后再用临时变量去赋值。所以这个例子赋值顺序对结果是无影响的,其结果仍然是 a = 2, b = 1。
不用再像 C 语言那样写交换函数再内联了,相当于把脏活丢给编译器干了。
这个问题我好像不是很明白面试官的意图,他说要问我这种赋值方式的实现原理,但是实现原理其实就是我写出的代码,难道是我想复杂了?
MySQL存储引擎
MySQL 中最常见的存储引擎有:InnoDB、MyISAM 和 MEMORY,其中 InnoDB 是 MySQL 5.1 之后默认的存储引擎,它支持事务、支持外键、支持崩溃修复和自增列,它的特点是稳定(能保证业务的完整性),但数据的读写效率一般。
MySQL 有很多存储引擎(也叫数据引擎),所谓的存储引擎是指用于存储、处理和保护数据的核心服务。也就是存储引擎是数据库的底层软件组织。在 MySQL 中可以使用“show engines”来查询数据库的所有存储引擎,如下图所示:
在上述列表中,我们最常用的存储引擎有以下 3 种:
- InnoDB
- MyISAM
- MEMORY
下面我们分别来看。
InnoDB
InnoDB 是事务型数据库的首选引擎,支持事务安全表 (ACID),支持行锁定和外键。MySQL5.5.5 之后,InnoDB 作为默认存储引擎,InnoDB 主要特性有:
- InnoDB 给 MySQL 提供了具有提交、回滚和崩溃恢复能力的事务安全(ACID 兼容)存储引擎。InnoDB 锁定在行级并且也在 SELECT 语句中提供一个类似 Oracle 的非锁定读。这些功能增加了多用户部署和性能。在 SQL 查询中,可以自由地将 InnoDB 类型的表与其他 MySQL 的表的类型混合起来,甚至在同一个查询中也可以混合。
- InnoDB 是为处理巨大数据量的最大性能设计。它的 CPU 的效率可能是任何其它基于磁盘的关系数据库引擎所不能匹敌的。
- InnoDB 存储引擎完全与 MySQL 服务器整合,InnoDB 存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB 将它的表和索引存在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘分区)。这与 MyISAM 表不同,比如在 MyISAM 表中每个表被存在分离的文件中。InnoDB 表可以是任何尺寸,即使在文件尺寸被艰制为 2GB 的操作系统上。
- InnoDB 支持外键完整性约束(FOREIGN KEY)。 存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB 会被每一行生成一个 6B 的 ROWID,并以此作为主健。
- InnoDB 被用在众多需要高性能的大型数据库站点上。 InnoDB 不创建目录,使用 InnoDB 时,MySQL 将在 MYSQL 数据目录下创建一个名为 ibdata1 的 10MB 大小的自动扩展数据文件,以及两个名为ib_logfile() 和 ib_logfile1 的 5MB 大小的日志文件。
InnoDB 的优势是支持事务、支持外键、支持崩溃修复和自增列;它的缺点是读写效率较差、占用的数据空间较大。
MyISAM 存储引擎
MyISAM 基于 ISAM 的存储引擎,并对其进行扩展。它是在 Web、数据存储和其他应用环境下最常用的存储引擎之一。MyISAM 拥有较高的插入、查询速度,但不支持事务。不支持行级锁,因此在添加和修改操作时,会执行锁表操作,所以它的写入效率较低。在 MySQL5.5.5 之前的版本中,MyISAM 是默认存储引擎。MyISAM 主要特性有:
- 大文件(达 63 位文件长度)在支持大文件系统和操作系统上被支持。
- 当把删除、更新及插入操作混合使用的时候,动态尺寸的行产生更少的碎片。这要通过合并相邻被删除的块,以及若下一个块被删除,就扩展到下一块来自动完成。
- 每个 MyISAM 表最大索引数是 64,这也可以通过编译来改变。对于键长度超过 250B 的情况,一个超过 1024B 的键将被用上。
- BLOB 和 TEXT 列可以被索引。
- NULL 值被允许在索引的列中。这个值占每个键的 0~1 个字节。
- 所有数字键值以高字节优先被存储以允许一个更高的索引压缩。
- 每表一个 AUTO_INCREMENT 列的内部处理。MyISAM 为 INSERT 和 UPDATE 操作自动更新这一列。使得 AUTO_INCREMENT 列更快(至少 10%)。在序列顶的值被删除除之后就不能再利用。
- 可以把数据文件和索引文件放在不同目录。
- 每个字符列可以有不同的字符集。
- 有 VARCHAR 的表可以固定或动态记录长度。
- VARCHAR 和 CHAR 列可以多达 64KB
使用 MyISAM 引擎创建数据库,将生产 3 个文件。文件的名字以表的名字开始,扩展名指出文件类型:frm 文件存储表定义,数据文件的扩展名为 .MYD(MYData),索引文件的扩展名是 .MYI(MYIndex)。
MyISAM 引擎保存了单独的索引文件 .myi,且它的索引是直接定位到 OFFSET 的,而 InnoDB 没有单独的物理索引存储文件,且 InnoDB 索引寻址是先定位到块数据,再定位到行数据,所以 MyISAM 的查询效率是比 InnoDB 的查询效率要高。但它不支持事务、不支持外键,所以它的适用场景是读多写少,且对完整性要求不高的业务场景。
MEMORY 存储引擎
MEMORY 存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。同样不支持事务、不支持外键。MEMORY 支持 Hash 索引或 B 树索引,其中 Hash 索引是基于 key 查询的,因此查询效率特别高,但如果是基于范围查询的效率就比较低了。MEMORY 主要特性有:
- MEMORY 表的每个表可以多达 32 个索引,每个索引 16 列,以及 500B 的最大键长度。
- MEMORY 存储引擎引擎执行 HASH 和 BTREE 索引。
- 可以在一个 MEMORY 表中有非唯一键。
- MEMORY 不支持 BLOB 或 TEXT 列。
- MEMORY 表使用一个固定的记录长度格式。
- MEMORY 支持 AUTO_INCREMENT 列和对包含 NULL 值的列索引。
- MEMORY 表内容被存在内存中,内存是 MEMORY 表和服务器在查询处理时的空闲中创建的内部表共享。
- MEMORY 表在所有客户端之间共享(就像其他任何非 TEMPORARY 表)。
- 当不再需要 MEMORY 表的内容时,要释放被 MEMORY 表使用的内存,应该执行 DELETE FROM 或 TRUNCATE TABLE,或者删除整个表(使用 DROP TABLE)。
MEMORY 读写性能很高,但 MySQL 服务重启之后数据会丢失,它不支持事务和外键。适用场景是读写效率要求高,但对数据丢失不敏感的业务场景。
区别
功能 | MyISAM | Memory | InnoDB |
---|---|---|---|
存储限制 | 265TB | RAM | 65TB |
支持事务 | No | No | Yes |
支持全文索引 | Yes | No | No |
支持数索引 | Yes | Yes | Yes |
支持哈希索引 | No | Yes | No |
支持数据缓存 | No | N/A | Yes |
支持外键 | No | No | Yes |
MyISAM 和 InnoDB 更详细的区别:
区别 | MyISAM | InnoDB |
---|---|---|
事务 | 不支持 | 支持 |
存储结构 | 每个MyISAM在磁盘上存储成三个文件 | 所有的表都保存在同一个数据文件中 |
存储空间 | 可被压缩,存储空间较小 | 会在主内存中建立其专用的缓冲池(需要更多内存和存储) |
可移植性 | 跨平台的数据转移中会很方便,在备份和恢复时可单独针对某个表进行操作 | 拷贝数据文件、备份 binlog,或者用 mysqldump |
事务支持 | 每次查询具有原子性,但不支持事务 | 提供事务支持事务 |
表锁差异 | 只支持表级锁 | 支持行级锁 |
全文索引 | 支持 | 不支持 |
表主键 | 允许没有任何索引和主键的表存在 | 如果未设置主键,会自动生成 |
表总行数 | 保存有 | 没有保存 |
CURD | 对于select支持更好 | INSERT/DELETE支持更好 |
外键 | 不支持 | 支持 |
查询效率 | 小型应用可以考虑使用 | 高并发、复杂情况表现更优 |
使用场景
MyISAM适合:(1)做很多count 的计算;(2)插入不频繁,查询非常频繁;(3)没有事务。
InnoDB适合:(1)可靠性要求比较高,或者要求事务;(2)表更新和查询都相当的频繁,并且行锁定的机会比较大的情况。
MySQL索引有哪些?
按数据结构分类可分为:B+tree索引、Hash索引、Full-text索引。
按物理存储分类可分为:聚簇索引、二级索引(辅助索引)。
按字段特性分类可分为:主键索引、普通索引、前缀索引。
按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)。
按数据结构分类
MySQL索引按数据结构分类可分为:B+tree索引、Hash索引、Full-text索引。
InnoDB实际上也支持Hash索引,但是InnoDB中Hash索引的创建由存储引擎引擎自动优化创建,不能人为干预是否为表创建Hash索引。
又聊到了这个问题,B+ 树,先前有文章去分析过它,这里简单写一些当时没在意的内容。
B+tree 是MySQL中被存储引擎采用最多的索引类型。B+tree 中的 B
代表平衡(balance),而不是二叉(binary),因为 B+tree 是从最早的平衡二叉树演化而来的。下面展示B+tree数据结构与其他数据结构的对比。
B+树 和 B树的对比
B-tree 中的每个节点根据实际情况可以包含多条数据信息和子节点,如下图所示为一个3阶的B-tree:
相对于B-tree,B+tree有以下三点不同:
- B+tree 非叶子节点只存储键值信息, 数据记录都存放在叶子节点中。而B-tree的非叶子节点也存储数据。所以B+tree单个节点的数据量更小,在相同的磁盘I/O次数下,能查询更多的节点。
- B+tree 所有叶子节点之间都采用单链表连接。适合MySQL中常见的基于范围的顺序检索场景,而B-tree无法做到这一点。
- B+tree 的子树数量等于它的关键字的数量,而 B-tree是关键字数量 + 1.
B+树 和 Hash 的对比
Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。虽然 Hash 索引效率高,但是 Hash 索引本身由于其特殊性也带来了很多限制和弊端,主要有以下这些:
Hash 索引仅仅能满足
=
,IN
和<=>
(表示NULL安全的等价) 查询,不能使用范围查询。由于 Hash 索引比较的是进行 Hash 运算之后的 Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
Hash 索引无法适用数据的排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash值,而且Hash值的大小关系并不一定和 Hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
Hash 索引依然需要回表扫描。
Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键可能存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
Hash索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个Hash值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下
由于范围查询是MySQL数据库查询中常见的场景,Hash表不适合做范围查询,它更适合做等值查询。另外Hash表还存在Hash函数选择和Hash值冲突等问题。因此,B+tree索引要比Hash表索引有更广的适用场景。
在这里又要挖一个坑,后面肯定学习一致性算法。
按物理存储分类
MySQL索引按叶子节点存储的是否为完整表数据分为:聚簇索引、二级索引(辅助索引)。全表数据存储在聚簇索引中,聚簇索引以外的其他索引叫做二级索引,也叫辅助索引。
聚簇索引
聚簇索引的每个叶子节点存储了一行完整的表数据,叶子节点间按id列递增连接,可以方便地进行顺序检索。
InnoDB表要求必须有聚簇索引,默认在主键字段上建立聚簇索引,在没有主键字段的情况下,表的第一个非空的唯一索引将被建立为聚簇索引,在前两者都没有的情况下,InnoDB将自动生成一个隐式的自增id列,并在此列上建立聚簇索引。
以MyISAM为存储引擎的表不存在聚簇索引。
MyISAM表中的主键索引和非主键索引的结构是一样的,索引的叶子节点不存储表数据,存放的是表数据的地址。所以,MyISAM表可以没有主键。
MyISAM表的数据和索引是分开存储的。MyISAM表的主键索引和非主键索引的区别仅在于主键索引的B+tree上的key必须符合主键的限制,非主键索引B+tree上的key只要符合相应字段的特性就可以了。
二级索引
二级索引的叶子节点并不存储一行完整的表数据,而是存储了聚簇索引所在列的值。
回表查询
由于二级索引的叶子节点不存储完整的表数据,索引当通过二级索引查询到聚簇索引列值后,还需要回到聚簇索引也就是表数据本身进一步获取数据。
回表查询 需要额外的 B+tree 搜索过程,必然增大查询耗时。
需要注意的是,通过二级索引查询时,回表不是必须的过程,当SELECT的所有字段在单个二级索引中都能够找到时,就不需要回表,MySQL称此时的二级索引为覆盖索引或触发了索引覆盖。
可以用Explain命令查看SQL语句的执行计划,执行计划的Extra字段中若出现Using index,表示查询触发了索引覆盖。
按字段特性分类
MySQL索引按字段特性分类可分为:主键索引、普通索引、前缀索引。
主键索引
建立在主键上的索引被称为主键索引,一张数据表只能有一个主键索引,索引列值不允许有空值,通常在创建表时一起创建。
唯一索引
建立在UNIQUE字段上的索引被称为唯一索引,一张表可以有多个唯一索引,索引列值允许为空,列值中出现多个空值不会发生重复冲突。
普通索引
建立在普通字段上的索引被称为普通索引。
前缀索引
前缀索引是指对字符类型字段的前几个字符或对二进制类型字段的前几个bytes建立的索引,而不是在整个字段上建索引。前缀索引可以建立在类型为char、varchar、binary、varbinary的列上,可以大大减少索引占用的存储空间,也能提升索引的查询效率。
按索引字段个数分类
MySQL索引按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)。
单列索引
建立在单个列上的索引被称为单列索引。
联合索引(复合索引、组合索引)
建立在多个列上的索引被称为联合索引,又叫复合索引、组合索引。
总结
以上就是今天晚上的面试中我学习到的内容,并不是全部,还有关于算法和分布式系统的内容,我觉得内容会比较多,所以还是后面的博客里见吧,这次绝对不会再拖沓了,我要在考试周结束之前把所有之前欠下的东西补完。
我是个大**(自动消音:哔~),之前一直说的要沉淀沉淀,现在看来好像全是笑话。
参考
问题三
问题四