持续更新中……
滴滴go语言开发一面(1~16):
1.mysql有那些存储引擎?
常用的是innodb,还有其它不常用的,比如myisam、memory(把数据存储在内存中,生命周期短,一般是一次性的)、archive(主要用于数据归档,压缩比非常高,比较适合用来存储历史数据)之类的。
2.说说innodb和myisam区别?
(1)事务:INNODB支持事务,而MyISAM强调的是性能,每次查询具有原子性,不提供事务支持。
(2)外键:INNODB支持外键,MyISAM不支持外键。
(3)锁机制:innodb是行锁,MyISAM是表锁,如果进行大量的插入、删除操作,使用innodb性能会更高,因为innodb使用的是行锁,只需要锁定操作行就行,而myisam是表锁,整个表都会被锁住,效率会低一些。
(4)如果只是执行大量的查询, MyISAM的执行速度比innodb快,因为myisam只需要缓存索引块,而innodb要缓存数据块,缓存数据块是比较花费性能的。
(5)Innodb不保存表的具体行数,而myisam内置了一个计数器,执行没有where条件的count(*)时直接从计数器中读数据,而innodb要进行全表扫描。
(6)Innodb和Myisam存储数据的格式不一样,innodb用.frm存储表定义,用.ibd存储数据,而myisam用.frm格式存储表定义,用.myd存储数持,用.myi存储索引。
(7)存储消耗不同,myisam会进行压缩存储,存储空间较小,而innodb会在内存中开辟缓冲池来缓冲数据和索引,占用空间比myisam大。
(8)InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的。在查询时如果不能使用覆盖索引在索引树中找到全量数持,就得做回表扫描,先在索引树中查询到主键,然后再通过主键查询到数据。MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。
3.mysql的事务是什么,有哪些特性?
事务是一系列的数据库操作,是数据库应用的基本单位,主要用于处理操作量大,复杂度高的数据。
ACID(A:原子性,C:一致性,I:隔离性,D:持久性)
原子性(Atomic):要么全部执行,要么全部不执行;
一致性(Consistency):事务的执行使得数据库从一种正确状态转化为另一种正确状态;
隔离性(Isolation):在事务正确提交之前,不允许把该事务对数据的任何改变提供给其他事务;
持久性(Durability):事务提交后,其结果永久保存在数据库中。
4.用过外键吗?用的是逻辑外键还是物理外键?都有什么好处?哪个好?
用过,两种外键都用过,物理外键的好处是数据库会帮我们维护两张表之间的关联,不用我们自己在代码里去连接两张表。但是,物理外键的性能是存在很大问题的:
a.数据库需要维护外键的内部管理;
b.外键等于把数据的一致性事务实现,全部交给数据库服务器完成;
c.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;
d.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况
5.说说数据库有哪些索引方式(主要hash索引,b+树),什么时候用hash索引什么时候用b+树,这两个优劣
哈希索引只支持等值查询,不支持范围查询,在离散型高、数据基数大的等值查询时,哈希索引有优势。在范围查询、模糊查询、排序、分组的场景下,哈希索引无法满足要求,应该使用B+树索引。
6.简单说说索引怎么实现的(工作室之前讲过),工作中哪些情况用索引,为什么要用到
B+树索引(范围查询)、哈希索引(非范围查询)
7. '%xxx’会不会走索引,为什么(个人感觉答的不错,有机会以后给大家讲讲),索引什么情况下会失效
后导模糊查询 like 'xx%'
是可以走索引的,前导模糊查询 like '%xx'
,一般是不走索引的,如果还想走索引就必须把使用覆盖索引,即把要查询的字段改为一次索引查询就能查到的字段,而不用回到聚集索引做回表扫描。
索引失效的几种情况:
1.在复合索引中跳过了第一列字段
2.在复合索引中跳过了中间字段,这样中间字段之前的字段会走索引,中间字段之后的字段不走索引
3.在索引上进行计算、函数、类型转换
4.使用范围查询后,该索引之后的索引会失效
5.使用不等于操作(!=或<>)
6.使用is not null
7.使用前导模糊查询
8.不走索引会有更好的查询性能时索引失效
8.是不是什么情况下都可以建立索引,哪些情况不行,为什么
适合建立索引的情况:
1.频繁做为where条件的字段
2.用做不同表之间关联的字段要建立索引,如做为外键的字段
3.做为排序的字段
4.用来分组的字段(分组前一般要先排序)
5.常用来计数的字段
不适合建立索引的情况:
1.频繁更新的字段
2.where条件用不到的字段
3.表数据可以确定比较少的字段不需要建立索引
3.唯一性较差的字段,性别、布尔值
4.经常要参与运算的字段
9.说说脏读、不可重复读、幻读,怎么解决的
一种解决方案是使用悲观锁,另一种解决方案是使用乐观锁
悲观锁:
当sql第一次读取到数据时,就将这些数据加锁,这样其他事务就无法修改这些数据,解决了不可重复读。但这样无法锁住insert的数据,当事务A先前读取了一些数据,事务B还可以插入数据,这时事务A就会发现莫名其妙多了一条原来没有的数据,这就是幻读,幻读无法通过行锁来避免。需要使用可串行化隔离级别,读用读锁,写用写锁,读锁与写锁互斥,这样可以有效避免脏读、不可重复读、幻读,但会极大降低数据库的并发能力。
乐观锁
乐观锁大多是基于数据版本记录实现的,一般通过为数据表增加一个”version“字段来实现。读取数据时将版本号一同读出,在提交更新前对此版本号加一,然后和数据库表对应的版本号进行比对,如果大于数据库当前的版本号则执行提交,否则数据已被其他事务更新过,执行提交失败。
10.分库分表了解过吗,为什么要分(数据量大才要分,要不然会慢),然后问了一个左前缀什么的,这个不会
为什么要分库分表?
在数据量很大时通过分库分表可以提升性能、增加可用性。
什么是分库分表?
去中心化,不把鸡蛋都放在一个篮子里
分库:从单个数据库拆分成多个数据库的过程,将数据散落在多个数据库中
分表:从单张表拆分成多张表的过程,将数据散落在多张表内。
分库分表有几种方式?
概括:垂直拆结构、水平拆数据
垂直拆分:
分库:根据不同的业务将数据库拆分成不同的库,比如商城数据库可以拆分为会员库、商品库、订单库
分表:根据业务需求,对一张表的字段拆分成不同的部分,比如用户表,可以拆分成基础表和信息表,基础表只有用户id、用户名和密码字段,主要用户登录业务逻辑;信息表有用户id和用户的联系方式、收货地址等字段,可以用于订单业务
特点:
1.每个库(表)的结构都不一样
2.每两个需要关联的库(表)至少有一列字段是一样的
3.每个库(表)的并集是全量数据
优点:
1.拆分后业务更清晰(专库专用按业务拆分)
2.数据维护简单、不同的业务放到不同的机器上
缺点:
1.如果单表数据量大,读写压力会很大
2.一个业务往往会成为数据库的瓶颈,比如双十一的订单业务是影响数据库性能的主要瓶颈
3.增加了开发的难度
水平拆分:
数据库或表的结构都不用改变,只需要拆分数据,将结构相同的数据分散到不同的机器上
特点:
1.每个库(表)的结构都一样
2.每个库(表)的数据都不一样
3.每个库(表)的并集是全量数据
优点:
1.单库(表)存的数据量减小了,有助于提高性能
2.拆分表的结构相同,程序改动较少,比如和数据库对应的类或结构体的属性不需要改动
缺点:
1.扩容难度大,比如根据id模2的奇数或偶数将数据拆分到两台机器,如果要多加一台机器,就会面临大量的数据迁移问题
2.拆分规则很难抽象出来
3.存在分布式事务的一致性问题
分库分表带来的问题?
1.分布式问题
2.跨库连接查询
3.分布式全局唯一ID(shardingsphere默认使用雪花算法)
4.开发成本高,对程序员要求高
解决方案:使用开源框架
jdbc直连层:shardingsphere(可以兼容不同的数据库,只要实现jdbc接口即可,但对除java之外的编程语言兼容性不太好)
proxy代理层:mycat(不支持跨数据库,但支持跨语言)
11.有哪些nosql (redis,mongodb) ,什么情况下用nosql
常用的有redis、mongodb
性能对比:redis全部数据在内存,定期与入磁盘,内存不够时使用LRU(Least Recently Used)算法删除数据,mongodb也是把数据存放在内存,但内存不够时,只把热点数据存放在内存,总体上redis的TPS高于mongodb,所以性能上redis优于mongodb。
可靠性对比:mongodb使用binlog方式持久化,redis一般使用AOF快照方式持久化,将每一条命令追加到磁盘,在处理巨大的写入时,性能会降低,所以可靠性上mongodb优于redis。
一致性对比:mongodb不支持事务,要靠客户端自身保证,redis支持事务,比较弱,仅能保证事务的操作按顺序执行。
应用场景:mongodb主要应用于海量数据访问效率的提升,redis主要应用于较小数据量访问效率的提升。
数据分析:mongodb内置数据分析功能,redis不支持数据分析。
什么情况下使用
1.高并发的数据库请求,传统的关系型数据库会成为性能瓶颈,使用nosql可以满足高并发的数据库请求。
2.海量数据的分布式存储,使用nosql性价比更高。
3.如果关系型数据库的表字段经常新增,会带来很大的性能开销,比如重建索引,使用nosql可以解决这个问题。
4.数据库表字段是复杂数据类型时使用nosql,nosql以json方式存储数据,效率远远高于传统关系型数据库。
12.redis数据类型(string, set什么的)
string、list、set、zset、hash
13.什么情况下用redis(短信验证码,用户session), 为什么要把session存redis
1.在高并发场景下,可以使用缓存空间换时间,先把数据缓存到redis,然后多线程异步写入数据库,可以防止数据库阻塞。
2.将用户session存入redis。
为什么要把session存redis?
这样可以实现分布式session,当客户端发送一个请求,经过负载均衡后该请求会分配到服务器中的其中一个,这个服务器要访问其他服务器保存的session,就可以通过redis来实现分布式session一致性。
14.简单讲讲redis底层实现
redis的总体数据结构
最顶层有一个结构体,这个结构体有好几个字典属性,第一个字典是用来存放键值对数据的,第二个哈字典是用来存放过期时间的,然后还有和阻塞相关的字典。
我主要讲存放键值对数据的字典,这个字典内部维护了两个hashtable,一个是用来存放数据的,另一个是用来rehash的。hashtable就是一个数组,通过对hashtable长度取模的方式定位到数组中的一个位置,然而在这个位置存放的是一个指针,而不是k-v键值对,这个指针会指向一个k-v存储单元,数据实际存储在在个k-v存储单元中,redis中解决哈希冲突是使用拉链法,就是每个k-v存储单元都有一个指针指向下一个存储单元,在哈希冲突时,redis是采用头插法,将数据插入链表的头部,采用头插法是因为redis希望新插入的数据很快就会被用到,插入到头部方便快速访问。
redis的rehash
当数据的个数超过hashtable的大小时,redis会将hashtable成倍地扩容,然后通过rehash将原来的数据散落在新的hashtable里,rehash不是一次性的,而是渐近式的,每get或set某个哈希槽的数据时,会将这个槽的数据搬一部分到新的hashtable里,当所有数据都搬完之后,再把老的hashtable释放掉。
rehash访问数据的顺序:如果要取数据,先把老的hashtable里取,老的hashtable里没有就去新的hashtable里去,如果要插入新数据,直接往新的hashtable里插入数据
redis string类型的设计
redis string类型的设计
redis的键是可以使用任意类型的,不过不管你使用什么数据类型,redis都会把你的键转成字符串,在redis中,字符串和c语言中的字符串是不一样的,redis没有像c语言一样使用\0作为字符串的结束符,redis重新定义了一种叫sds的数据结构,这个数据结构会用一个整型来描述这个字符串有多长,这样即使字符串中间有\0,读取也不会突然中断,当然描述字符串长度的整型,redis没有使用int类型,因为一个int占4个字节,共32位,太浪费空间了,redis的整型有好多类,比如占5位的,占8位的、16位的,每一个存放字符串的结构体内部都有一个字符型变量char来描述数据类型和长度,一个char字符类型虽然只占一个字节,但redis中设计得很精妙,redis把这一个字节的8bit位分成了两半,前三位用来描述数据类型,即到底是占多少位的整型,后五个bit位是用来描述这个字符串有多长,当然如果这5个bit位不够描述,redis还会再使用稍微更大的空间去描述。redis存放字符串的字符数组是支持成倍扩容的,如果字符数组容量不够了,redis会重新开辟一个新增加的长度与原长度之和的两倍大小
的字符数组,然后将原来字符数组的内容复制过来。
zset的底层实现
当数据比较少时,zset使用ziplist(压缩列表)数据结构来存,当数据元素超过一定数量时,使用skiplist(跳跃表)存储,具体超过多少是可以自己配置的,当单个元素大小超过一定值时,使用skiplist(跳表)存储,单个元素达到多大使用skiplist(跳跃表)可以自己配置。
ziplist(压缩列表)
是一个很紧凑的数据结构,是顺序存储的,类似数组,但和数组又不一样,ziplist每个节点大小不一样,根据实际需要分配,非常节省内存,每个节点记录了必要的偏移量信息,这样可以很方便的跳到上一个节点或下一个节点。
skiplist(跳跃表)
是一种可以进行二分查找的有序链表,跳跃表在原有的有序链表的上面增加了多级索引,使得查找、插入、删除的时间复杂度都是O(logn)的。在大量的新节点插入原链表后,上层的索引就会变得不够用,由于插入和删除节点是不可预测的,很难用一种有效的算法来保证跳跃表索引部分始终均匀,于是跳跃表中采用了一种随机的解决方案,就是一个元素插入到原链表后,通过一定的概率决定这个新节点是否要提拔到上一层,这个概率通常是50%,提拔完后继续随机决定是否要继续往上提拔,直到提拔结束。
15.放大招了,问我如果公司redis挂了怎么办… ,蒙了一下结果蒙对了
可以使用哨兵集群机制Sentinel解决,哨兵集群会对redis服务器进行监控,如果发现是从服务器挂了,就给它作标记,表示这个从服务器不在了,如果是主服务器挂了,不会马上认定主服务器真的挂了,当超过半数的哨兵服务器认为这台主服务器挂了,才客观认定这台主服务器挂了,这时就要在从服务器中选举出一台主服务器,选举之前先筛选,把挂掉的和经常掉线的服务器去掉,接下来第一轮选举是看剩下的从服务器谁的优先级高,这个优先级是可以手动事先设置的,把优先级最高的设为主服务器,如果优先级相同,进入第二轮选举,看哪个从服务器复制进度最快,然后把复制进度最快的从服务器做为主服务器,如果复制进度都差不多,进入第三轮,由于每台服务器的ID值都是不相同的,第三轮把ID号最小的做为主服务器。
挂掉的redis在重启后会自动加入到主从架构中成为从服务器,并自动完成数据同步。如果redis在挂掉之前实现了持久化,只需要连接到主服务器,实现增量同步。
Redis高可用架构图解
Redis主从复制过程
Redis Sentinel高可用集群
Sentinel节点挂了怎么办?
点击回到目录
###
16.给你1mb内存,对1000万个int32数排序,磁盘空间无限大,要求时间复杂度
bitmap解法:
申请长度为一千万位的位向量bit[10000000],所有位置为0,顺序读取待排序文件,每读入一个数i,便将big[i]置为1,当所有数据读入完成时,便对bit从头到尾遍历,如果bit[i] = 1,则输出到文件,当遍历完成,文件则已排好序。时间复杂度O(n)。
归并排序:
将1000万个数均分为40块,然后对这40块分别排序,排序时可以使用多线程,对这40块文件排好序后开始归并,归并就是取这40块文件中最小的元素写入文件,由于每一块数据都是从小到大排列的,取最小的元素只要对每一块数据的第一个元素进行比较,当所有元素都写入文件时,归并结束,文件里的元素已排好序。
17.10个数字,给出一个数字判断是不是在这十个里面,用map好还是数组遍历的好(10个肯定是数组的好)问我时间复杂度(因为固定长度了,都是常数),聊到了map,问我知不知道c++的map底层是什么(红黑树),说到go的map (哈希),开始比较红黑树和哈希了(感觉讲的还行),Linux底层epoll为什么用红黑树而不是哈希,还有写别的挺细的
红黑树与哈希的区别:
1.红黑树是有序的,哈希是无序的,可以根据是否需要排序来选择用哪个。
2.红黑树占用的内存小(仅需要为其存在的节点分配内存),而哈希事先应该分配足够的内存存储散列表,即便有些哈希槽可能弃用,可以根据内存限制情况选择用哪个。
3.红黑树查找和删除的时间复杂度都是O(logn),哈希查找和删除的时间复杂度都是O(1),但哈希并不一定就比红黑树快,因为哈希还有哈希函数耗时,还可能产生哈希冲突。
Linux底层epoll为什么用红黑树而不是哈希:
1.哈希表是不容易缩容的,如果某个时刻,有大量的网络请求通过哈希来记录,触发哈希表扩容,之后这个哈希表很难缩容回去,这也是为什么一些go程序 oom 的问题,都是go map太大了无法gc。
2.哈希表虽然是o(1)时间复杂度,但网络连接数相当大时,底层需要那么多哈希运算,不一定有long(n)红黑树快。
18.tcp三次握手四次挥手,最后面试官都夸我了解的深
常见标志位:
ACK标志:确认标志。通常称携带ACK标志的TCP报文段为确认报文段。
SYN标志:表示请求建立一个链接。通常称携带SYN标志的TCP报文段称为同步报文段。
FIN标志:关闭标志,通常称携带FIN标志的TCP报文段为结束报文段。
![]() | ![]() |
三次握手过程:
1、发送端发送连接请求,带上同步标志位(SYN)和自己的序号。
2、接收端同意发送端的连接请求,也带上同步标志位(SYN)和自己的序号,同时带上确认标志位(ACK)和确认序号,确认序号为发送端序号加1。
3、发送端同意接收端的连接请求,带上确认标志位(ACK)和确认号,确认号为接收端序号加1。
此时实现了全双工通信,即三次握手完成。
涉及到的问题:为什么需要三次握手,而不是四次或两次?
为什么不是四次?
四次的过程如下:
发送方:我要连你了。
接收方:好的。
接收方:我准备好了,你连吧。
发送方:好的。
显然,接收方同意连接并准备好是可以合并的,这样可以提高连接效率。
为什么不是两次?
两次的过程如下:
发送方:我要连你了。
接收方:好的。
假设发送方是客户端,接收方是服务器,客户端发送完连接请求,服务器发出确认信息,此时连接就建立了,这样会导致单方面连接,客户端连上了服务器,但服务器没有连接上客户端,也就是客户端可能收不到服务器发出的确认信息,如果客户端没有收到服务器的确认信息,就会不断的重新发送建立连接请求,不断地建立连接是会浪费资源的,如果采用三次握手,在服务器收到了客户端发出的确认信息后才会真正建立连接,如果服务器收不到客户端的确认,服务器就会重新发送确认信息,直到得到客户端的回复。
四次挥手过程:
1、发送方发送关闭请求,带上关闭标志位(FIN)和自己的序号。
2、接收方回复确认标志位(ACK)和确认序号,确认序号为发送方序号加1。
3、接收方回复关闭标志位(FIN)和自己的序号,以及确认标志位(ACK)和确认序号,确认序号和上次相同。
4、发送方回复确认标志位(ACK)和确认序号,确认序号和接收方的序号加1。
涉及到的问题:为什么需要四次握手,而不是三次?
三次的过程是这样的:
发送方:我不再给你发送数据了。
接收方:好的,我也不给你发了。
发送方:好的,拜拜。
当接收方收到关闭的请求后,能立马响应的就是确认关闭,这里的确认关闭是关闭“发送方发给接收方”这条通道,而接收方是否需要关闭“接收方发给发送方”这条通道由操作系统决定,操作系统通常要sleep个几秒再关闭,如果合并成三次,可能造成发送方不能及时收到确认请求,超时后会重新发送关闭请求(“发送方重试发送:我不再给你发送数据了”)。
为什么需要有 TIME_WAIT 状态存在?
简单来说有两点原因如下:
a. 在最后发送方发出确认信息后,仍然不能保证接收方能收到信息,万一没收到,那接收方就会重试,而此时发送方已经真正关闭了,就接受不到重试请求了。
b. 为了保证信道中已经没有还在途中的消息。如果途中还有消息,且发送方继续发送连接请求,由于消息在途中跑的速度不一样,可能会出现刚连接完又收到了确认关闭的消息,导致乱套了。
为什么时长是 2MSL 呢?
发送确认信息到达接收方,到达时间最长是 MSL,而接收方如果没接受到,会再重试,时间最长也是 MSL,那发送方等 2MSL,如果还没收到请求,证明接收方已经正常收到了。
点击回到目录