Redis-五种基本数据结构

文章目录

  • Redis 简介
  • Redis 优势
  • Redis 五种基本数据结构
    • 字符串(string)
      • 设置和获取键值对
      • 批量设置键值对
      • 过期set命令
      • 不存在创建存在不更新
      • 计数
    • 列表(list)
      • 链表的基本操作命令
      • 左右边插入键值对
      • 获取指定的区域的元素
        • 通过索引获取列表中的元素
      • 修改特定位置的值
    • hash(字典)
      • 解决冲突
      • 扩展和收缩的条件是什么呢?
      • 渐进式 rehash
      • 字典的基本操作命令
    • set(集合)
    • zset(有序列表)
      • 有序列表基础操作命令

Redis 简介

Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • Redis支持数据的备份,即master-slave模式的数据备份

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis 的安装以及更加详细的资料可以看这里 菜鸟教程

Redis 五种基本数据结构

  • string(字符串)
  • list(列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

字符串(string)

string字符串是 Redis 中的最基础的数据结构,我们保存到 Redis 中的 key,也就是键,就是字符串结构的。除此之外,Redis 中其它数据结构也是在字符串的基础上设计的。

除此之外,Redis 中的字符串结构可以保存多种数据类型,如:简单的字符串、JSON、XML、二进制等,但有一点要特别注意:Redis 规定了字符串的长度不得超过 512 MB。

Redis的字符串是动态字符串,是可以修改的字符串,它的底层实现有点类似于 Java 中的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间,如图中所示:

Redis-五种基本数据结构_第1张图片

Redis 官方提供了在线的调试器,你可以在里面敲入命令进行操作:调试器,如下图:
Redis-五种基本数据结构_第2张图片

设置和获取键值对

> set name "哈哈"
OK
> get name
"哈哈"

使用 set和 get 来设置和获取字符串值。

上面说过值可以是简单的字符串、JSON、XML、二进制等,需要注意的是不要超过 512 MB 的最大限度就好了。

当 key 存在时,set 命令会覆盖掉上一次设置的值:

> set name "嘻嘻"
OK
> get name
"嘻嘻"

还可以使用 exists 和 del 关键字来查询是否存在和删除键值对。

> exists name
(integer) 1
> del name
(integer) 1

在次get获取name的值为空。

> get name
(nil)

批量设置键值对

> set name1 "哈哈"
OK
> set name2 "嘻嘻"
OK
> mget name1 name2 name3
1) "哈哈"
2) "嘻嘻"
3) (nil)
> mset name1 "name1" name2 "name2"
OK
> mget name1 name2
1) "name1"
2) "name2"

过期set命令

过期set是通过设置一个缓存key的过期时间,使得缓存到期后自动删除从而失效的机制。

> set name "name"
OK
> get name
"name"
> expire name 6
(integer) 1
> get name
(nil)

还有一种方式:setex key seconds value

> setex name 6 "liancan"
OK
> get name
(nil)

不存在创建存在不更新

> setnx name "哈哈" #如果 key 不存在则 set 成功
(integer) 1
> setnx name "嘻嘻" #如果 key 存在则 set 失败
(integer) 0
> get name
"哈哈"

计数

如果字符串的内容是一个整数,那么还可以将字符串当成计数器来使用。

> set counts 50
OK
> get counts
"50"
> incrby counts 70
(integer) 120
> get counts
"120"
> decrby counts 70
(integer) 50
> get counts
"50"
> incr counts #等价于incrby counts 1
(integer) 51
> decr counts #等价于 decrby counts 1
(integer) 50

计数器是有范围的,自增的范围必须在signed long的区间访问内,[-9223372036854775808,9223372036854775808]。

> set count1 9223372036854775807
OK
> incr count1
(error) ERR increment or decrement would overflow
> set count1 -9223372036854775808
OK
> decr count1
(error) ER

列表(list)

Redis 的列表相当于 Java 语言中的 LinkedList,注意它是一个双向链表数据结构而不是数组。支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

Redis-五种基本数据结构_第3张图片

从源码的adlist.h/listNode 中来看对其的定义:

/* Node, List, and Iterator are the only data structures used currently. */

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    //节点的值
    void *value;
} listNode;

可以看到,多个 listNode 可以通过 prev 和 next 指针组成双向链表:

Redis-五种基本数据结构_第4张图片

typedef struct list {
   // 表头节点
    listNode *head;
     // 表尾节点
    listNode *tail;
     // 链表所包含的节点数量
    unsigned long len;
    // 节点值复制函数
    void *(*dup)(void *ptr);
       // 节点值释放函数
    void (*free)(void *ptr);
      // 节点值对比函数
    int (*match)(void *ptr, void *key);
} list;

Redis-五种基本数据结构_第5张图片
列表中字段的含义:

  • head 指向头部节点,获取的时间复杂度为O(1)
  • tail 指向尾部节点,获取的时间复杂度为O(1)
  • len 3 保存着列表的长度信息,获取的时间复杂度为O(1)
  • dup 函数用于复制链表节点所保存的值
  • free 函数用于释放链表节点所保存的值
  • match 函数则用于对比链表节点所保存的值和另一个输入值是否相等

链表的基本操作命令

lpush 和 rpush 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;
lrange 命令可以从 list 中取出一定范围的元素;
lindex 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 get(int index) 操作;

左右边插入键值对

//左边插入
> lpush spacedong name1
(integer) 1
//右边插入
> rpush spacedong name2
(integer) 2
//左边弹出
> lpop spacedong
"name1"
//右边弹出
> rpop spacedong
"name2"

获取指定的区域的元素

> rpush spacedong name2
(integer) 1
> lpush spacedong name1
(integer) 2
> lrange spacedong 0 2
1) "name1"
2) "name2"
通过索引获取列表中的元素
> lindex spacedong 1
"name2"

修改特定位置的值

> lindex spacedong 1
"name2"
> lset spacedong 1 name3
OK
> lindex spacedong 1
"name3"

hash(字典)

Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的链地址法来解决部分 哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。

源码dict.h/dictht 定义如下:
哈希表

typedef struct dictht {
    //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
    dictEntry **table;     
    //哈希表table的大小,初始化大小为4
    unsigned long size;     
    //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
    unsigned long sizemask; 
    //记录哈希表已有的节点(键值对)数量。
    unsigned long used;     

哈希节点

typedef struct dictEntry { 
 void *key; //key
 union { 
       void *val; 
       uint64_t u64; 
       int64_t s64;
       double d;
 } v; //value 
 //指向下一个hash节点,用来解决hash键冲突(collision)
 struct dictEntry *next;

字典

typedef struct dict { 
   //指向dictType结构,dictType结构中包含自定义的函数,
   //这些函数使得key和value能够存储任何类型的数据。
    dictType *type; 
   //私有数据,保存着dictType结构中函数的参数。
    void *privdata; 
   //两张哈希表。
    dictht ht[2]; 
   //rehash的标记,rehashidx==-1,表示没在进行rehash  
   long rehashidx; 
   //正在迭代的迭代器数量 
   int iterators; 

} dict;

从上面的源码中看到,实际上字典结构的内部包含两个 HashTable,通常情况下只有一个 HashTable是有值的,但是在字典扩展和收缩时,需要分配新的 HashTable,然后进行渐进式搬迁 。

代码结构图如下:

Redis-五种基本数据结构_第6张图片
这里需要主要一点的是:ht是一个数组,ht[1]为空,是用来进行散列的。

解决冲突

在解决冲突之前,我们先看(k0,v0)为什么会存在下标为1的位置?

这其实是哈希算法,先计算hash值(hash=dict->type->hashFunction(key)),再计算索引值(index=hash&dict->ht[x].sizemask。

那如果再有一个(k2,v2),他的索引值也是下标为1,那就会出现两个值在同一位置的情况。这就是冲突啦。

redis的哈希表采用链地址法来解决键冲突,上面的整个结构图中的哈希节点dictEntry有一个next指针,他是指向下一个节点的。

最新的节点添加到链表的表头位置,这样是为了速度考虑。

扩展和收缩的条件是什么呢?

正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。

当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。

渐进式 rehash

Redis 中的 hash 存储的value只能是字符串值,此外扩容与 Java 中的 HashMap 也不同。Java 中的HashMap在扩容的时候是一次性完成的,而 Redis 考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式 rehash 策略。

渐进式 rehash 指的是并非一次性完成,它是多次完成的,渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,所以 Redis 中的 hash(字典) 会存在新旧两个 hash 结构,在 rehash 结束后也就是旧 hash 的值全部搬迁到新 hash 之后,就会使用新的 hash 结构取而代之。

rehash步骤如下:

1、为 ht[1] 分配空间。
Redis-五种基本数据结构_第7张图片
2、在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为0,表示 rehash 正式开始工作。

Redis-五种基本数据结构_第8张图片
3、rehash 过程中,逐渐将 rehashidx 加1。
Redis-五种基本数据结构_第9张图片
4.以此类推,rehash 结束,将 reshidx 属性的值设为-1,表示 rehash 工作已完成。

Redis-五种基本数据结构_第10张图片

字典的基本操作命令

> hset boots java "think in java" //hash插入值,字典不存在则创建
1
> hset books python "python cookbook"
1
> hgetall books //获取字典中所有的key和value
1) "python"
2) "python cookbook"
> hget books java //获取字典中的指定key的value
(nil)
> hget boots java
"think in java"
> hset books java "head first java"
1
> hmset books java "effetiev java" python "learning python" //批量设置值
OK
> hlen books //获取指定字典的key的个数
2

set(集合)

Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL,集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。

Redis-五种基本数据结构_第11张图片

由于结构比较简单,直接命令来看是如何使用的:

sadd key member [member …]:增加元素 可以一次增加多个元素,元素不能重复。

> sadd key zhangsan
(integer) 1
> sadd key zhangsan
(integer) 0
> sadd name java python golang
> sadd keyjava python golang
(integer) 2

smembers key:查看集合中所有的元素,注意是无序

> smembers keyjava
1) "golang"
2) 

sismember key member:查询集合中是否包含某个元素

> sismember keyjava golang
(integer) 1
> sismember keyjava golangs
(integer) 0

scard key:获取集合的长度

> scard keyjava
2

spop key [count]:弹出元素,count指弹出元素的个数

> spop keyjava
"python"
> spop keyjava 1
1) "golang"

zset(有序列表)

这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。

Redis-五种基本数据结构_第12张图片

zset底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。

这里简单说一下跳跃列表的原理,由于比较复杂(下一章详细讲解)。

Redis-五种基本数据结构_第13张图片
一家公司中,刚开始只有几个人,大家都是一样的职位,后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人又是员工又有组长的身份。

再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。

跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。最终形成了一个金字塔的结构。

想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > …,就是这样一个结构!

有序列表基础操作命令

zadd key [NX|XX] [CH] [INCR] score member [score member …]:
增加元素 通过zadd指令可以增加一到多个value/score对,score放在前面

> zadd ireader 4.0 python
(integer) 1
> zadd ireader 4.0 java 1.0 go
(integer) 2

zrange key start stop [WITHSCORES]
按照score权重从小到大排序输出集合中的元素,权重相同则按照value的字典顺序排序。

> zrange ireader 0 1 //获取所有元素吗,按照score的升序输出
1) "go"
2) "java"
> zadd ireader 10 net //添加score为10的元素
(integer) 1
> zrange ireader 0 2 //key相等则按照value字典排序输出
1) "go"
2) "java"
3) "python"
> zrange ireader 0 3
1) "go"
2) "java"
3) "python"
4) "net"
> zrange ireader 0 -1 withscores //输出权重
1) "go"
2) 1.0
3) "java"
4) 4.0
5) "python"
6) 4.0
7) "net"
8) 10.0

zrevrange key start stop [WITHSCORES]
按照score权重从大到小输出集合中的元素,权重相同则按照value的字典逆序排序

> zrevrange ireader 0 -1 withscores
1) "net"
2) 10.0
3) "python"
4) 4.0
5) "java"
6) 4.0
7) "go"
8) 1.0

**zrangebyscore key min max [WITHSCORES] [LIMIT offset count]*:
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

 zrange ireader 0 -1 withscores
1) "go"
2) 1.0
3) "java"
4) 4.0
5) "python"
6) 4.0
7) "net"
8) 10.0
> zrangebyscore ireader 6 9
(empty list or set)
> zrangebyscore ireader 2 5
1) "java"
2) "python"
> zrangebyscore ireader 2 5 withscores
1) "java"
2) 4.0
3) "python"
4) 4.0
> zrangebyscore ireader -inf 4
1) "go"
2) "java"
3) "python"
> zrangebyscore ireader inf 4
(empty list or set)
> zrangebyscore ireader -inf +inf
1) "go"
2) "java"
3) "python"
4) "net"
> zrangebyscore ireader (2 5
1) "java"
2) "python"
> zrangebyscore ireader (2 (2.1
(empty list or set)
> zrangebyscore ireader (2 (10.1
1) "java"
2) "python"
3) "net"
> zrangebyscore ireader (2 (11
1) "java"
2) "python"
3) "net"

zrem key member [member …]:移除有序集 key 中的一个或多个成员,不存在的成员将被忽略

> zrange ireader 0 -1 withscores
1) "go"
2) 1.0
3) "java"
4) 4.0
5) "python"
6) 4.0
7) "net"
8) 10.0
> zrem ireader go
1
> zrange ireader 0 -1 withscores
1) "java"
2) 4.0
3) "python"
4) 4.0
5) "net"
6) 10.0

更多的命令请查看教程:菜鸟教程

你可能感兴趣的:(redis,redis,数据结构,缓存)