哎,干的好好的,突然公司没钱了,被裁员了,被迫又走上了面试之路。
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 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;