在如今的技术面试中,Go 语言作为一种高效、简洁、并发性强的编程语言,逐渐成为了不少互联网公司的首选。最近,我经历了一场 Go 语言的技术面试,这场面试让我不仅加深了对 Go 语言的理解,也让我体会到了在实际工作中运用 Go 语言的挑战与乐趣。下面我将分享我的 Go 语言面试经历,希望对正在准备 Go 语言面试的同学有所帮助。
面试前的准备
1. 理解 Go 语言的基础语法和特性
面试前,我首先复习了 Go 语言的基础知识,包括:
- Go 的基本语法:数据类型、控制结构(
if
、for
、switch
)、函数、错误处理等。 - Go 的并发机制:Go 的 goroutine 和 channel 是面试中的常见话题,因此我深入理解了这些并发机制。
- Go 的标准库:Go 的标准库非常强大,特别是
net/http
、encoding/json
、sync
、os
、time
等库,我都做了系统的学习。 - Go 的内存管理:Go 的垃圾回收机制、内存分配及优化等也是我复习的重点。
2. 做一些实战项目
除了理论上的复习,我还做了一些小项目,实践中学习了如何用 Go 语言实现 Web 服务器、并发任务处理等功能。例如,我构建了一个简单的 RESTful API 服务,并使用 goroutine 和 channel 处理并发任务,尝试在 Go 中实现了队列系统和消息推送等功能。通过这些项目,我加深了对 Go 的实际应用理解。
3. 面试题库
我还参考了很多网上的 Go 语言面试题库,做了大量的练习,涵盖了:
- 数据结构(如链表、树、图等)的实现。
- 算法题(如排序算法、查找算法、动态规划等)。
- Go 特性(如指针、接口、并发编程等)。
面试过程
技术面试
面试的第一部分是技术面试,主要考察我的编码能力和 Go 语言的实际应用能力。面试官首先通过几个简短的理论问题来了解我的基础知识,然后转向了实际的编程题。
1. 理论问题
面试官问了一些关于 Go 语言基础的问题,如:
Go 语言的特点是什么?
我简要回答了 Go 语言的高效并发、简洁的语法、丰富的标准库和内存管理等特点。Go 语言中的接口和其他语言的接口有何不同?
我解释了 Go 语言中接口的实现是隐式的,不需要显式声明,而是通过方法集的匹配来实现。Go 中的
goroutine
和channel
是如何协作的?
我简要阐述了goroutine
的创建、并发执行和通过channel
进行通信的机制。
2. 编程题
面试官给了我几道编程题,分别测试了我的算法和并发编程能力。下面是其中一道题目:
题目: 实现一个并发的 Web 爬虫,要求爬取指定的 URL 列表,获取页面的标题并输出。要求使用 Go 的并发机制。
解题思路:
- 我首先定义了一个
Worker
结构体,用来处理每个网页的抓取工作。 - 使用 goroutine 并通过 channel 来并发执行多个任务。
- 通过
sync.WaitGroup
来等待所有任务完成。 - 使用
net/http
请求网页,解析 HTML 内容,通过正则表达式提取网页标题。
一、make和new差别,引用类型的意义
(一) make:
- make 用于为引用类型(如 slice、map 和 channel)分配内存并初始化它们。
- make 只能用于引用类型,不能用于值类型。
(二) new:
- 用于分配内存并返回指向该内存的指针。
- new 可以用于任何类型,无论是值类型还是引用类型。
- 对于值类型,new 分配的内存会被初始化为零值。
- 对于引用类型,new 分配的内存会被初始化为 nil。
(三) 引用类型的意义:
- 方便一些大对象变量的传递
二、逃逸分析
(一) 逃逸分析命令
go run -gcflags -m main.go
(二) 何为逃逸
- 简单来说就是
-
- 变量在当前作用域(函数)结束后依然没有销毁,反而存储的地方由栈->堆。
(三) 如何避免逃逸
- 减少在闭包函数内使用外部变量,尤其是使用 go 语法创建的新协程。
- 减少函数返回内部变量。尤其是指针类型
- 减少大数据变量的使用,超出栈空间会导致逃逸。
-
- 可以使用 ulimit -a 命令查看机器上栈允许占用的内存的大小。
- 减少引用类型的变量去引用别的指针类型
-
- 比如在chan 或者 slice 中存储指针数据
三、channel的实现
- 内部有一个互斥锁用来保证读写的正确
- 有一个环形队列用来存储当前信道内的数据
-
- 分别有当前读哪个数据和最后存的数据的索引位置
(一) chan 是基于通信进行共享内存,所以是并发安全的
1. 基于通信的共享内存 和 基于共享内存的通信,有什么本质上的区别?
- 定义与基本原理:
-
- 基于通信的共享内存:消息传递模型中的共享内存。
-
-
- 在这种模式下,进程之间通过共享内存进行数据交换,但数据的传输是通过消息的形式来完成的。
- 例如,在某些分布式系统中,一个进程可以将数据写入共享内存,而另一个进程则通过读取消息的方式获取这些数据。
-
-
- 基于共享内存的通信:利用共享内存作为多个进程间通信的媒介。
-
-
- 所有参与通信的进程都映射到同一块物理内存区域,从而实现数据的直接访问和修改。
- 这种方式不需要额外的消息传递机制,数据的读写操作直接在内核空间中进行,因此具有更高的效率。
-
- 实现方式:
-
- 基于通信的共享内存:需要定义明确的消息协议和格式,进程之间通过发送和接收消息来进行数据交换。
-
-
- 这种模式下,操作系统或应用程序需要提供相应的系统调用来支持消息的发送和接收。
-
-
- 基于共享内存的通信:不需要显式的消息传递机制,进程直接对共享内存进行读写操作。
-
-
- 这种方式减少了系统调用的开销,提高了数据传输的速度。
-
- 性能特点:
-
- 基于通信的共享内存:由于涉及到消息的发送和接收,每次数据交换都需要进行系统调用,这会增加一定的开销。
-
-
- 此外,消息传递模型在分布式环境中更容易实现,但可能需要更多的编程复杂度。
- 是并发安全的
-
-
- 基于共享内存的通信:因为数据直接在内存中传输,没有额外的拷贝过程,所以其性能通常比基于消息传递的共享内存要高。
-
-
- 这种模式适用于需要频繁且大量数据交换的场景。
- 并发不安全
-
- 适用场景:
-
- 基于通信的共享内存:适合于需要灵活控制数据传输顺序和同步机制的场景,如分布式计算中的负载均衡和任务分配。
- 基于共享内存的通信:更适合于需要高效、快速数据交换的应用程序,如多线程或多进程之间的数据共享和同步。
- 基于通信的共享内存(CCM)和基于共享内存的通信(SCM)本质区别在于数据访问和同步的方式:
-
- 基于通信的共享内存(CCM):
-
-
- 在CCM中,多个处理器或核心通过消息传递来访问共享内存。
- 每个处理器都有自己的私有内存,但可以通过发送消息来请求访问共享内存中的数据。
- 需要显式地发送和接收消息,因此通信开销较大。
-
-
- 基于共享内存的通信(SCM):
-
-
- SCM假设所有处理器或核心共享同一块物理内存。
- 数据访问和同步是通过内存操作来完成的,类似于多线程编程中的共享内存。
- 通信开销较小,因为处理器可以直接读写共享内存,但需要考虑内存访问的同步问题。
-
四、gmp,协程模型(主要:网络io等待队列)
(一) 简述gmp
- gmp是go语言管理、调度多协程的一个模型。
(二) gmp分别代表什么
- g 是 Goroutine,是 go 运行最小单元
-
- 理论上 g 没有上限,由程序决定。
- 每个 g 初始占用 2Kb左右。
- m 是系统线程级别执行程序, 通过 p 来 执行 g。
-
- go 程序启动时,会设置 M 的最大数量,默认 10000。
-
-
- runtime/debug 中的 SetMaxThreads 函数,可以设置 M 的最大数量
-
-
- m 是唯一直接和系统进行交互的。系统感知不到 g 和 p。
- p 是 具体执行 g 的 逻辑处理器,他存在一个自己的本地队列。
-
- 默认数量和系统 cpu 数量一致,使用 `runtime.GOMAXPROCS(1)` 来控制并行 处理器 的数量。
- p 存储了 g 执行过程中的上下文信息和状态,这也是 g 可以被不同 m 调用执行的关键。
- p 在自己本地队列中切换 g 是不需要加锁的。
- 在 g m p 之外还有一个全局队列,全局队列的 g 的操作是需要加锁的。
- 如果一个 p 的队列空了,会优先取其他 p 的本地队列,取一半,没有再取全局队列。
- 如果所有 p 的本地队列和全局队列都为空,m 会进行自旋。
(三) m 和 p
1. 阻塞(p和绑定m的切换)
- 用户态阻塞
-
- 当 G 出现阻塞性操作时,M 会将对应的 G 放入等待队列,并修改G的状态,M 会在当前P的本地队列中寻找可以被执行的 G。
- 如果此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态。
- 当等待的G被唤醒,修改状态为可执行,尝试加入唤醒G的的下一个执行队列(携带上下文),或者当前P的本地队列,或者全局队列。
- 系统调用阻塞
-
- 一个 M 阻塞了,会创建新的 M。
- 当 G 出现系统级别的阻塞,同时M也会被阻塞,与当前P解绑,让 P寻找空闲的M或者创建一个新的。
- 当前 G 可以被执行了,会重新加入到P的队列,或者全局队列。
M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
2. P 和 M 何时会被创建
- P 何时创建:在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。
- M 何时创建:没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。
五、gc,垃圾回收
(一) 当前最新版本的gc
- Golang 的垃圾回收是自动的
-
- runtime.GC() // 手动触发 GC
- 三色标记+混合写屏障
(二) 三色标记
1. 颜色
- 黑色:根节点
- 白色:存在引用对象,等待下一次扫描,确定是否可被清除。
- 白色:无引用对象,可以被清除。
2. 三色标记存在的问题
- 漏标问题:指的是在用户协程与 GC 协程并发执行的场景下,部分存活对象未被正确标记,从而被误删的情况。
- 多标问题:指的是在用户协程与 GC 协程并发执行的场景下,部分垃圾对象被误标记,从而导致 GC 未按时将其回收的问题。
3. 强弱三色模式
- 强三色不变式:不允许黑色对象引用白色对象(直接破坏黑色引用白色对象)
- 弱三色不变式:黑色对象可以引用白色对象,但白色对象上游必须有灰色对象(保障白色对象还有机会被 GC 扫描标记)
(三) 屏障机制
1. 插入写屏障
- 插入写屏障:规则维护了强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障,将白色对象置为灰色,再建立引用。
- 在并发标记阶段,程序的正常执行和垃圾收集是并发进行的。
-
- 写屏障的作用是当程序修改某个对象的引用时,会通知垃圾收集器,以确保被引用的对象不会被错误地标记为垃圾。
- 写屏障并不能完全避免一些特殊情况下的遗漏 - 栈内存中变量的引用变化。
2. 删除写屏障
- 删除写屏障:实现了弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用。
- 弊端:一个白色对象的引用被删除后,置灰,即使没有其他存活的对象引用它,它仍然会活到下一轮。
-
- 会产生很多的冗余扫描成本,且降低了回收精度。
3. 混合写屏障规则(hybrid write barrier)
- 混合写屏障的精度虽然比插入写屏障稍低,但它完全消除了对栈的 STW 重新扫描,从而进一步减少了 STW 的时间。
- GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
- GC期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色(删除写屏障)。
- 被添加的对象标记为灰色(插入写屏障)。
PS: 屏障技术是不在栈上应用的,因为要保证栈的运行效率。
(四) STW
- Stop-The-World
-
- 停止工作 ,进行垃圾回收的逻辑
- 为什么需要进行 stw
-
- 因为垃圾回收和程序运行是并行的
(五) Go GC 的发展经历了三个重要的历史阶段:
- Go 1.3 标记清除,实现简单,但整个 GC 过程都需要 STW,严重影响性能;后续优化为清除阶段不再需要 STW,提升部分性能;
- Go 1.5 并发三色标记法 + 插入写屏障,引入并发垃圾回收器,实现 GC 与业务程序并行处理,可以完成并发的标记和清除,进一步减少 STW 时间,更好的利用多核处理器,提升 GC 性能;但在一次正常的三色标记流程结束后,需要进行一次 STW, re-scan 栈上对象,影响性能。
- Go 1.8 三色标记 + 混合写屏障机制,消除了对栈本身的重新扫描,STW 的时间进一步得到缩减。
(六) 内存压缩
- 没有找到太好的资料,后面再说吧
- 减少内存碎片并提高内存使用效率。
(七) gc 过程
- 开启stw,停止程序。
- 开启屏障机制并恢复程序执行。
-
- 此时所有的栈节点都是黑色。
- 之后被删除的对象标记为灰色(删除写屏障)。
- 之后被添加的对象标记为灰色(插入写屏障)。
- 开始扫描(同时处理屏障机制标记为灰色的节点)
-
- 初始时,所有的对象都是白色。
- 将所有的根节点全部标记为灰色。
- 扫描灰色节点。
-
-
- 将当前节点标为黑色。
- 将当前节点可达节点标记为灰色。
-
-
- 重复 3.3
- 直到所有节点都是黑色或者白色。
- 黑色节点就是需要保留的,白色的不可达节点就是需要被清除的。
- 进行内存压缩。
- 标记完成,执行清除。
(八) 垃圾回收的触发时机
- 主动触发:用户代码中调用
runtime.GC()
函数会主动触发 GC。
-
- 这种情况通常不常见,因为 Golang 的垃圾回收机制是自动的,但在某些特定场景下,开发者可能需要手动触发 GC。
- 定期触发:默认情况下,每 2 分钟未产生 GC 时,Golang 的守护协程
sysmon
会强制触发 GC。这是为了确保即使没有内存分配增长,系统也能定期进行垃圾回收,以保持内存管理的效率。 - 内存分配触发:当 Go 程序分配的内存增长超过阈值时,会触发 GC。
-
- 这个阈值是由 Golang 的垃圾回收控制器动态计算的,目的是在内存使用量达到一定程度时进行垃圾回收,以避免内存过度占用。
- 这个阈值是上一次GC中”活跃”对象所占用的内存的倍数,这个倍数(也称为GC百分比,GC Percent)默认是100,表示新分配的内存达到上一次GC活跃对象内存的两倍时,会触发新的GC。
- 可以通过
debug.SetGCPercent
函数来修改这个比例。
- 时间周期触发:当距离上一个 GC 周期的时间超过一定时间时,将会触发 GC。这个时间周期以
runtime.forcegcperiod
变量为准,默认是 2 分钟。
六、map的实现,重点问题(sync.map的实现,map实现随机的方法)
(一) map 的实现
(二) sync.map
mysql
一、为什么用b+树不用b树
- 根本原因
-
- B 树的 数据在叶子结点
-
-
- B 树如果要读取一段连续数据,假如不在一个叶子结点,那么就还需要再进行一次 磁盘I/O。
-
-
- B + 树的非叶子节点只存了索引信息和主键
-
-
- 通过二次回表查到的所有数据。
- B+ 树的数据会通过一个链表存储所有的数据,这对大批量读取某段连续数据很有利。
-
- 所以
-
- B + 树 比 B 树能够一次性加载更多的索引数据,减少 磁盘I/O 操作。
以第一题为分界线,答出来了问下面的
二、redo,undo,binlog的作用和实现
(一) 基本解释
- undo log:回滚日志,用于事务需要撤销,数据要进行恢复时使用的。以保证数据原子化
-
undo log
的日志内容是逻辑日志,非物理日志。
-
-
- 即 insert sql 或生成一条对应的 delete sql
- update sql 会有一条 update table set field = old_val 的 sql
-
- redo log:用于事务在提交时出现问题,中断了执行,会使用redo 日志来进行重写,以完成持久化。
-
- 只有redo log 真正写成功之后才会返回成功
- 对 undo log 磁盘修改也会同步到redo,因为undo 同样需要保证持久化。
- undo 和 redo 这两种日志是属于innodb存储引擎的日志
-
- 它们的区别在于:
-
-
- redolog记录了此次事务完成后的数据状态,记录的是更新之后的值
- undolog记录了此次事务开始前的数据状态,记录的是更新之前的值;
-
- binlog:是数据库级别的日志
-
- 数据库命令执行记录,用于数据库数据恢复和问题排查。
- binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
-
-
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
-
(二) 作用和实现
1. undo log
- 作用:
-
- 数据回滚
- MVCC 版本链
- 实现
-
- 存储:
-
-
- 存储位置:innodb_undo_directory
-
-
- 存储结构
-
-
- undo log 开始的地址
- 下一条记录的地址
- 当前undo log 的编号
- 事务id
- 表id
- undo log 类型:update、insert
- 主键
- 数据信息,数据长度
-
-
- 通过下一条记录的地址,开始的地址,可以完整的形成一个版本链
- 生成及删除
-
- 生成:会在事务进行数据变更的时候,生成 Undo Log
-
-
- 优先写入缓存区,后台有专门的落盘进程
-
-
- 删除
-
-
- insert 类型的日志,会在事务提交后直接进行删除,因为这个类型的日志只能被当前事务所见。
- update 类型,因为要支持 MVCC 做为版本链使用,生成快照,所以有专门的删除进程(后台purge线程)。
-
2. redo log
- 作用
-
- 数据持久化
- 事务恢复
- 实现
-
- 结构
- 生成及删除
-
- 生成
-
-
- 每次事务内的操作都会对应一个redo log,包括undo log 的修改,这样是为了保证事务可以被记录下来,可以被恢复。
- 落盘时机
-
-
-
-
- redo log 是先 redo log buffe -> redo log file -> file。
- 可以通过 innodb_flush_log_at_trx_commit 来控制落盘时机
-
-
-
-
-
-
- 0:不会主动进行落盘,需要等后台进程进行落盘。
-
-
-
-
-
-
-
-
- InnoDB 的后台线程每隔 1 秒
-
-
-
-
-
-
-
-
- 1(默认),每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
- 2,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。
-
-
-
-
-
-
-
-
- InnoDB 的后台线程每隔 1 秒
-
-
-
-
-
-
-
- 数据安全性:参数 1 > 参数 2 > 参数 0
- 写入性能:参数 0 > 参数 2> 参数 1
-
-
3. bin log
- 作用
-
- 数据备份、恢复
- 主从复制、同步
- 实现
-
- 结构
- 生成及删除
-
- 事务提交后,会生成 mysql 级别的 bin log,undo 和 redo 是引擎级别的日志。
-
-
- 先写入 bin log cache,然后等待落盘。
-
-
- 通过 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:
-
-
- 0, 默认,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
- 1,表示每次提交事务都会 write,然后马上执行 fsync;
- N (N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
-
-
- 一般情况下,不会主动删除 bin log
4. 为什么有了bin log,还需要 redo log,redo log 能否作为 数据恢复来使用?
- 这两个日志有四个区别。
-
- 适用对象不同:
-
-
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
- redo log 是 Innodb 存储引擎实现的日志;
-
-
- 文件格式不同:
-
-
- binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED。
- redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
-
-
- 写入方式不同:
-
-
- binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
-
-
-
-
- 需要先找到位置,再写入,所以磁盘操作是随机写
-
-
-
-
- redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
-
-
-
-
- 使用了追加操作, 所以磁盘操作是顺序写
-
-
-
- 用途不同:
-
-
- binlog 用于备份恢复、主从复制;
- redo log 用于掉电等故障恢复。
-
- 能使用 redo log 文件恢复数据吗?
-
- 不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。
-
-
- 因为 redo log 文件是循环写(循环写),是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。
- binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
-
(三) 事务提交
- 两段式提价
-
- 将一个事务的提交分为两个部分,准备和提交阶段。
-
-
- 必须要等 redo 和 bin log 都落盘成功,才可以真正提交,
-
-
- 解决的问题是避免 redo log 或者 bin log 一个成功,另一个失败,造成的主从数据不一致。
-
-
- redo 成功, bin log 失败。
-
-
-
-
- mysql 重启后,通过 redo log 恢复事务,将字段修改为 新值,但是 bin log 失败,从库没有成功同步,从库还是旧值。
-
-
-
-
- bin log 成功, redo 失败,
-
-
-
-
- 由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 主库还是 旧值。
- 而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,从库成功同步了新值,但是主库还是旧值。
-
-
- 组提交
-
- 因为两段式提交,会导致每次事务提交时都要等待磁盘io结束,那么就会造成io阻塞,系统吞吐量降低。
- mysql 会将几个同时提交的事务打包为一次提交两段式提交。
-
-
- 减少io操作。
-
三、说说对mvcc的理解
(一) 事务级别
- 事务的特性
-
- 原子性:当一个事务开始时,会记录下当前数据库的状态,并将所有操作都写入undo日志中。如果事务执行成功,则将undo日志清除;如果事务执行失败,则根据undo日志恢复到事务开始前的状态
- 持久化:事务一旦提交,其结果就永久保存在数据库中。MySQL通过重做日志(redo log)和撤销日志(undo log)来实现持久性。在提交事务时,MySQL首先将事务的所有更改写入重做日志文件,然后才将数据写入磁盘。这样即使系统崩溃,只要重做日志没有丢失,就可以通过重做日志恢复数据。
- 隔离性:并发事务之间相互独立,一个事务的执行不应受到其他事务的影响。MySQL通过锁机制和隔离级别来实现隔离性。
- 一致性:MySQL通过MVCC(多版本并发控制)机制来保证事务的一致性。在RR(可重复读)隔离级别下,每个事务看到的数据都是启动时的数据,从而避免了脏读和不可重复读的问题
- 数据库事务级别:MySQL InnoDB 引擎的默认隔离级别可重复读
-
- 串行化
- 可重复读
- 读已提交
- 读未提交
- 不同级别下存在的问题
-
- 脏读:一个事务读取了另一个未提交事务的数据,而该数据在提交前被回滚,从而导致读取到无效数据的情况。
- 不可重复读:一个事务范围内多次读取相同的数据行时,结果却返回不同的数据值。
- 幻读;幻读是指在一个事务中多次执行相同的查询时,由于另一个事务插入或删除了一些数据,导致该事务再次执行查询时看到的结果集发生了变化。
(二) MVCC(多版本并发控制)
MVCC 是 mysql 用于在可重复读级别下解决幻读的问题。
1. 如何解决的幻读
- 查询:在事务开启后,第一次查询时就创建快照,这样只要事务还在进行中,那么同一条件下就永远是一个结果。
- 修改、插入、删除:通过添加间隙锁,保证其他事务无法使当前事务涵盖的数据及区间不会发生数据变化
-
- 比如 update table set name = 'name2' where id in (3,5)
-
-
- 那么数据 id 在 >=3 and <= 5 的区间内,数据都无法修改和插入
-
(三) 事务的实现
- 上面 undo 、redo 、mvcc 综合起来。
(四) 简单面试题
- 索引怎么建
ALTER TABLE TABLE
ADD INDEX `idx_account`(`user`);
- 联合索引最左前缀
-
- idx_a_b_c(a,b,c)
-
-
- where a = 1 and b = 1,生效
- where b = 1 and c =1 ,不生效
- where a = 1 and c = 1, 生效
- where a like '%aa%' and b = 1,不生效
-
- 聚簇索引与回表
-
- 聚簇索引就是主键
- 回表:因为 mysql 所使用的 B+tree 在非叶子结点中,只存储了索引信息、主键和完整记录的指针,所以如果查询字段不止所用到的索引,就需要进行二次查找操作,这个动作叫回表。
- 索引覆盖:查询的信息只是索引字段,不需要回表的查询。
- 索引下推:只有联合索引才会用到这个机制
-
- 联合索引中,遇到非等值判断且满足最左原则。
- 假设:索引 idx_a_b_c,条件 where a > 1 and b = 1
- 5.6之前:需要将所有 a > 1 的数据全部回表,然后再过滤 b。
- 5.6 之后:在遇到 a > 1 时,会先过滤 b = 1 的,数据,再进行 a > 1 的过滤。
redis
redis 和 ercd 对比
数据模型
- etcd:键值存储,支持原子性和分布式事务,树状结构,键可为任意字节数组,值可任意二进制
- redis:键值存储,支持多种数据结构:字符串,哈希,列表,集合,有序集合,可存储多种类型数据
一致性
- etcd:强一致性,副本始终保持相同状态,就是说,你写了数据到主节点,从节点会立即更新,保证数据一致性及可用性
- redis: 保证最终一致性,存在短暂的数据不一致的情况,其持久化机制为:rdb和aof,以保证数据最终一致性
可用性
- etcd: 高可用,支持Raft分布式一致性算法,就是说即使有节点挂掉了也会保证数据一致性及其他节点的可用性
- redis: 高可用,主从复制和哨兵选择模式,保证数据在最短时间内可以恢复
性能选择
- etcd: 高性能,可并发读写,高吞吐量,低延迟
- redis: 高性能,基于内存的存储,多种结构储存方式,读取速度快
可扩展性
- etcd: 水平扩展,多节点可提高集群的容量及性能
- redis: 垂直扩展,添加多个节点的内存和cpu资源可提交性能
特点对比
场景对比
- etcd:
分布式协调服务,如服务发现、配置管理、锁服务
存储需要强一致性、高可用性和并发读写的关键数据 - Redis:
缓存服务,加速数据库或其他慢速存储的访问
会话管理,存储用户会话信息和状态
实时分析,存储和处理流数据
实战用法
etcd:
使用强一致性特性来保证数据的可靠性和可用性
启用 Raft 日志压缩以减少存储空间占用
监控集群健康状况,及时发现和解决问题
Redis:
根据数据访问模式选择合适的持久化策略
使用主从复制或哨兵模式提高可用性和故障恢复能力
优化数据结构以提高查询性能
深度理解
etcd 和 Redis 都是分布式数据存储的优秀选择,但它们具有不同的特性和适用场景。etcd 提供强一致性、高可用性和并发读写能力,适合于需要这些特性的场景。Redis 提供多种数据结构、高性能和读密集型特性,适合于缓存、会话管理和实时分析等场景。通过理解它们的差异,开发人员可以根据具体需求选择最合适的解决方案。
3. 讨论
面试官在我写完代码后,还进行了代码的讨论,主要内容包括:
- 为什么选择 goroutine 和 channel 进行并发?
- 如何处理爬虫中的错误,比如请求失败或者解析失败?
- 如果要爬取更大的数据集,你会如何优化性能?
- 你觉得 Go 中的并发机制相比其他语言(如 Python)的线程或协程有哪些优势?
我根据自己的理解,回答了如何利用 Go 的并发模型提高爬虫的效率,并谈到如何在生产环境中进行爬虫的优化,如使用 goroutine pool
、限流等。
二、行为面试
技术面试之后,面试官进行了行为面试,主要问了以下问题:
- 你如何理解团队协作?有没有参与过团队项目,如何解决团队中的冲突?
- 你在过去的项目中遇到过哪些困难,如何解决的?
- 你是如何进行时间管理和工作安排的?
这些问题考察了我的团队合作精神、解决问题的能力以及工作方法。我尽量通过举例说明自己如何在过去的项目中与团队协作,如何高效地安排自己的时间,并处理项目中的突发问题。
面试结果
最终,我顺利通过了面试,成功获得了这份工作。这次面试让我深刻体会到,Go 语言的并发特性和标准库的强大使得开发效率大大提高,同时也对实际开发中可能遇到的挑战有了更深入的理解。
总结
通过这次面试,我总结出以下几点对 Go 语言面试的准备方法:
- 扎实的基础知识:熟练掌握 Go 语言的基本语法、数据结构和算法是通过面试的基础。
- 并发编程能力:Go 的并发机制是面试中的重点,要深入理解 goroutine 和 channel 的使用。
- 项目实战经验:通过实际项目的练习,熟悉 Go 的应用场景和最佳实践。
- 问题的解决思路:面试不仅考察技术能力,也考察解决问题的思维方式和团队协作能力。
希望我的经历能够帮助大家更好地准备 Go 语言面试,祝你们面试成功!