这是我的第一篇掘金博客,开启掘金写作之路。
选择题
Day1、【多选】Golang 通过plugin.(*Plugin).Lookup函数可以查找到插件里面定义的哪些东西?
a. 变量
b. 函数
c. 类型
d. 包
答案:a、b;
a和b都是能被赋值给interface{}类型的变量,但是c和d不能。因此Lookup方法返回的结果是一个interface{}类型(Symbol类型)的变量,因此c和d不能通过Lookup返回。
Day2、【多选】下列关于Join 运算不正确的是:
a. Nested Loop Join 不能使用索引做优化。链接
b. 如果左表太大,不能放入内存中,则不能使用 Hash Join。链接
c. 如果 Join 的一个输入表在 Join Key 上有序,则一定会使用 Sort Merge Join。链接1
d. Broadcast Join 适用于一张表很小,另一张表很大的场景。链接
答案:
a、b、c;
Nested Loop Join 可以使用索引优化(Index Nested Loop Join);
如果左表太大,不能放入内存,可以先对量表做分区,再对每个分区做 Hash Join (Parititioned Hash Join);
如果Join的一个输入表在Join Key上有序,也可以 Hash Join,具体使用哪种 Join 方式取决于优化器的代价估计结果。
MySQL官方文档中提到,MySQL只支持Nested Loop Join这一种join algorithm
决定使用哪种类型的内连接。
· 取决于表大小
· 取决于连接列是否有索引
· 取决于连接列是否排序
Nested Loop Join的三种
Simple Nested Loop Join 两层循环暴力扫描两张表,得到符合的记录,比较耗时
Index Nested Loop Join 外表整表循环,内表查询索引,但如果查询的是非聚簇索引,就会增加随机I/O操作
MySQL优化器在索引可用的情况下,会优先使用INLJ算法
Block Nested Loop Join 在SNLJ的基础上使用了join buffer,会提前读取内循环需要的数据到buffer
控制join buffer大小的参数名是join_buffer_size
Hash Join
优化器使用两个表中较小的表放入内存中当作hash表,然后扫描另一个表寻找匹配项,如果放不下就会被分成若干个分区,不能放入内存的分区就放入磁盘的临时段,hash-join只适用于等值连接。
Merge Join
根据上表如果创建聚族索引没有使用Unique关键字则不会使用MergeJoin。
Merge Join是为那些在连接列上有索引的表,索引可以是聚族索引或者非聚族索引。Merge是这种情况最好的Join类型,需要两个表都有索引,所以它已经排好序并更容易匹配和返回数据。
Broadcast Join
是将其中一张小表广播分发到另一张大表所在的分区节点上,分别并发地与其上的分区记录进行hash join。broadcast适用于小表很小,可以直接广播的场景
\
Day3、【多选】绝大多数硬盘可以提供哪些写入保证?
a. 单个sector原子写入
b. 单个page原子写入
c. 硬盘顺序执行文件系统发送的操作
d. 以上都不可以
答案:
d;
本题主要考察对于一个数据密集型应用开发者(比如数据库开发者、文件系统开发者),在数据不能出错的前提下,可以依赖磁盘的哪些能力,事实证明几乎没有什么能力可以依赖。
在随时可能断电的情况下,大多数硬件能提供单个sector的原子性写入;但是也有少数硬件连单个sector的写入都无法保证;如果一个page对应多个sector,则单个page的完整写入总是无法得到保障。更加详细的情况可以查看这个讨论:crash - Are disk sector writes atomic?。
对于同时发给硬盘的多个操作(比如写多个不连续的sector),硬盘并不保证操作的顺序。结合其弱原子性,所以硬盘可能处在任何一个中间状态。这主要是由于机械硬盘的寻址优化策略和固态/机械硬盘的缓存策略导致的。
Day4、【单选】go test 默认是以什么顺序执行测试的?
a. 多个 module 并发执行,单 module 下多个测试并发执行
b. 多个 module 并发执行,单 module 下多个测试串行执行
c. 多个 module 串行执行,单 module 下多个测试并发执行
d. 多个 module 串行执行,单 module 下多个测试串行执行
答案
b;
多个 modules 会并发编译,然后并发执行测试,除非添加了额外的参数-p=1。单个 modules 下多个测试会串行执行,除非在测试函数内执行t.Parallel()。
Day5、【单选】使用 SQL 语句进行分组检索时,为了去掉不满足条件的分组,应当:
a. 使用 WHERE 子句
b. 在 GROUPBY 后面使用 HAVING 子句
c. 先使用 WHERE 子句,再使用 HAVING 子句
d. 先使用 HAVING 子句,再使用 WHERE 子句
答案:C
having 是对分组统计结果进行筛选,where 是对基础数据进行筛选。
为什么没有选 b: 这个与sql执行过程有关,where 在最开始发挥作用,having对group by之后的结果发挥作用,所以选择尽量前置,降低group by等阶段的操作数据量。 b 的意思是把where的选择放到having里,所以相当于所有选择都后置了。 注意理解题意哟。
\
Day6、【单选】下列关于 Python 的说法错误的是?
a. Python 是强类型语言
b. Python 中所有变量本质上都是指针
c. Python 运行时会根据类型提示(type hints)检查变量类型
d. CPython 不支持尾递归优化\
答案:
c;
Python 运行时并不会检查变量类型, 类型提示主要给第三方工具使用, 比如IDE可以据此给出方法/属性的自动补全建议。
Day7、【多选】下面关于 HTTP1.x 的性能优化方式,正确的有:
a. 对域名进行分片,使得客户端可以创建更多的 TCP 连接提高请求并发度
b. 设置 Connection: Keep-Alive Header 保持长连接,减少 TCP 连接握手的开销
c. 利用 ServerPush 将页面上的关键静态资源直接推送到客户端,无需等待客户端请求
d. 将小的静态资源直接嵌入到页面中,减少 HTTP 请求次数
答案:
a,b,d;
ServerPush 为 HTTP2 协议才具备的能力,无法应用在 HTTP1.x 的优化中。
Day8、【单选】以下描述正确的是:
a. 表达式和运算符都是执行计划的组成部分。
b. 在火山模型中,执行计划子节点对应的运算符执行完成后,父节点对应的运算符才能开始执行。
c. 排序算法仅仅在 Sort 运算符中使用。
d. 当使用 Index Scan 的时候,任何情况下都需要再回表读取数据。
答案:
a;
在火山模型中,很多情况下执行计划子节点和父节点的运算符是同时执行的。排序算法也可以在 Join,Aggregation 等运算符中使用。Index Scan 如果能够覆盖所有的查询字段,不需要再回表读取数据。
\
主观题
Day1、假如在抖音中发布视频时,可以选择带上位置信息,请设计一种数据结构或方案,用于存储检索位置信息(简化为平面坐标 x, y),以实现搜索附近视频的功能(如附近 3km)。
\
答案:
坐标范围检索,有四叉树、geohash 等几种标准解法。这道题本质并不是考察对高阶算法的掌握,而是想发掘在学习教材 btree 等基础二分思想后,能否进一步思考解出更复杂的问题;
另外考察思维灵活程度,看是否能变通的解决问题,如距离并没有限定必须是欧式距离;位置可以不精确,可以容忍有误差等。
- 方法 1,四叉树(QTree) :在二叉树左、右节点的思想下,加入上、下、前、后等更多的方向,演进为四叉树和八叉树。高阶树比较超纲,相关实现省略
- 方法 2,geohash:把二维问题降为一维
如坐标(示例非标准 geohash,只是演示了思想):
(12, 345) -> (012, 345) -> "031425"
(13, 348) -> (013, 348) -> "031438"
(2, 789) -> (002, 789) -> "070829"
最终做字符串前缀匹配,可得 "031425" 和 "031438" 匹配到的位数最多,二者距离最近。求 3km 内的坐标,只需提前算出需匹配前几位即可,如匹配前 4 位,按 sql 表达是 LIKE '0314%'
- 方法 3,变通距离为 方圆 3km(曼哈顿距离) ,即 deltaX = 1500, deltaY = 1500,通过数据库解决 Create table tb_name ( x int, y int ) 并添加索引。
假如原点是 (x0, y0),sql 如下:
WHERE (x > x0 - 1500) AND (x < x0 + 1500) AND (y > y0 - 1500) AND (y < y0 + 1500)
geohash
另外redis可以直接实现这个功能
Day2、给定一个正整数数组 arrs 和整数 K ,请找出该数组内乘积小于等于 k 的连续的子数组的个数,算法时间复杂度o(n)。
\
双指针思想,对于每个right, 不断单调的移动left,直到满足需求。
Day3、判断一棵二叉树是否是平衡二叉树。(平衡二叉树要求:树中节点左右子树树高差不超过1。)
Day4、【分布式文件处理,获取最多的 URL】如果有一个 20g 的日志文件,日志文件记录着用户访问过的 url,每一行为一个 url,给你一台 512M 的主机,找出出现次数最多的 10 个 url。
\
- IP 地址最多有 2^32=4G 种取值情况,所以不能完全加载到内存中处理;
- 可以考虑采用“分而治之”的思想,按照 IP 地址的 Hash(IP)%1024 值,把海量 IP 日志分别存储到 1024 个小文件中。这样,每个小文件最多包含 4MB 个 IP 地址;
- 对于每一个小文件,可以构建一个 IP 为 key,出现次数为 value 的 Hash map,同时记录当前出现次数最多的那个 IP 地址;
- 可以得到 1024 个小文件中的出现次数最多的 IP,再依据常规的排序算法得到总体上出现次数最多的 IP;
\
Day5、实现一个 key 为字符串,value 也是字符串的,而且并发安全的 map,拥有方法 set(key string, value string)、get(key string) string、del(key string)。
扩展要求1: 字典初始有64个桶,当有一半以上的队列有多个元素时,进行自动扩容,将桶的数量翻倍。
扩展要求2: 当一半以上的队列都为空或只有一个元素,并且这种情况持续1分钟,则自动缩容,最小缩容到64队列。
为了满足基础要求,可以实现一个hash map,并且加一个大锁(读写锁或者互斥锁都可以)即可实现;更近一步,可以考虑为每个桶加一把独立的锁,可以实现更小粒度的锁。接下来可以使用go test的banchmark能力去测试下不同读写压力、不同锁粒度、不同类型的锁下的性能对比。
如果为了实现字典的自动扩缩容,则需要实现两个hash map,一个是current hash map,另一个是next hash map。在每次读写中,将current hash map中的几个key迁移到next hash map。在读操作中,先找current hash map,如果key不存在,再找next hash map;在写操作中,会先删除current hash map中这个key,再写入next hash map。随着不断的读写,则会慢慢current hash map会为空,此时将next hash map改成current hash map即可。
这里有几个难点 :
如何去遍历current hash map,来做迁移?
需要记录上次遍历到的桶和key,以便于下次继续遍历
如何判断current hash map已经为空了?
记录一下map中key的个数,如果为0了,则说明为空了。可以使用atomic这个库来实现。
如何判断需要扩容 / 缩容?
也记录一下不为空的桶的数量。
\
Day6、给定包含 N 个任务 task 的数组 tasks 和整数 K,和一个可并发调用的执行函数 execute,要求实现以下逻辑:
- execute并发调用次数不超过10
- 以最快速度执行完所有task
- 使用golang
type Task int
func handle(tasks []Task, execute func(task Task)) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, 10)
for i, _ := range tasks {
ch <- struct{}{}
wg.Add(1)
task := tasks[i]
go func() {
defer wg.Done()
// execute task
execute(task)
<-ch
}()
}
wg.Wait()
}
\
Day7、时间复杂度 O(nlogn) 空间复杂度 O(1) (非递归) 的限制下从单链表中找出第 K 大的节点 。
类似题
\
Day8、某应用需要一个可靠的审核管道,为大量用户提供文章的审核入口,并对接了专业的文章审核团队,请为该管道设计一个数据结构。【实现一个并发安全的环形队列】
要求:
- 因为审核团队的人力有限,管道要起到流量控制作用,满了之后文章无法提交审核
- 高峰期时,多篇文章同时送审的事情常有发生,审核团队的多位同学也可能同时审核文章
- 先提交送审的文章应先被审核
- 进入审核管道的文章不能遗失、重复
- 每天有大量的文章送审,要尽可能节省审核管道的开销
首先,提炼题目要求:实现一个并发安全的环形队列,进阶/加分项是可以无锁;
然后,并发安全的环形队列的实现是相对比较简单的,主要考察数组的下标操作和边界情况的考虑;
最后,无锁主要有两个思路,考察操作系统/体系结构的基础是否扎实、是否具备较好的高性能并发编程思维:
- 内存屏障/内存格栅:操作系统未开放 API,对于 C/C++ 语言可以使用编译器的支持来实现,参考 DPDP rte_ring 等。
- 原子操作:C/C++/Java/Go/Rust 等语言都有支持,相对于内存屏障是更佳的选择。
特别注意,基于 Go channel 的实现不是无锁。
\