前言

Redis 是用 C 语言开发的一个开源的高性能**键值对(key-value)**内存数据库。
它提供五种数据类型来存储值:字符串类型(String)、散列类型(Hash)、列表类型(List)、集合类型(Set)、有序集合类型(SortedSet),是一种 NoSQL 数据库。

单机版安装

mac os 安装 redis

字符串(String)

Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity ,一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。

redis存集合取集合_redis存集合取集合

赋值取值
> set name lks
OK
> get name
"lks"
//取值并赋值
> getset name lks1
"lks"
> get name
"lks1"

如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错。

incr num
(integer) 1
> decr num
(integer) 0
//增加指定的数
> incrby num 5
(integer) 5
//减少指定的数
> decrby num 4
(integer) 1
散列(Hash)

Hash相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

redis存集合取集合_redis_02


不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

redis存集合取集合_字符串_03


渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的hash结构取而代之。

当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
格式:指令 键 字段 字段值

> hset user username lks
(integer) 1
> hget user username
"lks"
//批量赋值
> hmset user username lks password admin
OK
//批量取值
> hmget user username password
1) "lks"
2) "admin"
//取出所有字段和字段值
> hgetall user
1) "username"
2) "lks"
3) "password"
4) "admin"
列表(List)

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),获取越接近两端的元素速度就越快。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

右边进左边出:队列

rpush names lks0 lks1 lks2
(integer) 3
> lpop names
"lks0"
> lpop names
"lks1"

右边进左边出:栈

rpush names lks0 lks1 lks2
(integer) 3
> rpop names
"lks2"
> rpop names
"lks1"
> rpop names
"lks0"

LRANGE 命令是列表类型最常用的命令之一,获取列表中的某一片段,将返回 start、stop之间的所有元素(包含两端的元素),索引从 0 开始。索引可以是负数,如:“-1”代表最后边的一个元素。

> lrange names 0 -1 //O(n)慎用
1) "lks0"
2) "lks1"
3) "lks2"

如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

redis存集合取集合_字符串_04

集合(set)

set 类型即集合类型,其中的数据是不重复且没有顺序。集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的 Redis 内部是**使用值为空(value为null)**的散列表实现,所有这些操作的时间复杂度都为0(1)。 Redis还提供了多个集合之间的交集、并集、差集的运算。

sadd animal cat dog //添加元素
(integer) 2
> sadd animal cat //添加重复元素,添加失败
(integer) 0
> srem animal cat 删除元素
(integer) 1
> scard animal 获取集合个数
(integer) 1
> sismember animal dog //判断元素是否存在
(integer) 1
> spop animal 1 //弹出一个
1) "dog"
有序集合(zset)

在集合类型的基础上,有序集合类型为集合中的每个元素都关联一个分数,这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前 N 个元素、获取指定分数范围内的元素等与分数有关的操作。

在某些方面有序集合和列表类型有些相似:

二者都是有序的。
二者都可以获得某一范围的元素。
但是,二者有着很大区别:

列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。
有序集合类型使用散列表实现,所有即使读取位于中间部分的数据也很快。
列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现)
有序集合要比列表类型更耗内存。

> zadd scoreSet 90 lks0 80 lks1 70 lks2 //添加元素
(integer) 3
> zadd scoreSet 95 lks0 //元素已经存在则会用新的分数替换原有的分数。
(integer) 0
> zrevrange scoreSet 0 1 withscores //按分数从大到小排序
1) "lks0"
2) "95"
3) "lks1"
4) "80"
> zrange scoreSet 0 1 withscores //按分数从小到大排序
1) "lks2"
2) "70"
3) "lks1"
4) "80"
> zrem scoreSet lks0 //删除元素
(integer) 1
> zscore scoreSet lks1 //获取元素分数
"80"

跳跃列表

zset 内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂。

因为 zset 要支持随机的插入和删除,所以它不好使用数组来表示。我们先看一个普通的链表结构。

redis存集合取集合_字符串_05


我们需要这个链表按照 score 值进行排序。这意味着当有新元素需要插入时,要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到,那该怎么办?

想想一个创业公司,刚开始只有几个人,团队成员之间人人平等,都是联合创始人。随着公司的成长,人数渐渐变多,团队沟通成本随之增加。这时候就会引入组长制,对团队进行划分。每个团队会有一个组长。开会的时候分团队进行,多个组长之间还会有自己的会议安排。公司规模进一步扩展,需要再增加一个层级 —— 部门,每个部门会从组长列表中推选出一个代表来作为部长。部长们之间还会有自己的高层会议安排。

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

想想你老家在世界地图中的位置:亚洲–>中国->安徽省->安庆市->枞阳县->汤沟镇->田间村->xxxx号,也是这样一个类似的结构。

redis存集合取集合_redis_06


「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于 L0、L1 和 L2 层,可以快速在不同层次之间进行「跳跃」。

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问,那新插入的元素如何才有机会「身兼数职」呢?

跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。

首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。

还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。

> set name lks
OK
> expire name 100
(integer) 1
> ttl name
(integer) 94
> set name lks
OK
> ttl name
(integer) -1