- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
内容大纲
- 五大基础数据结构
- 高级数据结构
- 简单动态字符串/SDS
- 链表/LinkedList
- 字典/hashtable
- 跳跃表/skiplist
- 整数集合/intset
- 压缩列表/ziplist
- quicklist
- RedisObject
- 不同对象的编码方式
- 内存回收
- 对象共享
- 对象的空转时长
1.Redis五大基础数据结构
2.Redis三个高级数据结构
2.1 Bitmaps位图
现代计算机用二进制位作为信息的基础单位,1字节=8位,比如在ASCLL码下字符“a”对应97,对应二进制位就是0110 0001,许多开发语言都提供了操作位的功能,合理地使用位能够提高内存使用率和效率。抖音、快手等数亿流量的APP他们的日活、月活、留存、漏斗分析等如何做到秒级响应,这其中就有BitMap技术的支持。
Bitmaps本身不是一种新的基础数据结构,实际上底层它就是String,但它支持对bit进行操作;可以把Bitmaps想象成以bit为单位的数组,数组的每个单元只能存储0或1,数组的下标在Bitmaps中叫做偏移量(offset)。因为Redis底层是由C语言编写,C语言中的字符串并非二进制安全,所以Redis的字符串和C中的字符串不一样,而是采用名叫SDS的结构实现,所以Redis中字符串的最大长度是512MB,所以Bitmaps的偏移量最大2^32。
比如需要知道后端打卡记录如果采用传统记录方式则记录为:
int[ ] record = {0,3,6,7}
那么总共占用的空间就是4*4byte = 16byte, 如果采用Bitmaps,则可以用一个offset=8的bit数组表示:
那么使用Bitmaps所使用的空间大小只有1个字节。假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps 分别存储活跃用户,很明显,假如用户 id 是 Long 型,64 位,则集合 类型占据的空间为 64 位 x50 000 000= 400MB,而 Bitmaps 则需要 1 bit×100 000 000=12.5MB,可见 Bitmaps 能节省很多的内存空间。
2.2 HyperLogLog
HyperLogLog 也不是一种新的数据结构,底层也是由String类型实现,但是它是一种基数算法,它可以利用极小的内存空间完成独立总数的统计。HyperLogLog 基于概率论中伯努利试验并结合了极大似然估算方法,并做了分桶优化。 基数算法:是指一个集合中不重复元素的个数。LogLog Counting(以下简称LLC)出自论文“Loglog Counting of Large Cardinalities”。LLC的空间复杂度仅有[公式],使得通过KB级内存估计数亿级别的基数成为可能,因此目前在处理大数据的基数计算问题时,所采用算法基本为LLC。
2.3 GEO
Redis 3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来 实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。 地图元素的位置数据使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治 天文台) 为界,东正西负。 业界比较通用的地理位置距离排序算法是 GeoHash 算法,Redis也使用GeoHash 算法。GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的 元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。 在Redis里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset 的 value是元素的
底层编码实现
3.简单动态字符串SDS
Redis虽然是通过C语言编写,但是他的字符串类型并没有采用C语言的字符串,而是自己定义了一个字符串类型叫SDS来作为Redis字符串类型的默认实现。
例如执行SET name '张三',name作为key也是使用的SDS,value张三仍然是一个SDS对象作为底层实现。再例如RPUSH student '张三' '李四',student作为key使用的是SDS,键值对的值是Redis五大结构中的List列表对象,列表对象底层也有对应的底层实现,那么列表的元素张三、李四仍然是通过SDS存储的。
SDS除了用来保存数据库中的字符串值以外,SDS还用来作为一些缓冲区Buffer,例如AOF日志的缓冲区也是SDS实现的。
3.1 SDS结构定义
3.2 SDS和C语言的字符串区别
- SDS获取字符串的长度更方便,时间复杂度O1,因为它额外维护了len属性。
- SDS不会发生缓冲区溢出,SDS在对字符串进行修改时会判断SDS的空闲大小和当前容量来做判断。
- SDS对字符串的修改无需频繁的内存重分配,C语言的字符串被修改了后会重新进行内存的分配,SDS因为维护了free和len属性,可以对字符串的有效字符进行管控,通过未使用空间free属性,SDS实现了`空间预分配`和`惰性空间释放`两种优化策略。
- 二进制安全地保存字符串,C语言通过后缀\0来区分字符串的末尾,但是如果这样的话Redis就没法保存一些特殊字符了,所以SDS因为维护了len属性,所以可以保存一些敏感字符。
3.3 SDS的空间预分配和惰性空间释放
3.3.1 空间预分配
当SDS修改字符串时,如果需要对容量进行扩容,SDS会进行额外的冗余分配。
- 如果对SDS修改后,SDS的len属性值<1MB,那么SDS就会分配len属性一样大小的free空间,此时len == free属性。例如SDS的len修改后是15,那么程序会为此分配15字节的未使用空间作为冗余。
- 如果对SDS修改后,SDS的len属性值>1MB,那么冗余分配就固定新分配1MB的空闲大小,因为1MB大小的字符串已经够大了,如果不加节制的扩容冗余会对内存浪费。
3.3.2 惰性空间释放
很简单,如果对SDS修改后,字符串长度变短了,那么原来那些多余的字符串并不会立即释放,因为SDS里面维护了len和free两个属性,可以正确表示真实的字符串内容。多余未释放的会后续在需要的时候通过SDS的API进行释放。
4.链表
因为C语言本身没有提供链表这种数据结构,所以Redis自己构建了一个链表的结构来作为一些高级数据结构的底层支撑。例如List、发布订阅功能、监视器功能,都用到了链表。
5.字典
Redis本身自己也是一个字典,底层就是基于hash思想的一个hashtable。
5.1 Hash算法
JDK的使用的是hashCode( )方式获取hash值,Redis则采用MurmurHash2算法来获取hash值,这个算法的好处是hash值有很好的随机分布性,速度也很快。
5.2 Hash冲突
Redis和JDK的手段类似,采用拉链法,发生了hash冲突后通过构建一个链表形式来处理。但是Redis的链表是头插法。
5.3 rehash扩缩容
扩容和缩容是通过对元素的重新散列来完成的。load_factor负载因子 = used / size计算得出,used代表已有真实节点的数量,size代表数组大小。扩容的触发条件:
- 服务器没有执行bgsave或者bgrewriteaof命令,并且负载因子>=1,自动触发。
- 服务器正在执行bgsave或者bgrewriteaof命令,并且负载因子>=5,自动触发。
当负载因子<0.1时,hash表自动缩容。
5.4 渐进式rehash
Redis的rehash并不是一次性地完成,而是分批次渐进式的完成。在rehash过程中,服务器维护了2个hash表,当服务器读取key时先访问老的hash表,如果找不到再找新的hash表。如果有新的元素添加,则直接添加到新的hash表中,老的hash表会渐渐变成空表,从而释放掉。
6.跳跃表 skiplist
skiplist是一种有序的数据结构,通过在每一个元素节点中维持多个指向其它节点的指针从而达到快速访问的目的。跳表的平均时间复杂度是O(logn),最坏也是O(n),大部分情况下跳表和平衡树相媲美,而且跳表实现更为简单。Redis的zset数据结构底层就是基于跳表实现。
例如,如果要在一个有序链表查询id=23的值,如果使用一维链表,则需要遍历链表,时间复杂度为O(n)依次比较。
如果冗余一些指针呢,例如在0、7、15节点上冗余一份儿指针,那么查询效果就比依次遍历链表更快。例如遍历0号节点,判断目标节点是否大于7,如果是就不用依次从3号开始遍历了而是跳到7号元素继续遍历。
依次按照上面的逻辑继续冗余指针,以空间换时间这种设计就会大幅提高数据检索效率。
6.1 Redis跳表
6.1.1 跳表层数level
每一个元素节点创建的时候会根据一定的概率学手段来生成一个[1,,32]的数字(Redis后期是64)来代表这个节点的高度。每一层level的节点都会各自指向当前level的元素,形成一个链表。
6.1.2 跨度span
跳表或者zset提供了查询元素的排位rank的功能,redis的skiplilstNode元素维护了一个span属性来冗余便于查询元素在所有集合中的排位。
6.2 为什么不采用平衡树?
1.跳表相对于平衡树的实现更加简单。 2.平衡树对数据的添加和移除对树的结构有较大影响,而跳表不会有太大影响。
7.整数集合 intset
用于保存整数值的集合。它能够保存int16_t,int32_t,int64_t的整数值,且保证集合中不会出现重复的元素。
7.1 集合升级和降级
因为集合支持3种长度的整数值,Redis并没有使用int64_t这种最长的整数来表示,而是由小到大,遇到装不下的整数大小的时候会进行数组整数类型编码的升级。Redis整数集合不支持降级,一旦升级以后就无法降级。
8.压缩列表 ziplist
压缩列表是Redis五大数据结构中List和Hash的实现底层。
压缩列表的每一个entry节点可以保存一个字节数组或者一个整数值。字节数组的类型也有根据数组长度进行细分,整数值也根据大小位数做了细分。
encoding:这个属性里面记录了节点保存的真实数据的类型和长度。
content:真实的数据。
8.1 连锁更新
如果遇到对节点数据的修改,可能会造成ziplist的频繁内存分配操作,可能对性能有影响。
8.2 listpack
Redis5.0版本后提出了一个新的数据结构listpack,它是对ziplist的改进,在存储空间上更加的节省空间,结构也比ziplist要简单。但是listpack并未完全取代ziplist,因为ziplist的使用率和兼容问题不易替代。listpack只用在了Stream上。
9.quicklist
Redis早期版本存储List数据结构采用的就是ziplist和普通的链表linkedlist,后期提出了quicklist这样的数据结构代替了他俩,quicklist是ziplist和linkedlist的混合体,将linkedlist按段切分,每一段使用ziplist来存储,多个ziplist是用哦过双向指针连接。
五大数据结构编码选型
10.RedisObject
11.五大数据结构的底层编码实现
11.1 字符串对象SDS
字符串对象的编码方式可以有int、raw、embstr三种。
11.1.1 int
如果字符串是一个数字的话,Redis会将其ptr属性保存真实的数据本身。
11.1.2 raw
如果字符串本身就不是数字且字符串长度>39,那么Redis就采用raw编码方式存储。
11.1.3 embstr
embstr结构和raw一致,只不过embstr专为短字符串设计,raw编码方式Redis会申请2次内存空间(RedisObejct+SDS),embstr只会申请一次内存块(RedisObject+SDS一起申请)。
高并发写入场景中,在条件允许的情况下,建议字符串长度控制在 39 字节以内,减少创建 redisobject 内存分配次数,从而提高性能。
int、raw、embstr三种编码是可以转换的。
11.2 List列表对象
List类型的底层实现有2种:普通链表LinkedList、压缩列表ziplist。
11.2.1 ziplist
List使用压缩列表ziplist需要达成2个条件,否则转为普通链表LinkedList:
- 列表保存的任何元素长度要<64字节。
- 列表对象保存的元素数量<512个
这两个参数都是可以配置的。
11.2.2 linkedList
11.3 Hash哈希对象
Hash也是有2种编码方案实现:ziplist和字典hashtable。
11.3.1 ziplist
Hash使用压缩列表ziplist需要达成2个条件,否则转为字典hashtable:
- Hash对象保存的key和value的值都要<64字节。
- Hash对象保存的键值对数量<512个。
这两个参数都是可以配置的。
11.3.2 hashtable
11.4 Set集合对象
Set集合对象也有2种编码选择:intset、hashtable。
11.4.1 intset
Set使用intset需要达成2个条件,否则转为字典hashtable:
- Set集合的所有元素都要是数字。
- 集合元素个数<512个。这个参数可调。
11.5 ZSet有序集合对象
ZSet有序集合对象也有2种编码方案可选:ziplist、skiplist。
11.5.1 ziplist
有序集合如果使用ziplist压缩列表编码方案,集合元素会按照分值从小到大排序存放。
ZSet使用ziplist需要达成2个条件,否则用跳表skiplist实现:
- 元素个数<128个。
- 元素大小<64字节。
这俩阈值都是可以调的。
12.内存回收
refCount维护了一个引用计数,当一个Redis对象创建时这个计数器初始值为1。 当对象被程序使用时,计数器+1。反之-1。 当对象的refCount == 0时,对象就会被回收。
13.对象共享
Redis为了节省空间,提出了对象共享的设计方案。不同键可以指向同一个对象;Redis在服务器启动时会预先创建一万个对象,这些对象包含了[0,9999]的整数值,如果有地方用到了这些值,Redis就会使用这些对象作为共享对象,而不是创建新对象。这个阈值可以配置。 注意:Redis不共享包含了字符串的对象,因为校验对象的内容一致性,数值型时间复杂度是O(1),字符串类型就是O(n)。
14.对象的空转时长
RedisObject结构中维护了一个lru字段,表示这个对象最近访问的时间。要得到一个对象的空转时长,只需要将当前时间减去这个lru时间就可以。这个字段的作用就是在打开了了maxmemory情况下,配合内存淘汰算法allkeys-lru或者volatile-lru来实现key的回收工作。
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。