大厂面试题详解三:Redis篇

236 阅读8分钟

基本数据类型,底层实现

string:int、raw或者embstr

hash:ziplist,hashtable dict字典实现

set:intset 整数集合 O(logn),hashtable

zset,ziplist,skiplist跳表+字典 head tail 头尾节点 节点数量 最大层数,类似平衡树

list:ziplist 压缩链表,连续内存存储,存储不大的对象,O(n)

linkedlist:双向链表 pre next指针,每增加一个节点,就分配一次内存

Redis快的原因

  • 基于内存操作

  • 数据结构简单,优化过的数据结构

  • 单线程,无上下文切换和竞争

  • 多路复用IO

过期策略

定期删除:redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

惰性删除:获取key的同时redis检查key是否过期,过期就删除,不返回内容

内存淘汰

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

基于HashMap 双向链表,key存储,value指向LRU Node节点

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int CACHE_SIZE;
    /*** 传递进来最多能缓存多少数据    
     ** @param cacheSize 缓存大小     
     */public LRUCache(int cacheSize) {
        // true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
        CACHE_SIZE = cacheSize;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
        return size() > CACHE_SIZE;
    }
}

Redis cluster

  • 自动将数据进行分片,每个 master 上放一部分数据

  • 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的

节点通信原理(gossip)

所有节点持有一份元数据(节点信息等),不同的节点出现元数据的变更,不断将元数据发送给其他节点,让其他节点也进行元数据变更。

meet :某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信

ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。

pong:返回 ping 和 meet,包含自己的状态和其它信息,也用于信息广播和更新。

fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。

hash slot

redis cluster 有固定的 16384个 hash slot,对每个 key计算 CRC16值,然后对 16384 取模,可以获取 key 对应的 hash slot。

redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag来实现。

任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。

主备切换

判读节点宕机:在cluster-node-timeout时间范围内,如果某个节点没有返回pong,就默认为pfail,之后在ping消息中发送给其他节点,如果超过半数的节点都认为pfail,就变成fail宕机

节点过滤:对于宕机的master node,从slave node中选择一个切换为master

节点选举:和master断开连接的时长、本身节点数据量多少,超过半数投票则选举成功

Redis高可用

redis主从复制原理

(无法故障自动恢复)

异步复制:slave启动,发送pysnc命令到master,如果第一次连接master,触发一次全量复制,生成RDB快照,发送给slave,同时内存里的写命令也会同步给slave,slave收到文件,先写入磁盘,再从磁盘加载到内存

大厂面试题详解三:Redis篇

复制过程网络中断:支持断点续传,从上次复制的地方继续复制,master保存backlog

哨兵集群实现高可用

(无法负载均衡)

集群监控:负责监控master slave是否正常运行

消息通知:redis实例故障,发送消息警报

故障转移:master挂掉,自动转移到slave

配置中心:如果故障转移发生,通知client新的master地址

集群节点必须>2,如果只有两个,quorum=1

选举算法

  • 跟 master 断开连接的时长

  • slave 优先级

  • 复制 offset,哪个slave复制数据越多、offset越靠后,优先级越高

  • run id,选择run id较小的

主备切换数据丢失问题

异步复制:master挂了,数据没有复制到slave导致丢失

脑裂:(master脱离了正常网络,但是实际还在运行,导致从新选举了master,出现2个master),client没有来得及切换到新的master继续向旧master写数据,旧master恢复网络变成slave,清空自己数据,这部分数据未同步导致丢失

解决方案:min-slaves-to-write 1,min-slaves-max-lag 10。至少有一个slave,数据复制的延迟不能超过10秒,如果脑裂,最多丢失10秒数据。只能减少数据丢失,不能完全避免

持久化机制

RDB就是快照,AOF就是binlog。

RDB:对redis数据进行周期性的持久化,将当前内存中的数据快照保存到磁盘,rdbSave 生成RDB文件到磁盘,rdbLoad RDB文件重新载入内存

优点:对redis影响小,fork出字进程执行磁盘IO读写,相比AOF,重启恢复更快

缺点:快照文件生成间隔时间长(5分钟),生成文件时如果文件过大,可能导致服务暂停

AOF:每条写入命令,append模式写入日志文件

优点:间隔时间短(一般1秒),最多丢失1秒数据,append写入,性能高,可以通过可读的方式记录,适合做灾难时误删除紧急恢复

缺点:对于同一份数据来说,AOF比RDB更大,开启后,写QPS比RDB低,因为一般配置成1秒一次异步同步

分布式锁

利用底层setnx expire,设置锁的过期时间,防止客户端崩溃导致死锁

zookeeper:创建临时节点,找到最小的序列节点,创建成功则获得锁,创建释放需要创建和删除节点比较耗费资源

热key

所谓热key问题就是,突然有几十万的请求去访问redis上的某个特定key。那么,这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis的服务器宕机,缓存击穿,可能导致DB崩溃,雪崩

解决方案:

  1. 二级缓存,提前加载热key数据到内存中,直接走内存查询,设置过期时间

  2. 热key打散到各个redis服务器

  3. 高可用,支持redis服务器扩展

缓存击穿

单个key并发访问过高,过期时导致所有请求直接到db上

  • 加锁更新,double check

保证只有单个线程真正请求到db,拿到数据后进行缓存

后续并发线程会拿到缓存中的值

  • 永不过期

将过期时间,组合在value中

通过异步方式更新刷新过期时间

缓存穿透

查询不存在缓存和DB中的数据,每次请求都会打到DB

  • 布隆过滤器 (误判,集合小,数据量大,所有点都变成1就会发生误判)。布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了,如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。可能的实现方式是将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
  • 缓存空值,直接返回null

缓存雪崩

当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上

  • 集群保证高可用

  • 限流、二级缓存

  • 过期时间随机

一致性hash

  • 整个hash值空间组成虚拟的圆环,将master节点的ip或者主机名hash取模确定节点的位置。

  • 节点太少容易导致缓存热点问题,容易导致缓存击穿

  • 虚拟节点,数据均匀分布

大厂面试题详解三:Redis篇