大家好,我是王不错。
今天分享的是百度后端开发实习生的二面面经。本篇面经偏向对MySQL数据库、Redis缓存、算法、go语言的考察。
面试部门:百度网盘,主要负责百度网盘的服务端研发工作等,主要技术栈为go。
为了助力25届暑期实习和校招,我将陆续分享小伙伴们提供的面经,和我去年暑期实习面试的题目总结,同时从我的角度为每一道题提供一份答案,为了你能第一时间看到我分享的面经,可以把我的公众号置为星标哦!
先简单介绍下自己。
你研究生阶段的研究方向是什么?接下来是针对研究生方向上的一些问题深问。
使用过MySQL数据库吗?如果数据量非常大,有哪些优化方案?
这里将MySQL数据量大时的优化方案做一个汇总:
简单来说,优化数据表的方案分成两种:一是单表优化,二是分库分表(不要上来就想着分库分表,因为表一旦被拆分,开发、运维的复杂度会直线上升,而大多数公司是欠缺这种能力的)。
a. 单表优化
(1)表分区:可以看做是水平拆分,分区表需要在建表的需要加上分区参数;分区表底层由多个物理子表组成,但是对于代码来说,分区表是透明的;SQL中的条件中最好能带上分区条件的列,这样可以定位到少量的分区上,否则就会扫描全部分区。
(2)主从复制、读写分离:最常用的优化手段,写主库读从库;
(3)增加缓存:主要的思想就是减少对数据库的访问,缓存可以在整个架构中的很多地方,比如:数据库本身就有缓存,客户端缓存,数据库访问层对SQL语句的缓存,应用程序内的缓存,第三方缓存(如Redis等);
(4)字段设计:单表不要有太多字段;VARCHAR的长度尽量只分配真正需要的空间;尽量使用TIMESTAMP而非DATETIME;避免使用NULL,可以通过设置默认值解决。
(5)索引优化:索引不是越多越好,针对性地建立索引,索引会加速查询,但是对新增、修改、删除会造成一定的影响;值域很少的字段不适合建索引;尽量不用UNIQUE,不要设置外键,由程序保证;
(6)SQL优化:尽量使用索引,也要保证不要因为错误的写法导致索引失效;比如:避免前导模糊查询,避免隐式转换,避免等号左边做函数运算,in中的元素不宜过多等等;
(7)NoSQL:有一些场景,可以抛弃MySQL等关系型数据库,拥抱NoSQL;比如:统计类、日志类、弱结构化的数据;事务要求低的场景。
b. 表拆分
数据量进一步增大的时候,就不得不考虑表拆分的问题了:
(1)垂直拆分:垂直拆分的意思就是把一个字段较多的表,拆分成多个字段较少的表;上文中也说过单表的字段不宜过多,如果初期的表结构设计的就很好,就不会有垂直拆分的问题了;一般来说,MySQL单表的字段最好不要超过二三十个。
(2)水平拆分:就是我们常说的分库分表了;分表,解决了单表数据过大的问题,但是毕竟还在同一台数据库服务器上,所以IO、CPU、网络方面的压力,并不会得到彻底的缓解,这个可以通过分库来解决。水平拆分优点很明显,可以利用多台数据库服务器的资源,提高了系统的负载能力;缺点是逻辑会变得复杂,跨节点的数据关联性能差,维护难度大(特别是扩容的时候)。
详细说下你提到的主从复制与读写分离?
读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
在一些读多写少的场景下多会考虑使用读写分离。
主从复制包括三种:
(1)基于语句的复制(STATEMENT):在主服务器上执行的SQL语句,在从服务器上执行同样的语句。MySQL默认采用基于语句的复制,效率比较高。
(2)基于行的复制(ROW):把改变的内容复制过去,而不是把命令在从服务器上执行一遍。
(3)混合类型的复制(MIXED):默认采用基于语句的复制,一旦发现基于语句无法精确复制时,就会采用基于行的复制。
内存缓存Redis有了解嘛?说一下缓存击穿、缓存穿透、缓存雪崩,谈谈概念和解决方案。
(1)缓存击穿:指数据在缓存中无法读取直接访问到了数据库,一般原因是缓存中的某个热点数据过期了。解决方案有两个:a. 读取不到直接返回空值或默认值;b. 热点数据不设置过期时间,永不过期,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
(2)缓存穿透:数据既不在缓存中也不在数据库中,一般原因是业务误操作,将数据库中的数据都被误删除了,另外也可能是黑客恶意攻击,故意访问数据库中不存在的数据;解决方案:a.限制非法请求,比如同一IP的多次请求都造成了缓存穿透,直接拦截;b. 缓存空值或默认值;c.使用布隆过滤器先进行一次判断。
(3)缓存雪崩:大量数据无法从缓存中读取,直接访问数据库,一般原因是大量数据同时过期,或Redis故障宕机。应对方法有:a. 均匀设置数据过期时间;b. 服务熔断或限流。
Redis怎么实现一个分布式锁?
可以通过setnx+expire命令来实现,首先通过setnx命令来抢锁,抢到之后根据expire命令给锁设置一个过期时间,防止忘记锁释放;为了防止忘记设置expire,可以通过set key vlaue[EX seconds]方式设置原子操作。
算法:提到锁,你就用两个协程交替打印出1-100吧,然后使用锁来保证协程并发安全
使用条件变量(sync.Cond)、互斥锁(sync.Mutex)、sync.WaitGroup 来同步两个 goroutine (以及主线程)的执行,完整代码如下:
package main
import (
"sync"
)
var (
// 定义一个变量,区分奇偶数
condition int = 1
// 计数器
count int = 1
wg sync.WaitGroup
)
// 交替打印时,一个函数打印奇数部分
func printOdd(c1, c2 *sync.Cond) {
for {
// 条件变量加锁
// 获取 c1 条件变量的互斥锁
c1.L.Lock()
// 如果 condition 不是 1(意味着当前不应该打印奇数),则等待 c1 条件变量
if condition != 1 {
c1.Wait()
}
// 当 condition 为 1 时,释放锁并打印当前 count 值,然后 count 自增
c1.L.Unlock()
if count <= 100 {
println(count)
count++
}
// 获取 c2 条件变量的互斥锁,设置 condition 为 2(表示现在应该打印偶数),发送信号给等待 c2 的 goroutine,然后释放锁
c2.L.Lock()
condition = 2
c2.Signal()
c2.L.Unlock()
// 如果 count 大于 100,函数会返回,结束执行
if count > 100 {
wg.Done()
return
}
}
}
// 打印偶数:过程与 printOdd 类似
func printEven(c1, c2 *sync.Cond) {
for {
// 条件变量加锁
c1.L.Lock()
if condition != 2 {
c1.Wait()
}
c1.L.Unlock()
if count <= 100 {
println(count)
count++
}
c2.L.Lock()
condition = 1
c2.Signal()
c2.L.Unlock()
if count > 100 {
wg.Done()
return
}
}
}
func main() {
// 创建一个互斥锁 m ,两个基于该锁的条件变量 c1 和 c2
// 互斥锁确保同一时间只有一个 goroutine 可以更改共享资源
m := &sync.Mutex{}
c1 := sync.NewCond(m)
c2 := sync.NewCond(m)
wg.Add(2)
go printOdd(c1, c2)
go printEven(c2, c1)
// 主线程等待两个协程执行完毕再退出
wg.Wait()
}
反问:部门常用的技术栈。
面试官很负责,对部门的技术栈讲解的非常全面。
语言:百度之前主要是C++和PHP,现在逐步转到了go、Rust;
数据库需要熟悉MySQL,以后你会看到实践中怎样使用读写分离;
会使用Redis做缓存和消息队列,分布式锁;
也有一些中间件,比方说消息队列Kafka;
分布式应用开发:redis集群,Raft协议,zookeeper
项目部署上,很多场景并发量比较高,几百万请求QPS
最后项目管理,我们也会使用Docker, K8S容器技术。
暂无评论内容