redis 源码阅读杂记

Posted tmortred

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis 源码阅读杂记相关的知识,希望对你有一定的参考价值。

Misc

  • rehash 是分 db 的
  • redis db 中的 字典什么情况下会自动 rehash
  • redis 中的 key 淘汰, 定时被动淘汰(有2 种模式)。 另外则是每次访问到 key 都会检查一下 key 是否过期则删除(也能减少部分 key)
  • key 的读写分多套接口,基本上读写的功能函数是分离的。这是因为 read 要统计命中效率的。也是具体实现细节吧
  • expires 的 key 居然放在另外的 一个 dict 中, 想象也是合理。不然就要全量遍历 dict 中的所有 key。 不过 expires dict 和 ht[0】、ht[1] 底层存储的数据是共享的
  • redis master-slave 是干啥的? slave 的数据无法保证是最新的啊 (slave不会主动删除过期键,会读到脏数据) 。主从主要是为了解决单点故障? 或者说 主从能接受一定的数据不一致
  • scan 的游标如何保证在多次 scan 之间新增数据的会被 scan 到。 通过游标的方式不会有问题 (官方文档已经写了,只保证在 scan 开始的时候数据能被读取到,但是 scan 开始后的数据无法保证)
    • 据说 dictScan 的算法很精妙,感觉说的很有道理。但是又不知道道理在那里
    • scan 可能对同一个值可能会多次返回!通常说的多次就是 2次。因为有2 个哈希表。如果在旧表中被被返回过一次。rehash 时候被迁移到新桶时候又被访问一次(错!扩容不会导致同一个数据被多次返回。缩容才会)
    • hash 用的还是开放地址法解决冲突
    • 高位的进一能有效避免重复返回数据(重复返回数据只会和 rehash 有关, 高位进一的方法能保证在 rehash 的时候知道哪些桶被访问过。因为 rehash 后桶的序号信息中还是能看出来 这个桶在分裂前的序号是多少(做一次0111 这样的与运算就可以了)。 低位进一则不可以。如果 序号位 7 的桶,采用低位进一的方法活变成序号为14,15 两个桶。如果 cursor 为 6 的话。则依然会返回大量的重复数据!。这也能解释为什么游标开始后的新增的数据是否能被访问到是未定义行为
  • 慎用命令 rename 可能会删除旧的同名 key (删除一个超大的 hash key 会拖慢系统), 所以 rename 可能会影响 redis 响应速度
  • db 也会 rehash ? 如果会,那就有个很奇特的现象。即db 中的 hash key 会 rehash。同时 db 也在 rehash。这就好玩呢。所以真实情况是啥?
    • db 和 hash 都是用 dict 结构 (hash 的底层数据结构也有可能是 intset)。 db 的 rehash 比较复杂,采用的是渐进式 rehash (在 db 每次的 find、add、delete 都会完成部分的 rehash 操作。 而且 rehash 和上面的 scan 命令还有一点关系。 更多细节参见美团 blog https://tech.meituan.com/2018/07/27/redis-rehash-practice-optimization.html
  • redis dict 不同于常规的 dict 的 c 实现的部分,就是有大量的 rehash 、iterator 相关的逻辑
    • 我也不知道当时,自己写的是啥
  • 如果在 rehash 的过程中。 db 突然被插入了很多 key。会怎么样?
    • 直接插入到 ht[1] 表,和插入 key 数量无关
  • redis dict 中 unsafe iterator 是干什么的? 和 safe iterator 有什么区别?
    • 依然不是很了解这部分的逻辑
    • redis command 基本上都没有用到 iterator 相关的逻辑, master-slave、replicate 反而用到了 iterator 逻辑
  • redis dict find 算法不会使用 cuckoo filter 做快筛?
    • 因为 leveldb 和 pg 都会有 bloom 做内存命中查询的,如果没有不在内存中才会去读磁盘。但是 redis 本身就是内存中啊。 而且 bloom filter 在大数据量时的 fpr 数据贼差,不如 cuckoo。 但是 cuckoo 又是适合读的 hash 结构~~
  • redis set 命令会检查所有的 watch key 的 clients,所以对同一个 key 还是不要 watch 的好。虽然 redis 性能很高,但是也经不起这样的折腾啊!
    • 除了 set, expire 等任何和 key 相关的指令都会给所有 watch 该 key 的clients 加标志的!
  • keys 命令会使用到 dict safe iterator
    • scan 和 dict iterator 是两回事,但是这两者在语义上又有混淆。1) 第一做的事情差不多。 2)在有的人的描述 scan 命令直接就用 迭代(iterator) 这个词。 那么就有个问题,为什么 scan 要用 curos (算法有精妙的部分,但是还是很粗糙。即用了某一方面的精妙,做了另外一方面粗糙的事情)。为什么 scan 不用常规意义上的 iterator 呢?还是因为 redis 是多少客户端的。使用 iterator 如何保持每个客户端的 iterator 状态呢? 此外,每个 client 也可以有多个 iterator, 这又该如何迭代呢
    • 为什么 keys 命令可以直接使用 iterator? 因为它只要返回所有就好了。由 client 保证一切~~(如 keys 命令拖慢了线上系统~~)
    • dict safe iterator 有什么特性? 会暂时将 dict find、add、delete 命令触发的 rehash 冻结住。等所有的 safe iterator 全部迭代完后才会重新做 渐进式 rehash
  • 为什么 redis 在返回结果前 要 incrRefCount 呢? 避免 key 被删除吧。同时在 networking.c 会把 obj 写成对应的协议后写到 buf 中去,后面才会 decrefcount 的。
    • 问题来了,redis 是单线程的。一个 key 不用 incrRefcount, 也不用担心会被 expire 掉啊。networking 发生数据那里是另外一个线程?(networking 不是另外一个线程。需要 incr 的原因是,要等到 epoll 可写的时候才会写数据。所以需要保证数据不被回收掉)
  • hash 的插入和查找的复杂度 o(1) 操作还是 o(lgN)
    • 如果 hash 的结构是固定的 slot 且用 链表法解决冲突,则可以认为是 o(1)。 会占用大量的空间, 且需要不定期 rehash
    • 如果 hash 的底层存储使用的 红黑树 或这 skiplist, 则认为是 o(lgN)
    • t_set.c 、t_string.c 、t_list.c 等 t_ 开头的文件是什么? 意思是基本数据类型?
      • t_ 开头代表是用户能直接访问的数据类型,底层可能是 intset、dict、skiplist 等实现的~。 如 t_hash 的底层数据结构可能是 intset 或者 dict
    • redis 最大只支持 512 M 的字符串是后台硬编码实现的

pubsub

  • 看样子使用 pubsub 不会拖慢系统~。以前认为 pubsub 会 make system slowdown (以前认为 watch 可能会拖慢系统, 其实 watch 也不会。但是一个 key 里面有 上千个 client 在 watch 可能有问题)
  • client A publish 信息后会什么时候返回?等发送消息给所有 client 后? 还是 redis-server 收到消息后里面返回? 还是其他情况?
    • 从代码上来看,会把消息发送给所有 clients 后才会返回(不知道 addReply 会不会导致网络消息发送,即不知道 networking.c 中的处理逻辑
    • addReply 只是会发送数据,不代表能发送成功把。比如 客户端 宕机了?
    • 客户端已经宕机了很久,会被 redis 感知到?(server 是无法感知的,可以依靠定时任务将 idle time 超过 max_idle_time 的 client 回收掉。但是默认配置是 max_idle_time 为无限)
  • 需要等看懂网络层后才可以理解 pubsub 的工作原理!
    • 其实也没有啥好看的

transaction 和 pipeline

    • redis 的事务真是 搞笑, 自定义一套的 acid 标准(原子: cpu 指令级别的。 redis 自定义的命令原子级别 )。不过事务相关的代码只有 300 行,也不会有多么牛逼功能的!
    • 原子的 lua 操作,也只是保证中间不会有其他命令插入进来。lua 脚本在 pop list 等操作后执行失败,list 中的数据不会被 rollback
        
        local v1 = redis.call("LPOP", "lst")
        print(v1)
        redis.call("LPUSH", "nlist", "DDDD") // 这里会出错, nlist 是一个 字符键不是 列表键。
        return v1
        
    
    • pipeline
        
            redis-cli 不支持 pipeline, 但是 sdk 支持。理论上所有使用 协议 交互的 client-server 架构服务都支持 pipeline (redis 等),只要 server 从 recieve buffer 接受一条不完整的数据能继续等待,以及接受到一条完整的数据后不会将多余的数据当作错误数据清除掉。 大多数人都犯了一个错误,即认为 pipeline 具有原子性。pipeline 支持批量发送数据。另外,pipeline 和 multi 没有木有关系。multi 中的命令是一条条返送到 server 端 queued 住,等到一条 exec 后才会执行的。 但是有的语言的 sdk 实现比较奇特,会在 client 端 queue multi 命令,直到用户程序请求 exec 时才会将 multi 到 exec 之间的命令通过 pipeline 方式发送到 redis !
            redis-cli 中的 recieve buffer 有多大?

            一个用于测试 pipeline 的命令: (printf "PING
PING
PING
"; sleep 1) | nc localhost 6379
        
    

client server 握手过程

    
        ./server *:6379(print_trace+0x60) [0x562da7feb6e0]
        ./server *:6379(aeCreateFileEvent+0x49) [0x562da7f969b9]  # 将readQueryFromClient 注册为 epoll 回调函数
        ./server *:6379(createClient+0x55) [0x562da7fa6b25]
        ./server *:6379(+0x2d2de) [0x562da7fa72de]
        ./server *:6379(acceptTcpHandler+0x63) [0x562da7fa73d3]
        ./server *:6379(aeProcessEvents+0x128) [0x562da7f96e08]
        ./server *:6379(aeMain+0x2b) [0x562da7f9717b]
        ./server *:6379(main+0x2b6) [0x562da7f95d76]
    

此外 redis-cli 版本大于 4.0 的话,在连接上 server 后会自动执行一下 command 命令, 而且 执行结果不会展示给用户

redis 事件模型

    
    每次进入到事件循环前,会调用 beforeSleep 函数。 beforeSleep 函数会主动 expire key、主从同步、AOF 数据写回到磁盘(非强制,可能还在 linux io buffer 中)、处理哪些 blocked on key 中的 clients(blocked client 本身不是时间敏感的。所以某个 client  set key 后不会立即处理 blocked client 的请求)
    进入到事件循环后,会先计算最早需要处理的时间事件,根据这个计算出 epoll_wait 的等待时间。后续调用 epoll_wait 获取需要处理的 io 事件。处理完 io 事件后会处理所有需要处理的时间事件

    当然改了系统时钟,redis 会做处理。具体的逻辑记得不太清楚了
    

以上是关于redis 源码阅读杂记的主要内容,如果未能解决你的问题,请参考以下文章

Python代码阅读(第19篇):合并多个字典

阅读杂记

Python代码阅读(第26篇):将列表映射成字典

如何进行 Java 代码阅读分析?

Python代码阅读(第41篇):矩阵转置

Python代码阅读(第25篇):将多行字符串拆分成列表