Redis数据库里面每个键值对(key-value pair)都是由对象(object)组成的,其中:
当我们聊redis数据结构的时候,应该关注那些问题?
1、SDS简单动态字符串
数据结构&特性
每个sds.h/sdshdr结构表示一个SDS值:
struct sdshdr{
// 记录buf中已使用的字节的数量,等于SDS保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组用于保存字符串
char[] buf;
}
free表示未使用空间,0未分配任何未使用空间;
len已经使用空间,标识一个7字节长的字符串;
buf是一个char类型数组,存放字符串对应字符,最后一位保存空字符'\0';
相比C字符串特性
SDS的len属性记录了SDS本身长度,所以获取SDS字符串长度的时间复杂度为O(1),而C字符串要遍历整个字符串找到'\0'结尾,时间复杂度为O(n);
C字符串不记录自身长度,还容易造成缓冲区溢出;SDS的空间分配策略杜绝了这种情况发生:SDS API需要对SDS修改时会检测空间是否满足要求,不满足会自动扩展,然后才执行修改操作;
SDS会进行空间预分配,空间扩展时,除了分配必要空间,还会分配一部分未使用空间free,避免频繁扩展;
SDS空间空闲不会立刻释放,使用free记录,等待将来使用,惰性释放,避免频繁释放;
SDS二进制安全、兼容部分C函数;
2、链表
数据结构&特性
redis链表提供了顺序的节点访问能力,增删改节点灵活;作为列表键、发布订阅、慢查询、监视器等功能的底层实现;
链表由链表节点和列表两个结构:
typedef struct listNode{
//前置节点
struct listNode * prev;
//后置节点
struct listNode * next;
//节点值
void * value;
}listNode;
链表节点通过prev和next指针组成双端列表;
typedef struct list{
// 表头节点
listNode * head;
// 表尾节点
listNode * tail;
// 链表包含节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free) (void *ptr);
// 节点值对比函数
void (*match) (void * prt,void *key);
}list;
双端、无环、带表头节点、表尾结点、带链表长度计数器、多态(使用void*指针保存节点值,并通过list结构的dup\free\match三个属性为节点值设置特定类型的函数);
3、字典
redis提供字典结构来保存键值对的数据;作为哈希键(哈希键的键值比较多或者键值对中的元素都是比较长的字符串)、Redis数据库的底层数据结构;
数据结构&特性
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemask;
//该哈希表已有节点数量
unsigned long used;
}dictht;
table是一个数组,每个元素指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
size记录哈希表的大小,数组的大小;sizemask总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的那个索引上面。
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privatedata;
//哈希表
dictht ht[2];
//rehash索引,rehash不进行时,值为-1;
in trehashidx;
}dict;
根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组指定的索引上面。
每个哈希表使用链地址法来解决哈希冲突;因为链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(时间复杂度为O(1));
扩展或收缩哈希表的操作可以由rehash(重新散列)完成;redis采用渐进式的rehash操作,是一种分而治之的思想,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash带来的庞大计算量。rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以期间字典的增删改查会在两个哈希表上进行。例如查找是会现在ht[0]中查找,查找不到再去ht[1]中查;期间新添加的键值对一律会被保存到ht[1]里面,ht[0]不再进行任何添加操作,保证ht[0]包含键值对只减不增,并随着rehash进行最终变为空表。
4、跳跃表
是一种有序数据结构,每个节点维护多个指向其他节点的指针,从而达到快速访问节点的目的;平均O(logN)、最坏O(N)复杂度的节点查找;作为有序集合键(包含元素较多或元素成员为较长字符串)、集群节点 的底层实现;
数据结构&特性
跳跃表节点zskiplistNode和跳跃表zskiplist两个结构
header:指向跳跃表的表头节点;
tail:指向跳跃表的表位节点;
level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内);L1\L2\L3的字样代表各个层,每个层有两个属性,前进指针和跨度;前进指针用于访问位于表尾方向的其他节点,跨度用于记录前进指针指向节点和当前节点的距离。箭头连线上的数自即为跨度。当程序遍历时,访问会沿着层的前进指针进行;
length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内);
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}zskiplistNode;
zskiplistNode包含以下属性:
层:跨度和前进指针组成,通过层来加快访问其他节点的速度,层数越多,访问其他节点的速度越快。每次创建一个新节点,程序根据幂次定律随机生成一个1到32之间的值作为level数组的大小,这个大小就是层的高度。
后退指针:节点中BW字样的标记节点,指向前一个节点,用于表尾向表头遍历;
分值:各节点中1.0、2.0、3.0是节点保存的分值。在跳跃表中,节点按照各节点所保存的分值从小到大排列;分值相同则按成员对象在字典序中的大小进行排序;
成员对象:各个节点中的o1\o2和o3是节点锁保存的成员对象;
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header,*tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
header和tail指针分别指向跳跃表的表头节点和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的时间复杂度为O(1)
通过length属性记录节点数量,O(1)复杂度返回跳跃表长度;
level属性用于O(1)复杂度内获取跳跃表中层数高数最大的节点的层数量,表头节点的层高并不计算在内;
5、整数集合
intset整数集合用于保存整数值的集合,并且整数值不多,可以保存int16_t、int32_t或者int64_t的整数值。并且保存整数集合不会出现重复。
数据结构&特性
typedef struct intset{
// 编码方式
uint32_t encoding;
//集合包含元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
6、压缩列表
当列表键和哈希键包含少量元素,并且每项要么是小整数值,要么是长度较短的字符串,redis就使用压缩列表作为底层数据结构实现。由一系列特特殊编码的连续内存块组成顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值;
数据结构&特性
压缩列表和压缩列表节点组成
zlbytes:表示压缩列表的总长度
zltail:标识如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量x,就可以计算出表尾几点entryN的地址;
zllen:表示压缩列表节点数
previoids_entry_length:表示压缩列表中前一个节点的长度。previous_entry_length字节或5字节;用于计算前一个节点的起止地址;连锁更新时,需要重新分配压缩列表节点的字节数为5;但性能可接受
7、Redis位图Bitmap
通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身,value对应0或1,我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。
Redis从2.2.0版本开始新增了setbit、
getbit、
bitcount
等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit
等命令只不过是在set
上的扩展。
应用场景一:位图计数统计
位图计数统计的是bitmap中值为1的位的个数。位图计数的效率很高,例如,一个bitmap包含10亿个位,90%的位都置为1,在一台MacBook Pro上对其做位图计数需要21.1ms。
例子:日活跃用户
为了统计今日登录的用户数,我们建立了一个bitmap,每一位标识一个用户ID。当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位置为1。
每次用户登录时会执行一次redis.setbit(daily_active_users, user_id, 1)。将bitmap中对应位置的位置为1,时间复杂度是O(1)。统计bitmap结果显示有今天有9个用户登录。Bitmap的key是daily_active_users,它的值是1011110100100101。
因为日活跃用户每天都变化,所以需要每天创建一个新的bitmap。我们简单地把日期添加到key后面,实现了这个功能。例如要统计某一天有多少个用户访问,可以把这个bitmap的key设计为daily_active_users:2019-03-27。当用户访问进来,我们只是简单地在bitmap中把标识这个用户的位置为1,时间复杂度是O(1)。
8、Geo地理位置
redis目前已经到了3.2版本,3.2版本里面新增的一个功能就是对GEO(地理位置)的支持。
使用案例:
key
中。具体参考:https://www.cnblogs.com/simibaba/p/7090350.html