新的一年,希望观看本文的各位朋友offer拿到手软,薪资翻倍!
今天分享的是训练营的朋友在阿里一面的面试题,内容涉及Redis的核心知识点和LRU算法的实现。
这些内容非常实用,尤其是对正在准备面试或工作中需要用到Redis的同学。一起来看看吧!
Redis 数据消失的原因
Redis 中的数据如果既没有设置过期时间也没有被显式删除但仍然消失了,可能是因为以下原因之一:
- 配置了持久化策略
如果 Redis 配置了 RDB 快照或 AOF 日志持久化策略,在重启后可能会因为数据加载失败而导致数据丢失。 - 执行了清空命令
如果不小心执行了FLUSHDB
或FLUSHALL
命令,会导致当前数据库或所有数据库的数据被清空。 - 内存驱逐策略
当 Redis 达到配置的最大内存限制时,会根据驱逐策略(如allkeys-lru
或volatile-lru
)自动移除部分键值对以释放内存空间。 - 主从复制故障切换
在主从复制场景中,如果发生故障切换且主节点的数据未完全同步到从节点,可能会导致部分数据丢失。
Redis 的数据结构
Redis 支持五种核心数据结构:
- 字符串(String):适用于存储单值类型的数据,如用户密码、计数器等。
- 哈希表(Hash):适用于存储键值对类型的复杂对象,如用户信息。
- 列表(List):适用于需要按顺序存储和操作数据的场景,如消息队列。
- 集合(Set):适用于需要去重和快速查找的场景,如关注列表。
- 有序集合(Sorted Set):结合了集合和排序的功能,适用于需要按分数排序的场景,如排行榜。
Redis GeoHash 的使用和实现
Redis 的 GeoHash 功能可以将经纬度坐标编码成一个字符串,从而简化位置数据的管理和检索。在内部,Redis 使用有序集合(ZSet)来保存每个地点的位置信息,其中元素的分数代表了该地点的 GeoHash 编码值。
应用场景:
- 周边商家推荐
- 地理位置签到
实现原理:
- 将经纬度坐标转换为 GeoHash 字符串。
- 将 GeoHash 字符串作为 ZSet 的成员,并将其对应的分数设置为该字符串的哈希值。
- 通过 ZSet 的范围查询功能,快速找到指定区域内的所有地点。
ZSet 的存储方式
ZSet 是 Redis 中一种特殊的集合类型,允许为每个成员关联一个浮点数分值,并按此分值升序排列。为了保证高效的插入、删除和查找操作,Redis 在内部采用了跳跃表(Skiplist)与哈希表相结合的方式组织数据:
- 跳跃表:确保了快速的顺序访问(O(log n) 时间复杂度)。
- 哈希表:提供了 O(1) 复杂度的随机访问能力。
应用场景:
- 排行榜(如游戏积分排行)
- 分布式锁
RocketMQ 如何保障高性能
RocketMQ 是一款高性能的消息队列系统,其核心性能优势来源于以下设计:
- 异步刷盘
RocketMQ 将消息写入内存后立即返回给生产者,随后异步将消息刷盘,减少了 I/O 阻塞。 - 批量发送
生产者可以将多个消息批量发送到 Broker,减少了网络开销。 - 零拷贝技术
通过 mmap 文件映射和 PageCache 机制,减少了文件读写的开销。 - 多线程模型
RocketMQ 采用生产者线程池和消费者线程池的设计,充分利用 CPU 资源。 - 文件索引管理
RocketMQ 对消息文件进行了预分配和索引优化,提升了消息的读取效率。
批量提交的好处
批量提交指的是将多个独立的操作合并成一次请求发送给服务器。这种方式有以下好处:
- 减少交互次数
批量提交可以显著减少客户端与服务端之间的交互次数,降低通信成本和整体延迟。 - 提高资源利用率
减少了每次请求的上下文切换和资源分配过程,系统能够更加高效地利用计算资源。 - 促进资源共享
多个任务可以在单次调用中得到统一处理,减少了并发控制带来的额外负担。
固态硬盘(SSD)和机械硬盘(HDD)的区别
SSD 和 HDD 的主要区别如下:
特性 | SSD | HDD |
---|---|---|
存储介质 | 闪存颗粒 | 磁碟片 |
速度 | 快 | 慢 |
功耗 | 低 | 高 |
抗震性 | 好 | 差 |
寿命 | 较长 | 较短 |
单位容量成本 | 较高 | 较低 |
随机读和顺序读
- 随机读:按照不连续的地址序列访问数据块。对于 HDD 来说,随机读会导致频繁的磁头定位操作,延迟较高。对于 SSD 来说,随机读和顺序读的性能差异较小。
- 顺序读:沿着预先确定的路径连续读取一系列数据块。这种方式对于 HDD 来说更为有利,因为不需要频繁调整磁头位置。
MQ 消息如何保障不丢失
消息队列系统通常通过以下机制保障消息不丢失:
- 持久化存储
消息被写入磁盘后才会确认提交,即使发生故障也能恢复。 - 确认机制
消费者成功处理完消息后向 MQ 发送确认信号。如果消费者未确认或处理失败,消息会重新投递。 - 高可用部署
通过集群化配置和负载均衡技术,避免单点故障造成的服务中断。
注册中心挂了怎么办
如果注册中心发生故障,可以采取以下措施:
- 本地缓存
应用程序可以缓存服务实例信息,在注册中心不可用时依赖本地缓存继续运行。 - 健康检查与重试
应用程序可以定期检查注册中心的状态,并在注册中心恢复后重新连接。 - 高可用部署
注册中心本身应采用分布式部署策略(如负载均衡或多活数据中心),提高系统的稳定性和可靠性。
编程题:LRU 算法实现
实现思路
LRU(Least Recently Used)是一种常见的缓存淘汰算法。它的核心思想是:当缓存满时,移除最近最少使用的项。为了高效实现 LRU 算法,我们可以结合双向链表(用于维护元素的使用顺序)和哈希表(用于快速查找元素)。
Go 语言实现代码
go复制package main
import (
"container/list"
"fmt"
)
// LRUCache 定义了一个基于 LRU 策略的缓存结构。
type LRUCache struct {
capacity int // 缓存容量
cache map[int]*list.Element // 哈希表,用于存储键及其对应的链表节点
lru *list.List // 双向链表,用于维护元素的使用顺序
}
// Entry 表示缓存中的一个条目。
type Entry struct {
key int // 键
value int // 值
}
// Constructor 初始化一个新的 LRUCache 实例。
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
lru: list.New(),
}
}
// Get 根据键获取对应的值。如果键不存在,则返回 -1。
func (cache *LRUCache) Get(key int) int {
if elem, ok := cache.cache[key]; ok {
cache.lru.MoveToFront(elem) // 将最近使用的元素移动到链表头部
return elem.Value.(*Entry).value
}
return -1 // 如果键不存在,返回 -1
}
// Put 插入或更新键值对。如果缓存已满,则移除最近最少使用的元素。
func (cache *LRUCache) Put(key int, value int) {
if elem, ok := cache.cache[key]; ok {
cache.lru.MoveToFront(elem) // 更新现有条目,并将其移动到链表头部
elem.Value.(*Entry).value = value
} else {
if cache.lru.Len() >= cache.capacity {
// 移除链表尾部的元素(最近最少使用的)
old := cache.lru.Back()
if old != nil {
cache.lru.Remove(old)
delete(cache.cache, old.Value.(*Entry).key)
}
}
// 将新条目添加到链表头部
elem := cache.lru.PushFront(&Entry{key, value})
cache.cache[key] = elem
}
}
func main() {
cache := Constructor(2 /* 容量 */)
cache.Put(1, 1)
cache.Put(2, 2)
fmt.Println(cache.Get(1)) // 输出:1
cache.Put(3, 3) // 移除键 2
fmt.Println(cache.Get(2)) // 输出:-1
cache.Put(4, 4) // 移除键 1
fmt.Println(cache.Get(1)) // 输出:-1
fmt.Println(cache.Get(3)) // 输出:3
fmt.Println(cache.Get(4)) // 输出:4
}
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。