Redis中的字符串
简单的操作
Redis中的列表
一个列表可以有序地存储多个字符串,并且列表里的元素是可以重复的
命令与行为
LPUSH将元素推入列表的左端
RPUSH将元素推入列表的右端
LPOP从列表左端弹出元素
RPOP从列表右端弹出元素
LINDEX获取列表在给定位置上的一个元素
LRANGE获取列表在给定范围上的所有元素
具体实例如下图:
Redis中的集合
SADD将元素添加到集合 成功添加返回1,如果返回0则表示集合中已经有这个元素了
SREM从集合里面移除元素 存在返回1,不存在返回0
SISMEMBER快速地检查一个元素是否已经存在于集合中
SMEMBERS获取集合包含的所有元素
Redis中的散列
HSET 在散列里面关联起给定的键值对
HGET 获取指定散列键的值
HGETALL 获取散列包含的所有键值对
HDEL 如果给定键存在于散列里面,那么移除这个键
Redis中的有序集合
有序集合的键被成为成员,每个成员都是各不相同的。有序集合的值被成为分值,分值必须为浮点数。
有序集合是redis里面唯一一个既可以根据成员访问元素,又可以根据分值以及分值的排列顺序来访问元素 的结构。
ZADD 将一个带有给定分值的成员添加到有序集合里面
ZRANGE 根据元素在有序排列中所处的位置,从有序集合里面获取多个元素
ZRANGEBYSCORE 获取有序集合在给定分值范围内的所有元素
ZREM 如果给定成员存在于有序集合,那么移除这个成员
redis数据结构底层实现
string
使用一种叫简单动态字符串(SDS)的数据类型来实现。
/*
* 保存字符串对象的结构
*/
struct sdshdr {
int len; // buf 中已占用空间的长度
int free; // buf 中剩余可用空间的长度
char buf[]; // 数据空间
};
SDS 相比C 字符串的优势:
SDS保存了字符串的长度,而C字符串不保存长度,需要遍历整个数组(找到’\0’为止)才能取到字符串长度。
修改SDS时,检查给定SDS空间是否足够,如果不够会先拓展SDS 的空间,防止缓冲区溢出。C字符串不会检查字符串空间是否足够,调用一些函数时很容易造成缓冲区溢出(比如strcat字符串连接函数)。
SDS预分配空间的机制,可以减少为字符串重新分配空间的次数。
list
使用双向链表来实现。
hash
hash结构里其实是一个字典,有许多的键值对(类似于python的dict类型)。
redis的哈希表是一个dictht结构体:
typedef struct dictht {
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值
unsigned long used;//该哈希表已有节点的数量
}
typeof struct dictEntry{
void *key;//键
union{ //不同键对应的值的类型可能不同,使用union来处理这个问题
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
}
其中解决哈希冲突的方法是拉链法。
为了让哈希表的装载因子维持在一个合理的范围之内,需要对哈希表的大小进行扩展或者收缩,这叫做rehash。字典中总共有两个哈希表dictht结构体,ht[0]用来存储键值对,ht[1]用于rehash时暂存数据,平时它指向的哈希表为空,需要扩展或者收缩ht[0]的哈希表时才为它分配空间。
比如扩展哈希表,就是为ht[1]分配一块大小为ht[0]两倍的空间,然后把ht[0]的数据通过rehash的方式全部迁移到ht[1],最后释放ht[0],使ht[1]成为ht[0],再为ht[1]分配一个空哈希表。收缩哈希表类似。
渐进式rehash:redis并不是专门找时间一次性地进行rehash,而是渐进地进行,rehash期间不影响外部对ht[0]的访问,要求修改字典时要把对应数据同步到ht[1]中,全部数据转移完成时,rehash结束。
set
set可以用intset或者字典实现。
intset
只有当数据全是整数值,而且数量少于512个时,才使用intset,intset是一个由整数组成的有序集合,可以进行二分查找。
字典
不满足intset使用条件的情况下都使用字典(拉链法),使用字典时把value设置为null。
zset
zset中的每个元素包含数据本身和一个对应的分数(score)。
经典例子:一个zset的key是"math",代表数学课的成绩,然后可以往这个key里插入很多数据。输入数据的时候,每次需要输入一个姓名和一个对应的成绩。那么这个姓名就是数据本身,成绩就是它的score。
zset的数据本身不允许重复,但是score允许重复。
zset底层实现原理:
- 数据少时,使用ziplist:ziplist占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。ziplist为了节省内存,每个元素占用的空间可以不同,对于大的数据(long long),就多用一些字节来存储,而对于小的数据(short),就少用一些字节来存储。因此查找的时候需要按顺序遍历。ziplist省内存但是查找效率低。
- 数据多时,使用字典+跳表:
字典用来根据数据查score,跳表用来根据score查找数据(查找效率高)。
理论上来讲,查找、插入、删除以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度和跳表是一样的。
redis使用跳表而不是红黑树的原因:
- 按照区间查找数据这个操作,红黑树的效率没有跳表高。跳表可以在 O(logn)时间复杂度定位区间的起点,然后在原始链表中顺序向后查询就可以了。
- 相比于红黑树,跳表还具有代码更容易实现、可读性好、不容易出错、更加灵活等优点。
- 插入、删除时跳表只需要调整少数几个节点,红黑树需要颜色重涂和旋转,开销较大。
跳表插入删除过程:
跳表是基于一条有序单链表构造的,通过构建索引提高查找效率,空间换时间,查找方式是从最上面的链表层层往下查找,最后在最底层的链表找到对应的节点:
插入:逐层查找位置,然后插入到最底层链表。注意需要维护索引与原始链表的大小平衡,如果底层结点大量增多了,索引也相应增加,避免出现两个索引之间结点过多的情况,查找效率降低。同理,底层结点大量减少时,索引也相应减少。
删除:如果这个结点在索引中也有出现,那么除了要删除原始链表中的结点,还要删除索引中的这个结点。
跳表查找的时间复杂度为O(log(n))。索引占用的空间复杂度为 O(n)。
时间复杂度:时间复杂度 = 索引的层数 * 每层索引遍历元素的个数。
首先看索引层数,假设每两个节点抽一个出来作为上一级索引的结点,而且最高一级索引有3个节点,则索引层数为log2(n)。
然后看每层遍历多少个元素,首先最高层最多遍历3个节点,就能往下走了,同理,次高层也最多遍历三个节点,就能往下走。取平均之后,可以认为每层遍历2个节点。
因此时间复杂度=2log2(n),同理,如果是每k个节点取一个索引的话,就是klogk(n)
空间复杂度:也是以每两个节点取一个索引为例,第一层n个节点,第二层n/2,第三次n/4,等比序列求和,或者取极限,可以认为索引节点数量无限接近于n,所以空间复杂度为O(n)。