概述
Redis存储常见的5种数据类型,分别是:String、List、Set、Zset、Hash
| 标题 | 存储结构 | 能力 |
|---|---|---|
| STRING | 字符串、整数或浮点数 | 对整个字符串或字符串的一部分进行操作对整数或浮点数进行自增或自减操作 |
| LIST | 一个链表,链表上的每个节点都包含一个字符串 | 对链表的两端进行push和pop操作,读取单个或多个元素;据值查找或删除元素 |
| SET | 包含字符串的无序集合集合中每个字符串都是独一无二的 | 添加、获取、删除元素。检查一个元素是否存在于集合中计算交集、并集、差集 |
| Hash | 包含键值对的无序散列表 | 添加、获取、删除单个元素取所有键值对元素,元素计数 |
| Zset | 和散列一样,用于存储键值对字符串成员与浮点数分数之间的有序映射元素的排列顺序由分数的大小决定 | 添加、获取、删除单个元素根据分值范围或成员来获取元素 |
一、STRING (字符串)
Redis的字符串是 动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用 预分配冗余空间 的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
127.0.0.1:6379> set name xiaoming
OK
127.0.0.1:6379> get name
"xiaoming"
127.0.0.1:6379> expire name 5 # 5s 后过期
(integer)1
127.0.0.1:6379> get name # 5s 后获取
(nil)
127.0.0.1:6379> setex name 5 xiaoming # 5s 后过期,等价 set+expre
OK
127.0.0.1:6379> setnx name xiaoming # 如果 name 不存在就执行 set 创建
(integer)1
127.0.0.1:6379>set age 15
OK
127.0.0.1:6379>incr age # 年龄自增
(integer)16
127.0.0.1:6379>incrby age 6 # 年龄指定自增
(integer)22
二、list (列表)
Redis将列表数据结构命名为list而不是array,是因为列表的存储结构用的是链表而不是数组,而且链表还是 双向链表。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优。如果list的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。
队列/堆栈 链表可以从表头和表尾追加和移除元素,结合使用 rpush/rpop/lpush/lpop 四条指令,可以将链表作为 队列或堆栈使用,左向右向进行都可以。
127.0.0.1:6379> rpush animals dog cat cow pig # 添加元素
(integer) 4
127.0.0.1:6379> lindex animals 2 # 根据index获取元素 O(n) 慎用
"cow"
127.0.0.1:6379> lrange animals 0 -1 # 获取取件所有元素 O(n) 慎用
1) "dog"
2) "cat"
3) "cow"
4) "pig"
127.0.0.1:6379> ltrim animals 2 -1 # 保留指定区间元素 O(n) 慎用
OK
127.0.0.1:6379> lrange animals 0 -1
1) "cow"
2) "pig"
快速列表
- 如果数据量比较多,链表的指针空间太大,造成浪费。
- Redis底层存储为快速链表quicklist的一个结构,先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
- Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
三、hash (字典)
哈希等价于Java语言的HashMap, 在实现结构上它使用二维结构,数组+链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值,链表的作用就是用来将产生了「hash碰撞」的元素串起来。
127.0.0.1:6379> hset student xiaoming 97 # 赋值元素
(integer) 1
127.0.0.1:6379> hset student xiaowang 88
(integer) 1
127.0.0.1:6379> hset student xiaohong 90
(integer) 1
127.0.0.1:6379> hget student xiaoming # 获取元素
"97"
127.0.0.1:6379> hlen student # 统计数量
(integer) 3
127.0.0.1:6379> hgetall student # 获取所有元素
1) "xiaoming"
2) "97"
3) "xiaowang"
4) "88"
5) "xiaohong"
6) "90"
127.0.0.1:6379> hset student xiaoming 100 # 更新元素
(integer) 0
127.0.0.1:6379> hget student xiaoming
"100"
127.0.0.1:6379> hincrby student xiaoming 1 # 元素计数
(integer) 101
rehash扩容
当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。所以Redis采用了 渐进式rehash的方案。它会同时 保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。
四、set (集合)
Redis 的集合相当于 Java 语言里面的 HashSet ,它内部的 键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL 当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
127.0.0.1:6379> sadd citys beijing # 添加元素
(integer) 1
127.0.0.1:6379> sadd citys beijing # 重复元素
(integer) 0
127.0.0.1:6379> sadd citys beijing shanghai shandong
(integer) 2
127.0.0.1:6379> smembers citys # 注意是无序的
1) "shandong"
2) "beijing"
3) "shanghai"
127.0.0.1:6379> sismember citys shanghai # 查找是否存在
(integer) 1
127.0.0.1:6379> scard citys # 获取长度
(integer) 3
127.0.0.1:6379> spop citys
"shanghai"
五、zset (有序集合)
类似于 Java SortedSet HashMap 的结合体, 一方面它是个 set ,保证 了内部 value 的唯一性,另方面它可 以给每个 value 赋予一个 score ,代表 这个 value 的排序权重。它的内部实现 用的是一种叫作 “跳跃列表” 的数据结构。
127.0.0.1:6379> zadd math 72 xiaoming
(integer) 1
127.0.0.1:6379> zadd math 90 xiaohong
(integer) 1
127.0.0.1:6379> zadd math 69 xiaowang
(integer) 1
127.0.0.1:6379> zadd math 72 xiaoli
(integer) 1
127.0.0.1:6379> zadd math 62 xiaoli # vlaue 唯一性
(integer) 0
127.0.0.1:6379> zscore math xiaoli
"62"
127.0.0.1:6379> zrange math 0 -1 # 按score升序
1) "xiaoli"
2) "xiaowang"
3) "xiaoming"
4) "xiaohong"
127.0.0.1:6379> zrangebyscore math -inf 70 withscores # 按score降序,-inf代表负无穷大
1) "xiaoli"
2) "62"
3) "xiaowang"
4) "69"
127.0.0.1:6379> zrank math xiaowang # 排名
(integer) 1
127.0.0.1:6379> zrank math xiaohong
(integer) 3
127.0.0.1:6379> zcard math # count
(integer) 3
127.0.0.1:6379> zrangebyscore math 0 70 # 列出区间分值遍历
1) "xiaoli"
2) "xiaowang"
127.0.0.1:6379> zrem math xiaoli # 删除元素
(integer) 1
跳跃列表
- 通常有新元素插入时,要计算插入的位置,要对score进行排序的,如果是 数组,可以使用 二分法 进行排序,链表不行。
- 跳跃列表最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。
- 跳跃列表之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于L0、L1和L2层,可以快速在不同层次之间进行「跳跃」。
- 定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问那新插入的元素如何才有机会「身兼数职」呢?
- 跳跃列表采取一个随机策略来决定新元素可以兼职到第几层,首先L0层肯定是100%了,L1层只有50%的概率,L2层只有25%的概率,L3层只有12.5%的概率,一直随机到最顶层L31层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。