redis大纲:
我们先来回顾下redis的基本类型:
string list hash set zset
一共有这5种基本类型, 那我们来聊以聊他们是数据结构
set一个数据的时候它的数据模型是什么?
当执行set hello world 的时候, 他的数据模型为:
dictEntry: redis 给每个键值对都会分配一个 dictEntry, 里面有 key 和 val 指针, 还有指向下一个 dictEntry 的
next 指针这个指针可以把多个hash值相同的键值对连接到一起, 形成一个链表,
从而解决 hash 冲突的问题 (链地址法)
sds: (Simple Dynamic String) 简单动态字符串, 长度可变的字符串
redisObject: val 的值是存在 redisObject 中的, redis 的5中基本类型都是以 redisObject 形式存储的, type 字段
表明这个value 是什么类型的, ptr 字段指向对象的存储地址, 值的数据结构是由 encoding 属性决定的
redisObject 是 redis 的核心, redis 的对象编码, 内部编码, 内存回收, 共享对象等功能都是需要 redisObject
的支持 这样的好处是: 对5种基本类型设置不同是数据结构, 从而优化在不同场景下的使用效率
内存分配器: dictEntry, sds, redisObject 都是需要内存分配器 (如: jemalloc) 分配内存的
jemalloc 是 redis 默认的内存分配器, 它在减小内存碎片方面做得比较好
在 64位的系统中, jemalloc 将内存空间分配为 小, 大, 巨大 三个范围, 每个范围又分配了很多的内存块单位,
当 redis 进行存储的操作时, 会选择最合适的内存块进行数据的存储
数据类型所对应的的编码格式:
type 对应的 encoding编码图:
redis中的string数据结构:
字符串对象底层实现可以是 int、ember、raw
ember 编码是通过调用一次内存分配器函数来分配一块连续的空间, raw 需要调用两次
int 编码和 ember 编码在一定情况下会转换为 raw 编码
ember: <= 39个字节
int : 8个字节的长整形
raw: 大于39个字节的字符串
测试编码:
/**
* 测试 string 格式的编码
*/
private static void testString() {
// key
String key = "hello";
jedis.set(key, "1");
// int
System.out.println(jedis.objectEncoding(key));
jedis.set(key, "world");
// embstr
System.out.println(jedis.objectEncoding(key));
jedis.set(key, "worldworldworldworldworldworldworldworldworld");
// raw
System.out.println(jedis.objectEncoding(key));
}
动态简单字符串 (SDS) :
这种结构像 Java 中的 ArrayList<Character> 长度是可变的
sds 源码:
常数复杂度获取字符串长度: SDS对象中有len属性, 所以获得SDS对象的长度时间复杂度是 O(1)
预分配空间: 如果对SDS进行修改可能有两种情况
1. SDS的长度(len) 小于1MB, 那么程序分配和len属性一样大小的空间, 这时 free 属性和 len 属性值相同
举个例子: 如SDS将len属性改为15字节, 程序也会分配15个字节给free, 这时buf数组的实际长度变为
15+15+1 = 31字节 这里的1是 ‘\0’的空字符的字节
2. SDS的长度(len) 大于1MB, 程序会分配1MB的可用空间
举个例子: 如SDS将len属性改为10MB, 程序会分配1MB的空间给 free, 这时 buf 数组的实际长度变为
10MB+1MB+1byte
惰性释放空间: 当执行截取字符串操作 (sdstrim) 后, SDS不会立马释放多出来的空间, 如果下次再进行拼接字符串操作,
且拼接没有刚才释放的空间大, 那些未释放的空间就派上了用场,
惰性释放空间避免了特定情况下字符串重新分配内存的的操作
杜绝缓冲区溢出: 使用C语言字符串操作时, 如果字符串长度增加 (strcat操作), 而忘记重新分配内存,
则很容易造成缓冲区溢出, 由于SDS记录了长度, 相应的操作在可能造成缓冲区溢出时会
自动重新分配内存, 从而杜绝缓冲区溢出
redis中的list数据结构:
list对象底层实现是 quicklist (快速队列, 是 ziplist 压缩队列 和 linkedlist 双向列表的组合)
list支持两边插入和弹出, 可以获得指定位置(范围)的元素, 可以充当数组, 队列, 栈
编码测试:
/**
* 测试 list 格式的编码
*/
private static void testList() {
jedis.lpush("list", "a", "b");
// quicklist
System.out.println(jedis.objectEncoding("list"));
}
quickList 源码:
linkedList (双端列表): 结构比较像Java中linkedList
从 linkedList 图中可以看到 redis 双端列表 list 的节点带有 prev指针、next指针、head指针和 tail 指针
获取前节点、后节点、头部节点、尾部节点他们的复杂度都是O(1), len的复杂度也是O(1)
ziplist (压缩列表):
当一个列表包含少量元素时, 并且是小数整数或比较短的字符串时, 那么redis就用 ziplist 作为 list 的底层实现
ziplist是为了节约内存而开发的, 是由一系列特殊编码的连续内存块 (而不像双端列表每个节点都是指针)
组成的顺序型数据结构, 具体结构比较复杂, 在新版本的redis中 使用quicklist替代了linkedList以及ziplist
双端列表与压缩列表对比:
压缩列表可以节省空间, 但是增删操作的复杂度太高,
当节点较少时可以使用压缩列表, 当节点很多时, 还是使用双端列表好
quickList (快速列表):
quickList是 linkedList和zipList的混合体, 它将 linkedList 按断切分, 每一段使用zipList来紧凑储存,
多个 zipList 之间使用双向指针串起来, 因为链表的附加空间太高, 每一个 prev 和 next 指针都要
占16个字节(64位系统8个字节), 另外每个节点的内存都是独立分配的, 这样会加剧内存的碎片化,
影响内存的管理效率
quickList的默认压缩深度为0, 也就是说不压缩, 为了支持快速的 pop/push 操作,
quickList的首尾两个 zipList 不压缩, 此时深度为1, 为了进一步的节省空间,
redis 还会对 zipList 进行压缩储存, 使用LZF压缩算法
redis中的hash数据结构:
hash 的底层对象可以是 zipList (压缩列表), 也可以是 hashTable (哈希表)
hashTable哈希表可以实现O(1)复杂度的读写操作, 一次效率非常高
当满足这两个条件时, 才会使用 zipList:
1. 哈希表中的数据小于512个
2. 哈希表中的每一个键值对的长都小于64个字节
编码测试:
/**
* 测试 hash 格式的编码
*/
private static void testHash() {
for (int i = 0; i < 100; i++) {
jedis.hset("hash", "key" + String.valueOf(i), String.valueOf(i));
}
// ziplist
System.out.println(jedis.objectEncoding("hash"));
for (int i = 0; i < 513; i++) {
jedis.hset("hash1", "key" + String.valueOf(i), String.valueOf(i));
}
// hashtable
System.out.println(jedis.objectEncoding("hash1"));
}
hashTable 源码:
hashTable 结构图:
hash的结构类似于JDK7之前的 hashMap, 当有两个或两个以上的键被分到哈希数组的同一个索引上,
会产生 hash 冲突, redis用链地址法来解决键冲突 ,每个哈希表节点都有一个next指针, 多个 hash 表节点
使用 next 指针构成单项链表, 链地址法就是将哈希冲突的对象组织成一个单项链表, 放在hash值对应的槽上
redis 的 hash 使用 hashTable 作为底层实现的话, 每个 hash 都会有两个hash表, 一个平时使用,
另一个仅在rehash (从新散列) 时使用, 随着对hash表的操作, key会逐渐的增多或者减少,
为了让hash表的负载因子维持在一个合理的范围内, redis会对hash表的大小进行扩张或收缩(rehash),
也就是将 ht [0] 的所有的键值进行多次的, 渐进式的rehash 到 ht [1] 里
redis中的set数据结构:
set集合的底层实现可以是 intset (整数集合) 或 hashTable (哈希表)
intset (整数集合) 当一个集合只包含整数的时候, 并且元素不多的时候会使用 intset, 作为 set 集合的底层实现
编码测试:
/**
* 测试 set 格式的编码
*/
private static void testSet() {
for (int i = 0; i < 10; i++) {
jedis.sadd("set", String.valueOf(i));
}
// intset
System.out.println(jedis.objectEncoding("set"));
for (int i = 0; i < 10; i++) {
jedis.sadd("set1", "s" + String.valueOf(i));
}
// hashtable
System.out.println(jedis.objectEncoding("set1"));
}
intset源码:
intset底层实现为有序, 无重复数组保存元素, intset这个结构里的整数数组的类型可以是16位, 32位, 64位
如果这个集合里的元素都是16位长度的, 添加一个17为长度的元素, 那么这整个集合会从16位升级成32位的数组,
升级可以提升intset的灵活性, 还可以节约内存, 但是 不可逆 (32位 可以变为64位, 但不能变成16位)
redis中的zset数据结构:
zset的底层实现可以是 zipList (压缩列表) 也可以是 skipList (跳跃表)
当一个有序集合的元素比较多或者成员是比较长的字符串时, redis就会使用skipList (跳跃表) 作为zset的底层实现
编码测试:
/**
* 测试 zset 格式的编码
*/
private static void testZSet() {
jedis.zadd("zset", 1, "a");
// ziplist
System.out.println(jedis.objectEncoding("zset"));
jedis.zadd("zset1", 1, "aaaaaaaaaaaaaaaaaaaaaaaaa");
// skiplist
System.out.println(jedis.objectEncoding("zset1"));
}
skipList源码:
skipList 结构图:
skipList的查找复杂度是logN, 可以和平衡二叉树相当, 实现起来比较简单
skipList是一种有序数据结构, 他通过在某个节点中维护多个指向其他节点的指针, 从而达到快速访问的目的
结束
这就是我对redis数据结构篇的总结 感觉有用就点个赞吧 如果有错误或更好的方法评论区请多多指出 相互学习共同进步