面试官:go 中的 singleflight 是如何实现的?
大家周二快乐,今天继续分享粉丝投稿的面经。
内容整理如下:
go
go singleflight 的底层实现
singleflight
是 Go 语言标准库中的一个很有用的包,它主要用来处理并发请求时的重复问题。比如在高并发场景下,如果多个请求同时访问同一个资源,singleflight
可以确保这些请求中只有一个实际执行,其他请求则等待这个结果。
具体来说,singleflight
里有一个核心结构叫做 Group
。当你调用 Do
方法时,它接收一个键(key)和一个函数(fn)。这个键是用来标识请求的唯一性,而函数则是实际要执行的操作。Do
方法首先会检查是否已经有相同的请求正在处理中。如果有,那么当前请求就会被放入一个等待队列,直到第一个请求完成并返回结果。这时,所有等待的请求都会收到相同的结果。
内部实现上,singleflight
使用了一个互斥锁(mutex)来保护其状态,并且有一个映射表(map)来存储正在进行的请求。对于每个请求,它会创建一个 call
对象,这个对象包含了实际的执行函数以及一个通道(channel),用于在请求完成后发送结果。当有多个请求使用相同的键时,它们会被添加到同一个 call
对象的等待队列中,等到第一个请求完成后,所有的请求都会被唤醒并返回相同的结果。
这种方式特别适用于缓存穿透或者需要避免重复计算的场景,因为它可以大大减少对后端服务的压力,提高系统的性能和效率。
mysql
使用数据库乐观锁 cas 操作判断的时候,受不受数据库隔离级别的影响?
乐观锁(CAS 操作)和数据库的隔离级别确实有一定的关系,但它们的作用方式不同。
乐观锁通常通过版本号或时间戳来实现。当一个事务尝试更新数据时,它会检查数据的版本号或时间戳是否与读取时一致。如果不一致,说明在这期间数据已经被其他事务修改了,那么当前事务就会失败并可能需要重试。
数据库的隔离级别则决定了事务之间可见性的规则。常见的隔离级别包括读未提交、读已提交、可重复读和序列化。不同的隔离级别对并发事务的可见性和一致性有不同的保证。
乐观锁的操作本身并不依赖于特定的隔离级别,但它可能会受到隔离级别选择的影响。例如:
读未提交:在这种隔离级别下,事务可以看到其他事务未提交的数据。这可能会导致乐观锁的版本检查出现问题,因为一个事务可能会看到另一个事务尚未提交的数据。
读已提交:在这种隔离级别下,事务只能看到已经提交的数据。这是最常见的隔离级别之一,适合使用乐观锁,因为它可以避免脏读。
可重复读:在这种隔离级别下,事务在执行过程中多次读取同一数据时,结果是一致的。这有助于确保乐观锁的版本检查是基于一致的数据视图。
序列化:这是最高的隔离级别,它提供了最严格的事务隔离。在这种隔离级别下,乐观锁通常也能很好地工作,但由于序列化的高开销,实际应用中不常用。
redis
介绍一下 redis 中常用数据结构的底层实现
1. String
用途:用于存储简单的键值对,可以是字符串、整数或浮点数。
底层实现:
Redis 的 String 类型内部使用
sds
(简单动态字符串) 结构来存储数据。sds
是 Redis 自己实现的一种字符串结构,它在 C 字符串的基础上增加了长度信息,并且提供了高效的内存管理和扩展能力。
2. Hash
用途:用于存储字段和值之间的映射关系,类似于 Java 中的 HashMap。
底层实现:
当哈希表中的元素较少时,Redis 使用压缩列表(ziplist)来存储数据。压缩列表是一种特殊的双向链表,它可以高效地存储小数量的数据,并且占用更少的内存。
当哈希表中的元素较多时,Redis 会将压缩列表转换为字典(Dictionary)。字典是一个由多个桶(bucket)组成的数组,每个桶中包含一个链表,用于处理哈希冲突。
转换阈值:
每个字段的最大长度:
hash-max-ziplist-value
(默认 64 字节)哈希表的最大字段数量:
hash-max-ziplist-entries
(默认 512 个字段)
3. List
用途:有序的字符串列表,可以在列表的两端进行插入和删除操作。
底层实现:
当列表中的元素较少时,Redis 使用压缩列表(ziplist)来存储数据。压缩列表可以高效地存储小数量的数据,并且占用更少的内存。
当列表中的元素较多时,Redis 会将压缩列表转换为双端链表(linked list)。双端链表允许在列表的两端进行高效的插入和删除操作,但访问中间元素的效率较低。
转换阈值:
每个元素的最大长度:
list-max-ziplist-value
(默认 64 字节)列表的最大元素数量:
list-max-ziplist-entries
(默认 512 个元素)
4. Set
用途:无序的字符串集合,不允许重复元素。
底层实现:
当集合中的元素较少时,Redis 使用整数集合(intset)来存储数据。整数集合是一个有序的整数数组,支持快速查找和插入操作。
当集合中的元素较多时,Redis 会将整数集合转换为字典(Dictionary)。字典提供高效的查找和插入操作,适用于大量数据的情况。
转换阈值:
整数集合中的最大元素数量:没有明确的配置项,但当集合中的元素不再是整数或元素数量超过一定阈值时,会自动转换为字典。
5. Zset (Sorted Set)
用途:有序的字符串集合,每个元素关联一个分数,通过分数进行排序。
底层实现:
Zset 内部使用两种数据结构来实现:
跳跃表(skiplist):跳跃表是一种概率数据结构,提供高效的范围查询和插入操作。跳跃表通过多层索引来加速查找过程。
字典(Dictionary):字典用于存储成员到分数的映射,以便快速查找成员的分数。
跳跃表和字典共同工作,确保 Zset 既能高效地进行范围查询,又能快速地进行成员查找。
转换阈值:
每个成员的最大长度:
zset-max-ziplist-value
(默认 64 字节)有序集合的最大成员数量:
zset-max-ziplist-entries
(默认 128 个成员)
redis 内存快把一台机器的内存占满了,例如一共 16g,现在用了 15.5g 这时候你该怎么办?
一、监控和分析内存使用情况
使用 Redis 的监控工具(如 RedisInsight)或者命令(如 INFO memory)来确定哪些数据占用了大量内存,以便后续采取针对性措施。
二、调整数据存储和过期策略
检查是否有一些数据可以设置过期时间,对于临时数据或者不经常使用的数据,设置合理的过期时间,让 Redis 自动清理这些数据。例如,使用 EXPIRE 或 PEXPIRE 命令设置键的过期时间。
优化数据结构,避免存储不必要的大字符串等占用大量内存的数据结构。
三、启用内存淘汰策略
选择合适的内存淘汰策略,如 allkeys-lru(淘汰最近最少使用的键)、volatile-lru(淘汰已设置过期时间且最近最少使用的键)等。可以通过 CONFIG SET maxmemory-policy 命令来设置淘汰策略。
四、数据持久化和清理
利用 Redis 的持久化机制(如 RDB 或 AOF),将数据定期持久化到磁盘上,这样可以在内存不足时,从磁盘恢复数据,释放内存空间。
根据业务需求,手动清理一些不再需要的数据,可以使用 DEL 命令删除单个键。
五、扩展 Redis 实例
如果条件允许,可以考虑为当前机器增加内存。
使用 Redis 集群或哨兵模式,将数据分片存储到多个 Redis 实例中,分散内存压力。
kafka
如何保证 kafka 消息顺序 (包括业务内有序和全局有序)
要保证 Kafka 消息的业务内有序,需确保相关业务消息被发送至同一个分区,这是因为同一分区内的消息处理是有序的。
而实现全局有序,通常需严格限制并发度,仅使用一个分区,但这会在一定程度上降低系统的性能和消息的吞吐量。
在实际应用中,要综合考虑业务需求、性能要求和资源配置等多方面因素来权衡消息顺序和系统效率之间的关系。
kafka 的可用性怎么保证的?
可以通过多种机制来保证高可用性,确保在出现故障时系统能够继续正常运行:
多副本:
Kafka 的每个 topic 可以划分为多个 partition,每个 partition 可以有多个副本(replica)。这些副本分布在不同的 broker 上。
其中一个副本被选为 leader,负责处理所有的读写请求;其他副本是 follower,它们从 leader 复制数据。
如果 leader 副本所在的 broker 宕机,Kafka 会自动从 follower 中选举一个新的 leader 继续提供服务。
ISR:
ISR 是一组与 leader 保持同步的副本集合。只有当 follower 副本的数据与 leader 一致时,才会被加入 ISR。
Kafka 通过维护 ISR 来确保数据的一致性和可靠性。如果某个 follower 落后太多或无法与 leader 通信,它会被移出 ISR。
ACK 机制:
生产者在发送消息时可以设置
acks
参数来控制消息的确认级别:acks=0
:生产者不等待任何确认。acks=1
:生产者等待 leader 副本确认。acks=all
:生产者等待所有 ISR 中的副本确认。设置
acks=all
可以确保消息被所有副本确认,从而提高数据的可靠性。ZooKeeper 用于元数据管理:
Kafka 使用 ZooKeeper 来管理和协调集群中的 broker、topic 和 partition 的状态。
ZooKeeper 监控 broker 的状态,并在 broker 宕机时触发 leader 选举和重新分配。
负载均衡:
Kafka 通过将 partition 分散到不同的 broker 上,实现负载均衡,避免单点压力过大。
这种分散存储的方式也提高了系统的整体吞吐量和可用性。
配置参数:
通过调整 Kafka 的配置参数,如
replication.factor
(副本数)、min.insync.replicas
(最小同步副本数)等,可以进一步优化高可用性。
kafka 宕机后那些正在消费中的消息该怎么处理?
会通过以下方式来处理那些正在被消费的消息:
自动切换到备份:
每个 topic 的 partition 都有多个副本(复制的数据),其中一个副本是 leader,负责处理读写请求。其他副本是 follower,它们从 leader 复制数据。
如果 leader 所在的 broker 宕机了,Kafka 会自动选择一个健康的 follower 副本作为新的 leader。这个过程对消费者是透明的,消费者可以继续从新的 leader 读取消息。
消费者重新分配:
当 broker 宕机后,消费者的消费组会进行一次重新平衡(rebalance)。这意味着 Kafka 会重新分配 partition 给消费者,确保每个 partition 只有一个消费者在读取。
重新平衡后,消费者可以从上次提交的位置继续消费消息。如果消费者之前已经提交了偏移量(offset),那么它可以从提交的位置开始继续消费,而不会丢失或重复消息。
偏移量管理:
消费者可以配置为自动提交偏移量,也就是每隔一段时间自动告诉 Kafka 已经消费到哪个位置了。这样即使消费者宕机,重启后也可以从上次提交的位置继续消费。
如果需要更精确的控制,消费者可以选择手动提交偏移量,在处理完一条消息后再提交偏移量,这样可以避免消息丢失或重复。
客户端自动重连:
Kafka 客户端(比如消费者)通常会有自动重连机制。如果连接断开了,客户端会尝试重新连接到 Kafka 集群。
客户端和 broker 之间还有心跳检测机制,如果发现连接中断,客户端会尝试重新建立连接。
kafka 重复消费问题怎么解决?
1. 幂等性处理
确保业务逻辑是幂等的:设计你的业务逻辑,使得多次处理同一条消息不会产生不同的结果。例如,如果消息涉及更新数据库记录,可以使用唯一键来防止重复插入。
2. 事务支持
使用 Kafka 事务:Kafka 支持事务,可以在同一个事务中同时处理消息和提交偏移量。这样即使在处理过程中出现故障,也不会导致重复消费。
生产者和消费者都可以参与事务,确保数据的一致性和完整性。
3. 手动提交偏移量
关闭自动提交:将
enable.auto.commit
设置为false
,然后在消息成功处理后再手动提交偏移量。同步提交:
consumer.commitSync()
,这种方式会阻塞直到提交完成。异步提交:
consumer.commitAsync()
,这种方式是非阻塞的,但需要处理提交失败的情况。
4. 去重机制
使用外部存储去重:利用外部存储(如 Redis、数据库)来记录已经处理过的消息 ID 或其他唯一标识符。每次处理消息前,先检查该消息是否已经被处理过。
这种方法适用于对消息去重有严格要求的场景,但会增加额外的复杂性和开销。
5. 布隆过滤器
使用布隆过滤器:对于大量数据,可以使用布隆过滤器来高效地检测消息是否已经被处理过。布隆过滤器是一种空间效率很高的概率型数据结构,适合用于大规模数据的去重。
项目
(跟他本人项目相关的,就不详细解释了)
你在项目中用了哪些方式保证可用性?
滑动窗口你是怎么实现的?为什么用 zet?用 list 不行吗?直接用 string 呢?
热榜为什么要定时计算??直接保存在缓存里面,然后添加的时候直接用 string + 1 不行吗?
在项目中你的资源配置变更了怎么处理?你的服务怎么知道你的资源变更了?
现在让你设计一个抢红包的系统,你会怎么设计?
早日上岸!
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:infoq 面试群。
评论