Redis知识总结
一、底层结构
参考文章最详细的Redis五种数据结构详解,图片选取参考文章,请以参考文章为准。
1.1 String
- 当字符串存储的是可以转化成int类型时,则使用C语言的int存储。
- 当字符串字节小于44(每个版本不一样,有的版本时39)时,使用embstr编码存储。因为redis初始化的时候一次分配redisObject时64字节,除了固定的19字节存储固有的数据,剩下的可以存储字符串,达到内存充分利用
- 当大于44字节时,使用SDS编码存储。SDS由len,free,char[]组成。
SDS与C字符串的区别:
-
常数复杂度获取字符串长度
-
杜绝缓冲区溢出,减少修改字符串时带来的内存重分配次数(空间预分配, 惰性空间释放)
-
空间预分配
当追加字符串时,如果len的长度小于1MB,则分配与len一样大小的空间给free,如果大于1MB时,分配1MB给free
-
惰性空间释放
减少字符串时,实际上不会释放内存,free不变,等空间预分配时,len=free来实际的释放内存。
-
-
二进制安全
1.2 List
1.2.1 ziplist
Redis中的列表在3.2之前的版本是使用ziplist和linkedlist进行实现的。在3.2之后的版本就是引入了quicklist。
- ziplist:
一系列特殊编码的连续内存块组成的顺序存储结构, 压缩列表并不是以某种压缩算法进行压缩存储数据,而是它表示一组连续的内存空间的使用,节省空间,类似于数组,但是不同之处在于每个节点(entry)所占用的大小可以是不同的。
压缩列表是列表value和哈希value底层实现的原理之一。压缩列表中每一个节点表示的含义如下所示:
zlbytes:4个字节的大小,记录压缩列表占用内存的字节数。
zltail:4个字节大小,记录表尾节点距离起始地址的偏移量,用于快速定位到尾节点的地址。zllen:2个字节的大小,记录压缩列表中的节点数。entry:表示列表中的每一个节点。zlend:表示压缩列表的特殊结束符号'0xFF'。
再压缩列表中每一个entry节点又有三部分组成,包括previous_entry_ength、encoding、content。
previous_entry_ength表示前一个节点entry的长度,可用于计算前一个节点的其实地址,因为他们的地址是连续的。encoding:这里保存的是content的内容类型和长度。content:content保存的是每一个节点的内容。
使用场景:当list, hash, set中存储的value为简单整型和字符串时。
1.2.2 双端链表:
- 带有指向前置节点和后置节点的指针,获取这两个节点的复杂度为O(1)
- 无环:表头节点的
prev和表尾节点的next都指向NULL,对链表的访问以NULL结束 - 链表长度计数器:带有
len属性,获取链表长度的复杂度为O(1) - 多态:链表节点使用
void*指针保存节点值,可以保存不同类型的值
1.3 Hash
- 结构
如上图,hash中真正存储数据的只有ht[0], 而ht[1]则为之后的扩展做准备。redis中是使用链地址法来解决hash冲突的。
- rehash
刚开始初始化hash的时候,hash会有一个默认的大小(例如上面的size=2),当新加入的hash值越来越多时,需要进行扩容操作,称之为rehash。
-
扩容操作:
由于ht[0]是一开始用来存储数据的,ht[0]大小无法存储新增的hash数据时, 会将ht[1].size变为ht[0].user的两倍后最接近2的整数幂的值,例如上面ht[0].user=3 扩容后ht[1]的size = 3 * 2=8(刚好是2的整数幂),之后将ht[0]存放的hash数据重新hash,分布在ht[1]当中。然后ht[1]更改下表为ht[0], ht[1]指向null。
-
收缩操作
ht[1] 的size为第一个大于等于 ht[0].used 的 2^n。也就是说,当要收缩时user < size 。
- 渐进式rehash
回顾上述rehash的过程,当数据量很庞大的时候,rehash的耗时肯定是不小的,此时会造成服务一点时间的不可用。为此,redis设计上采用了分布rehash,让 服务变成可用状态。具体的过程如下:
1. 字典中的rehashidx为-1表示没有进行rehash过程,当要开始rehash时,rehashidx值变为1。
2. redis需要对hash进行CRUD时,保证ht[0]只减不增加,也就是说,查询在ht[0]和ht[1]中查,删除只删除ht[0],更新两者一起更新,增加只在ht[1]中增加。
- rehashidx的作用很重要,由于rehash是分批次的,每100个key进行rehash后,rehashidx就+1,这样就能通过key的数量知道当前rehash的进度。
- 为什么是100个key呢,因为redis默认100个key的rehash不会超过100ms,不然会break掉这次分配的rehash。
在1.2.1ziplist中,也可以存储hash。
1.4 Sort set
- 结构skiplist
类似于B+数,但是每层都是链表,方便维护。但这样容易引出另一个问题,就是为什么mysql不用跳表而用B+树呢。详情可以参阅这篇文章。
总的来说,B+树一个节点可以存储更多的索引,能使树的深度更低,而跳表一个节点只能存储一个索引,层数更高,查找复杂度更高,但是由于是内存操作,所以redis采用了跳表。
1.5 Set
-
整数集合
-
intset
结合对象保存的所有元素都是整数值
集合对象保存的元素数量不超过512个
-
-
hashtable
参考1.3Hash
二、持久化机制
-
rdb
- 快照备份频率
- fork子线程
- COW
-
AOF
- 命令追加
- 日志瘦身
-
混合型
master AOF, slave RDB
三、过期策略和淘汰机制
3.1 定期删除+惰性删除
- 定时随机抽查过期key,进行删除
- 访问的key若过期,则删除
3.2 淘汰机制
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 a
llkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
四、redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
五、常见性能问题
5.1 主从问题
1、Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数,会阻塞主线 程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务
2、如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一
3、为了主从复制的速 度和连接的稳定性,Master 和 Slave 最好在同一个局域网
4、尽量避免在压力很大的主库上增加从
5、主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1<- Slave2 <- Slave3… 这样的结构方便解决单点故障问题,实现 Slave 对 Master的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。
5.2 消息队列
- 基于List的 LPUSH+BRPOP 的实现
- PUB/SUB,订阅/发布模式
- 基于Sorted-Set的实现
- 基于Stream类型的实现
redis灵魂拷问:如何使用stream实现消息队列 - 云+社区 - 腾讯云 (tencent.com)
5.3 分布式锁
- 命令
- 超时时间