头图

新的一年,希望观看本文的各位朋友offer拿到手软,薪资翻倍

今天分享的是训练营的朋友在阿里一面的面试题,内容涉及Redis的核心知识点和LRU算法的实现

这些内容非常实用,尤其是对正在准备面试或工作中需要用到Redis的同学。一起来看看吧!


Redis 数据消失的原因

Redis 中的数据如果既没有设置过期时间也没有被显式删除但仍然消失了,可能是因为以下原因之一:

  1. 配置了持久化策略
    如果 Redis 配置了 RDB 快照或 AOF 日志持久化策略,在重启后可能会因为数据加载失败而导致数据丢失。
  2. 执行了清空命令
    如果不小心执行了 FLUSHDBFLUSHALL 命令,会导致当前数据库或所有数据库的数据被清空。
  3. 内存驱逐策略
    当 Redis 达到配置的最大内存限制时,会根据驱逐策略(如 allkeys-lruvolatile-lru)自动移除部分键值对以释放内存空间。
  4. 主从复制故障切换
    在主从复制场景中,如果发生故障切换且主节点的数据未完全同步到从节点,可能会导致部分数据丢失。

Redis 的数据结构

Redis 支持五种核心数据结构:

  • 字符串(String):适用于存储单值类型的数据,如用户密码、计数器等。
  • 哈希表(Hash):适用于存储键值对类型的复杂对象,如用户信息。
  • 列表(List):适用于需要按顺序存储和操作数据的场景,如消息队列。
  • 集合(Set):适用于需要去重和快速查找的场景,如关注列表。
  • 有序集合(Sorted Set):结合了集合和排序的功能,适用于需要按分数排序的场景,如排行榜。

Redis GeoHash 的使用和实现

Redis 的 GeoHash 功能可以将经纬度坐标编码成一个字符串,从而简化位置数据的管理和检索。在内部,Redis 使用有序集合(ZSet)来保存每个地点的位置信息,其中元素的分数代表了该地点的 GeoHash 编码值。

应用场景

  • 周边商家推荐
  • 地理位置签到

实现原理

  1. 将经纬度坐标转换为 GeoHash 字符串。
  2. 将 GeoHash 字符串作为 ZSet 的成员,并将其对应的分数设置为该字符串的哈希值。
  3. 通过 ZSet 的范围查询功能,快速找到指定区域内的所有地点。

ZSet 的存储方式

ZSet 是 Redis 中一种特殊的集合类型,允许为每个成员关联一个浮点数分值,并按此分值升序排列。为了保证高效的插入、删除和查找操作,Redis 在内部采用了跳跃表(Skiplist)与哈希表相结合的方式组织数据:

  • 跳跃表:确保了快速的顺序访问(O(log n) 时间复杂度)。
  • 哈希表:提供了 O(1) 复杂度的随机访问能力。

应用场景

  • 排行榜(如游戏积分排行)
  • 分布式锁

RocketMQ 如何保障高性能

RocketMQ 是一款高性能的消息队列系统,其核心性能优势来源于以下设计:

  1. 异步刷盘
    RocketMQ 将消息写入内存后立即返回给生产者,随后异步将消息刷盘,减少了 I/O 阻塞。
  2. 批量发送
    生产者可以将多个消息批量发送到 Broker,减少了网络开销。
  3. 零拷贝技术
    通过 mmap 文件映射和 PageCache 机制,减少了文件读写的开销。
  4. 多线程模型
    RocketMQ 采用生产者线程池和消费者线程池的设计,充分利用 CPU 资源。
  5. 文件索引管理
    RocketMQ 对消息文件进行了预分配和索引优化,提升了消息的读取效率。

批量提交的好处

批量提交指的是将多个独立的操作合并成一次请求发送给服务器。这种方式有以下好处:

  1. 减少交互次数
    批量提交可以显著减少客户端与服务端之间的交互次数,降低通信成本和整体延迟。
  2. 提高资源利用率
    减少了每次请求的上下文切换和资源分配过程,系统能够更加高效地利用计算资源。
  3. 促进资源共享
    多个任务可以在单次调用中得到统一处理,减少了并发控制带来的额外负担。

固态硬盘(SSD)和机械硬盘(HDD)的区别

SSD 和 HDD 的主要区别如下:

特性SSDHDD
存储介质闪存颗粒磁碟片
速度
功耗
抗震性
寿命较长较短
单位容量成本较高较低

随机读和顺序读

  • 随机读:按照不连续的地址序列访问数据块。对于 HDD 来说,随机读会导致频繁的磁头定位操作,延迟较高。对于 SSD 来说,随机读和顺序读的性能差异较小。
  • 顺序读:沿着预先确定的路径连续读取一系列数据块。这种方式对于 HDD 来说更为有利,因为不需要频繁调整磁头位置。

MQ 消息如何保障不丢失

消息队列系统通常通过以下机制保障消息不丢失:

  1. 持久化存储
    消息被写入磁盘后才会确认提交,即使发生故障也能恢复。
  2. 确认机制
    消费者成功处理完消息后向 MQ 发送确认信号。如果消费者未确认或处理失败,消息会重新投递。
  3. 高可用部署
    通过集群化配置和负载均衡技术,避免单点故障造成的服务中断。

注册中心挂了怎么办

如果注册中心发生故障,可以采取以下措施:

  1. 本地缓存
    应用程序可以缓存服务实例信息,在注册中心不可用时依赖本地缓存继续运行。
  2. 健康检查与重试
    应用程序可以定期检查注册中心的状态,并在注册中心恢复后重新连接。
  3. 高可用部署
    注册中心本身应采用分布式部署策略(如负载均衡或多活数据中心),提高系统的稳定性和可靠性。

编程题: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,备注:面试群。


王中阳讲编程
833 声望319 粉丝