redis底层数据结构
Redis底层数据结构——SDS
文章目录
一、IntSet介绍
Redis 中的 intset 是一个只存储整数的有序集合。intset是redis数据结构Set的底层实现之一,当Set中存放的元素全是整数时,redis会选择intset来存储。如果此时往Set中插入一个字符串,此时Set的存储方式会切换为hashtable。
二、IntSet源码分析
1. intset结构体
// intset.h
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
encoding:intset的内部编码,决定了contents中的整数采用16位、32位还是64位存储
length:contents中元素的个数
contents:灵活数组,存放真实数据,类型可以是int16_t、int32_t、int64_t
encoding的宏定义如下:
// sds.c
#define INTSET_ENC_INT16 (sizeof(int16_t)) // contents中的每个元素用 2 byte 来存储
#define INTSET_ENC_INT32 (sizeof(int32_t)) // contents中的每个元素用 4 byte 来存储
#define INTSET_ENC_INT64 (sizeof(int64_t)) // contents中的每个元素用 8 byte 来存储
虽然length的最大值为2^32-1,但redis对intset能存储的元素个数限制在了512个。可以通过config get set-max-intset-entries
查看。
个人在刚学习时有这样的疑问:
既然encoding类型只有3种,length最大长度也被限制在了512,为啥不将encoding和length类型设置为uint8_t?这样就能节省三倍的空间,就算考虑到将来要扩展,那为什么不设置为unit16_t?2^16-1个元素足够应付绝大多数场景。查了一圈后都说这跟内存对齐有关,uint32_t刚好满足4字节对齐。那为什么sds还存在len和alloc都是uint8_t类型的sdshdr8呢,这时候又不考虑内存对齐了吗?
2. intset升级
当向intset中添加一个与原数据编码方式不同的整数时,intset需要进行升级。例如,如果intset中存放的是1,2,3,此时插入一个100000,由于1,2,3都是用int16_t来存储的,而100000超过了2^15-1,因此需要将intset中的1,2,3的int16_t编码方式升级到int32_t。
/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
// 获取当前intset的编码
uint8_t curenc = intrev32ifbe(is->encoding);
// 获取新插入值的编码,虽然传入的value类型为int64_t,但_intsetValueEncoding函数还是会对value进行判断
uint8_t newenc = _intsetValueEncoding(value);
// 获取元素个数
int length = intrev32ifbe(is->length);
// 判断新插入值是大于0还是小于0
int prepend = value < 0 ? 1 : 0;
/* First set new encoding and resize */
// 将当前intset的编码设置为新插入值的编码
is->encoding = intrev32ifbe(newenc);
// 重置数组大小
is = intsetResize(is,intrev32ifbe(is->length)+1);
/* Upgrade back-to-front so we don't overwrite values.
* Note that the "prepend" variable is used to make sure we have an empty
* space at either the beginning or the end of the intset. */
// 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
while(length--)
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
/* Set the value at the beginning or the end. */
// 根据正负插入新元素
if (prepend)
_intsetSet(is,0,value); // 往队首插入
else
_intsetSet(is,intrev32ifbe(is->length),value); // 往队尾插入
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
需要注意的是,在搬运原来的元素到新的位置时,需要倒序。例如,数组中的元素从2个字节变成了4个字节,内存地址发生了改变。如果正序,则前面的数据会覆盖掉后面的数据。
3. intset元素查找
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
/* The value can never be found when the set is empty */
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
/* Check for the case where we know we cannot find the value,
* but do know the insert position. */
// 如果待查找的值大于intset的最大值 或者 小于最小值,则表示没查找到
if (value > _intsetGet(is,max)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
// 二分查找
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
// 如果待查找的值等于cur,则找到,否则没找到
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
时间复杂度:O(logN)
4. intset元素插入
//is:待插入的intset,value:要插入的值,succes:是否插入成功
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
// 获取新插入值的编码
uint8_t valenc = _intsetValueEncoding(value);
// 要插入的位置
uint32_t pos;
if (success) *success = 1;
/* Upgrade encoding if necessary. If we need to upgrade, we know that
* this value should be either appended (if > 0) or prepended (if < 0),
* because it lies outside the range of existing values. */
// 判断新插入值是否超过了当前intset的编码
if (valenc > intrev32ifbe(is->encoding)) {
/* This always succeeds, so we don't need to curry *success. */
//超出编码范围,需要升级
return intsetUpgradeAndAdd(is,value);
} else {
/* Abort if the value is already present in the set.
* This call will populate "pos" with the right position to insert
* the value when it cannot be found. */
// 在iniset中查找value,如果找到,可以不用插入新元素,则返回找到的位置pos;
if (intsetSearch(is,value,&pos)) {
if (success) *success = 0;
return is;
}
// 如果没找到,则需要对intset扩容
is = intsetResize(is,intrev32ifbe(is->length)+1);
// 移动数组中pos之后的元素到pos+1,给新元素腾出空间
if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
}
// 插入新元素
_intsetSet(is,pos,value);
is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
return is;
}
时间复杂度:
- 如果触发升级,则需要将原数组中的元素全部往后移动,此时时间复杂度为O(N)
- 如果没触发升级,需要执行二分查找,此时时间复杂度为O(logN)
5. intset元素删除
intset *intsetRemove(intset *is, int64_t value, int *success) {
// 获取待删除整数的编码
uint8_t valenc = _intsetValueEncoding(value);
uint32_t pos;
if (success) *success = 0;
// 如果待删除整数的编码小于等于当前intset的编码 且 存在于intset中
if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {
uint32_t len = intrev32ifbe(is->length);
/* We know we can delete */
if (success) *success = 1;
/* Overwrite value with tail and update length */
if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); //将后面的数据往前移动一个位置
is = intsetResize(is,len-1); //重新给intset分配内存
is->length = intrev32ifbe(len-1); //设置长度-1
}
return is;
}
时间复杂度:
由于要执行二分查找,考虑最坏情况,删除intset第一个元素,需要将后面的元素全部往前移动,此时时间复杂度为O(N)
三、一些函数和自定义宏
1. 自定义宏
// endianconv.h
#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p) ((void)(0))
#define memrev32ifbe(p) ((void)(0))
#define memrev64ifbe(p) ((void)(0))
#define intrev16ifbe(v) (v)
#define intrev32ifbe(v) (v)
#define intrev64ifbe(v) (v)
#else
#define memrev16ifbe(p) memrev16(p)
#define memrev32ifbe(p) memrev32(p)
#define memrev64ifbe(p) memrev64(p)
#define intrev16ifbe(v) intrev16(v)
#define intrev32ifbe(v) intrev32(v)
#define intrev64ifbe(v) intrev64(v)
#endif
如果机器是小端字节序,则不做任何操作;如果是大端字节序,需要进行数据翻转。
2. 函数
2.1 _intsetValueEncoding
//给定一个整数,返回编码格式,例如INTSET_ENC_INT16
static uint8_t _intsetValueEncoding(int64_t v)
2.2 intsetResize
//给定原intset和元素个数,给intset分配 len*encoding大小的内存,返回intset指针
static intset *intsetResize(intset *is, uint32_t len)
2.3 _intsetGetEncoded
//根据encoding,将intset中pos位置处的元素拷贝到一个临时变量中,并返回这个临时变量。该函数用于intset升级时拷贝原数据
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc)
2.4 intsetMoveTail
//将intset中from位置处的元素移到to位置处
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to)
2.5 字节序翻转
void memrev16(void *p) {
unsigned char *x = p, t;
t = x[0];
x[0] = x[1];
x[1] = t;
}
uint16_t intrev16(uint16_t v) {
memrev16(&v);
return v;
}