面试基础题

哎,干的好好的,突然公司没钱了,被裁员了,被迫又走上了面试之路。

go语言slice和map底层实现原理

在 Go 语言中,slicemap 是非常常用的数据结构,它们的底层实现机制虽然隐藏在语言内部,但理解它们的原理对于优化性能、正确使用和避免一些潜在的问题是非常有帮助的。以下是 Go 语言中 slicemap 的底层实现原理。

Slice(切片)底层实现

slice 是 Go 中基于动态数组的抽象,它实际上是一个 动态数组的视图,能够动态调整长度。slice 并不存储元素本身,而是提供对底层数组的引用。其底层结构可以用一个简单的结构体来理解:

type Slice struct {
    array unsafe.Pointer  // 指向底层数组的指针
    len   int             // 切片的长度
    cap   int             // 切片的容量
}
  1. array:底层数组
    slice 实际上是对底层数组的引用,数组的大小是固定的。切片之所以可以动态扩展,是因为当容量不足时,会自动生成一个更大的数组,将原有数据复制到新的数组中。

  2. len:长度
    切片的长度表示当前切片中的有效元素个数,由 len() 函数返回。即使底层数组可能更大,len 只代表当前视图中可以访问的元素数。

  3. cap:容量
    容量是指从切片的第一个元素开始,到底层数组末尾的最大元素数。cap() 函数返回容量。当切片的长度超过容量时,Go 会自动扩容。

扩容机制

当切片的容量不足时,Go 会自动将切片扩容。扩容的策略通常是以 2 倍 的方式增长(也有例外,特别是大于 1024 的情况会使用不同的策略)。扩容时,会创建一个新的更大的底层数组,然后将旧数据拷贝到新的数组中。

底层数据共享

切片中的多个变量可能共享相同的底层数组。例如,当你通过 slice[a:b] 创建一个子切片时,新切片仍然引用原切片的底层数组。修改子切片中的元素可能会影响到原切片中的元素。

Map(映射)底层实现

map 是 Go 中的哈希表实现,底层通过 散列表(Hash Table)来实现,能够在常数时间内(O(1))查找、插入和删除元素。

map 的底层结构可以简单地表示为:

type hmap struct {
    count     int            // map 中实际存储的键值对数量
    buckets   unsafe.Pointer // 指向 bucket 数组的指针
    B         uint8          // bucket 数组的大小是 2^B
    hash0     uint32         // 用于哈希函数的随机种子
    ...
}

核心概念

  1. buckets:桶(bucket)
    map 的核心是一个 bucket 数组,每个 bucket 存储多个键值对。通过哈希函数将键映射到某个 bucket,然后在该 bucket 中查找、插入或删除键值对。

  2. 哈希函数
    Go 使用哈希函数将键映射到 bucket 上。为了减少哈希冲突,Go 对每个 map 生成一个随机的种子 hash0,使得相同的键在不同的 map 中可能有不同的哈希值。

  3. 哈希冲突
    当多个键映射到同一个 bucket 时,会发生哈希冲突。Go 的 map 通过 链地址法(在 bucket 中存储多个键值对)处理冲突。

  4. 扩容机制
    map 中存储的元素过多时,会自动扩容。扩容时,Go 会创建一个新的、更大的 bucket 数组,并将所有的键重新分配到新的 bucket 中。这种操作被称为 rehashing,因为所有的键需要根据新的哈希值重新计算位置。

内存布局

  • bucket 数组:map 的核心存储结构。每个 bucket 包含若干个键值对以及指向溢出 bucket 的指针。
  • 溢出 bucket:当某个 bucket 中的元素过多时,Go 会为该 bucket 分配一个溢出 bucket,用来存储更多的键值对。
  • 指针管理:map 的 key 和 value 通过指针引用,避免直接复制大型对象,从而提高效率。

性能注意事项

  1. 切片的扩容成本:当切片需要扩容时,会发生内存分配和元素复制操作,因此在高性能场景下,需要合理地初始化切片的容量,避免频繁的扩容。

  2. map 的扩容和哈希冲突:虽然 map 的查找、插入和删除操作的平均时间复杂度是 O(1),但如果哈希冲突较多或者发生扩容,性能可能会下降。因此,合理的 map 大小初始化可以提高性能,特别是在高并发场景中使用时。

总结:

  • slice:动态数组的抽象,包含底层数组的引用、长度和容量,支持自动扩容。底层数据可以共享,扩容时会复制数据到新数组。
  • map:基于哈希表实现,通过哈希函数将键映射到桶(bucket),通过扩容来应对冲突和存储空间不足。

go语言协程调度原理,协程为什么快

Go 语言中的协程(goroutine)是 Go 并发编程的核心,其高效的调度机制使得 Go 能够在大规模并发程序中表现出色。相比传统的线程,Go 的协程更加轻量且高效,这源于其独特的 协程调度器用户态线程模型。下面我们将详细探讨 Go 协程的调度原理以及它为何如此快速。

协程(goroutine)的特点

  1. 轻量级:相比操作系统线程,协程的创建和销毁开销很小,通常只占用几 KB 的内存。而操作系统线程通常需要更大的内存栈(例如 1 MB 左右),这使得 Go 可以在同一时间内调度大量协程。
  2. 协作式调度:Go 的协程由 Go 运行时(runtime)来管理,并通过调度器来进行协作式调度,而不是交由操作系统内核进行调度,这大大降低了上下文切换的开销。
  3. 动态栈大小:Go 的协程具有动态增长的栈,初始栈空间很小(通常是 2KB 左右),并且随着程序的需求自动增长和收缩,避免了大量内存的浪费。

Go 语言的协程调度模型

Go 的协程调度器实现了类似于 M:N 的用户级线程调度模型,其中:

  • M 表示操作系统线程(OS Thread)。
  • N 表示 Go 的协程(goroutine)。

Go 使用了一个叫做 GMP 模型来管理协程调度,其中包含三大核心组件:

  • G(Goroutine):表示 Go 协程,所有的 Go 代码都在 Goroutine 中执行。每个 Goroutine 有自己的栈、指令计数器和其他上下文信息。
  • M(Machine):表示操作系统线程。M 是实际运行 G 的操作系统线程。
  • P(Processor):表示调度器执行 Goroutine 的上下文。P 持有一个本地运行队列,可以包含多个 Goroutine,P 负责将其队列中的 Goroutine 分配给 M 来执行。

GMP 模型的工作原理

  1. 调度器的结构
    Go 的调度器通过 G-P-M 模型 实现 Goroutine 的调度。每个 M(操作系统线程)都会绑定一个 P(执行上下文),并从 P 的运行队列中获取待执行的 Goroutine。M 和 P 是一对一绑定的,而多个 Goroutine 可以被放在 P 的本地队列中排队等待执行。

  2. 本地队列和全局队列
    每个 P 有一个本地队列,存放待执行的 Goroutine。调度器还维护了一个全局队列,如果 P 的本地队列中没有 Goroutine 可以运行,它就会从全局队列中取任务。如果一个 P 的 Goroutine 过多,它会将部分 Goroutine 偷偷移动到全局队列或其他空闲的 P 中。

  3. 抢占调度
    虽然 Go 的调度器大部分情况下是协作式的,但在 Go 1.14 中引入了 抢占式调度。这意味着 Goroutine 如果运行了太长时间(例如长时间没有发生函数调用),调度器可以强制其暂停并调度其他 Goroutine。这种机制防止了 Goroutine 长时间占用 CPU 而导致其他 Goroutine 饿死。

  4. M 和 P 的动态调整
    Go 运行时会根据负载情况动态调整 M(操作系统线程)的数量。通过 GOMAXPROCS 环境变量可以设置可以使用的 P 的数量,默认情况下,Go 会设置为逻辑 CPU 核心数。M 是与操作系统交互的实际执行线程,而 P 是调度 Goroutine 的核心单位。

协程为什么快?

  1. 轻量级的上下文切换
    相比操作系统级的线程上下文切换,Go 的协程上下文切换开销小得多。Go 协程上下文切换只需保存和恢复少量的栈和寄存器信息,完全在用户态完成,避免了陷入内核态带来的开销。而操作系统线程的上下文切换需要大量的状态保存和恢复,并且涉及到内核态的切换。

  2. 动态栈管理
    Go 的 Goroutine 初始栈空间非常小(2KB),并且会根据需要动态扩展到较大的栈(最高可以扩展到 1GB)。相比之下,操作系统线程通常会预先分配较大的栈空间(通常是 1MB)。Go 的动态栈避免了为每个 Goroutine 预分配大量内存的开销。

  3. 调度策略高效
    Go 的调度器采用协作式调度,减少了不必要的抢占调度。由于调度完全由用户态代码控制,减少了与操作系统内核的交互,进一步提高了调度效率。

  4. 资源复用与抢占调度
    在抢占式调度之前,协程调度依赖于协作式调度机制,即协程在进行 I/O 或系统调用时,主动交出 CPU。抢占调度机制加入后,长时间占用 CPU 的协程可以被强制中断,确保其他协程也能被及时调度,这种灵活的调度机制确保了 Go 可以处理大量的并发任务。

  5. 内存管理和垃圾回收
    Go 的垃圾回收机制(GC)经过多次优化,支持并发回收,使得内存管理在高并发场景下依旧高效。同时,Go 的栈内存管理基于 分段栈 模型,使得 Goroutine 可以在低内存消耗下高效运行,减少了内存压力。

Go 协程调度的优势总结

  • 轻量级:Goroutine 非常轻量级,内存占用极小,成千上万的 Goroutine 可以轻松被创建和调度。
  • 低开销调度:GMP 模型使得 Goroutine 调度更有效率,避免了频繁的系统调用和内核态切换。
  • 动态扩展:Go 的调度器可以根据 CPU 核心数和系统负载动态调整 Goroutine 的执行模式,并动态调整栈大小和操作系统线程数量。
  • 抢占机制:抢占式调度确保 Goroutine 不会因为执行时间过长而阻塞其他 Goroutine 的执行,提升整体系统的响应性。

总结

Go 协程的快速性来源于它轻量级的设计、有效的用户态调度模型(GMP 模型)、抢占式调度、动态栈管理,以及调度器的高效运行机制。相比操作系统的线程,Go 的协程更加高效和灵活,尤其在大规模并发场景中表现出色,因此成为 Go 语言高并发编程的基础。

mysql索引分为几种(b+tree hash表)

MySQL 中的索引是一种用于加速数据检索的结构,它能够显著提高查询性能。常见的 MySQL 索引类型主要有 B+树索引哈希索引,此外还有一些特殊类型的索引。每种索引在不同场景下都有其适用性和优缺点。

1. B+树索引

B+树索引 是 MySQL 中最常见的索引类型,主要应用在 InnoDBMyISAM 存储引擎中。B+树是一种平衡的树形结构,能够快速定位记录。大部分情况下,B+树索引适用于范围查询、排序等操作。

特点:

  • 顺序存储:叶子节点按键值从小到大顺序排列,叶子节点存储了实际的数据记录或者数据记录的指针。
  • 适用于范围查找:B+树索引支持 BETWEEN>, <, >=, <= 这样的范围查找。
  • 自动维护平衡:B+树会自动保持平衡,以确保查询性能始终保持较快。
  • 多层索引:B+树的层数通常比较少,因为它是一个高度平衡的树结构。随着数据量增大,树的高度增加会比较慢。

适用场景:

  • 等值查询(=
  • 范围查询(BETWEEN>, <
  • 排序查询
  • 联合查询

B+树索引示例:

在 MySQL 中创建 B+树索引的方式:

-- 创建普通索引
CREATE INDEX idx_column ON table_name(column);

-- 创建联合索引(多列索引)
CREATE INDEX idx_columns ON table_name(column1, column2);

B+树索引的不足:

  • 对于大量重复值的列,B+树索引的优势会下降,因为树结构中的分支效果有限。
  • 不支持直接的哈希匹配,这在需要高效的等值查询时会成为劣势。

2. 哈希索引

哈希索引 是基于哈希表实现的一种索引类型,它只适用于 Memory 存储引擎。哈希索引通过对键进行哈希运算,将哈希值作为索引存储,这使得它能够非常快速地进行等值查找操作。

特点:

  • 高效的等值查询:哈希索引在等值查询时表现非常出色,时间复杂度为 O(1),即能够直接定位到目标数据。
  • 不支持范围查询:由于哈希索引基于哈希值进行查找,数据的顺序关系被打乱,因此不支持范围查询(如 BETWEEN, >, < 等操作)。
  • 哈希冲突:如果多个键值通过哈希函数计算后得到了相同的哈希值,会产生哈希冲突。MySQL 采用链表法解决冲突问题。
  • 不支持排序:因为哈希索引不保留数据的顺序,无法用于排序操作。

哈希索引示例:

MySQL 中,哈希索引主要应用于 Memory 存储引擎。可以通过以下方式创建哈希索引:

-- 在 Memory 表中创建哈希索引
CREATE TABLE memory_table (
    id INT PRIMARY KEY,
    name VARCHAR(50)
) ENGINE=Memory;

-- 将索引类型指定为哈希索引
CREATE INDEX idx_name ON memory_table(name) USING HASH;

哈希索引的不足:

  • 不支持范围查询,例如 BETWEEN><
  • 不支持排序,因为哈希索引无法维护元素的顺序。
  • 哈希冲突可能影响查询性能,尤其是在哈希函数设计不当的情况下。

3. 全文索引(Fulltext Index)

全文索引 是用于对大量文本数据进行搜索的索引类型,主要用于 InnoDBMyISAM 存储引擎。在进行大量文本的模糊搜索或关键词匹配时,全文索引比普通的 LIKE 查询效率更高。

特点:

  • 适用于全文搜索:全文索引能够对文本内容进行分词,并建立倒排索引,适合于大文本内容的关键词匹配和全文搜索。
  • 支持复杂查询:通过 MATCHAGAINST 语句,能够进行复杂的全文搜索,支持布尔模式和自然语言模式的查询。

示例:

-- 创建全文索引
CREATE FULLTEXT INDEX idx_content ON articles(content);

-- 使用全文索引进行查询
SELECT * FROM articles WHERE MATCH(content) AGAINST('keyword');

全文索引的不足:

  • 不支持 InnoDB 的事务特性(早期版本),在现代 MySQL 版本中已有改进。
  • 不适用于小文本:适合大文本字段,不适合较短文本数据。

4. 空间索引(Spatial Index)

空间索引 是用于地理数据类型(如 POINT, LINESTRING, POLYGON 等)的索引,主要用于 MyISAM 存储引擎中,InnoDB 在 MySQL 5.7 开始支持空间索引。它通常用于地理信息系统(GIS)应用,用于处理二维空间的数据。

特点:

  • 适用于地理空间查询:用于地理空间数据的存储和查询。
  • 基于 R-Tree:空间索引是基于 R-Tree(或 B+树)实现的,能够高效地进行地理位置的查询。

示例:

-- 创建空间索引
CREATE SPATIAL INDEX idx_location ON locations(geo_point);

空间索引的不足:

  • 存储引擎限制:仅部分存储引擎支持空间索引,并且在 MySQL 中的应用相对较为专业。

5. 聚簇索引(Clustered Index)

聚簇索引 是 InnoDB 中默认的索引类型,在聚簇索引中,表中的数据行实际上存储在 B+树的叶子节点上。InnoDB 的主键索引就是一种聚簇索引,每个表只能有一个聚簇索引。

特点:

  • 数据存储和索引结合:数据行存储在索引的叶子节点,因此聚簇索引实际上就是数据本身的排序。
  • 高效的范围查询:由于数据按主键顺序存储,聚簇索引对于范围查询有较好的性能。

聚簇索引的不足:

  • 插入开销较大:如果新插入的数据顺序与现有数据顺序不一致,可能导致大量的页分裂,影响性能。
  • 只有一个聚簇索引:每张表只能有一个聚簇索引(通常是主键)。

总结

MySQL 中常见的索引类型包括:

  1. B+树索引:用于大部分查询场景,支持范围查找和排序,是 MySQL 中最常用的索引类型。
  2. 哈希索引:用于高效的等值查询,但不支持范围查询,适合特定存储引擎。
  3. 全文索引:用于全文搜索和文本匹配,适用于大文本字段的搜索操作。
  4. 空间索引:适用于地理空间数据的查询,在 GIS 应用中较为常见。
  5. 聚簇索引:InnoDB 存储引擎中默认的索引类型,数据按主键顺序存储,支持高效的范围查询。

Redis是单线程的,但Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容