哎,干的好好的,突然公司没钱了,被裁员了,被迫又走上了面试之路。
go语言slice和map底层实现原理
在 Go 语言中,slice 和 map 是非常常用的数据结构,它们的底层实现机制虽然隐藏在语言内部,但理解它们的原理对于优化性能、正确使用和避免一些潜在的问题是非常有帮助的。以下是 Go 语言中 slice 和 map 的底层实现原理。
Slice(切片)底层实现
slice 是 Go 中基于动态数组的抽象,它实际上是一个 动态数组的视图,能够动态调整长度。slice 并不存储元素本身,而是提供对底层数组的引用。其底层结构可以用一个简单的结构体来理解:
type Slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度
cap int // 切片的容量
}
array:底层数组
slice实际上是对底层数组的引用,数组的大小是固定的。切片之所以可以动态扩展,是因为当容量不足时,会自动生成一个更大的数组,将原有数据复制到新的数组中。len:长度
切片的长度表示当前切片中的有效元素个数,由len()函数返回。即使底层数组可能更大,len只代表当前视图中可以访问的元素数。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 // 用于哈希函数的随机种子
...
}
核心概念
buckets:桶(bucket)
map的核心是一个 bucket 数组,每个 bucket 存储多个键值对。通过哈希函数将键映射到某个 bucket,然后在该 bucket 中查找、插入或删除键值对。哈希函数
Go 使用哈希函数将键映射到 bucket 上。为了减少哈希冲突,Go 对每个map生成一个随机的种子hash0,使得相同的键在不同的map中可能有不同的哈希值。哈希冲突
当多个键映射到同一个 bucket 时,会发生哈希冲突。Go 的map通过 链地址法(在 bucket 中存储多个键值对)处理冲突。扩容机制
当map中存储的元素过多时,会自动扩容。扩容时,Go 会创建一个新的、更大的 bucket 数组,并将所有的键重新分配到新的 bucket 中。这种操作被称为 rehashing,因为所有的键需要根据新的哈希值重新计算位置。
内存布局
- bucket 数组:map 的核心存储结构。每个 bucket 包含若干个键值对以及指向溢出 bucket 的指针。
- 溢出 bucket:当某个 bucket 中的元素过多时,Go 会为该 bucket 分配一个溢出 bucket,用来存储更多的键值对。
- 指针管理:map 的 key 和 value 通过指针引用,避免直接复制大型对象,从而提高效率。
性能注意事项
切片的扩容成本:当切片需要扩容时,会发生内存分配和元素复制操作,因此在高性能场景下,需要合理地初始化切片的容量,避免频繁的扩容。
map 的扩容和哈希冲突:虽然
map的查找、插入和删除操作的平均时间复杂度是 O(1),但如果哈希冲突较多或者发生扩容,性能可能会下降。因此,合理的map大小初始化可以提高性能,特别是在高并发场景中使用时。
总结:
-
slice:动态数组的抽象,包含底层数组的引用、长度和容量,支持自动扩容。底层数据可以共享,扩容时会复制数据到新数组。 -
map:基于哈希表实现,通过哈希函数将键映射到桶(bucket),通过扩容来应对冲突和存储空间不足。
go语言协程调度原理,协程为什么快
Go 语言中的协程(goroutine)是 Go 并发编程的核心,其高效的调度机制使得 Go 能够在大规模并发程序中表现出色。相比传统的线程,Go 的协程更加轻量且高效,这源于其独特的 协程调度器 和 用户态线程模型。下面我们将详细探讨 Go 协程的调度原理以及它为何如此快速。
协程(goroutine)的特点
- 轻量级:相比操作系统线程,协程的创建和销毁开销很小,通常只占用几 KB 的内存。而操作系统线程通常需要更大的内存栈(例如 1 MB 左右),这使得 Go 可以在同一时间内调度大量协程。
- 协作式调度:Go 的协程由 Go 运行时(runtime)来管理,并通过调度器来进行协作式调度,而不是交由操作系统内核进行调度,这大大降低了上下文切换的开销。
- 动态栈大小: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 模型的工作原理
调度器的结构
Go 的调度器通过 G-P-M 模型 实现 Goroutine 的调度。每个 M(操作系统线程)都会绑定一个 P(执行上下文),并从 P 的运行队列中获取待执行的 Goroutine。M 和 P 是一对一绑定的,而多个 Goroutine 可以被放在 P 的本地队列中排队等待执行。本地队列和全局队列
每个 P 有一个本地队列,存放待执行的 Goroutine。调度器还维护了一个全局队列,如果 P 的本地队列中没有 Goroutine 可以运行,它就会从全局队列中取任务。如果一个 P 的 Goroutine 过多,它会将部分 Goroutine 偷偷移动到全局队列或其他空闲的 P 中。抢占调度
虽然 Go 的调度器大部分情况下是协作式的,但在 Go 1.14 中引入了 抢占式调度。这意味着 Goroutine 如果运行了太长时间(例如长时间没有发生函数调用),调度器可以强制其暂停并调度其他 Goroutine。这种机制防止了 Goroutine 长时间占用 CPU 而导致其他 Goroutine 饿死。M 和 P 的动态调整
Go 运行时会根据负载情况动态调整 M(操作系统线程)的数量。通过GOMAXPROCS环境变量可以设置可以使用的 P 的数量,默认情况下,Go 会设置为逻辑 CPU 核心数。M 是与操作系统交互的实际执行线程,而 P 是调度 Goroutine 的核心单位。
协程为什么快?
轻量级的上下文切换
相比操作系统级的线程上下文切换,Go 的协程上下文切换开销小得多。Go 协程上下文切换只需保存和恢复少量的栈和寄存器信息,完全在用户态完成,避免了陷入内核态带来的开销。而操作系统线程的上下文切换需要大量的状态保存和恢复,并且涉及到内核态的切换。动态栈管理
Go 的 Goroutine 初始栈空间非常小(2KB),并且会根据需要动态扩展到较大的栈(最高可以扩展到 1GB)。相比之下,操作系统线程通常会预先分配较大的栈空间(通常是 1MB)。Go 的动态栈避免了为每个 Goroutine 预分配大量内存的开销。调度策略高效
Go 的调度器采用协作式调度,减少了不必要的抢占调度。由于调度完全由用户态代码控制,减少了与操作系统内核的交互,进一步提高了调度效率。资源复用与抢占调度
在抢占式调度之前,协程调度依赖于协作式调度机制,即协程在进行 I/O 或系统调用时,主动交出 CPU。抢占调度机制加入后,长时间占用 CPU 的协程可以被强制中断,确保其他协程也能被及时调度,这种灵活的调度机制确保了 Go 可以处理大量的并发任务。内存管理和垃圾回收
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 中最常见的索引类型,主要应用在 InnoDB 和 MyISAM 存储引擎中。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)
全文索引 是用于对大量文本数据进行搜索的索引类型,主要用于 InnoDB 和 MyISAM 存储引擎。在进行大量文本的模糊搜索或关键词匹配时,全文索引比普通的 LIKE 查询效率更高。
特点:
- 适用于全文搜索:全文索引能够对文本内容进行分词,并建立倒排索引,适合于大文本内容的关键词匹配和全文搜索。
-
支持复杂查询:通过
MATCH和AGAINST语句,能够进行复杂的全文搜索,支持布尔模式和自然语言模式的查询。
示例:
-- 创建全文索引
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 中常见的索引类型包括:
- B+树索引:用于大部分查询场景,支持范围查找和排序,是 MySQL 中最常用的索引类型。
- 哈希索引:用于高效的等值查询,但不支持范围查询,适合特定存储引擎。
- 全文索引:用于全文搜索和文本匹配,适用于大文本字段的搜索操作。
- 空间索引:适用于地理空间数据的查询,在 GIS 应用中较为常见。
- 聚簇索引:InnoDB 存储引擎中默认的索引类型,数据按主键顺序存储,支持高效的范围查询。
Redis是单线程的,但Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;