Redis 5种基本数据结构

在线redis操作

http://try.redis.io/#run

1)设置和获取键值对

SET key valueOK
GET key"value"
值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一张 .jpeg 图片,只需要注意不要超过 512 MB 的最大限度就好了。

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

SET key newValueOK
GET key"newValue"

另外你还可以使用 EXISTS 和 DEL 关键字来查询是否存在和删除键值对:

EXISTS key(integer) 1
DEL key(integer) 1
GET key(nil)

批量设置键值对

SET key1 value1OK
SET key2 value2OK
MGET key1 key2 key3 # 返回一个列表1) "value1"2) "value2"3) (nil)
MSET key1 value1 key2 value2
MGET key1 key21) "value1"2) “value2”

过期和 SET 命令扩展
可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。(过期可以是任意数据结构)

SET key value1
GET key"value1"
EXPIRE name 5 # 5s 后过期… # 等待 5s
GET key(nil)

等价于 SET + EXPIRE 的 SETEX 命令:

SETEX key 5 value1… # 等待 5s 后获取
GET key(nil)

SETNX key value1 # 如果 key 不存在则 SET 成功(integer) 1
SETNX key value1 # 如果 key 存在则 SET 失败(integer) 0
GET key"value" # 没有改变

计数
如果 value 是一个整数,还可以对它使用 INCR 命令进行 原子性 的自增操作,这意味着及时多个客户端对同一个 key 进行操作,也决不会导致竞争的情况:

SET counter 100
INCR counter(integer) 101
INCRBY counter 50(integer) 151

返回原值的 GETSET 命令
对字符串,还有一个 GETSET 比较让人觉得有意思,它的功能跟它名字一样:为 key 设置一个值并返回原值:

SET key value
GETSET key value1"value"
这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 INCR 命令操作一个 key,当需要统计时候你就把这个 key 使用 GETSET 命令重新赋值为 0,这样就达到了统计的目的。

2)列表 list

Redis 的列表相当于 Java 语言中的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 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;
typedef struct listIter {
    listNode *next;
    int direction;
} listIter;
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;

可以看到,多个 listNode 可以通过 prev 和 next 指针组成双向链表:
在这里插入图片描述
虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 adlist.h/list 结构来持有链表的话,操作起来会更加方便:
在这里插入图片描述
链表的基本操作
LPUSH 和 RPUSH 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;
LRANGE 命令可以从 list 中取出一定范围的元素;
LINDEX 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 get(int index) 操作;
示范:

rpush mylist A(integer) 1
rpush mylist B(integer) 2
lpush mylist first(integer) 3
lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有1) "first"2) "A"3) “B”

list 实现队列
队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序:

RPUSH books python java golang(integer) 3
LPOP books"python"
LPOP books"java"
LPOP books"golang"
LPOP books(nil)

list 实现栈
栈是先进后出的数据结构,跟队列正好相反:

RPUSH books python java golang
RPOP books"golang"
RPOP books"java"
RPOP books"python"
RPOP books(nil)

3)字典 hash

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

dict.h/dictht 定义:
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;
typedef struct dict {
    dictType *type;
    void *privdata;
    // 内部有两个 dictht 结构
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

table 属性是一个数组,数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针,而每个 dictEntry 结构保存着一个键值对:

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

可以从上面的源码中看到,实际上字典结构的内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 渐进式搬迁 (下面说原因)。

渐进式 rehash
大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 渐进式 rehash 小步搬迁:
在这里插入图片描述
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。

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

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

字典的基本操作
hash 也有缺点,hash 结构的存储消耗要高于单个字符串,所以到底该使用 hash 还是字符串,需要根据实际情况再三权衡:

HSET books java "think in java"    # 命令行的字符串如果包含空格则需要使用引号包裹
(integer) 1> HSET books python "python cookbook"
(integer) 1> HGETALL books    # key 和 value 间隔出现
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"> HGET books java"think in java"> HSET books java "head first java"  
(integer) 0        # 因为是更新操作,所以返回 0> HMSET books java "effetive  java" python "learning python"    # 批量操作
OK

4)集合 set

Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
集合 set 的基本使用
由于该结构比较简单,我们直接来看看是如何使用的:

SADD books java
(integer) 1> SADD books java    # 重复
(integer) 0> SADD books python golang
(integer) 2> SMEMBERS books    # 注意顺序,set 是无序的 
1) "java"
2) "python"
3) "golang"> SISMEMBER books java    # 查询某个 value 是否存在,相当于 contains
(integer) 1> SCARD books    # 获取长度
(integer) 3> SPOP books     # 弹出一个"java"

5)有序列表 zset

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

它的内部实现用的是一种叫做 「跳跃表」 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了:
在这里插入图片描述
想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人又是员工又有组长的身份。

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

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

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

有序列表 zset 基础操作

ZADD books 9.0 “think in java”
ZADD books 8.9 “java concurrency”
ZADD books 8.6 “java cookbook”

ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围1) "java cookbook"2) "java concurrency"3) “think in java”

ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围1) "think in java"2) "java concurrency"3) “java cookbook”

ZCARD books # 相当于 count()(integer) 3

ZSCORE books “java concurrency” # 获取指定 value 的 score"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题

ZRANK books “java concurrency” # 排名(integer) 1

ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset1) "java cookbook"2) “java concurrency”

ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。1) "java cookbook"2) "8.5999999999999996"3) "java concurrency"4) “8.9000000000000004”

ZREM books “java concurrency” # 删除 value(integer) 1
ZRANGE books 0 -11) "java cookbook"2) “think in java”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值