1
头图

愉快的五一假期已经结束了, 又要投入到学习和工作当中了。

今天分享一位同学在阿里的Go后端实习面经详解, 希望对你有帮助。

Go里有哪些数据结构是并发安全的?

并发安全就是程序在并发的情况下执行的结果都是正确的;

Go中数据类型分为两大类:

  • 基本数据类型:字节型、整型、布尔型、浮点型、复数型、字符串
  • 复合数据类型:数组、切片、指针、结构体、字典、通道、函数、接口

字节型、布尔型、整型、浮点型取决于操作系统指令值,在64位的指令集架构中可以由一条机器指令完
成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的,但是这个也跟操作系统的位
数有关,比如int64在32位操作系统中,它的高32位和低32位是分开赋值的,此时是非并发安全的。

复数类型、字符串、结构体、数组,切片,字典,通道,接口, 这些底层都是struct,不同成员的赋值
都不是一起的,所以都不是并发安全的。

Go如何实现一个单例模式?

单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。

饿汉式

饿汉式实现单例模式非常简单,直接看代码:

package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
    return instance
}

singleton 包在被导入时会自动初始化 instance 实例,使用时通过调用 singleton.GetSingleton() 函数即可获得 singleton 这个结构体的单例对象。

这种方式的单例对象是在包加载时立即被创建,所以这个方式叫作饿汉式。与之对应的另一种实现方式叫作懒汉式,懒汉式模式下实例会在第一次被使用时被创建。

需要注意的是,尽管饿汉式实现单例模式的方式简单,但大多数情况下并不推荐。因为如果单例实例化时初始化内容过多,会造成程序加载用时较长。

懒汉式

接下来我们再来看下如何通过懒汉式实现单例模式:

package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

相较于饿汉式的实现,懒汉式将实例化 singleton 结构体部分的代码移到了 GetSingleton() 函数内部。这样能够将对象实例化的步骤延迟到 GetSingleton() 第一次被调用时。

不过通过 instance == nil 的判断来实现单例并不十分可靠,如果有多个 goroutine 同时调用 GetSingleton() 就无法保证并发安全。

  1. 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 上
  • 对于删除数据则直接通过标记来延迟删除

    数据结构
    type Map struct {
      // 加锁作用,保护 dirty 字段
      mu Mutex
      // 只读的数据,实际数据类型为 readOnly
      read atomic.Value
      // 最新写入的数据
      dirty map[interface{}]*entry
      // 计数器,每次需要读 dirty 则 +1
      misses int
    }

    其中 readOnly 的数据结构为:

    type readOnly struct {
      // 内建 map
      m  map[interface{}]*entry
      // 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty
      amended bool
    }

    entry 数据结构则用于存储值的指针:

    type entry struct {
      p unsafe.Pointer  // 等同于 *interface{}
    }

    属性 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语义的向后兼容性也是该版本的一个关键


王中阳Go
799 声望295 粉丝