Kaito
很荣幸,能在第二季继续给大家带来加餐文章,分享我学习Redis的经验。
这篇文章和大家分享我阅读源码的经验和心得,当然,文章里提到的方法是「通用」的,不仅限于读Redis代码,读任何项目的源码都可以按这个思路来,希望我的分享能够帮助到大家!
2021-09-18
31
Milittle
这节课我学到了什么:
1. 第一、redis不是单线程的,而是一个主线程,处理IO,另外有三个线程分别处理关闭fd、异步AOF刷盘、延迟释放。
2. 我们从bio文件中可以看到函数之间的配合。
回答一下课后题目:
可以看到最新版的代码,redis开发人员已经把这个重构了:
bio_job里面包含了fd(给关闭文件和异步刷盘用的),lazy_free_fn、free_args给延迟释放用的,一个是函数,一个是函数所需要用的参数。
备注:结合老师讲的源码版本和自己结合最新的版本看,你会在这个过程里面学到不少,有一些时候问题,就被解决了,你也可以从中学习到一些。
2021-08-23
4
曾轼麟
回答老师的提问,ziplist 能支持的最大整数是多大?
分析步骤:
1、按照问题范围,首先我去查看了zipTryEncoding的实现(ziplist.c),其中在给encoding赋值的时候划分了:极小值,int8,int16,int24,int32和int64
2、此外发现当value大于int32最大值的时候,会统一使用ZIP_INT_64B去编码
3、但是在zipTryEncoding开头处发现了一个判断,entrylen >= 32的时候是不允许编码,意思就是传入的数据如果entrylen大于32直接跳过不编码为int
4、最后调用string2ll尝试将string value编码为long long(转换成功为1失败为0)
5、若转换成功,编码类型放到encoding中,值放到v中
根据第三步个人判断,传入的int值大小不会超过32位,那么最大值应该就是int32的最大值
2021-08-08
3
Kaito
1、Redis 中的 dict 数据结构,采用「链式哈希」的方式存储,当哈希冲突严重时,会开辟一个新的哈希表,翻倍扩容,并采用「渐进式 rehash」的方式迁移数据
2、所谓「渐进式 rehash」是指,把很大块迁移数据的开销,平摊到多次小的操作中,目的是降低主线程的性能影响
3、Redis 中凡是需要 O(1) 时间获取 k-v 数据的场景,都使用了 dict 这个数据结构,也就是说 dict 是 Redis 中重中之重的「底层数据结构」
4、dict 封装好了友好的「增删改查」API,并在适当时机「自动扩容、缩容」,这给上层数据类型(Hash/Set/Sorted Set)、全局哈希表的实现提供了非常大的便利
5、例如,Redis 中每个 DB 存放数据的「全局哈希表、过期key」都用到了 dict:
// server.h
typedef struct redisDb {
dict *dict; // 全局哈希表,数据键值对存在这
dict *expires; // 过期 key + 过期时间 存在这
...
}
6、「全局哈希表」在触发渐进式 rehash 的情况有 2 个:
- 增删改查哈希表时:每次迁移 1 个哈希桶(文章提到的 dict.c 中的 _dictRehashStep 函数)
- 定时 rehash:如果 dict 一直没有操作,无法渐进式迁移数据,那主线程会默认每间隔 100ms 执行一次迁移操作。这里一次会以 100 个桶为基本单位迁移数据,并限制如果一次操作耗时超时 1ms 就结束本次任务,待下次再次触发迁移(文章没提到这个,详见 dict.c 的 dictRehashMilliseconds 函数)
(注意:定时 rehash 只会迁移全局哈希表中的数据,不会定时迁移 Hash/Set/Sorted Set 下的哈希表的数据,这些哈希表只会在操作数据时做实时的渐进式 rehash)
7、dict 在负载因子超过 1 时(used: bucket size >= 1),会触发 rehash。但如果 Redis 正在 RDB 或 AOF rewrite,为避免父进程大量写时复制,会暂时关闭触发 rehash。但这里有个例外,如果负载因子超过了 5(哈希冲突已非常严重),依旧会强制做 rehash(重点)
8、dict 在 rehash 期间,查询旧哈希表找不到结果,还需要在新哈希表查询一次
课后题:Hash 函数会影响 Hash 表的查询效率及哈希冲突情况,那么,你能从 Redis 的源码中,找到 Hash 表使用的是哪一种 Hash 函数吗?
找到 dict.c 的 dictFind 函数,可以看到查询一个 key 在哈希表的位置时,调用了 dictHashKey 计算 key 的哈希值:
dictEntry *dictFind(dict *d, const void *key) {
// 计算 key 的哈希值
h = dictHashKey(d, key);
...
}
继续跟代码可以看到 dictHashKey 调用了 struct dict 下 dictType 的 hashFunction 函数:
// dict.h
dictHashKey(d, key) (d)->type->hashFunction(key)
而这个 hashFunction 是在初始化一个 dict 时,才会指定使用哪个哈希函数的。
当 Redis Server 在启动时会创建「全局哈希表」:
// 初始化 db 下的全局哈希表
for (j = 0; j < server.dbnum; j++) {
// dbDictType 中指定了哈希函数
server.db[j].dict = dictCreate(&dbDictType,NULL);
...
}
这个 dbDictType struct 指定了具体的哈希函数,跟代码进去能看到,使用了 SipHash 算法,具体实现逻辑在 siphash.c。
(SipHash 哈希算法是在 Redis 4.0 才开始使用的,3.0-4.0 使用的是 MurmurHash2 哈希算法,3.0 之前是 DJBX33A 哈希算法)
2021-07-31
110
Ethan New
评论区也太强了吧,瑟瑟发抖
2021-07-28
10
Kaito
重新看了一下源码目录,结合这篇文章的内容,整理了一下代码分类(忽略.h头文件),这也更清晰一些:
数据类型:
- String(t_string.c、sds.c、bitops.c)
- List(t_list.c、ziplist.c)
- Hash(t_hash.c、ziplist.c、dict.c)
- Set(t_set.c、intset.c)
- Sorted Set(t_zset.c、ziplist.c、dict.c)
- HyperLogLog(hyperloglog.c)
- Geo(geo.c、geohash.c、geohash_helper.c)
- Stream(t_stream.c、rax.c、listpack.c)
全局:
- Server(server.c、anet.c)
- Object(object.c)
- 键值对(db.c)
- 事件驱动(ae.c、ae_epoll.c、ae_kqueue.c、ae_evport.c、ae_select.c、networking.c)
- 内存回收(expire.c、lazyfree.c)
- 数据替换(evict.c)
- 后台线程(bio.c)
- 事务(multi.c)
- PubSub(pubsub.c)
- 内存分配(zmalloc.c)
- 双向链表(adlist.c)
高可用&集群:
- 持久化:RDB(rdb.c、redis-check-rdb.c)、AOF(aof.c、redis-check-aof.c)
- 主从复制(replication.c)
- 哨兵(sentinel.c)
- 集群(cluster.c)
辅助功能:
- 延迟统计(latency.c)
- 慢日志(slowlog.c)
- 通知(notify.c)
- 基准性能(redis-benchmark.c)
下面解答课后问题:
Redis 从 4.0 版本开始,能够支持后台异步执行任务,比如异步删除数据,你能在 Redis 功能源码中,找到实现后台任务的代码文件么?
后台任务的代码在 bio.c 中。
Redis Server 在启动时,会在 server.c 中调用 bioInit 函数,这个函数会创建 3 类后台任务(类型定义在 bio.h 中):
#define BIO_CLOSE_FILE 0 // 后台线程关闭 fd
#define BIO_AOF_FSYNC 1 // AOF 配置为 everysec,后台线程刷盘
#define BIO_LAZY_FREE 2 // 后台线程释放 key 内存
这 3 类后台任务,已经注册好了执行固定的函数(消费者):
- BIO_CLOSE_FILE 对应执行 close(fd)
- BIO_AOF_FSYNC 对应执行 fsync(fd)
- BIO_LAZY_FREE 根据参数不同,对应 3 个函数(freeObject/freeDatabase/freeSlowsMap)
之后,主线程凡是需要把一个任务交给后台线程处理时,就会调用 bio.c 的 bioCreateBackgroundJob 函数(相当于发布异步任务的函数),并指定该任务是上面 3 个的哪一类,把任务挂到对应类型的「链表」下(bio_jobs[type]),任务即发布成功(生产者任务完成)。
消费者从链表中拿到生产者发过来的「任务类型 + 参数」,执行上面任务对应的方法即可。当然,由于是「多线程」读写链表数据,这个过程是需要「加锁」操作的。
如果要找「异步删除数据」的逻辑,可以从 server.c 的 unlink 命令为起点,一路跟代码进去,就会看到调用了 lazyfree.c 的 dbAsyncDelete 函数,这个函数最终会调到上面提到的发布异步任务函数 bioCreateBackgroundJob,整个链条就串起来了。
2021-07-28
157
Kaito
看过第一季 Redis 专栏的朋友,应该都认识我了,去年在第一季 Redis 专栏被大家叫做「课代表」,在评论区持续输出了 4 个月之久,解答了很多 Redis 问题,另外还参与了专栏的「审稿、勘误」等工作。
当时很多人问我是怎么学习 Redis 的?我的回答中肯定少不了:「看源码」,想要进阶 Redis,我认为源码是必读的。
从去年到现在,这一晃,时间过得真快,现在 Redis 源码课来了,这次和大家一起,再次进阶 Redis!
2021-07-27
105
黄海峰
也买了上一门课程,学完获益良多,10星好评,那时评论区还有个牛人分享很多经验,而当时我正好在某大厂负责一个分布式存储系统的开发维护,工作轻松舒适无需加班,还时刻可以摸鱼在外面边喝咖啡边收听极客时间,那种工作轻松又能摸鱼学习充实自己的时光真美好。。。后面疫情公司裁员一批人,从此沦落到小厂加班加点各种不适应,工作换了几次,年纪又大,技术也不是特别牛,游走于被行业淘汰边缘,浑浑噩噩了一阵,产生了转行的念头并打算开始设法实施,现在又看到老师新课,熟悉的声音熟悉的知识点让我回忆反思了很多,虽然境况已经大不相同,但肯定还是要购买学习的,对我的意义已经不只是增强技术顺利面试了,还是一种精神寄托啊。
2021-07-26
31
可怜大灰狼
记得自己是从2020年8月4日跟着老师学习《Redis核心技术和施展》。虽然我很菜,对比当时评论区大神Kaito的精彩回答,自己当时留言并不多。但是这一年来我一直在学习Redis,专栏也看了好几遍,源码也针对3.0版本看了好几遍,然后根据之前学习,都针对每章整理了相应xmind脑图。现在每天晚上为同为后端开发的朋友讲解Redis源码。真的非常感谢老师把我领进门。这次看到老师的第二个专栏,迫不及待购买,知道自己又可以补充脑图和源码讲解课。哈哈。
作者回复:加油,我们一起努力!
有问题也欢迎多多交流。
2021-07-26
4
木几丶
期待已久的redis第二季,果断第一时间订阅,希望跟着老师有所收获
作者回复:希望能给你带来些收获 :)
2021-07-26
编辑推荐
讲师的其他课程
包含这门课的学习路径
Java工程师
29门课程 154.7w人学习
看过的人还看了