Redis底层数结构
1 SDS
在Redis中使用简单动态字符串(SDS)来表示字符串
sdshdr8的结构:
各属性为:
len: 已使用的长度 不包括'\0'
alloc:当前字符数组分配的长度 不包括'\0'
flag: 表示字符数组的类型
// flags值定义
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
buf: 保存的字符串内容,以'\0'结束
Redis3.2后共有5中类型的sds结构体
即 sdshdr sdshdr8 sdshdr16 sdshdr32 sdshdr64
// 3.0
struct sdshdr {
// 记录buf数组中已使用字节的数量,即SDS所保存字符串的长度
unsigned int len;
// 记录buf数据中未使用的字节数量
unsigned int free;
// 字节数组,用于保存字符串
char buf[];
};
// 3.2
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {...};
struct __attribute__ ((__packed__)) sdshdr32 {...};
struct __attribute__ ((__packed__)) sdshdr64 {...};
这些结构体的区别主要是记录长度的变量的数据类型不同
分别是unsigned unit8_t unit16_t unit32_t unit64_t
其实xxx_t是某种数据类型的别名
/* exact-width unsigned integer types */
typedef unsigned char uint8_t; 0 ~ 255
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t; 0 ~ 2^32-1
typedef unsigned long int uint64_t; 0 ~ 2^32-1
所以Redis会根据字符串的长度来选择合适的结构体
char与unsigned char的区别:
都是8bit
char最高位是符号位,范围是-128 ~ 127
而后者没有符号位 0 ~ 255
如果有符号位,在把它赋值给int、long时,进行符号扩展
如果没有符号位,就不用扩展
符号扩展:将扩展后的数据的高(32-n)位置置为原来的符号位
无符号位扩展:将扩展后的数据的高(32-n)位置置为0
SDS对比C语言字符串的优势:
1 快速获取字符串长度:
c以‘\0’来判断字符串是否结束,所以获取字符串长度需要O(n),
但是sds采用len属性记录当前的长度,所以是O(1)
sds为了兼容c字符串函数,仍以‘\0’结束
2 减少内存重新分配的次数
c每次增加或缩短字符串长度,都需要重新分配内存,
如果append时不重新分配内存,容易缓存区溢出
如果截断tirm操作后不重新分配内存来释放不用的空间,容易产生内存泄漏
但是SDS记录了已使用的空间和总空间大小,实现了空间预分配和惰性空间释放
解决了字符串拼接后额截取时的空间问题
3 二进制安全,可存放二进制图片
c的字符串的除了末尾以外,不能有空字符串,即‘\0’,否者认为字符串结束了
所以c字符串只能保存文本
sds以二进制的形式来处理存放在buf数组中的内容,以len属性来判断是否结束
所以SDS可以存放图像的二进制数据
2 字典:
SET msg "hello world
字典主要由两个哈希表实现,哈希表又是由数组+链表构成,
数组中每个元素指向一个链表
链表中每个节点是一个kv键值对
Redis使用字典来实现键值对和散列表(Hash)
结构图如下:
dict是字典最顶层的结构体,dicht就是表示哈希表的结构体
dicht结构体中的table属性,就是哈希表中的桶数组,指向了一个dictEntry数组
dictEntry就是哈希表中链表的节点,即kv键值对
1 dict
dict结构体是字典中最顶层的结构体,用于指向两个哈希表,
两个哈希表是为了在扩容时使用
typedef struct dict {
// 和类型相关的处理函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引,当rehash不再进行时,值为-1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 迭代器数量
unsigned long iterators; /* number of iterators currently running */
} dict;
2 dicht
相当于哈希表中的桶数组
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,等于size-1
unsigned long sizemask;
// 哈希表已有节点的数量
unsigned long used;
} dictht;
3 dictEntry
链表中的kv键值对
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
hash扩容:
1 将第二个哈希表即ht[1]的长度= len(ht[1]) * 2
2 将ht[0]上的数据rehash到ht[1]上,即重新计算哈希地址
3 释放ht[0],将ht[1]设置为ht[0],然后创建新的ht[1]
渐进式扩容:
当字典中数据到达百万级别时,上述扩容方式中的一次性rehash就无法运行,
而是多次将ht[0]中的数据rehash到ht[1],即每次只rehash一条链表上的数据
1 为ht[1]分配空间,并初始化一个索引计数器,初始值=0
2 每当redis访问或修改索引计算器对应链表时,就会把该链表上所有的键值对rehash到ht[1]中
然后将索引计数器+1,表示下一次要rehash的链表
3 经过一段时间后,ht[0]中的所有数据都转移到ht[1]上了,再把索引计数器赋值为-1
3 链表
链表的特点:
1 双向链表,获取头尾的复杂度为O(1)
2 无环
3 用len来记录长度,获取长度的复杂度为O(1)
4 多态:用void指针指向节点保存的值,可以保存任何类型的值
链表的定义:
是一个带头尾节点的双向链表
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点值
void *value;
} listNode;
typedef struct list {
// 链表头节点
listNode *head;
// 链表尾节点
listNode *tail;
// 节点值复制函数 复制节点保存的值
void *(*dup)(void *ptr);
// 节点值释放函数 释放节点保存的值
void (*free)(void *ptr);
// 节点值对比函数 用于判读节点保存的值与要输入的值是否相等
int (*match)(void *ptr, void *key);
// 链表所包含的节点数量
unsigned long len;
} list;
链表结构图:
4 跳表:
调表的结构:
由多层链表构成,每层之间部分节点相连,且每层链表的数据都是有序的
最底层的链表是双向的,且包含所有元素,可实现类似二分查找,
在调表中的查找次数接近层数,时间复杂度为O(logn)
用随机化来决定哪些节点需要上浮
如下图中的L1是最原始的跳表,我们通过抛硬币的方式让部分节点上浮到L2中,
来构成一个新链表,再用同样的方式来生成新链表
跳表中的查找:
类似二分查找,从最顶层开始查找,大数向右查找,小数向左查找
如查找17,查找路径为 13 -> 46 -> 22 -> 17
查找35 13 -> 46 -> 22 -> 46 -> 35
查找 54 13 -> 46 -> 54
跳表中的插入:
1 向在最底层链表中找到合适的位置,并插入
2 然后用抛硬币的方式决定它是否要要上浮
加入此时链表共3层,需要抛两次硬币,来决定它上浮几次
跳表的实现:
1 使用zskipListNode来保存跳表中每个节点
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// 成员对象 (robj *obj;) 指向一个sds
sds ele;
// 分值 用于实现zset
double score;
// 后退指针 指向前一个节点
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
// 跨度实际上是用来计算元素排名(rank)的,在查找某个节点的过程中,将沿途访过的所有层的跨度累积起来,得到的结果就是目标节点在跳跃表中的排位
unsigned long span;
} level[];
} zskiplistNode;
2 使用zskiplist结构体来保存当前调表的整体信息
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
跳表与平衡树、哈希表的比较
1 skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。
因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
2 在做范围查找的时候,平衡树比skiplist操作要复杂。
在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。
如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。
而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
3 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,
而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
4 从内存占用上来说,skiplist比平衡树更灵活一些。
一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
5 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;
而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。
所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
从算法实现难度上来比较,skiplist比平衡树要简单得多。
Redis中的数据类型
1 string
基于SDS实现
用于计数:粉丝数
还可用于保存用户的session,前提是保证redis的高可用
增加/减小数字 incr/decr/incrby/decrby
获取指定区间范围内的值 getrange start end 闭区间
给指定位置或范围复制 setrange
当key不存在时才富新值 setnx key value
批量赋值和取值 mset/mget/msetnx
注意:在msetnx中,只要有一个key有value,整个赋值操作都不会被执行
先获取旧值再赋新值 getset
2 list:
基于双向链表实现,修改list中间的数据时,时间复杂度较高
适用于粉丝列表 消息列表
可以使用lrange key start index来实现高性能分页
可以用于实现消息队列 brpop阻塞式地从表尾取出数据
从左/右边插入数据 lpush rpush
从左边/右边出一个数据 lpop / rpop
从左到右遍历数据 lrange 0 -1
根据索引取值 lindex key index
根据索引赋值 lset key index value (索引不能超出范围))
在指定元素的前后插入元素 linsert key before/after pivot value
阻塞式地从表尾取出数据 brpop key
求长度 llen
删除多个相同的value: lrem key count element
截取list 闭区间 ltrim key start end
从源list的末尾删除一个元素并插入目标list的头部 rpoplpush source target
3 set
基于字典实现
功能与list类似,但可以自动去重 ,但无序
可以用set来求交集,如共同关注、共同粉丝、共同喜好
使用srandommember来获取随机数
相对于jvm的HashSet,redis的set可以实现全局去重
创建一个set: sadd values
遍历set smembers key
判断某个values是否存在 sismemeber key value
获取set中元素个数 scard key
删除set中的指定元素 srm key value
从set中随机提取指定个数的元素 srandmemeber key number
随机出栈元素 spop key
移动元素到其它set smove source target value
差集/交集/并集 sdiee/sinter/sunion
保存两个set的交集到key1 sinterstore key1 key2 key3
差集就是存在第一个set但不在第二个set中的元素的集合
4 Hash
基于字典实现
适用于存储对象,前提是这个对象没有嵌套其它对象
给对象的属性赋值 hset key field value
获取对象中的某个属性 hget key field
批量赋值/取值 hmset/hmget
删除对象属性 hdel key field
对象的属性个数 hlen key
判对象中是否在某个属性 hexists key fiels
获取对象的所有属性以及对应的value hgetall key
获取对象的所有属性 hkeys key
获取对象的所有value hvals key
增长整型或浮点型value hincrby/hincrbyfloat key value number
给不存在的属性赋值 hseetnx key field value
5 zset:
基于跳表实现
无重复值且有序的集合,默认按分数递增排序
每个value由两部分构成 真实值和对应的分数
适用于各种排行榜信息,如文章访问量排行榜
带权重的列表
添加内容 zadd key score1 member1 score2 member2
遍历zset zrange key withscores
zrange key 0 -1 -withscores
指定分数范围来遍历zset zrangebyscore key min max 在分数前加"("表示开区间,默认为闭区间
逆序遍历指定范围scoer的元素zrevrangebyscore key max min
提取排名前n的元素 zrevrange key 0 n-1
删除某个元素 zrem key member
获取元素个数 zcard key
获取分数区间内的元素个数 zcount key min max
获取memeber对应的排名 zrank key member
逆序获取memeber对应的排名 zrevrank key meember
获取memeber对应的分数 zscore key
使用Redis实现文章访问量排序:
把文章id和访问量存入zset zadd paper 10000 p1 8000 p2 9000 93
按访问量从高到低排序 zrevrange paper 0 -1 withscores
查看指定访问量的文章并排序 ZREVRANGEBYSCORE paper 10000 9000
转载:https://blog.csdn.net/u012712556/article/details/106178568