1 Redis是不是单线程

Redis 使用后台线程执行耗时的 I/O 操作,以免阻塞主线程
bio_close_file:后台 I/O 线程之一,异步关闭文件
bio_aof_fsync:后台 I/O 线程之一,异步地将 AOF(Append Only File)日志同步到磁盘
bio_lazy_free:异步释放内存,有些内存释放操作可能比较耗时,因此这些操作可以异步完成,以免阻塞主线程
jemalloc_bg_thd:这是由 jemalloc 内存分配器产生的后台线程。jemalloc 是 Redis 默认使用的内存分配器,因为它在多线程环境中表现出色,能够有效地管理内存碎片。这个后台线程通常用于维护和优化内存使用(例如回收空闲内存)。
如果有io_thd_1之类的,则是在处理网络IO
一般认为Redis是单线程,是因为Redis的命令处理是单线程的
1.1 为什么Redis是单线程
单线程的局限:
- 不能有耗时操作,包括cpu运算和阻塞io
但Redis仍存在耗时操作
io密集型:
- 磁盘io:有两种方式持久化,一种是bio_aof_fsync,开启一个线程持久化,另外一种是rdb,通过fok进程,在子进程持久化
- 网络io:Redis需要处理多个服务,Redis采用Reactor网络模型,实现IO多路复用;若数据请求或返回数据量比较大,则会开启io多线程
cpu密集型:
- Redis使用高效的数据结构,并允许数据结构切换,当数据量大的时候,需使用O(1)、O(lgn)复杂度的数据结构
- 渐进式数据迁移
Redis为什么不采用多线程?
采用多线程需加锁,加锁复杂,加锁粒度不好控制,加锁会造成频繁的CPU上下文切换,抵消多线程的优势
1.2 单线程为什么快
-
Redis使用了内存数据库
-
Redis是一个key-value数据库,数据存储在hashtable中,复杂度是O(1),为了保证O(1)的复杂度,hash冲突不能太激烈。
若数据太多,而hashtable太小,则非常容易冲突。
而Redis是内存数据库,一开始就分配很大空间,浪费内存,因此Redis动态分配数组大小,允许进行扩容、缩容操作。
负载因子:used / size
- 如果负载因子 > 1 ,则会发生扩容
- 如果正在 fork (在 rdb、aof 复写以及 rdb-aof 混用情况下)时,会阻止扩容
- 但是此时若负载因子 > 5 ,索引效率大大降低, 则马上扩容
- 如果负载因子 < 0.1 ,则会发生缩容
扩容:位于0号的元素,会分别散落在0号和4号,其余同理

渐进式Rehash:
若hashtable的size非常大,进行翻倍迁移的时候,是一个非常耗时的操作,但Redis仍然需要服务用户,因此不能一次性迁移。
server.h:6.2.12版本
dict:存储的keys
expires:过期的keys
blocking_keys:阻塞的keys

可以看到有一个ht[2]
,即hashtable有两个,在没有扩容和缩容的时候,通常只使用ht[0]
,扩容时,会将ht[0]
中的数据放入到ht[1]
中,并将ht[1]
的大小翻倍。

rehash步骤:
将 ht[0]
中的元素重新经过 hash 函数生成 64 位整数,再对 ht[1]
长度进行取余,从而映射到 ht[1]
渐进式rehash:
- 将数据的rehash操作,分摊在增删改查操作中,每次操作一个索引中的全部元素,直到rehash结束,将
ht[1]
赋值给ht[0]
,并将ht[1]
置为空 - 在定时器中,在redis空闲时,最大执行一毫秒 rehash ;每次步长 100 个数组槽位
- 高效的reactor网络模型
1.3 scan
KEYS *
命令非常耗时,若想获取所有keys,可以使用scan命令
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

采用高位进位加法的遍历顺序,rehash 后的槽位在遍历顺序上是相邻的

接下来应该遍历16号索引

2 string
三种编码
- int:字符串长度小于等于20且能转成整数
- 对于大整数,int占的字节比字符串更少
- raw:字符串长度大于44
- embstr:字符串长度小于等于44

在Redis中,string被实现成sds,它包含一些头部,但仍返回实际存储数据的地址。
最后的char buf[]
是柔性数组,使用sizeof会不包含char buf[]
。
面试题:为什么Redis中字符串选择44个字节作为分界线?
embstr顾名思义就是嵌入字符串,嵌入到redisObject中


redisObject共占用4 + 4 + 8字节
长度为44的话,选择了sdshdr8的结构(表示长度0-128),sdshr8头部占用了3个字节
cpu cache line最小访问单位为64个字节
同时sds为了兼容strlen等函数,在柔性数组最后加上'\0'分隔符
因此64 - (4 + 4 + 8) - 3 - 1 = 44
3 Redis跳表
跳表(多层级有序链表)结构用来实现有序集合,redis 需要实现 zrange 以及 zrevrange功能,需要节点间最好能直接相连并且增删改操作后结构依然有序
节点数量大于 128 或者有一个字符串长度大于 64,则使用跳表(skiplist)

4 Redis IO多线程原理
对于一个Redis请求,需要经过read、decode、compute、encode、send这5个流程。
而有时候read、decode、encode、send过程很慢,把它们放在主线程操作,很浪费时间,因此Redis使用了IO多线程。

将多个IO分发到多个线程(包括主线程)中,但所有的compute仍在主线程中,因此,这与Redis是单线程的并不冲突。