
面试题-Golang
你知道redis缓存击穿 雪崩 穿透吗
- 缓存击穿(Cache Breakdown):
- 场景: 缓存击穿指的是某个热点数据在缓存中过期时,同时有大量请求访问这个数据,导致这些请求穿透缓存直接访问数据库,造成数据库负载剧增。
- 解决方案: 使用互斥锁或分布式锁,确保只有一个请求能够重新生成缓存数据,其他请求等待缓存数据刷新。
- 缓存雪崩(Cache Avalanche):
- 场景: 缓存雪崩是指缓存中的一批数据在同一时间内失效,导致大量请求直接落到数据库上,增加了数据库的负载。
- 解决方案: 给缓存数据设置不同的过期时间,避免同一时间大量缓存数据同时失效。另外,使用热点数据预热、缓存数据永不过期等策略也可以减缓缓存雪崩的影响。
- 缓存穿透(Cache Penetration):
- 场景: 缓存穿透是指请求查询一个在缓存中不存在的数据,且这个数据对应的数据库中也不存在,导致每次请求都要查询数据库,增加数据库负载。
- 解决方案: 使用布隆过滤器(Bloom Filter)等机制,预先判断请求的数据是否存在于数据库中。另外,对于查询结果为空的情况,也可以设置一个空值缓存,防止频繁查询同一个不存在的数据。
redis有哪几种模式
Redis 支持多种模式,其中最常见的包括以下几种:
- 单机模式(Standalone Mode):
- 单机模式是最简单的部署方式,一个 Redis 实例运行在一台服务器上,所有的数据都存储在这一台服务器的内存中。这种模式适用于小规模应用或者开发环境。
- 主从复制模式(Master-Slave Replication):
- 主从复制模式包括一个主节点(Master)和多个从节点(Slave)。主节点负责写操作,从节点负责复制主节点的数据,提供读操作。主从复制模式提高了系统的可用性和可靠性。
- 哨兵模式(Sentinel Mode):
- 哨兵模式是基于主从复制的模式,引入了 Sentinel 进程,用于监控主节点的健康状况。当主节点宕机或不可达时,哨兵会自动将一个从节点晋升为新的主节点,确保系统的高可用性。
- 集群模式(Cluster Mode):
- Redis 集群模式将数据分片存储在多个节点上,每个节点负责一部分数据。这种模式提高了系统的横向扩展性和容错性。集群模式采用分区(partitioning)和副本(replication)相结合的方式。
- 持久化模式:
- Redis 提供两种持久化方式,分别是 RDB 持久化和 AOF 持久化。RDB 持久化将数据保存在磁盘上的二进制文件中,而 AOF 持久化则记录每个写操作的日志,以便在重启时进行恢复。
这些模式可以根据具体的应用场景和需求进行选择和组合。例如,对于需要高可用性的应用可以使用主从复制或哨兵模式,对于需要横向扩展的应用可以使用集群模式。每种模式都有其适用的场景和优缺点。
redis 持久化方案有哪些
Redis 提供了两种主要的持久化方案,分别是 RDB(Redis DataBase)持久化和 AOF(Append Only File)持久化。这两种方案可以单独使用,也可以同时使用,根据具体的需求和应用场景进行配置。
RDB 持久化:
- RDB 持久化是将 Redis 的数据以二进制形式保存到磁盘上的一个文件中。这个文件是一个经过压缩的二进制文件,包含了某个时间点上所有键值对的数据快照。
- RDB 持久化的优点是文件小巧、加载速度快,适用于备份和全量恢复。它适用于周期性的数据快照和灾难恢复。
配置方式:
save 900 1 # 如果 900 秒内至少有 1 个键被修改,则执行快照 save 300 10 # 如果 300 秒内至少有 10 个键被修改,则执行快照 save 60 10000 # 如果 60 秒内至少有 10000 个键被修改,则执行快照
AOF 持久化:
- AOF 持久化是通过追加方式记录 Redis 服务器接收到的每个写操作,以日志的形式保存到一个文件中。当服务器重启时,可以通过重新执行这个文件中的写操作来还原数据。
- AOF 持久化的优点是对数据的持久化更加实时,可通过配置不同的同步方式(always、everysec、no)来平衡性能和可靠性。
配置方式:
appendonly yes # 启用 AOF 持久化 appendfilename "appendonly.aof" # AOF 文件的名称
混合持久化:
- 可以同时使用 RDB 持久化和 AOF 持久化,充分发挥两者的优势。Redis 在启动时会先使用 AOF 文件进行数据恢复,如果 AOF 文件不存在或者恢复失败,则尝试使用 RDB 文件进行数据恢复。
这两种持久化方案各有优劣,选择合适的方案取决于具体的应用场景和对数据恢复、性能等方面的需求。
mysql有哪些锁
MySQL 提供了多种类型的锁,用于控制并发访问数据库的数据。以下是一些常见的 MySQL 锁类型:
共享锁(Shared Locks):
- 共享锁(也称为读锁)允许多个事务同时读取同一资源,但不允许任何事务对该资源进行写入。多个事务可以持有共享锁,不会相互阻塞。
SELECT * FROM table_name LOCK IN SHARE MODE;
排他锁(Exclusive Locks):
- 排他锁(也称为写锁)阻塞其他事务对同一资源的访问,包括读取和写入。只有一个事务可以持有排他锁。
SELECT * FROM table_name FOR UPDATE;
行级锁(Row-level Locks):
- 行级锁是一种粒度更细的锁,它仅锁定数据表中的某一行,而不是整个表。行级锁允许事务仅锁定需要修改的行,而不是整个表。
SELECT * FROM table_name WHERE column_name = 'value' FOR UPDATE;
表级锁(Table-level Locks):
- 表级锁是一种锁定整个数据表的锁,它会阻塞其他事务对整个表的读写操作。
LOCK TABLES table_name [AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}; UNLOCK TABLES;
意向锁(Intention Locks):
- 意向锁是一种高层次的锁,用于表示事务准备在某个层次上加锁。它包括意向共享锁(
IS
)和意向排他锁(IX
)。
LOCK TABLES table_name [AS alias] {READ | WRITE};
- 意向锁是一种高层次的锁,用于表示事务准备在某个层次上加锁。它包括意向共享锁(
自增锁(Auto-increment Locks):
- 当使用自增主键插入数据时,InnoDB 使用自增锁来确保并发插入的唯一性。
INSERT INTO table_name (auto_increment_column, other_columns) VALUES (NULL, 'value');
间隙锁(Gap Locks):
- 间隙锁用于锁定一个范围,而不是具体的行。它可以防止其他事务插入新的行,从而避免新插入的行与当前事务中的行发生冲突。
MySQL 的锁机制是复杂的,不同的存储引擎可能有不同的锁行为。因此,在实际使用中,需要了解具体存储引擎的锁行为,并根据应用场景选择合适的锁策略。
go的GMP模型有了解吗
在 Go 语言中,GMP 指的是 Goroutine、M(线程)、P(处理器)模型,这是 Go 语言并发模型的核心组成部分。下面简要介绍一下 GMP 模型:
Goroutine(G):
- Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时(runtime)管理。与传统线程相比,Goroutine 的创建和销毁开销非常小,可以轻松创建数以千计的 Goroutine,而不担心系统资源的耗尽。
- Goroutine 通过关键字
go
来创建,它运行在 Go 运行时的调度器之上。
go func() { // Goroutine 的代码逻辑 }()
M(Machine):
- M 是 Go 语言中的操作系统线程(Machine),用于执行 Goroutine。每个 M 都关联一个操作系统线程,而 Goroutine 则由 M 来执行。
- M 的数量由 Go 运行时根据系统的 CPU 数量动态调整。M 与真正的操作系统线程有一定的复用关系,可以在多个 Goroutine 之间切换。
P(Processor):
- P 是 Go 语言中的处理器(Processor),它负责管理和调度 Goroutine。P 的数量也由 Go 运行时动态调整,每个 P 可以同时运行多个 Goroutine。
- P 的主要任务是从全局的 Goroutine 队列中获取 Goroutine,并将其分配给某个 M 执行。当某个 M 上的 Goroutine 阻塞时,P 负责将其换到其他可执行的 M 上执行。
GMP 模型的关键是实现了高效的 Goroutine 调度和管理机制。它使得开发者可以使用轻量级的 Goroutine 来实现并发,而不必过多关心线程的创建和销毁的开销。通过 GMP 模型,Go 语言能够在多核系统上充分利用硬件资源,实现高性能的并发编程。
go中 channel的数据结构是什么
在 Go 语言中,channel
是一种用于在 Goroutine 之间传递数据的并发安全的数据结构。channel
的底层数据结构并没有直接暴露给开发者,但是我们可以了解一些大致的工作原理。
channel
的数据结构主要包括两个部分:一个指向数据的指针和两个指向等待发送和接收 Goroutine 的队列。以下是一个简单的描述:
- 指向数据的指针:
- 当有数据被发送到
channel
时,channel
内部会保存一个指向数据的指针。这个指针指向被发送的数据在内存中的位置。
- 当有数据被发送到
- 发送 Goroutine 队列:
- 当一个 Goroutine 尝试发送数据到一个已满的
channel
时,这个 Goroutine 将会被阻塞,并被加入到channel
的发送 Goroutine 队列中。一旦channel
可以接收数据,其中的一个 Goroutine 将会被唤醒并执行发送操作。
- 当一个 Goroutine 尝试发送数据到一个已满的
- 接收 Goroutine 队列:
- 当一个 Goroutine 尝试从一个空的
channel
中接收数据时,这个 Goroutine 会被阻塞并加入到channel
的接收 Goroutine 队列中。一旦channel
中有数据可用,其中的一个 Goroutine 将会被唤醒并执行接收操作。
- 当一个 Goroutine 尝试从一个空的
通过这种方式,channel
的数据结构保证了并发的安全性,多个 Goroutine 可以在不使用显式锁的情况下安全地进行数据传递。channel
内部的调度和同步工作由 Go 运行时(runtime)负责,使得开发者可以更方便地进行并发编程。
对已经关闭的channel分别读写会发生什么
对于已经关闭的 channel
,Go 语言规定了一些行为:
读取已关闭的
channel
:- 如果一个
channel
已经关闭,那么对于该channel
的后续读取操作将不会阻塞,并且会立即返回该类型的零值。即使channel
中还有未读取的数据,读取已关闭的channel
也不会导致数据被丢弃,而是会将已经在channel
中的数据读取出来。
ch := make(chan int, 2) ch <- 1 ch <- 2 close(ch) // 读取已关闭的 channel,会返回 channel 中的数据和一个表示 channel 是否已关闭的标志 for i := 0; i < cap(ch)+1; i++ { data, ok := <-ch fmt.Println(data, ok) }
- 如果一个
写入已关闭的
channel
:- 如果一个
channel
已经关闭,再向该channel
进行写入操作会导致 panic。这是因为关闭后的channel
不再允许写入数据,试图写入会触发运行时异常。
ch := make(chan int, 2) close(ch) // 向已关闭的 channel 写入数据,会导致 panic ch <- 3
- 如果一个
在使用 channel
时,通常建议在写入前先检查 channel
是否已关闭,可以使用可选的第二个返回值(ok
)来判断 channel
的状态。例如:
// 示例:在写入前检查 channel 是否已关闭
ch := make(chan int, 2)
close(ch)
select {
case ch <- 3:
fmt.Println("写入成功")
default:
fmt.Println("channel 已关闭")
}
这样可以避免在已关闭的 channel
上进行写入而导致 panic。