阿里实习生:面试阿里其实并没有那么难。
![阿里实习生:面试阿里其实并没有那么难。](https://static001.geekbang.org/infoq/94/94f7a3cdcd50dd64db36fb9012233ca0.jpeg)
愉快的五一假期已经结束了, 又要投入到学习和工作当中了。
今天分享一位同学在阿里的 Go 后端实习面经详解, 希望对你有帮助。
![](https://static001.geekbang.org/infoq/ae/ae9207b430f7ccd257881c151f7036a9.png)
Go 里有哪些数据结构是并发安全的?
并发安全就是程序在并发的情况下执行的结果都是正确的;
Go 中数据类型分为两大类:
基本数据类型:字节型、整型、布尔型、浮点型、复数型、字符串
复合数据类型:数组、切片、指针、结构体、字典、通道、函数、接口
字节型、布尔型、整型、浮点型取决于操作系统指令值,在 64 位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的,但是这个也跟操作系统的位数有关,比如 int64 在 32 位操作系统中,它的高 32 位和低 32 位是分开赋值的,此时是非并发安全的。
复数类型、字符串、结构体、数组,切片,字典,通道,接口, 这些底层都是 struct,不同成员的赋值都不是一起的,所以都不是并发安全的。
Go 如何实现一个单例模式?
单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。
饿汉式
饿汉式实现单例模式非常简单,直接看代码:
singleton 包在被导入时会自动初始化 instance 实例,使用时通过调用 singleton.GetSingleton() 函数即可获得 singleton 这个结构体的单例对象。
这种方式的单例对象是在包加载时立即被创建,所以这个方式叫作饿汉式。与之对应的另一种实现方式叫作懒汉式,懒汉式模式下实例会在第一次被使用时被创建。
需要注意的是,尽管饿汉式实现单例模式的方式简单,但大多数情况下并不推荐。因为如果单例实例化时初始化内容过多,会造成程序加载用时较长。
懒汉式
接下来我们再来看下如何通过懒汉式实现单例模式:
相较于饿汉式的实现,懒汉式将实例化 singleton 结构体部分的代码移到了 GetSingleton() 函数内部。这样能够将对象实例化的步骤延迟到 GetSingleton() 第一次被调用时。
不过通过 instance == nil 的判断来实现单例并不十分可靠,如果有多个 goroutine 同时调用 GetSingleton() 就无法保证并发安全。
sync.map 的底层实现
什么是 sync.Map
Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent map writes。
因此官方另外引入了 sync.Map 来满足并发编程中的应用。
sync.Map 的实现原理可概括为:
通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
读取 read 并不需要加锁,而读或写 dirty 都需要加锁
另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
对于删除数据则直接通过标记来延迟删除
数据结构
其中 readOnly 的数据结构为:
entry 数据结构则用于存储值的指针:
属性 p 有三种状态:
p == nil: 键值已经被删除,且 m.dirty == nil
p == expunged: 键值已经被删除,但 m.dirty!=nil 且 m.dirty 不存在该键值(expunged 实际是空接口指针)
除以上情况,则键值对存在,存在于 m.read.m 中,如果 m.dirty!=nil 则也存在于 m.dirty
Map 常用的有以下方法:
Load:读取指定 key 返回 value
Store: 存储(增或改)key-value
Delete: 删除指定 key
channel 在什么情况下会 panic?
关闭为 nil 的 channel
关闭一个已经关闭的通道
向一个已经关闭的通道写数据
关闭通道导致发送阻塞的协程 panic
redis 有哪些数据结构,分别常用于哪些场合?
redis 的基本数据结构有: 1、String(字符串);2、Hash(哈希);3、List(列表);4、Set(集合);5、zset(有序集合)。
String(字符串)
String 类型是 Redis 中最基本、最常用的数据类型,甚至被很多用户当成 Redis 少数的数据类型去使用。String 类型在 Redis 中是二进制安全(binary safe)的,这意味着 String 值关心二进制的字符串,不关心具体格式,你可以用它存储 json 格式或 JPEG 图片格式的字符串。
应用:
存储一些配置数据:在前后分离式开发中,有些数据虽然存储在数据库,但是更改特别少。比如有个全国地区表。当前端发起请求后,后台如果每次都从关系型数据库读取,会影响网站整体性能。我们可以在名列前茅次访问的时候,将所有地区信息存储到 redis 字符串中,再次请求,直接从数据库中读取地区的 json 字符串,返回给前端。
缓存对象:将对象转为 json 存储,比如商品信息,用户信息。
数据统计:redis 整型可以用来记录网站访问量,某个文件的下载量,签到人数、视频访问量等等。(自增自减)
时间内限制请求次数:比如已登录用户请求短信验证码,验证码在 5 分钟内有效的场景。当用户首次请求了短信接口,将用户 id 存储到 redis 已经发送短信的字符串中,并且设置过期时间为 5 分钟。当该用户再次请求短信接口,发现已经存在该用户发送短信记录,则不再发送短信。
订单号(全局少数):有时候你需要去生成一个全局少数值的时候可以通过 redis 生成。关键命令:incrby(原子自增)。
分布式 session:当我们用 nginx 做负载均衡的时候,如果我们每个从服务器上都各自存储自己的 session,那么当切换了服务器后,session 信息会由于不共享而会丢失,我们不得不考虑第三应用来存储 session。
Hash(哈希)
Hash 的数据结构我们可以简单理解为 java 中的 Map,这种结构就特别适合存储对象,上面的 String 的类型确实也可以存储对象,但每次修改对象中的某一个属性,都要拿出整个 json 字符串在修改这个属性,之后在重新插入,而 hash 的接口特点让我们可以只修改该对象的某一个属性。
hash 数据类型在存储上述类型的数据时具有比 String 类型更灵活、更快的优势,具体的说,使用 String 类型存储,必然需要转换和解析 json 格式的字符串,即便不需要转换,在内存开销方面,还是 hash 占优势。
应用:
Redisson 分布式锁:Redisson 在实现分布式锁的时候,内部的用的数据就是 hash 而不是 String。因为 Redisson 为了实现可重入加锁机制。所以在 hash 中存入了当前线程 ID。
购物车列表:以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的 3 个要素。
缓存对象:hash 类型的 (key, field, value) 的结构与对象的(对象 id, 属性, 值)的结构相似,也可以用来存储对象。
List(列表)
List 类型是按照插入顺序排序的字符串链表,一个列表非常多可以存储 2^32-1 个元素。我们可以简单理解为就相当于 java 中的 LinkesdList。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis 将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。
应用:
消息队列:lpop 和 rpush(或者反过来,lpush 和 rpop)能实现队列的功能。
Set(集合)
Redis 中的 set 和 Java 中的 HashSet 有些类似,它内部的键值对是无序的、少数的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
应用:
抽奖活动:存储某活动中中奖的用户 ID ,因为有去重功能,可以保证同一个用户不会中奖两次。
zset(有序集合)
Sorted-Sets 中的每一个成员都会有一个分数(score)与之关联,Redis 正是通过分数来为集合中的成员进行从小到大的排序。成员是少数的,但是分数(score)却是可以重复的。
应用: 作为有序的,不可重复的列表,可以做一些排行榜相关的场景:
排行榜(商品销量,视频评分,用户游戏分数)
新闻热搜
说下缓存击穿,缓存穿透,缓存雪崩有什么区别?
缓存击穿
当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩
缓存击穿
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿。
缓存穿透
当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
主键索引和唯一索引的区别
主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
唯一性索引列允许空值,而主键列不允许为空值。
主键可以被其他表引用为外键,而唯一索引不能。
一个表最多只能创建一个主键,但可以创建多个唯一索引。
主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
在 RBO 模式下,主键的执行计划优先级要高于唯一索引。 两者可以提高查询的速度。
约束主要有:主键约束、外键约束、非空约束、检査约束(bentwen and ,大于、小于、等于、不等于)、唯一约束。
索引为什么使用 B+树,而不使用跳表?
B+树是多叉树结构,每个结点都是一个 16k 的数据页,能存放较多索引信息,所以扇出很高。三层左右就可以存储 2kw 左右的数据(知道结论就行,想知道原因可以看之前的文章)。也就是说查询一次数据,如果这些数据页都在磁盘里,那么最多需要查询三次磁盘 IO。
跳表是链表结构,一条数据一个结点,如果最底层要存放 2kw 数据,且每次查询都要能达到二分查找的效果,2kw 大概在 2 的 24 次方左右,所以,跳表大概高度在 24 层左右。最坏情况下,这 24 层数据会分散在不同的数据页里,也即是查一次数据会经历 24 次磁盘 IO。
因此存放同样量级的数据,B+树的高度比跳表的要少,如果放在 mysql 数据库上来说,就是磁盘 IO 次数更少,因此 B+树查询更快。
**而针对写操作,B+树需要拆分合并索引数据页,跳表则独立插入,**并根据随机函数确定层数,没有旋转和维持平衡的开销,因此跳表的写入性能会比 B+树要好。
计算机网络的多层模型简要介绍
应用层(Application):为用户的应用程序提供网络服务
表示层(Presentation):将信息表示为一定形式和格式的数据流
会话层(Session):负责通信主机之间会话的建立、管理和拆除,协调通信双方的会话
传输层(Transport):负责通信主机间端到端的连接
网络层(Network):负责将分组从源机送到目的机,包括寻址和最优路径选择等
数据链路层(Data Link):提供可靠的帧传递,实现差错控制、流控等等
物理层(Physical):提供透明的比特流(01 流)传递
http2.0 相比与 http1.1 的优化
HTTP2.0(Hypenext TransferProtocol version2)是超文本传输协议的第二版,HTTP2.0 相比于 HTTP1x,大幅度的提升了 web 性能,同时向下兼容 HTTP1.X 协议版本。
主要核心优势有
1、采用二进制格式传输数据,而非 htp1.1 文本格式,二进制格式在协议的解析和优化扩展上带来了跟多的优势和可能
2、对消息头采用 Hpack 进行压缩传输,能够节省消息头占用的网络流量,htp1.1 每次请求,都会携带大量冗余的头信息,浪费了很多宽带资源,
3、异步连接多路复用
4、Server Push,服务器端能够更快的把资源推送到客户端。
5、保持与 HTTP 1.1 语义的向后兼容性也是该版本的一个关键
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
本文首发在我的同名公众号:王中阳Go,未经授权禁止转载。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/04da63614a016b8c5777cbf66】。文章转载请联系作者。
评论