Redis五种数据类型,底层存储数据结构,以及相关命令

14 阅读14分钟

redis提供了五种数据类型供使用者使用,分别是string类型,hash类型(类似于对象结构),list类型,set类型,以及zset类型(有序set),redis在底层对这些数据进行保存时使用了SDS(Simple Dynamic String),压缩列表,双向链表,快速列表,整数集合,hash表以及跳表,其中hash表在redis中使用最为广泛,具体如下

redis保存数据底层数据结构

string类型:

string类型底层是通过redis自身源码设计的数据结构SDS(Simple Dynamic String)存储,他是C语言中的一个结构体,代码结构类似如下

struct __attribute__ ((__packed__)) sdshdr {
    uint8_t len;
    uint8_t alloc;
    unsigned char flags;
    char buf[];
};

相较于普通字符串来说,SDS可以动态的管理char数组的长度(C语言保存字符串是通过char数组),其实动态管理字符数组长度并不难,普通字数组一样能做到,无非是根据字符数组长度来分配合适的内存节省空间,不过字符数组获取长度需要遍历整个字符串,并且还需要判断最后一个字符是否为null来判断是否是结尾(字符数组结构以null结尾),这一过程十分浪费性能,并且还要通过长度判断剩余空间防止缓存溢出,而通过结构体进行记录这些信息则可以提高性能,其中字符数组的扩容机制是未达到1M则翻倍,否则增加1M,在3.2版本以后则是通过sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64来进行数据的保存和扩容,其中除了sdshdr5以外,结构和之前的sdshdr一样,而sdshdr5则将alloc省略,并且将len融入flags中,结构如下

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags;
    char buf[];
};

这种结构下,当我们保存短字符串时,内存则被充分的使用了,而sdshdr后面的数字的意义则是保存长度信息所用的位数,比如sdshdr5用5位表示长度,最大长度位31,而sdshdr8用一个字节表示长度,也就是8位,以此类推。

hash类型:

hash数据保存的是键值对格式的数据,当保存的键值对数量较少时且每个键值对的长度较短时会使用压缩列表保存键值对字符数组,使用压缩列表保存数据可以节省内存,但当键值对数量较多(redis配置文件中的hash-max-ziplist-entries,默认值为512)或单个键值对的键和值较大(redis配置文件中的hash-max-ziplist-value,默认为 64 字节。),压缩列表的查询速度会变慢,此时redis使用hash表结构储存数据,这种转换操作是不可逆的,也就是说一旦转换为hash表结构,即使数量缩小,也不会转换会压缩列表。 hash表结构是和hashmap源码中维护的数据结构基本一致的数据结构。首先,hash表内部维护了一个数组结构,当一个键值对储存在其中时会计算key的hash值,并且将hash值与数组长度取余得到一个数组索引,并将保存key和value的指针的结构体存入其中,当当前位置已经有其他值时,会和已有元素组合成为链表,将自己的指针放入当前链表中最后一个元素的结构体的next属性中。和hashmap不同的时,首先对于负载因子,hashmap的默认为0.75,而redis实现的hash表的负载因子为1,也就是说,redis只有当数组全部占满时才会扩容,并且redis不允许自定义负载因子,而hashmap可以自定义(这也是可以理解的,redis使用的是内存资源,内存资源不像硬盘,内存资源较为珍贵,必须节省使用)。并且hashmap在数组中单个元素维护的链表过长时会将其转化为红黑树,而redis维护的hash表对于链表过长没有做处理,大致结构如下图。

; List类型

在老版本的redis中储存list数据类型使用压缩列表和双向链表两个结构,其中压缩列表是在一块连续内存中保存了header(zlbytes列表字节数,zltail到最后一个元素的偏移量,allen列表长度),entry(数组内容)以及end(0xFF列表结尾表示符),结构如下

而双向链表则是讲元素变为记录前后元素指针的节点,与传统双向链表不同的是,redis实现双向链表时通过一个结构体保存了该双向链表的相关信息以及操作函数,结构体如下

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;

元素默认以压缩列表的数据结构存储,当元素数量超过某个阀值(redis配置文件中的list-max-ziplist-entries,默认值为512),或者单个元素超过某个阀值时(redis配置文件中的list-max-ziplist-value,默认为 64 字节。),压缩链表会转换成双向链表。这种转换操作是不可逆的,也就是说一旦转换为双向链表,即使数量缩小,也不会转换会压缩链表

在新版本的redis中,redis通过快速链表保存数据,而快速链表实际上就是压缩列表和双向链表的结合体,他通过锁哥压缩列表,并且每个压缩列表记录前后压缩列表的指针实现快速链表。如下图

set类型

对于set类型数据,redis采用当数据全部是整形,并且数量不多时(redis配置项为set-max-intset-entries,默认是512),使用整形数组的结构保存数据,当数量过多或保存数据中包含非整形数据时则使用hash表存储,和hash类型使用的hash表基本一致,不过set类型没有key,所以在使用hash表结构存储数据时会直接使用元素本身转化为hash值进行取余运算。

zset类型

zset类型在维护数据时会同时维护两个数据结构,分别是hash表和跳表,当对元素进行增删改操作时会对两个数据结构都进行增删改操作,其中hash表不存在有序判断,所以zset的有序性是由跳表维护的。zset其中保存的键值对为key:score,当我们用key去查询分数时,redis会通过hash表查找,而当我们用score查找key时,则通过跳表查找,通过这样的方式,redis提高有序性的查找速度。 跳表以一种以空间换时间的形式提高查询速度,其查询方式类似于平衡二叉树。首先跳表分为多个层级,其中最底层有全部元素,第二层则有部分元素,第三层则有第二层的部分元素,以此类推,查找元素时会先从最上层开始比较,如果第一个元素比查找元素大,那么则从下一层的第一个元素找,如果第一个元素比查找元素小,则会继续向右查找,直到下一个元素比查找元素大时,则会进入下一层,从当前节点的右侧开始查找,查找过程依旧是向右查找,直到下一个元素比当前查找元素大,当找到相等的时,则查找成功。 除了最底层外,每层分布的元素是不规律的,一个元素是否出现在上一层取决于概率算法,比如说我们指定概率为二分之一,那么第一层元素出现在第二层的概率就是二分之一,而第二层元素出现在第三层的概率也是二分之一,以此类推,元素通过概率运算跃迁至上一层的过程叫升级。

对应的,层数在跳表中也是依据概率的,看元素通过概率算法升级到了多少层,就有多少层,也就是说,即使只有一个元素,也可能通过概率变为好多层(实际可能会避免这种情况),在这种概率算法下,每层元素都大概是下一层的2/1,层数也是元素越多层数越多,查找方式也更类似于平衡二叉树。 redis的跳表升级概率为四分之一。

; redis相关指令及示例

string类型

SET:设置指定 key 的值

SET mykey "Hello"

GET:获取指定 key 的值

GET mykey

GETSET:将给定 key 的值设置为 value,并返回 key 的旧值

GETSET mykey "World"

SETNX:只有在 key 不存在时,设置 key 的值

SETNX mykey "Hello"

SETEX:设置 key 的值,并指定该 key 的过期时间(以秒为单位)

SETEX mykey 10 "Hello"

PSETEX:设置 key 的值,并指定该 key 的过期时间(以毫秒为单位)

PSETEX mykey 10000 "Hello"

MSET:同时设置一个或多个 key-value 对

MSET key1 "Hello" key2 "World"

MGET:获取所有给定 key 的值

MGET key1 key2

INCR:将 key 中存储的数字值增一

INCR mykey

INCRBY:将 key 所储存的值加上给定的增量值

INCRBY mykey 5

INCRBYFLOAT:将 key 所储存的值加上给定的浮点增量值

INCRBYFLOAT mykey 1.5

DECR:将 key 中存储的数字值减一

DECR mykey

DECRBY:将 key 所储存的值减去给定的减量值

DECRBY mykey 5

APPEND:将值附加到 key 的现有值之后

APPEND mykey " World"

STRLEN:获取 key 所储存的字符串值的长度

STRLEN mykey

GETRANGE:获取存储在 key 中的字符串的子字符串

GETRANGE mykey 0 4

SETRANGE:用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始

SETRANGE mykey 6 "Redis"

MSETNX:同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在

MSETNX key1 "Hello" key2 "World"

BITCOUNT:计算给定字符串中,被设置为 1 的比特位的数量

BITCOUNT mykey

BITOP:对一个或多个存储在 key 中的字符串值,进行位元操作,并将结果存储在目标 key 中

BITOP AND result key1 key2

BITPOS:返回字符串里面第一个被设置为1或者0的比特位的位置

BITPOS mykey 1

hash类型

HSET:将哈希表中的字段设置为指定值

HSET myhash field1 "value1"

HGET:获取存储在哈希表中指定字段的值

HGET myhash field1

HSETNX:仅当字段不存在时,才设置哈希表字段的值

HSETNX myhash field2 "value2"

HMSET:同时将多个字段设置为指定值

HMSET myhash field1 "newvalue1" field3 "value3"

HMGET:获取所有给定字段的值

HMGET myhash field1 field3

HINCRBY:为哈希表中的字段值加上指定增量

HINCRBY myhash field4 5

HINCRBYFLOAT:为哈希表中的字段值加上指定浮点数增量

HINCRBYFLOAT myhash field5 1.5

HDEL:删除一个或多个哈希表字段

HDEL myhash field1

HEXISTS:检查哈希表中是否存在指定字段

HEXISTS myhash field1

HKEYS:获取所有字段名

HKEYS myhash

HVALS:获取所有字段值

HVALS myhash

HLEN:获取哈希表中字段的数量

HLEN myhash

HGETALL:获取哈希表中所有字段和值

HGETALL myhash

HSCAN:迭代哈希表中的键值对

HSCAN myhash 0 MATCH field*

list类型

LPUSH:将多个值插入到列表头部

LPUSH mylist "world" "hello"

RPUSH:将多个值插入到列表尾部

RPUSH mylist "!"

LPOP:移出并获取列表的第一个元素

LPOP mylist

RPOP:移出并获取列表的最后一个元素

RPOP mylist

LREM:根据参数 count 的值,移除列表中与参数 value 相等的元素

LPUSH mylist "hello" "world" "hello"

LREM mylist 1 "hello"

LLEN:获取列表长度

LLEN mylist

LRANGE:获取列表指定范围内的元素

LRANGE mylist 0 -1

LINDEX:通过索引获取列表中的元素

LINDEX mylist 1

LSET:通过索引设置列表元素的值

LSET mylist 1 "there"

LTRIM:对一个列表进行修剪,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除

LTRIM mylist 0 1

RPOPLPUSH:移除列表的最后一个元素,并将该元素添加到另一个列表并返回

RPOPLPUSH mylist myotherlist

BLPOP:移出并获取列表的第一个元素,如果列表没有元素则会阻塞列表直到等待超时或发现可弹出元素为止

BLPOP mylist 0

BRPOP:移出并获取列表的最后一个元素,如果列表没有元素则会阻塞列表直到等待超时或发现可弹出元素为止

RPUSH mylist "hello" "world"

BRPOP mylist 0

BRPOPLPUSH:从列表中弹出最后一个值,将弹出的元素插入到另一个列表的头部,并返回它;如果列表为空,阻塞直到发现可弹出元素为止

BRPOPLPUSH mylist myotherlist 0

LINSERT:在列表的元素前或后插入元素

LINSERT mylist BEFORE "hello" "there"

LPUSHX:将一个值插入到已存在的列表头部,列表不存在时操作无效

LPUSHX mylist "start"

RPUSHX:将一个值插入到已存在的列表尾部,列表不存在时操作无效

RPUSHX mylist "end"

BLMOVE:阻塞弹出一个列表中的元素并将其推入到另一个列表中

BLMOVE mylist myotherlist LEFT RIGHT 0

set类型

SADD:将一个或多个成员添加到集合中

SADD myset "value1" "value2"

SCARD:获取集合中的成员数量

SCARD myset

SDIFF:返回一个集合与其他集合之间的差集

SADD otherset "value2" "value3"
SDIFF myset otherset

SDIFFSTORE:将一个集合与其他集合之间的差集存储到另一个集合

SDIFFSTORE diffset myset otherset

SINTER:返回所有给定集合的交集

SINTER myset otherset

SINTERSTORE:将所有给定集合的交集存储到另一个集合

SINTERSTORE interset myset otherset

SISMEMBER:判断成员元素是否是集合的成员

SISMEMBER myset "value1"

SMEMBERS:返回集合中的所有成员

SMEMBERS myset

SMOVE:将一个成员从一个集合移动到另一个集合

SMOVE myset otherset "value1"

SPOP:移除并返回集合中的一个随机元素

SPOP myset

SRANDMEMBER:返回集合中的一个或多个随机成员

SRANDMEMBER myset 2

SREM:移除集合中的一个或多个成员

SREM myset "value1"

SUNION:返回所有给定集合的并集

SUNION myset otherset

SUNIONSTORE:将所有给定集合的并集存储到另一个集合

SUNIONSTORE unionset myset otherset

SSCAN:迭代集合中的元素

SSCAN myset 0 MATCH value*

zset类型

ZADD:向有序集合中添加一个或多个成员,或者更新已存在成员的分数

ZADD myzset 1 "one" 2 "two"

ZCARD:获取有序集合中的成员数量

ZCARD myzset

ZCOUNT:计算在有序集合中指定分数范围内的成员数量

ZCOUNT myzset 1 2

ZINCRBY:为有序集合中的成员的分数加上指定增量

ZINCRBY myzset 2 "one"

ZRANGE:通过索引区间返回有序集合成指定区间内的成员

ZRANGE myzset 0 -1 WITHSCORES

ZRANGEBYSCORE:通过分数区间返回有序集合指定区间内的成员

ZRANGEBYSCORE myzset 1 3 WITHSCORES

ZRANK:返回有序集合中指定成员的索引

ZRANK myzset "one"

ZREM:移除有序集合中的一个或多个成员

ZREM myzset "one"

ZREMRANGEBYRANK:移除有序集合中指定索引区间内的所有成员

ZREMRANGEBYRANK myzset 0 1

ZREMRANGEBYSCORE:移除有序集合中指定分数区间内的所有成员

ZREMRANGEBYSCORE myzset 1 3

ZREVRANGE:返回有序集合中指定索引区间内的成员,按分数从高到低排序

ZREVRANGE myzset 0 -1 WITHSCORES

ZREVRANGEBYSCORE:通过分数区间返回有序集合中指定区间内的成员,按分数从高到低排序

ZREVRANGEBYSCORE myzset 3 1 WITHSCORES

ZREVRANK:返回有序集合中指定成员的索引,按分数从高到低排序

ZREVRANK myzset "one"

ZSCORE:返回有序集合中指定成员的分数

ZSCORE myzset "one"

ZUNIONSTORE:计算给定的一个或多个有序集合的并集,并存储到目标有序集合中

ZUNIONSTORE myzset2 2 myzset otherset

ZINTERSTORE:计算给定的一个或多个有序集合的交集,并存储到目标有序集合中

ZINTERSTORE myzset2 2 myzset otherset

ZSCAN:迭代有序集合中的元素

ZSCAN myzset 0 MATCH "one*"