又是一年毕业季,给大家分享下最近三个月的中小厂社招面试的面经,这里已经整理出了我的答案或思路供大家参考。
go基础相关:
- slice和数组的区别
1.数组是定长的,是一片连续的内存,长度定义好后不能修改;切片是灵活的,可以动态扩容,切片是一个结构体,包括指向底层数组的指针、长度、容量;
2.作为参数传递时,数组是值传递,函数内对数组的值改变不影响原数组;切片是引用传递,函数内对元素的修改在函数外值也会改变。
- slice的扩容机制
Go1.18之前切片的扩容是以容量1024为临界点,当旧容量 < 1024个元素,扩容变成2倍;当旧容量 > 1024个元素,那么会进入一个循环,每次增加25%直到大于期望容量。
Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量 3*256)/4;
当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量 3*threshold)/4。
- go引用类型有哪些?
指针、slice、map、channel、interface,上述引用类型不可比较,值类型可以比较。
- 两个结构体可以进行等值比较吗?
1.结构体能比较是否相等,不能比较大小;
2.相同类型的结构体才能比较,结构体相同指属性类型和属性顺序都相同;
3.如果struct中所有成员都可以比较,则该struct就可以通过==或!=进行比较,比较时对每个成员逐个比较,每一项都相等,两个结构体才相等。
- go interface
interface定义了一组方法的集合,而不关心具体的实现。
多态性:允许不同的类型实现相同的方法。意味着可以使用接口类型来处理不同的对象,而不需要关心具体的类型。
解耦合:隐藏实现细节,只暴露必要的方法。
可扩展性:为interface添加了新方法,实现新方法即可,不需要修改已有代码。
应用场景:API设计、单元测试、插件系统、依赖注入。
- context是如何使用的?
context可用于在多个goroutine之间传递用户认证信息,通过context.WithValue设置,context.Value获取;
设置截止时间或超时,通过context.WithDeadline 或 context.WithTimeout设置;
监听取消信号,使用context.Done获取一个channel,goroutine可以通过监听这个channel来决定是否停止操作;
- 对未初始化的的channel进行读写,会怎么样?为什么?
读写未初始化的channel都会阻塞。未初始化的channel为nil,在goroutine向channel中读写时会导致goroutine阻塞。
- 对一个channel读写操作分别会有什么异常结果?
读已关闭返零值,写已关闭panic;无缓冲时接受在发送后会panic死锁,有缓冲时超出缓冲也会死锁。
- Channel能多次关闭吗?
不能,只能关闭一次,如果尝试多次关闭会导致panic。
- select与channel配合使用,select时如何执行的?
1select {
2case <-ch1:
3 //...
4case rec := <-ch2:
5 //...
6case ch3 <- 10:
7 //...
8default:
9 //默认操作
10}
每个case必须是一个IO操作,case是随机执行的,如果多个case同时满足,select会随机选择一个执行;如果所有case不能执行,则会执行default,如果没有default则会阻塞。
- map删除一个key 内存会不会释放?
在go中删除一个map中的key时,与该key关联的内存会被释放,但map本身的内存不会被释放。因为map是引用类型,内存分配是动态的,并且map的容量是不会减少的。即使删了所有的key也不会被回收,要想释放map本身内存,需要将map设置为nil。
- 介绍下GMP模型
M是指线程,G指goroutine,P负责衔接M和G的调度上下文,将等待执行的G与M对接。有P的原因是线程阻塞时可以放弃当前的上下文P,交给其他的M继续执行goroutine;另外也可以均衡的分配工作,当一个P跑完自身的goroutine队列后从其他有很长队列的P中偷来一半执行。
- golang怎么判断对象是分配到堆上还是栈上?
逃逸分析:编译器的优化过程,分析变量的生命周期,如果超出了函数的执行范围,变量需要分配到堆上,如果生命周期只在函数内部,变量就会分配到栈上。
其中编译器无法确定的参数类型放到堆中;
如果变量在函数外部存在引用,则放到堆中;
如果变量占用内存较大时,优先放入堆中;
如果变量在函数外部没有引用,优先放入栈中;
我们通常说的内存管理也是主要指堆内存的管理,栈的内存管理不需要程序操心。
- 栈和堆的对比:
栈:内存地址连续,编译器自动分配给每个goroutine一个自己的栈区,不能被其他goroutine使用;栈区往往存储函数参数、局部变量和调用函数栈,函数创建时分配,函数退出时销毁;不需要加锁;内存管理性能好;缓存性能好(存储空间更连续)。
堆:内存地址不连续,由编译器和工程师管理堆内存分配,由Runtime GC释放,垃圾回收器回收(标记清除-三色标记法)。有时需要加锁防止多线程冲突;内存管理性能差;缓存性能差。
- GC中的根节点是什么?
指被直接或间接引用的对象集合。通常包括:全局变量和静态变量、调用栈中的变量、当前执行的goroutine。
垃圾回收器从根节点开始遍历,查找所有可以访问到的对象,标记为可达,没有被标记的就是垃圾对象,可以被回收。
- 写一个方法的时候是传值好还是结构体好?
传值安全性高但内存占用较大,传引用不需要复制大块数据性能较好。
如果结构体很小,或者不想让函数修改原始值,传值;
如果结构体很大,或者想要函数修改原始值,传指针;
- Python和Go的区别:
语言特性:python是一种动态强类型的解释型语言,Go是静态类型的编译型语言。
内存占用:goroutine默认占用内存约为2KB,远比线程默认栈空间8MB小。
GC相关:Python主要依赖于引用计数和周期性垃圾回收,而Go则采用并发写屏障和三色标记-清除算法。
并发编程:pythonGIL锁限制了同一时刻只有一个线程可以执行,而goroutine由runtime管理,可以创建成千上万个goroutine来处理并发任务。
- 使用go实现1000个并发控制,并设置执行超时时间为1秒:
1func worker(c context.Context, wg *sync.WaitGroup, id int) {
2 defer wg.Done()
3
4 select {
5 case <-time.After(time.Second):
6 fmt.Sprintln("执行完成 %d", id)
7 case <-c.Done():
8 fmt.Sprintln("执行超时 %d", id)
9 }
10}
11
12func main() {
13 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
14 defer cancel()
15 var wg sync.WaitGroup
16 wg.Add(1000)
17 for i := 0; i < 1000; i {
18 go worker(ctx, &wg, i)
19 }
20 wg.Wait()
21}
mysql索引相关:
- B 树有什么特点?
非叶子节点存储键,叶子节点存数据,树高较低提高查询效率;减少磁盘IO次数;
叶节点之间双向指针便于范围查询;
- 为什么不用B树?
查询的速度差不多,因为b 树数据都在叶子节点,可以减少磁盘IO次数。
- 非聚簇索引和聚簇索引的区别?
聚簇索引决定了数据在磁盘上的物理存储顺序,聚簇索引的叶子节点包含了表中的所有行数据,通常基于主键索引创建;一个表中主键只有一个,所以聚簇索引只能有一个;
非聚簇索引的叶节点上存放的是指向聚簇索引或者数据行的指针;一个表可以有多个非聚簇索引,因为非聚簇索引不影响数据的物理存储顺序。
- 为什么非要把聚簇索引的键值放到非聚簇索引的叶节点上呢?回表不是会增加磁盘IO吗?
先访问非聚簇索引,再根据聚簇索引的键值去访问聚簇索引或直接访问数据行,这种设计的优势超过了额外的磁盘I/O开销。
灵活:允许数据库为不同的查询条件创建不同的索引;
覆盖索引:直接从非聚簇索引中获取所有需要的数据,而不需要回表到聚簇索引;
多列索引:提高多列查询的效率;
顺序访问:非聚簇索引的叶子节点通常是有序的,顺序访问可以通过预读等技术减少磁盘IO。
- 索引为什么要用id不用字符?
存储空间占用更小,更快地加载到内存;基于数值比基于字符串比较速度更快;整数类型的ID一般是自增的,顺序写磁盘,减少数据碎片;ID更具有唯一性;
- mysql中,如何判断一个字段是否适合建立索引?
1.该字段是否经常作为查询条件;
2.区分度高的字段;
3.列的数据类型,数值字段效率较文本字段效率高;
4.更新频率:写操作少的字段,经常发生写操作,维护B 树索引结构会降低效率;
- 索引失效的场景:
索引失效是指数据库索引无法被查询优化器使用,导致查询性能下降的情况。
使用LIKE操作符进行模糊匹配,查询条件中的数据类型与索引列的数据类型不匹配导致隐式类型转换,查询中对索引列进行了计算或使用了函数;
如果数据量小, 不走索引;当使用order by时, 如果发现走索引的效率比较慢, 也会舍弃索引;
- 最左前缀法则的原理是什么?
复合索引会按照索引列的顺序对数据进行排序,最左前缀表示当查询条件包含复合索引中最左边的列时,数据库能够利用索引来加速查询。
- 该语句为什么查询慢? 有什么优化思路?
select id,name,balance from account where update_time > '2020-09-19' limit 100000, 10
大偏移量:LIMIT 100000, 10表示跳过前100,000条记录,然后返回接下来的10条记录。查询效率低下,数据库需要先找到并计算前100000条记录,再返回接下来的10条记录,应避免使用大偏移量,使用分页查询(主键分页),每次查询从新位置开始,而不是跳过大量记录;
代码语言:javascript复制-- Get the last id of the previous page
SELECT id FROM account WHERE update_time > '2020-09-19' ORDER BY id DESC LIMIT 10;
-- Use the last id to get the next page
SELECT id, name, balance FROM account WHERE update_time > '2020-09-19' AND id < last_id_of_previous_page ORDER BY id DESC LIMIT 10;
索引缺失:如果update_time字段没有索引,会走全表扫描;
优化查询逻辑:分解为多个小查询;
- 是否做过做索引优化, 如何减少回表次数?
覆盖索引:索引包含了查询所需的所有字段;
索引下推:在索引扫描阶段过滤掉不满足条件的数据(5.6及以上版本支持);
使用分区表:将大表分解为小表,减少单个索引大小。
mysql事务相关:
- Innodb的ACID是如何保证的?
原子性:重做日志redoLog、两阶段提交
一致性:事务、外键约束
隔离性:MVCC、锁机制
持久性:重做日志redoLog、双写缓冲区
- 遇到过幻读吗? 如何解决幻读?
幻读:当前事务查询不到对应数据,但是插入数据插不进去。
可以使用锁机制,MVCC,或者select ... for update: 保证当前事务中查询的范围加锁, 不被其它事务修改。
mysql锁相关:
- Mysql什么时候是行锁什么时候是表锁?
InnoDB存储引擎:默认使用行锁,当对某一行数据操作时,锁定行而不是整个表;当全表扫描时使用表锁,如果事务涉及的操作无法通过行锁实现,也会使用表锁;
MyISAM存储引擎:只支持表锁,每次写操作会锁定整个表;
- 乐观锁和悲观锁的区别? 写多读少的情况, 应该使用乐观锁还是悲观锁?
悲观锁在数据访问时加锁,通常用于写密集型场景,或者在数据冲突概率较高的情况下使用,排他锁;
乐观锁在提交更新时进行冲突检查,通常用于读密集型场景,或者在数据冲突概率较低的情况下使用,版本号机制,时间戳机制;
- 两个线程, 同时向mysql中插入一条数据?
唯一索引冲突:如果数据库表中有唯一索引,如主键或唯一约束,那么两个线程的插入操作中只有一个会成功,另一个会因为违反唯一性约束而失败;使用事务和select for update检查数据是否存在,在事务中锁定数据行,另一个线程被阻塞,直到第一个线程的事务结束;
- 提交了怎么样的事务有可能导致死锁?
死锁:指两个或者多个事务在执行过程中,互相等待对方持有的资源而无法继续执行的情况。
1.交叉锁定资源:两个事务分别持有一个资源,并且尝试获取对方持有的资源;
2.资源请求顺序不一致:事务A先请求资源X再请求资源Y,事务B先请求资源Y再请求资源X;
3.长时间持有资源:事务长时间持有资源不释放,导致其他事务等待资源;
sql题:
数据库中有以下两张数据表:广告表 表名:ad 字段:id, title, create_time 订单表 表名:order 字段:id, ad_id, cost, create_time 请写一条 SQL,查询订单数量最多的前十条广告,并返回以下信息:广告ID、广告名称、订单数量总和、订单费用总和。
代码语言:javascript复制 1SELECT
2 a.id AS ad_id,
3 a.title AS ad_title,
4 COUNT(`order`.id) AS total_orders,
5 SUM(`order`.cost) AS total_cost
6FROM
7 ad a
8JOIN
9 `order` ON a.id = `order`.ad_id
10GROUP BY
11 a.id, a.title
12ORDER BY
13 total_orders DESC
14LIMIT
15 10;
计算机基础相关:
- 有线程为什么还需要协程? 主要是对线程的哪个问题进行了优化?
1.上下文切换开销/创建和销毁的开销:协程是用户态的轻量级线程,上下文切换开销小,线程是由操作系统内核管理,上下文切换需要内核态和用户态之间切换;
2.内存占用:协程内存占用更小;
3.同步机制:线程之间同步(互斥锁),协程由于在单线程内运行,可使用channel同步;
- 进程之间有哪些通信机制?
1.管道:允许单向数据流的通信机制。
2.消息队列:进程将信息发送到队列中,其他进程从队列中接受消息。
3.共享内存:允许多个进程访问同一块内存空间,需要互斥锁避免数据冲突。
4.信号:异步通知进程发生了某种事件,进程可设置信号处理函数来响应信号。
网络相关:
- http2.0和1.1的区别?
1.多路复用:1.1每个请求必须是独立的TCP请求,2.0采用多路复用(解决对头阻塞问题),一个TCP连接可以进行多次请求;
2.数据传输:2.0引入数据流,允许不同请求在同一连接交错发送;
3.头部处理:2.0采用HPACK算法对头部数据进行压缩,降低数据大小和网络开销;
4.二进制协议:1.1报头必须是文本,数据体可以是文本或者二进制,2.0头和数据体都是二进制,更加高效处理数据;
5.错误处理:1.1处理错误需要关闭连接,2.0可以在不关闭连接情况下处理错误;
- http协议和RPC协议的区别?
1.设计理念:http是基于请求/响应的应用层协议,用于web之间的通信;RPC是远程过程调用,允许客户端调用远程服务器上的函数或过程;
2.适用场景:内部不同服务间有高性能需要的通信一般用RPC,对外有安全性需求的接口一般用http;
3.连接方式:RPC通常基于长连接,如分布式系统中,服务间的相互调用,长连接在建立连接后保持连接状态,可以减少连接和断开连接的开销,不过在一些轻量级RPC调用场景中,通信不频繁时RPC会采用短连接;HTTP1.1之前是短连接,1.1开始引入持久连接(但本质上仍然是基于请求/响应),http2和3引入了多路复用,允许单个连接上并行发送多个请求和响应。
- webSocket是长连接还是短连接?使用websocket连接, 还需要自己实现心跳保活吗?
长链接,webSocket提供了客户端和服务器之间全双工通信渠道。webSocket和http都基于TCP,单http通信是单向的,即客户端发送请求服务器响应,webSocket是双向的;http协议适用于web服务和API通信,webSocket适用于实时通信的场景,如在线游戏、实时交易、聊天应用等。
不需要, websocket自己已经实现了心跳保活机制, 只需要设置pingInterval和pingTimeout即可。
- https的加密是对称加密吗?
内容传输是对称加密, 证书校验是非对称加密。
中间件相关:
- kafka挂了, 如何保证异步消息不丢失?
1.设置ack级别为-1, 所有副本都收到才算成功写入;
2.设置重试次数, 发送失败可以重试;
3.开启幂等性, 确保重试也不会产生重复的消息;
4.将消息写入mysql数据库, 然后再异步发送到kafka。
- kafka事务消息?
指生产者向kafka发送消息时, 要么全部发送成功, 要么全部发送失败并回滚。确保了消息的一致性,原子性操作和精准一次消费。
生产者产生一条事务消息, 获取一个事务id, 发送事务消息时, 是同步发送的, 保证消息一定顺利写入broker。
如果发送的是异步消息, 对于生产者来说, 发送后即显示发送成功, 但是下游broker的ack级别如果是-1, 那么只有该消息全部副本同步成功, 才算发送成功,所以异步消息还是会受ack级别影响。
- kafka的幂等性? 如何避免消息被重复消费?
幂等性:生产者重复发送多次消息,也只会被记录一次。
幂等性消费者:从Kafka 2.5版本开始,引入了幂等性消费者,在消费者端启用enable.idempotence配置;
手动提交位移:消费者在消息处理完手动提交位移;
事务性消费者:在事务中处理消息,在事务提交时提交位移。
- 常见的负载均衡策略
轮询:按顺序将请求分配到后端服务器;
加权轮询:根据处理能力分配不同权重;
最少连接:将请求分配到当前连接数最少的服务器;
IP哈希:根据客户端IP通过哈希表来分配请求,确保同一个客户端请求总是被分到一个服务器(适用于会话保持场景)。
一致性哈希:将请求和服务器映射到一个哈希环上,请求会被分配到顺时针方向的第一个服务器。
Redis相关:
- redis数据类型及应用场景:
string: 缓存对象、常规计数、分布式锁、共享session等。SDS(简单动态字符串实现)
List:消息队列(但是有两个问题:1.生产者需要自行实现全局唯一ID;2.不能以消费者组形式消费数据)等。quicklist实现。
Hash:缓存对象、购物车等。listpack实现。
Set:聚合计算的场景(交集、并集、差集),如点赞,共同关注,抽奖活动等。哈希表和整数集合实现。
ZSet:排序场景,比如排行榜、电话和姓名排序等。跳表和listpack实现。
BitMap:二值状态统计的场景,如签到、判断用户登录状态、连续签到用户总数等。
HyperLogLog:海量数据基数统计的场景,比如百万级网页UV计数。
GEO:存储地理位置,如滴滴叫车。
Stream:消息队列,相比于List实现消息队列,stream支持自动生成全局唯一消息ID,支持以消费组形式消费数据。
- redis为什么采用单线程还那么快?
1.大部分操作在内存中完成,采用了高效的数据结构。
2.单线程模型避免了多线程之间的锁竞争,省去线程切换的开销。
3.采用了IO多路复用处理大量客户端socket请求。
redis6.0采用了多线程提高网络并行度,但命令执行依然是单线程处理。
- Redis如何实现数据不丢失?
AOF日志:每执行一条写操作命令,将命令追加写到文件中;
RDB快照:某一时刻的内存数据,以二进制方式写入磁盘;
混合持久化方式集成了 AOF 和 RBD 的优点;
- Redis集群如何实现服务高可用?
主从复制(读写分离)、哨兵模式(故障转移)、切片集群模式(哈希槽处理数据和节点间的映射关系)
- Redis过期删除策略是什么? redis删除key时, 是新启动一个进程处理删除任务吗?
惰性删除:当一个过期的key被访问时,Redis会检查它是否过期,如果过期会删除这个key;
定期删除:定期检查过期的key并删除它们;
不是,删除key是由Redis的主线程在事件循环中处理,删除操作是同步的,会阻塞主线程。
- Redis内存淘汰机制有哪些?
不进行淘汰;
random 随机淘汰;
lru 淘汰最久未使用的键值;
淘汰最少使用的键值。
- Redis缓存设计
1.缓存雪崩:大量缓存在同一时间过期,大量用户请求打到数据库导致数据库宕机。
解决:缓存不过期或者失效时间随机打散。
2.缓存击穿 :热点数据过期,大量请求打到数据库。
解决:互斥锁或者不设置过期时间。
3.缓存穿透:用户访问的数据既不在缓存中也不在数据库,大量访问请求打到数据库。
解决:非法请求限制、设置空值或者默认值、使用布隆过滤器快速判断数据是否存在。
- 如何设计一个缓存策略,可以动态缓存热点数据呢?
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:
先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。
- 如何实现一个延迟队列?
可以使用zset实现,zset有一个score属性可以用来存储延迟执行时间,使用zadd score1 value1命令就可以一直往内存中生产消息,再利用zrangebyscore查询符合条件的所有待处理任务,循环执行队列任务即可。
系统设计相关:
- 如果让你设计一个消息队列,你会如何设计?需要考虑哪些问题?
思路:
功能上:消息模型是采用点到点还是发布订阅模型,消息是否需要持久化,是否需要保证消息顺序,如果消息传递失败是否要自动重试,如何处理消费失败的消息,确定消息传递是同步的还是异步的。
性能上:是否支持高可用,在组建故障时继续工作,是否支持水平扩展,如何确保安全性。
架构上:如何管理多个队列,包括创建、删除、监控等,如何在多个队列上分配负载,如何设计容错机制等。
- 假设需要请求第三方接口,而第三方接口不太稳定,你会怎么设计?
1.重试机制:请求失败时重试几次,或者重试之间间隔逐渐增加。
2.断路器模式:重试达到一定次数,暂停请求,给第三方接口恢复时间。
3.缓存:如果第三方接口不是实时变化的,可以使用本地缓存。
算法相关:
- 旋转数组:
func rotateArray(arr []int, k int) []int {
// 原地旋转
n := len(arr)
if arr == nil || n == 0 {
return arr
}
if k > n {
k %= n
}
if k == 0 || k == n {
return arr
}
reverse(arr)
reverse(arr[:k])
reverse(arr[k:])
return arr
}
func reverse(nums []int) {
n := len(nums)
for i, j := 0, n-1; i < j; i, j = i 1, j-1 {
nums[i], nums[j] = nums[j], nums[i]
}
}
func main() {
arr := []int{1, 2, 3, 4, 5, 6, 7}
k := 3
fmt.Println(rotateArray(arr, k))
}
- 简述动态规划思路:
动态规划(DP)的基本原理可以概括为:问题分解-存储子问题的解避免重复计算(记忆化)-自底向上求解-最优子结构。具体实现方式为:
a. 定义一个一维数组 ---> 一般用dp来命名。
b. 动态方程的设定 ---> 题中的F(N) = F(N - 1) F(N - 2)。
c. 初始化数值 ---> F(0) = 0和F(1) = 1。
上述的 a、b 和 c 点就是动态规划思想的几个核心要素。