Redis-五种基本数据结构

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 官方提供了在线的调试器,你可以在里面敲入命令进行操作:调试器,如下图:
在这里插入图片描述

设置和获取键值对

> 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)。

在这里插入图片描述

从源码的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 指针组成双向链表:

在这里插入图片描述

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;

在这里插入图片描述
列表中字段的含义:

  • 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,然后进行渐进式搬迁 。

代码结构图如下:

在这里插入图片描述
这里需要主要一点的是: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] 分配空间。
在这里插入图片描述
2、在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为0,表示 rehash 正式开始工作。

在这里插入图片描述
3、rehash 过程中,逐渐将 rehashidx 加1。
在这里插入图片描述
4.以此类推,rehash 结束,将 reshidx 属性的值设为-1,表示 rehash 工作已完成。

在这里插入图片描述

字典的基本操作命令

> 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,集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。

在这里插入图片描述

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

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 值,用来代表排序的权重。

在这里插入图片描述

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

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

在这里插入图片描述
一家公司中,刚开始只有几个人,大家都是一样的职位,后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人又是员工又有组长的身份。

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

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

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

有序列表基础操作命令

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

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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis有几种常用的数据结构,其中包括字符串(string)、哈希(hash)、列表(list)、集合(set)和有序集合(sorted set)。 1. 字符串(string):Redis的字符串类型是用简单动态字符串(SDS)实现的。它可以保存文本数据和二进制数据,并且获取字符串长度的复杂度为O(1),相比于C字符串的O(N)更高效。字符串类型在Redis中被广泛使用,可以存储简单的字符串、复杂的字符串(如XML、JSON)、数字(整数、浮点数)和二进制数据(如图片、音频、视频),但最大不能超过512M。 2. 哈希(hash):哈希是一种string类型的field和value的映射表,类似于JDK1.8之前的HashMap。在Redis中,哈希可以用于存储对象或者其他需要键值对映射的数据。通过哈希,你可以方便地修改对象中的某个字段的值,而不需要修改整个对象。比如可以使用哈希数据结构来存储用户信息、商品信息等等。 3. 列表(list):列表是一个有序的字符串元素集合,可以进行插入、删除、获取指定索引位置或范围的元素等操作。列表类型在Redis中被广泛用于实现队列、栈等数据结构,也可以用于保存一系列有序的数据。 4. 集合(set):集合是一个无序且唯一的字符串元素集合,可以进行添加、删除、判断某个元素是否存在等操作。集合类型在Redis中常用于实现去重、交集、并集等操作。 5. 有序集合(sorted set):有序集合是一个有序的字符串元素集合,每个元素都关联着一个分数,通过分数对元素进行排序。有序集合类型在Redis中被广泛用于实现排行榜、带权重的优先队列等场景。 以上是Redis常用的数据结构以及它们的用法。你可以根据具体的需求选择合适的数据结构来存储和操作数据。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值