本文主要讲解 redis 的五种基本数据类型:String、List、Set、Sorted Set、Hash。学习如何使用它们,并且了解它们的底层数据结构实现,这样我们才能在适当的应用场景选择最适合的数据类型来解决我们的需求。
String 是 redis 最简单的且最常用的数据类型,可以用来存储字符串、整型以及浮点型。
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> get name
"zhangsan"
虽然 get age 时返回的是字符串,但是可以通过 object encoding age 看到其真正的类型。
127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> object encoding age
"int"
# 自增
127.0.0.1:6379> incr age
(integer) 19
# 自减
127.0.0.1:6379> decr age
(integer) 18
写入的浮点型,但还是通过字符串来保存的,但是在使用的时候,会将字符串转换成浮点数。
127.0.0.1:6379> set height 1.77
OK
127.0.0.1:6379> get height
"1.77"
127.0.0.1:6379> object encoding height
"embstr"
# 浮点数操作
127.0.0.1:6379> incrbyfloat height 1
"2.77"
更多的操作请参考官网 (opens new window)
String 支持的编码类型有 int、embstr 和 raw,在不同条件时,会转换成对应的编码。
embstr 和 raw 的区别是什么呢?
创建 string 使用 raw 编码时,会调用两次内存分配来创建 redisObject 和 sds 的数据结构,而 embstr 只会调用一次来创建连续的内存空间来存储 redisObject 和 sds 。
当字符串较小时,embstr 少调用一次内存分配,释放也只需要一次,明显更快,并且连续的内存空间可以更好的利用 cpu 缓存。
在数据编码中提到 embstr 和 raw 都是使用 SDS 数据结构,那么这种数据结构的是怎么样的,有什么好处呢?
数据结构如下图所示:
struct sdshdr {
//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int len;
//记录buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
问题:String 为什么使用简单动态字符串来实现,而不是使用 C 传统字符串来实现呢?
在扩充字符串时,需要考虑缓冲区是否足够,SDS 提供 API 来帮助我们判断并扩充空间。
C 传统字符串需要遍历才能知道具体的长度,时间复杂度为 O(n),而 SDS 可以直接读 len 属性获取长度,时间复杂度为 O(1)
在扩大字符串长度时,需要重新申请更大的内存空间,然后拷贝到新的内存空间中,然后再释放旧的内存空间。
在缩小时也是一样,需要申请新的空间,再释放旧的空间。
在 SDS 中可以通过以下的方式来减少内存分配和释放,提高效率。
C 语言中是以空字符来表示字符串的结束,而一些二进制文件内容可能是包含空字符串的,因此 C 语言字符串无法正确读取,所以 SDS 都是以二进制方式处理 buf,并且不以空字符串判断是否结束,而是用 len 来判断。
命令 | 说明 |
---|---|
lpush | 将值从左边推入列表 |
rpush | 将值从右边推入列表 |
lpop | 将值从列表左边弹出返回 |
rpop | 将值从列表右边弹出返回 |
lrange | 根据索引查看列表中的数据 |
127.0.0.1:6379> lpush mylist 1 2 3
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> rpush mylist 4
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "3"
2) "2"
3) "1"
4) "4"
127.0.0.1:6379> lpop mylist
"3"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "4"
127.0.0.1:6379> rpop mylist
"4"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
在 redis3.2 之前使用的是 ziplist
和 linkedlist
两种编码方式。在数据量少的时候使用 ziplist,而数据多的时候使用 linkedlist。
而之后的版本使用的是 quicklist
127.0.0.1:6379> object encoding mylist
"quicklist"
该数据结构类似于一个数组,在数组的表头添加三个字段 zlbytes、zltail、zlen,再在表尾添加 zlend 表示列表结束。
通过这几个字段可以快速定位第一个元素和最后元素,时间复杂度为 O(1)。
除了表头与表尾之外,其他元素都是节点,节点的数据结构如下图:
连锁更新问题
对于 previous_entry_length:
previous_entry_length
长度需要用 1 字节保存长度值previous_entry_length
长度需要用 5 字节保存长度值有这一种场景,如果压缩列表中有多个连续的节点长度在 250-253 字节之间,这些节点 previous_entry_length
使用 1 个字节存储大小。
previous_entry_length
都需要从 1 字节扩充到 5 字节,起到连锁更新,后面的节点都需要更新。previous_entry_length
需要保存前面的 big 节点,需要扩充节点空间,并且后面发生连锁更新虽然连锁更新复杂度很高,但是造成的可能性很低。
优点
缺点
quicklist 是压缩列表和双向链表结合实现的,在宏观上它是一个双向链表,然后其每个结点上都挂了一个 ziplist。如下图所示:
ziplist 在列表数量大的时候性能会下降,很难分配一大块连续的内存空间,并且在删除和插入操作性能下降的更为厉害。
而双向链表会导致大量的碎片化空间,而且每个节点的头尾指针都会占用一定的内存空间。
而 quicklist 算是在两者之间取得了一个平衡。
是一个无序的集合,集合元素是唯一的,会自动对添加的数据进行去重。
命令 | 说明 |
---|---|
SADD | 向集合添加一个或多个元素 |
SCARD | 获取集合的数量 |
SMEMBERS | 获取集合全部元素 |
SISMEMBER | 判断集合中是否指定元素 |
127.0.0.1:6379> sadd myset zheng wen feng zheng
(integer) 3
127.0.0.1:6379> scard myset
(integer) 3
127.0.0.1:6379> smembers myset
1) "zheng"
2) "feng"
3) "wen"
127.0.0.1:6379> sismember myset feng
(integer) 1
该数据类型使用数据编码的是 intset
或者 hashtable
,当 set 满足以下条件时,使用的 inset,其他情况都是 hashtable。
127.0.0.1:6379> sadd myset2 1 2 4 4
(integer) 3
127.0.0.1:6379> SMEMBERS myset2
1) "1"
2) "2"
3) "4"
127.0.0.1:6379> OBJECT ENCODING myset2
"intset"
intset 数据结构如下所示:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents 数组中每一个元素的类型都是由 encoding 决定的,但是当原来的数据类型是 int16 时,现在要插入一个 int32 时,该如何操作呢?
升级
这个时候需要对 contents 中的每个元素都进行升级:
这么做的好处是,当所有的数据都是小整型时,可以节省内存的开销。
目前不支持降级。
是一个不允许重复元素的集合,但是每个元素会关联一个分数,可以通过分数对集合进行排序。
命令 | 说明 |
---|---|
ZADD | 向集合添加一个或多个元素 |
ZRANGE | 根据位置查询获取集合多个元素 |
ZREM | 如果元素存在集合中,则进行删除 |
127.0.0.1:6379> zadd myzset 100 zheng 99 wen 80 feng
(integer) 3
127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "wen"
3) "zheng"
127.0.0.1:6379> zadd myzset 91 hello
(integer) 1
# 默认是根据score升序查询
127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "hello"
3) "wen"
4) "zheng"
127.0.0.1:6379> zrem myzset wen
(integer) 1
127.0.0.1:6379> zrange myzset 0 -1
1) "feng"
2) "hello"
3) "zheng"
该类型的数据编码可以为 ziplist
或者 skiplist
,当满足以下条件时,使用的 ziplist,其他情况是 skiplist
当数据编码为 skiplist 时,redis 中使用的存储结构是 zset 数据结构,其中包含了 zskiplist
和 dict
typedef struct zset {
zskiplist *zs1;
dict *dict;
}
通过 zskiplist 可以快速的范围查询,dict 可以快速定位单个元素。
在有序的链表上添加了多级索引,通过索引位置的跳转,实现数据的快速定位。如下图所示:
优点:
用于存储 string 类型的键值对,适用于存储对象。
命令 | 说明 |
---|---|
hset | 设置键值对 |
hget | 获取指定 hash 的键对应的值 |
hgetall | 获取指定 hash 中的所有键值对 |
hdel | 删除指定 hash 中的键值对 |
127.0.0.1:6379> hset person name zhangsan
(integer) 1
127.0.0.1:6379> hset person age 18
(integer) 1
127.0.0.1:6379> hget persion name
"zhangsan"
127.0.0.1:6379> hgetall person
1) "name"
2) "zhangsan"
3) "age"
4) "18"
127.0.0.1:6379> hdel person age
(integer) 1
127.0.0.1:6379> hgetall person
1) "name"
2) "zhangsan"
使用的数据编码是 ziplist
或 hashtable
,满足以下条件选择使用 ziplist,其他情况使用 hashtable
redis 之所以快,正是因为其有着丰富的数据结构,所以我们需要理解它们,在设计方案时,就能正确的选择数据类型来实现我们的业务需求。