1 基本概念
1.1 常见考点
1、Redis 为何这么快?
1)基于内存;
2)单线程减少上下文切换,同时保证原子性;
3)IO多路复用;
4)高级数据结构(如 SDS、Hash以及跳表等)。
2、为何使用单线程?
- 官方答案
因为 Redis 是基于内存的操作,CPU 不会成为 Redis 的瓶颈,而最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
- 详细原因
1)不需要各种锁的性能消耗
Redis 的数据结构并不全是简单的 Key-Value,还有 List,Hash 等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
2)单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
所以单线程、多进程的集群不失为一个时髦的解决方案。
3、缓存三大问题以及解决方案?
- 缓存穿透:查询数据不存在
1)缓存空值
2)key 值校验,如布隆筛选器 ref
- 缓存击穿:缓存过期,伴随大量对该 key 的请求
1)互斥锁
2)热点数据永不过期
3)熔断降级
- 缓存雪崩:同一时间大批量的 key 过期
1)热点数据不过期
2)随机分散过期时间
4、先删后写还是先写后删?
- 先删缓存后写 DB
产生脏数据的概率较大(若出现脏数据,则意味着再不更新的情况下,查询得到的数据均为旧的数据)。
比如两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
- 先写 DB 再删缓存
产生脏数据的概率较小,但是会出现一致性的问题;若更新操作的时候,同时进行查询操作并命中,则查询得到的数据是旧的数据。但是不会影响后面的查询。
比如一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据。
解决方案
1)缓存设置过期时间,实现最终一致性;
2)使用 Cannel 等中间件监听 binlog 进行异步更新;
3)通过 2PC 或 Paxos 协议保证一致性。
5、如何保证 Redis 的高并发?
Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。
ref
6、Redis 如何保证原子性?
答案很简单,因为 Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。
但我们业务中常常有先 get
后 set
的业务常见,在并发下会导致数据不一致的情况。
如何解决
1)使用 incr
、decr
、setnx
等原子操作;
2)客户端加锁;
3)使用 Lua 脚本实现 CAS 操作。
7、有哪些应用场景?
Redis 在互联网产品中使用的场景实在是太多太多,这里分别对 Redis 几种数据类型做了整理:
1)String:缓存、限流、分布式锁、计数器、分布式 Session 等。
2)Hash:用户信息、用户主页访问量、组合查询等。
3)List:简单队列、关注列表时间轴。
4)Set:赞、踩、标签等。
5)ZSet:排行榜、好友关系链表。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
1.2 常用命令
1、Redis 有哪些常用操作?
- 终端连接
`redis-cli -h 127.0.0.1 -p 6379`
- key
- string
- hash
- list
- set
- zset
- 订阅发布
subscribe chat1 # 订阅频道
publish chat1 "hell0 ni hao" # 发布消息
pubsub channels # 查看频道
pubsub numsub chat1 # 查看某个频道的订阅者数量
unsubscrible chat1 # 退订指定频道 或 punsubscribe java.*
psubscribe java.* # 订阅一组频道
- 事务
- 备份
ref
1.3 数据结构
1、Redis 有哪些数据结构?
- 字符串 String
- 字典 Hash
- 列表 List
- 集合 Set
- 有序集合 Zset
2、String 类型的底层实现?
为了将性能优化到极致,Redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。以最常用的 String 为例,其底层实现就可以分为 int、embstr 以及 raw 这三种类型。这些特定的底层实现在 Redis 中被称为编码(Encoding),可以通过 OBJECT ENCODING [key]
命令查看。
Redis 中所有的 key 都是字符串,这些字符串是通过一个名为简单动态字符串(SDS) 的抽象数据类型实现的。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
3、说说 SDS 带来的好处?
我们知道 Redis 是使用 C 语言写的,那么相比使用 C 语言中的字符串(即以空字符 \0
结尾的字符数组),自己实现一个 SDS 的好处是什么?
1)常数复杂度获取字符串长度
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。
2)杜绝缓冲区溢出
3)减少修改字符串的内存重新分配次数
4)二进制安全
5)兼容部分 C 字符串函数
一般来说,SDS 除了保存数据库中的字符串值以外,还可以作为缓冲区(Buffer):包括 AOF 模块中的 AOF 缓冲区以及客户端状态中的输入缓冲区。
4、Redis 实现的链表有哪些特性?
链表是一种常用的数据结构,C 语言内部是没有内置这种数据结构的实现,所以 Redis 自己构建了链表的实现。
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
1)双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 O(1)。
2)无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
3)带长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
4)多态:链表节点使用指针来保存节点值,可以保存各种不同类型的值。
5、Redis 是如何实现字典的?
字典又称为符号表或者关联数组、或映射(Map),是一种用于保存键值对的抽象数据结构。
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht
- 哈希算法
Redis计算哈希值和索引值方法如下:
# 1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
# 2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用 MurmurHash2 算法来计算键的哈希值。这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。
ref哈希算法
- 扩容和收缩
当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤如下:
1)如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used * 2n
的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
2)重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
3)所有键值对都迁徙完毕后,释放原哈希表的内存空间。
- 触发扩容条件
1)服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子等于 1。
2)服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子等于 5。
(其中 负载因子 = 哈希表已保存节点数量 / 哈希表大小。)
6、什么是渐近式 rehash?
也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。 如果保存在 Redis 中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成 Redis 一段时间内不能进行别的操作。所以 Redis 采用渐进式 rehash。
这样在进行渐进式 rehash 期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行增加操作,一定是在新的哈希表上进行的。
7、有序集合 Zset 的底层实现?
zset 是 Redis 中一个非常重要的数据结构,其底层是基于跳表(skip list) 实现的。
跳表是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为 O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供 O(logN) 的时间复杂度。
跳表为了避免每次插入或删除带来的额外操作,不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。
-
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
8、Zset 为何不使用红黑树等平衡树?
1)跳跃表范围查询比平衡树操作简单。 因为平衡树在查询到最小值的时还需要采用中序遍历去查询最大值。 而跳表只需要在找到最小值后,对第一层的链表遍历即可。
2)平衡树的删除和插入需要对子树进行相应的调整,而跳表只需要修改相邻的节点即可。
3)跳表和平衡树的查询操作都是O(logN)的时间复杂度。
4)从整体上来看,跳表算法实现的难度要低于平衡树。
9、什么是 Redis 的 intset?
整数集合(intset) 是 Redis 用于保存整数值的集合抽象数据类型,它可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中不会出现重复元素。
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项。需要注意的是虽然 contents 数组声明为 int8_t 类型,但是实际上 contents 数组并不保存任何 int8_t 类型的值,其真正类型有 encoding 来决定。
- 集合升级过程
当我们新增的元素类型比原集合元素类型的长度要大时,需要对整数集合进行升级,才能将新元素放入整数集合中。具体步骤:
1)根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。
2)将底层数组现有的所有元素都转成与新元素相同类型的元素,并将转换后的元素放到正确的位置,放置过程中,维持整个元素顺序都是有序的。
3)将新元素添加到整数集合中(保证有序)。
- 集合是否降级
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
10、说说什么是压缩列表?
压缩列表(ziplist) 是 Redis 为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
压缩列表的每个节点构成如下:
- revious_entry_ength
记录压缩列表前一个字节的长度。previous_entry_ength 的长度可能是 1 个字节或者是 5 个字节,如果上一个节点的长度小于 254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于 254,则 previous_length 的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。
- encoding
节点的 encoding 保存的是节点的 content 的内容类型以及长度,encoding 类型一共有两种,一种字节数组一种是整数,encoding 区域长度为 1 字节、2 字节或 5 字节。
- content
content 区域用于保存节点的内容,节点内容类型和长度由 encoding 决定。
11、什么是 RedisObject?
我们知道,Redis 底层实现了很多高级数据结构,如简单动态字符串、双端链表、字典、压缩列表、跳跃表、整数集合等。然而 Redis 并没有直接使用这些数据结构来实现键值对的数据库,而是在这些数据结构之上又包装了一层 RedisObject(对象),也就是我们常说的五种数据结构:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码,即对象的底层实现数据结构
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS;
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
这样做有两个好处:
1)通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行该的命令。
2)可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。
12、五种类型分别对于哪种实现?
- 字符串(String)
字符串对象的 encoding 有三种,分别是:int、raw、embstr。
1)如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。比如 set number 10086
命令。
2)如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于 44 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw。
3)如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于 44 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串。
ref为何是 44 个字节
embstr 的编码方式的一些优点:
embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。embstr 最小占用空间为 19(16+3),而 64-19-1(结尾的\0)=44,所以 embstr 只能容纳 44 字节。
1)embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
2)释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。
3)因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw ,编码的字符串对象能够更好地利用缓存带来的优势。
- 哈希对象(hash)
哈希对象的编码有 ziplist 和 hashtable 两种。当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 hashtable 存储。
- 列表对象(list)
列表对象的编码有 ziplist 和 linkedlist 两种。当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用压缩列表存储,否则使用 linkedlist 存储。
- 集合对象(set)
列表对象的编码有 intset 和 hashtable 两种。当集合的长度小于 512,并且所有元素都是整数时,使用整数集合存储;否则使用 hashtable 存储。
- 有序集合对象(sort set)
有序集合对象的编码有 ziplist 和 skiplist 两种。当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。
intset(整数集合)和 ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在数据量小的时候使用整数集合存储。
1.4 数据存储
1、Redis 的数据过期策略是什么?
在回答词问题之前,首先需要回答另一个问题,就是如何设置 Redis 中数据的过期时间?
1)expire key time
(以秒为单位)–这是最常用的方式
2)setex(String key, int seconds, String value)
--字符串独有的方式
除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠 expire
方法来设置时间,如果没有设置时间,那缓存就是永不过期。 如果设置了过期时间,使用 persist key
让缓存永不过期。
- 常见的过期策略
1)定时删除
在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时,对 key 进行删除。
2)惰性删除
3)定期删除
每隔一段时间执行一次删除(在 redis.conf 配置文件设置,1s 刷新的频率)过期 key 操作。
- Redis采用的过期策略
Redis 采用了惰性删除+定期删除的方式处理过期数据。
- 惰性删除的流程
1)在进行get或setnx等操作时,先检查key是否过期;
2)若过期,删除key,然后执行相应操作;
3)若没过期,直接执行相应操作。
- 定期删除的流程
其核心是对指定个数个库的每一个库随机删除小于等于指定个数个过期 key:
1)历每个数据库(就是 redis.conf 中配置的 “database” 数量,默认为16);
2)检查当前库中的指定个数个 key (默认是每个库检查 20 个,相当于该循环执行 20 次):
2.1)如果当前库中没有一个 key 设置了过期时间,直接执行下一个库的遍历;
2.2)随机获取一个设置了过期时间的 key,检查是否过期,如果过期则删除;
2.3)判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
2、持久化文件对过期策略的处理?
过期 key 是不会写入 RDB 和 AOF 文件,同时数据恢复时也会做过期验证。
3、Redis 有哪些内存淘汰机制?
Redis 作为一个内存数据库,在内存空间不足的时候,为了保证命中率,就会和我们操作系统中的页面置换算法类似,选择一定的数据淘汰策略。
- volatile(设置过期时间的数据集)
1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
- allkeys(所以数据集)
5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction
8)no-enviction(驱逐):禁止驱逐数据,这也是默认策略。
意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用 no-enviction 策略可以保证数据不被丢失。
PS:在 redis.config 文件中,我们可以设置 maxmemory
的值来配置 Redis 的最大占用内存字节数。
ref Redis5.0数据淘汰策略详解
4、Redis 有哪些持久化机制?
RDB 和 AOF。
5、说说 Redis 的 RDB?
RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式。也就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。
RDB 支持 同步(save 命令)、后台异步(bgsave)以及自动配置三种方式触发。
优点
- RDB 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
- 生成 RDB 文件时支持异步处理,主进程不需要进行任何磁盘IO操作
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
缺点
RDB 快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。且在快照持久化期间修改的数据不会被保存,可能丢失数据。
6、说说 Redis 的 AOF?
全量备份总是耗时的,有时候我们提供一种更加高效的方式 AOF,其工作机制更加简单:会将每一个收到的写命令追加到文件中。
随着时间推移,AOF 持久化文件也会变的越来越大。为了解决此问题,Redis 提供了 bgrewriteaof
命令,作用是 fork 出一条新进程将内存中的数据以命令的方式保存到临时文件中,完成对AOF 文件的重写。
AOF 也有三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步。
优点
- AOF 可以更好的保护数据不丢失,一般 AOF 隔 1 秒通过一个后台线程执行一次 fsync 操作
- AOF 日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
- AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写
- AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复
缺点
-
对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大
-
AOF开启后,支持的写 QPS 会比RDB支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync,性能也还是很高的
7、RDB 和 AOF 该如何选择?
通过前面章节我们已经明白了两种机制的原理,接下来就该结合具体需求进行机制的选择,通常情况我们是二者结合使用的。
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 取决于刷盘策略 |
轻重 | 重 | 轻 |
1.5 Pipeline
1、什么是 Redis 的 Pipeline?
在出现 Pipeline 之前,我们梳理一下 Redis 客户端执行一条命令需要经过哪些步骤: 发送命令-〉命令排队-〉命令执行-〉返回结果
。
这个过程称为 Round trip time(简称RTT, 往返时间),mget 和 mset 有效节约了 RTT,但大部分命令(如 hgetall 并没有 mhgetall)不支持批量操作,需要消耗 N 次 RTT ,这个时候需要 pipeline 来解决这个问题。
2、原生批命令 (mset, mget) 与 Pipeline 区别?
1)原生批命令是原子性的,而 pipeline 是非原子操作。
2)原生批命令一命令多个 key, 但 pipeline 支持多命令(存在事务),非原子性。
3)原生批命令是服务端实现,而 pipeline 需要服务端与客户端共同完成。
1.6 多线程
1、Redis 6 之前真的是单线程吗?
Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的单线程。但如果严格来讲从 Redis 4 之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。
2、Redis 6 之前为什么使用单线程?
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
同时 Redis 通过 AE 事件模型以及 IO 多路复用等技术,即使单线程处理性能也非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。