首页|资源下载
登录|注册

您现在的位置是:首页 > 技术阅读 >  图解 Redis,真滴垃圾!

图解 Redis,真滴垃圾!

时间:2024-01-02

大家好,我是小林。

最近我们 新加入了两个 Redis 大佬。

  • 彬彬,是 Redis 7.0 源码贡献者,贡献源码超 1w+
  • 雅哥,是 Godis 开源作者(https://github.com/hdt3213/godis),Godis 项目应该很多玩过 Go 同学都接触过,是用 Go 手写了 Redis,非常强

他们回答 Redis 问题的角度和思路,比我强太多了,都是从源码和版本演进角度去答,这简直太强了,我瞬间觉得我图解网站上的图解 Redis 真垃圾啊!

Redis 7.0 源码贡献者Godis 开源作者

今天就给大家感受下专业的 Redis 解答!

学员提问-跳表问题

这里的意思是假如在Redis5.0的版本下,只要创建了跳表,他就会创建64个头节点,然后在创建节点的时候也判断随机数大小,如果小于0.25,那他的层数加1,然后对应层head就会顺着这层指向他,是这么理解的吗?

彬彬回答

他就会创建64个头节点

这个说法是错的,是指创建一个头节点,只是头节点默认的高度就是最高的,例如是 64 或者 32,这是 Redis 跳表的最大高度,定义的常量

然后在创建节点的时候也判断随机数大小,如果小于0.25,那他的层数加1

创建节点的时候,就会随机好节点的高度。例如默认是第一层,然后有 0.25 的概率这个节点会是两层高,有 0.25 * 0.25 的概率会是三层高,此役类推

然后对应层head就会顺着这层指向他

不一定是 head,取决于你在哪个位置插入节点,假设节点随机出来的高度是 n 的话,本质上就是要维护 n 条链表的有序性,即在 n 条链表中插入这个节点。

可能会有人感兴趣,这个跳表高度最大值怎么 32 -> 64 -> 32 改来改去的,这边是对应的最后一个修改 PR:https://github.com/redis/redis/pull/6818

从 64 改回 32,其实本质就是现实生活中基本,没办法碰到这么高层,按照 p = 0.25 的概率来说,正常来说百万个元素就是到达 10 层了,可以再往后算一下,20 层,32 层,需要多少个元素才能达到。目前现实生活中基本没办法撞到(首先内存就会爆炸)。所以才又改回 32,改回 32 可以节省一些内存资源、栈、性能

还有另外个 pending 的 PR,https://github.com/redis/redis/pull/3889,是说把 p = 0.25 给改成 p = 1/e,来缩短搜索时间。不过这个 PR 一直在 pending。

学员提问-为什么close文件需要异步?

关闭文件的是为什么耗时呢?

彬彬回答

异步 close 其实就主要用在 aof 部分,用 6.2 版本的 AOFRW 来举例(当然新版本里异步 close 也是只用在了 aof / rdb 的场景,都是为了规避同一个问题)

简单复习下 6.2 版本 AOFRW 重写完成时的流程:

在子进程重写完成后,会需要父进程收尾做一些善后工作 其中有几项工作是:

  • 将新重写完成的的 AOF 临时文件重命名回 aof 文件 关闭老的 aof 文件
  • 其实单单这样描述,是看不出啥问题,因为一个 rename 一个 close 正常来说其实都很快

不过我们想一下,AOFRW 除了我上面那样简单的描述,还有啥?

如果有开启 AOF,那么老的 aof 文件需要进行删除。rename new_aof to old_aof 其实就做了这个事情

如果删除的是一个很大的 AOF 文件,会有啥影响?

正常玩过 linux 删除文件/目录的都知道,删除一个大文件/大目录是很慢的 那么对于 Redis 来说,删除文件就意味着会有阻塞风险,主进程肯定是不能说因为删除文件而阻塞在这。

前面的 rename 和 close 都会有阻塞的风险,分别对应于 AOF 开启或者关闭的情景。

所以 Redis 用了个很巧妙的方式来规避这个阻塞。

Redis 会打开老的 aof 文件,也就是会 open 占有文件的 fd 的引用:

  • 如果 AOF 是开启的,那么本来老 aof 文件就已经 open 过,因为本身就会追加写 aof。
  • 如果 AOF 是关闭的(对的,aof 关闭也是可以触发重写的,很多备份场景都会这样用),redis 会 open 它来占有引用。

当进行 rename 操作大的时候,正常情况来说老 aof 文件会被删除,不过因为 redis 还 open 着老 aof 文件,占有引用,所以这个 rename 操作,不会触发文件的 unlink,此时是正常的但是如果主进程对老 aof 文件进行 close(old_aof_fd),因为 redis 是最后一个占用 fd 引用的进程,这个 close, 会触发 old aof file 的 unlink,那么就会阻塞主进程。

所以 Redis 把这个 close 的操作,放到子线程中去进行,其它有异步 close 的就都是为了规避这个问题。

这个的确是一个蛮细节的点,我自己倒是没有在外面有见到过有解释这个的(可能有但是不咋记得)

不过可惜的是这一段逻辑在 7.0 里因为 MP-AOF 引入给干掉了,随之而去的还有 AOFRW 里管道的用法

总体来说老的 AOFRW 实现,跟 MP-AOF 版本里的 AOFRW 都是很好的设计,有很多可以学习的。

学员提问-主从复制问题

彬彬回答

那个buffer实际上就是客户端的输出缓冲区,每个从连接上来后,就是一个 client,跟我们正常 redis cli连接上来的客户端是一个意思,只是从的话更多我们叫 slave replica 会有 flag 标识它是从

命令传播,主服务器,都是将命令写到从客户端的输出缓冲区,也就是这个 replication buf 。输出缓冲区的概念是每个客户端都会有,只是对于主从复制来说,这边我们叫它 buffer,如果知道是输出缓冲区,知道是也是个客户端,可能会更好理解一点,为什么每个 slave 都会有一个这个

当然这边也不用说考虑成本啥的,我觉得单纯是作者最早的方案就是写客户端这样去处理代码,实现简单。当然这的确是,内存上有点浪费,所以在 7.0 被改成了,全局共享复制缓冲区,复制积压缓冲区 和 所有从服务器的复制buf, 都是共享的了。这个功能特性是 百度云 提供的,都是 7.0版本的卖点功能。

学员提问-用Go实现redis的优势在哪里呢?

用Go实现redis的优势在哪里呢?

雅哥回答

redis 采用单线程内核设计极大的降低了开发难度同时也牺牲了性能,单线程应该说是一种「取舍」或者「设计理念」而不能简单的当做优势。比如今年发布的同类产品 Dragonfly 就采用面向并发、面向多核的设计取得了比 Redis 高很多的性能。使用 Go 语言开发内存数据库并发挥其高并发优势并无不妥。

顺带一提,内存数据库的瓶颈往往在 IO 上而不在其内核的处理速度上。目前 Godis 受 netpoller 性能限制确实没有发挥出其并发内核的优势,这就需要未来换用 io_uring 等更先进的模型来解决了。

内存数据库在生产实践上的痛点并不在单机吞吐量上,更多的是在横向扩容(集群)和内存消耗上。比如 Codis 以及云厂商提供的 redis 服务重点在于提升集群的可用性和易用性。Tendis 这类项目则使用冷数据沉降到磁盘的思路来降低内存消耗。

我写 godis 更多的还是作为一个玩具,去探索一下开发中间件的技术和挑战。Godis 目前并无用于生产环境的打算,也不必深究为什么采用 Go + Redis 这种玩法。

彬彬补充

对于上面那个对比 Dragonfly 的话,Redis 官方有写过一篇文章来回应:英文原文:https://redis.com/blog/redis-architecture-13-years-later/   由 redisLabs 的 CTO、首席架构师(redis core-team member 之一)、性能工程师(redis member 之一)一起写的 中文:来自Redis的官方吐槽:13年来,总有人想替Redis换套新架构

对于 io_uring 的话,redis 社区是有打算未来去弄https://github.com/redis/redis/pull/9440

最后,我来个总结!

历史好文:





推荐阅读