Redis作为key-value内存数据库,拥有丰富的数据类型(string、hash、list、set、zset等)。
在这些数据类型背后必然有多种数据结构支持,数据结构记录在redisObject.encoding属性(重点), 本文将以Java对象形式展示Redis数据结构。
无论哪种数据类型都是以键值对为单位存储的,而每个键值对会封装成dictEntry对象,因此这个对象是通用的。
dictEntry
public class dictEntry {
private sdshdr key;
private redisObject v;
private dictEntry next;
}
key:指向封装key值的sdshdr对象。
v:指向封装value值的Object对象。
next:指向下一个dictEntry对象。
sdshdr
public class sdshdr {
int len;
int free;
char[] buf;
}
len:记录buf已使用的字节的数量。
free:记录buf未使用的字节的数量。
buf:字节数组。
SDS遵循C字符串以空字符’\0’结尾的惯例,这么做的好处是SDS可以直接重用一部分C字符串函数库里的函数。
比如buf长度16,保存字符串"helloworld",那么len=10,free=5,空字符"\0"占一位。
为什么SDS数据结构比C字符串更适合Redis
- 降低获取字符串长度的复杂度:SDS获取字符串长度的复杂度O(1);C字符串获取字符串长度的复杂度O(N)。
- SDS杜绝缓冲区溢出:SDS在修改前检查空间是否满足修改所需的要求,如果不满足SDS会自动扩展至修改所需的大小(或更多),然后再执行实际修改操作。
- 减少修改字符串时的内存重分配次数:SDS实现了空间预分配和惰性空间释放两种优化策略。
- SDS可以保存文本或者二进制数据,支持字符串中间夹杂空字符’\0’;C字符串只能保存文本。
空间预分配策略
如果len < 1MB,预分配大小等同于len。
如果len >= 1MB,预分配1MB大小。
惰性空间释放策略
SDS进行缩短字符串操作时,不会执行内存重分配。
redisObject
public class redisObject {
private String type;
private String encoding;
private String lru;
private int refcount;
private Object ptr;
}
type:表示属于哪种数据类型(REDIS_STRING \ REDIS_LIST \ REDIS_HASH \ REDIS_SET \ REDIS_ZSET)。
encoding:表示属于哪种数据结构。
type | encoding | 描述 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 | |
REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 | |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 | |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_ENCODING_HT | 使用字典实现的哈希对象 | |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_ENCODING_HT | 使用字典实现的集合对象 | |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
lru:表示该对象最后被访问时间,用于过期策略。
refcount:表示该对象被引用次数。
ptr:指向封装value值的对象,由于不同encoding封装value值的对象数据结构不同,所以先用Object表示。
无环双端链表
encoding=“REDIS_ENCODING_LINKEDLIST”,ptr指向一个list对象。
list
public class list {
listNode head;
listNode tail;
long len;
boolean dup(listNode ptr);// 节点值复制函数
boolean free(listNode ptr);// 节点值释放函数
boolean match(listNode ptr, String value);// 节点值对比函数
}
head:指向头结点指针。
tail:指向尾结点指针。
len:链表包含节点数量。
list对象的三个函数成员不在本文讨论范围内。
listNode
public class listNode {
listNode prev;
listNode next;
redisObject value;
}
prev:前置节点(若当前节点为头节点,该属性为null)。
next:后置节点(若当前节点为尾节点,该属性为null)。
value:指向封装value值的redisObject对象,支持多种类型。
字典
encoding=“REDIS_ENCODING_HT”,ptr指向一个dict对象。
dict
public class dict {
dictType type;
sdshdr privdata;
dictht[2] ht;
int rehashidx;
}
type:指向dictType的指针。
privdata:与type属性一起针对不同类型键值对,为创建多态字典表而设置。
ht:哈希表数组,长度为2。一般只使用ht[0],进行rehash时使用到ht[1]。
rehashidx:渐进式rehash索引,记录的是rehash进行到dict.ht[0].table的哪个位置;当rehash不在进行时,值为-1。
dictType
public class dictType {
int hashFunction(String key);// 计算hash值函数
boolean keyDup(String privdata, String key);// 复制键函数
boolean valDup(String privdata, String obj);// 复制值函数
int keyCompare(String privdata, String key1, String key2);// 对比键函数
boolean keyDestructor(String privdata, String key);// 销毁键函数
boolean valDestructor(String privdata, String obj);// 销毁值函数
}
dictType对象的六个函数成员不在本文讨论范围内。
dictht
public class dictht {
dictEntry[] table;
long size;
long sizemask;
long used;
}
table:哈希节点数组。
size:记录table大小。
sizemask:该值总是等于size-1,用于计算索引值。
used:记录table数组已使用节点数量。
dictEntry
public class dictEntry {
sdshdr key;
redisObject v;
dictEntry next;
}
key:键。
v:值,可以存储指针、uint64整数、int64整数。
next:哈希冲突时形成无环单向链表,新元素总是添加在链表头部。
Redis使用的哈希算法
hash = dict.type.hashFunction(key);
index = hash & dict.ht[0].sizemask;
hashFunction函数底层使用MurmurHash2算法,其优点是随机分布性高。
哈希表扩展和收缩触发条件
负载因子 = dict.ht[0].used / dict.ht[0].table.size;
没有执行BGSAVE的时候,负载因子>=1触发哈希表扩展。
正在执行BGSAVE的时候,负载因子>=5触发哈希表扩展。
当负载因子<0.1时触发哈希表收缩。
哈希表扩展机制
- 初始化dict.ht[1].table,大小为大于dict.ht[0].used的第一个2的次方。
- dict.ht[0].table中的元素通过rehash移动至dict.ht[1].table。
- dict.ht[0]释放,并将dict.ht[1]设置为dict.ht[0]。
- 创建新的dict.ht[1]为下次rehash做准备。
渐进式rehash
渐进式rehash的目标是解决dict.ht[0].table保存键值对很多的情况下,rehash过程漫长的问题。
渐进式rehash触发条件:字典进行增删改查的操作时,进行一次渐进式rehash,dict.rehashidx+1。
渐进式rehash过程中,查询键值对会先查dict.ht[0],再查dict.ht[1]。
渐进式rehash过程中,所有新增动作都会添加至dict.ht[1]。
跳跃表
encoding=“REDIS_ENCODING_SKIPLIST”,ptr指向一个zskiplist对象。
zskiplist
public class zskiplist {
zskiplistNode header;
zskiplistNode tail;
long length;
int level;
}
header:指向跳跃表头节点。
tail:指向跳跃表尾节点。
length:跳跃表中节点数量(跳跃表头节点不计算在内,因为头节点比较特殊)
level:跳跃表中节点最大层数(跳跃表头节点不计算在内,因为头节点层数一直是32)
zskiplistNode
public class zskiplistNode {
zskiplistLevel[] level;
zskiplistNode backward;
double score;
Object obj;
}
level:跳跃表节点包含的层,每个节点层数是1至32之间的随机数。
backward:返回至上一个zskiplistNode的指针,返回时不能跳跃。
score:该节点分值。
obj:该节点成员对象。
zskiplistLevel
class zskiplistLevel {
zskiplistNode forward;
int span;
}
该类为zskiplistNode内部类。
forward:跳跃至下一个zskiplistNode的指针。
span:记录跨度值。
整数集合
encoding=“REDIS_ENCODING_INTSET”,ptr指向一个intset对象。
intset
public class intset {
int encoding;
int length;
int[] contents;
}
encoding:INTSET_ENC_INT16 \ INTSET_ENC_INT32 \ INTSET_ENC_INT64。
length:集合包含元素数量。
contents:元素数组。
压缩列表
encoding=“REDIS_ENCODING_ZIPLIST”,ptr指向一个sequential对象。
sequential
public class sequential {
int zlbytes;
int zltail;
int zlen;
entry entry1;
...
entry entryN;
int zlend;
}
sequential对象其实是一个Object[],其内部约定了以下属性的位置。
zlbytes:记录整个压缩列表占用字节数。
zltail:记录压缩列表尾节点位置,在第几个字节。
zlen:压缩列表节点数量。
entry1-N:压缩列表节点。
zlend:特殊值"0xFF",表示压缩列表结尾。
entry
class entry{
int previous_entry_length;
String encoding;
Object content;
}
previous_entry_length:前一节点字节长度,用于从后往前遍历。
encoding:记录content数据结构和长度,前2位表示数据结构(00、01、10代表char[],11代表int),后面表示长度。
content:具体记录的内容,可以是int,可以是char[]。
源码地址:RedisDataStructure模块