前言

不知道我是被pua了,还是真的不了解底层。但是我还是决定沉淀一下。

Redis

redis 作为常见考点,也是很常见的缓存方案,不过有观点认为考虑到网络延迟(TCP 往返 + 序列化 + 网络调度)有的时候还不如直接postgreSQL?

数据结构

  1. string:典中典,不过也不会有人问的

    其数据结构是SDS(Simple Dynamic String),说白了就是比C语言字符串多了个长度,放到结构体的头部(弱弱地插一句才知道redis是用c写的)

  2. hash:我说我没用过有没有懂得。但是为了用它,我没事找事,创造了需求,在秒杀环节,还是特别傻逼的能秒杀多个地业务场景(秒杀正常人类指的是买一个),可以通过用商品id作为key,用户的id作为field,value存储用户购买量。这时候你就会发现,哎呀,一个hash存了一坨,这不就是大key问题吗。所以我们用userid % 32,来进行分桶设计key。seckill:goodId:userId%32 userId

    然后我们进行底层学习:

    当它很小的时候是listpack或者ziplist。这个以后再说。hash-max-listpack-entries默认是512,hash-max-listpack-value是64,当元素不大于512,且value不大于64时,不会用hashtable。(当然此时不是O(1)的操作,不过CPU命中缓存了,是很快的线性便利)

    下面为hashtable:redis里面是有h[0] 和 h[1] 两个哈希库的,哈希冲突采用拉链法,h[0] 为旧库,h[1]为新库,初始的时候hash大小为4,当新key过多,超过负载因子的时候,我们就会渐进式扩容,每次扩展两倍。通过rehash的方式,将 h[0] 中的键值对逐步迁移到 h[1]中,修改的是索引,哈希函数不会改变,因为hash(key) & (size - 1)的size变了,所以需要调整。(&((size-1)就是取模)。当 h[0] 的数据全部搬空后,释放 h[0] 占用的内存,将 h[1] 的指针赋值给 h[0],重新初始化 h[1] 为空表。

  3. set:集合。其实秒杀对应的就是集合。同上,只要你不存在逆天想要秒杀多个,人类都会用这个。

    数据结构是intsethashtableset-max-intset-entries默认是512,数量小于等于512且在无字符串的时候会用inset。

  4. zset(sorted set):有序集合。用的是skiplist/listpack,用来做排行榜,不过目前没遇到这个业务需求。

    我们先了解传统的跳表skiplist。每一个元素都有随机(通常50%)的概率来添加一个升到上一层的复制品,所以跳表的层数是随机的。

    1
    2
    3
    4
    5
    6
    7
    
    typedef struct SkipListNode {
        int key;               
        int value;              
        int level;    // 当前节点的层级高度 (1 到 MAX_LEVEL)             
        // 柔性数组:存储每一层的前进指针
        struct SkipListNode *forward[]; // forward[i] 指向第 i 层的下一个节点
    } SkipListNode;
    

    然后是redis的定制版本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    typedef struct zskiplistNode {
        sds ele;          // 成员(member)
        double score;     // 分数
        struct zskiplistNode *backward; // 后退指针,用于反向遍历
        struct zskiplistLevel {
            struct zskiplistNode *forward; // 前进指针
            unsigned long span;            // 跨度,用于计算排名
        } level[];        // 柔性数组,表示多层
    } zskiplistNode;
    

    肉眼可见,它多了一个span所以可以

    • O(log N) 获取排名ZRANK/ZREVRANK):在查找节点的过程中,累加经过的 span 值,即可直接得出该节点的排名,无需遍历。
    • O(log N) 根据排名获取元素 (ZRANGE):可以通过二分查找式的跳跃,快速定位到指定排名的节点。

    还可以看到多了一个backward,这允许它反向遍历。

    但其实它还允许score是重复的(即key),如果相同则字典序比较ele。还限定的最高高度为32,因为2^32次方够你排序了,再多有点逆天了。

    最后要指出头指针指向的是最高层的节点,然后推荐自己脑袋模拟一下。

  5. bitmap 位图,如果你做过二进制枚举或者二进制dp压缩的问题,这个很好理解,就是一样的道理,其底层是sds。其场景如:用户签到,是否在线等二元情景。

    常见问题如用户采用uuid,跨度过大,浪费内存,我们就使用hash keyid:mapping field uuid value 用lua脚本写一个自增来进行映射。

  6. HyperLogLog 布隆过滤器,秒杀必备。

持久化

这个就是Redis Database(RDB)和Append Only File(AOF)

RDB简单粗暴,直接把内存存到文件里面,并且默认是启用的。唯一需要记住的就是有同步save和异步bgsave两种,父进程fork后交给子进程。

关键参数 save <seconds> <changes>,多少时间内多少次改变触发保存。

AOF,顺序读写,顺序读写最快了,有三个等级

1
2
3
appendfsync always    # 每次写都同步,最安全,最慢
appendfsync everysec    # 每秒同步,推荐折中方案
appendfsync no        # 由OS决定何时同步,最快,最不安全

随着时间推移,AOF 文件会变得越来越大。Redis 会在后台自动触发 AOF 重写:创建一个当前数据集的最小体积 AOF 文件(例如,如果一个 key 被设置了多次,最终只保留最后一次的值)。

目前生产常用混合持久化?

当开启混合持久化后,在进行 AOF 重写时,新产生的 AOF 文件不再全是纯命令日志,而是:

  1. 前半部分:以 RDB 二进制格式存储当前内存的全量快照。
  2. 后半部分:以 AOF 文本格式存储增量命令。

分布式容灾

redis挂了怎么办,虽然你妈挂了mysql都不会挂,但是redis可能挂,因为可能会oom,算了oom也不太可能,你就当被人拔电源了。

  1. 主从复制(Master-Slave Replication)

    Master 负责写,Slave 负责读和备份。Master 将写命令异步发送给 Slave。一个 Master 可以有多个 Slave。数据只能从 Master 流向 Slave。

    同步分为全量同步和增量同步。全量的时候发送的是rdb,或者 Master 不生成 RDB 文件到磁盘,而是直接通过 Socket 发送给 Slave。适合磁盘慢但网络快的场景。

    增量同步时,有一个全局的offset,每次master写入的时候,把操作指令发送给slave,slave会ack自己目前的offset。

    然后就是master挂了怎么办,master挂了就炸了,所以生产没有用主从结构的。

  2. 哨兵模式(Sentinel)

    一组 Sentinel 节点监控 Master 和 Slave 的状态。当 Master 不可达时,Sentinel 选举新的 Master,并通知客户端更新连接地址。

    Sentinel既可以和redis在一个服务器上跑,也可以放到独立的服务器来保证独立性,一般建议至少三台。

    哨兵系统主要完成以下三个任务:

    1. 监控(Monitoring): 哨兵会不断检查主节点和从节点是否运作正常。
    2. 通知(Notification): 当被监控的某个 Redis 节点出现问题时,哨兵可以通过 API 向管理员或其他应用程序发送通知。
    3. 自动故障转移(Automatic Failover): 如果主节点无法正常工作,哨兵会启动故障转移流程, 将一个从节点升级为主节点。 让其他的从节点改为复制新的主节点。 当客户端尝试连接失效的主节点时,集群也会向客户端返回新主节点的地址,使得集群可以使用新的主节点代替失效的主节点。

    然后哨兵也需要有一个主节点,当多个 Sentinel 认为 Master 客观下线(ODOWN)后,需要有一个 Sentinel 来主导故障转移流程(比如选择新的 Master、通知其他 Sentinel 和客户端)。这个主导者就是 Leader Sentinel。其过程类似Raft。

  3. Redis Cluster(分片集群)

    数据分片存储在多个 Master 节点上,每个 Master 有对应的 Slave。使用 Gossip 协议通信,客户端可直接连接任意节点。