1. Redis 简介
Redis 是一个开源的、高性能的、基于键值对的缓存与存储系统,通过提供多种键值数据类型来适应不同场景下的缓存与存储需求。同时 Redis 的诸多高层级功能使其可以胜任消息队列、任务队列等不同角色。
1.1 存储结构
Redis 是 REmote DIctionary Server(远程字典服务器)的缩写,它以字典结构存储数据,并允许其他应用通过 TCP 协议读写字典中的内容。同大多数脚本语言中的字典一样,Redis字典中的键值除了可以是字符串,还可以是其他数据类型。Redis 的基本数据类型如下:
- 字符串类型
- 散列类型
- 列表类型
- 集合类型
- 有序集合类型
在 Redis 字典结构的存储方式和对多种键值数据类型的支持使得开发者可以将程序中的数据直接映射到 Redis 中,数据在 Redis 中的存储形式和其在程序中的存储方式非常相近。使用 Redis 的另一个优势是其对不同的数据类型提供了非常方便的操作方式,如使用集合类型存储文章标签,Redis 可以对标签进行如交集、并集这样的集合运算操作。
1.2 内存存储与持久化
Redis 数据库中的所有数据都存储在内存中。由于内存的读写速度快于硬盘,因此 Redis 在性能上对比其他基于硬盘存储的数据库有明显的优势,在一台普通的笔记本电脑上,Redis 可以一秒内读写超过 10 万个键值。
将数据存储在内存中也有问题,比如程序退出后内存中的数据会丢失。不过 Redis 提供了对持久化的支持,即可以将内存中的数据异步写入到硬盘中,同时不影响继续提供服务。
-
Redis 内部使用一个 redisObject 对象来表示所有的 key 和 value。
-
type :代表一个 value 对象具体是何种数据类型。
-
encoding :是不同数据类型在 redis 内部的存储方式,比如:type=string 代表 value 存储的是一个普通字符串,那么对应的 encoding 可以是 raw 或者是 int,如果是 int 则代表实际 redis 内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。
-
vm 字段:只有打开了 Redis 的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。 Redis 使用 redisObject 来表示所有的 key/value 数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给 Redis 不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用。
1.3 功能丰富
Redis 虽然是作为数据库开发,但由于其提供了丰富的功能,越来越多的人将其用作缓存、队列系统等。
Redis 可以为每个键设置生存时间(Time To Live,TTL),生存时间到期后,键会自动被删除。这一功能配合出色的性能让 Redis 可以作为缓存系统来使用,而且 Redis 支持持久化和丰富的数据类型。
1.4 Redis 优势
-
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
-
丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
-
原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
-
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
1.5 Redis 为什么这么快?
-
纯内存操作:读取不需要进行磁盘 I/O,所以比传统数据库要快上不少;
-
单线程,无锁竞争:这保证了没有线程的上下文切换,不会因为多线程的一些操作而降低性能;
-
多路 I/O 复用模型,非阻塞 I/O:采用多路 I/O 复用技术可以让单个线程高效的处理多个网络连接请求(尽量减少网络 IO 的时间消耗);
-
高效的数据结构,加上底层做了大量优化:Redis 对于底层的数据结构和内存占用做了大量的优化,例如不同长度的字符串使用不同的结构体表示,HyperLogLog 的密集型存储结构等等;
2. Redis 五大数据类型
2.1 字符串类型
2.1.1 介绍
字符串类型是 Redis 中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。可以用其存储用户的邮箱、JSON 化的对象甚至是一张图片。一个字符串类型键允许存储的数据的最大容量是 512MB。
字符串类型是其他 4 种数据类型的基础,其他数据类型和字符串类型的差别从某种角度来说只是组织字符串的形式不同。例如,列表类型是以列表的形式组织字符串,而集合类型是以集合的形式组织字符串。
2.1.2 基本用法
(1)赋值与取值
SET key value
GET key
SET 和 GET 是 Redis 中最简单的两个命令,它们实现的功能和编程语言中的读写变量相似,如 key = "hello" 在 Redis 中是这样表示的:
redis> SET key hello
OK
redis> GET key
"hello"
在取值时,若键不存在则返回空结果。
(2)递增数字
INCR key
前面说过字符串类型可以存储任何形式的字符串,当存储的字符串是整数形式时,Redis 提供了一个实用的命令 INCR ,其作用是让当前键值递增,并返回递增后的值,用法为:
redis> INCR num
(integer) 1
redis> INCR num
(integer) 2
当要操作的键不存在时会默认键值为 0,所以第一次递增后的结果是 1,当键值不是整数时 Redis 会提示错误:
redis> SET foo lorem
OK
redis> INCR foo
(error) ERR value is not an integer or out of range
如果 Redis 同时只连接了一个客户端,那么上面的代码没有任何问题。可当同一时间有多个客户端连接到 Redis 时则有可能出现 竞态条件(race condition)。例如有两个客户端 A 和 B 都要执行我们自己设计的 incr 函数(自增函数)并准备将同一个键的键值递增,当它们恰好同时执行到取值代码行时,它们取到了同样的值 “5”,也同时递增至 “6”,但这不是我们预想的 “7”。
但实际在 Redis 中,包括 INCR 在内的所有 Redis 命令都是 原子操作(atomic operation),无论多少个客户端同时连接,都不会出现上述情况。
竞态条件:是指一个系统或者进程的输出,依赖于不受控制的事件的出现顺序或者出现时机。
原子操作:即不可拆分的意思,不会在执行过程中被其他命令插入打断。
更多命令详见Redis字符串操作命令.菜鸟教程
2.1.3 使用场景
常规key-value缓存应用。常规计数: 微博数, 粉丝数。
2.2 散列类型
2.2.1 介绍
我们现在知道 Redis 是采用字典结构以键值对的形式存储数据的,而散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他数据类型,换句话说,散列类型不能嵌套其他的数据类型,一个散列类型键可以包含至多 2^32 -1 个字段。
提示:除了散列类型,Redis 的其他数据类型同样不支持数据类型嵌套。比如集合类型的每个元素都只能是字符串,不能是另一个集合或散列表等。
散列类型适合存储对象,适用对象类别和 ID 构成键名,使用字段表示对象的属性,而字段值则存储属性值。例如要存储 ID 为 2 的汽车对象,可以分别使用名为 color、name 和 price 的 3 个字段来存储该辆汽车的颜色、名称和价格。如下图所示:
2.2.2 命令
(1)赋值与取值
HSET key field value
HGET key field
HSET 命令用于给字段赋值, HGET 用于获得字段的值。
HSET 命令的方便之处在于不区分插入和更新操作,这意味着修改数据时不用事先判断字段是否存在来决定要执行的是插入操作(insert)还是更新操作(update)。当执行的是插入操作时(即之前字段不存在)HSET 命令会返回 1,当执行的是更新操作时(即之前字段已经存在)HSET 命令会返回 0。更进一步,当键本身不存在时,HSET 命令还会自动建立它。
提示:在 Redis 中每个键都属于一个明确的数据类型,如通过 HSET 命令建立的键是散列类型,通过 SET 命令建立的键是字符串类型等等。使用一种数据类型的命令操作另一种数据类型的键会提示错误:“ERR Operation against a key holding the wrong kind of value”。
当需要同时设置多个字段的值时,可以使用 HMSET 命令。例如,下面这条语句
HMSET key field1 value1 field2 value2
相应地,HMGET 命令可以同时获得多个字段的值:
redis> HMGET car price name
1) "500"
2) "BMW"
如果想获取键中所有字段和字段值时,可以使用 HGETALL 命令:
redis> HGETALL car
1) "price"
2) "500"
3) "name"
4) "BMW"
更多命令详见Redis哈希表操作命令.菜鸟教程
2.2.3 应用场景
存储部分变更数据,如用户信息等。
2.3 列表类型
2.3.1 介绍
列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两端添加元素的时间复杂度为 O(1),获取越接近两端的元素速度越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的 10 条记录也是极快的。
这种特性使列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,我们关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的 100 条数据也是极快的。同样因为在两端插入记录的时间复杂度是 O(1),列表类型也适合用来记录日志,可以保证加入新日志的速度不会受到已有日志数量的影响。
借助列表类型,Redis 还可以作为队列使用。
2.3.2 命令
(1)向列表两端增加元素
LPUSH key value [value ...]
RPUSH key value [value ...]
LPUSH 命令用来向列表左边增加元素,返回值表示增加元素后列表的长度。
redis> LPUSH numbers 1
(integer) 1
(2)从列表两端弹出元素
LPOP key
RPOP key
更多命令详见Redis列表操作命令.菜鸟教程
2.4 集合类型
2.4.1 介绍
在集合中每个元素的类型都是不同的,且没有顺序。一个集合类型(set)键可以存储2^32 -1个字符串。
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在 Redis 内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1)。最方便的是多个集合类型键之间还可以进行交集、并集和差集运算。
2.4.2 命令
(1)增加/删除元素
SADD key member [member ...]
SREM key member [member ...]
SADD 命令用来向集合中增加一个或多个元素,如果键不存在则会自动创建。因为在一个集合中不能有相同的元素,所以如果要加入的元素已经存在于集合中就会忽略这个元素本身。本命令的返回值是成功加入的元素数量。例如:
redis> SADD letters a
(integer) 1
redis> SADD letters a b c
(integer) 2
SREM 命令用来从集合中删除一个或多个元素,并返回删除成功的个数,例如:
redis> SREM letters c d
(integer) 1
(2)获得集合中的所有元素
SMEMBERS key
SMEMBERS 命令会返回集合中的所有元素,例如:
redis> SMEMBERS letters
1) "b"
2) "a"
(3)集合间运算
SDIFF key [key ...]
SINTER key [key ...]
SUNION key [key ...]
更多命令详见Redis集合操作命令.菜鸟教程
2.5 有序集合类型
2.5.1 介绍
在集合类型的基础上有序集合为集合中的每个元素都关联了一个分数,这时的我们不仅可以完成插入、删除和判断元素是否存在等集合类型支持的操作,还能够获得分数最高(或最低)的前 N 个元素、获得指定分数范围内的元素等与分数有关的操作。虽然集合中每个元素都是不同的,但是他们的分数却可以相同。
有序集合类型在某些方面与列表类型有些相似。
(1)二者都是有序的。
(2)二者都可以获得某一范围的元素。
但是二者有着很大的区别,这使得它们的应用场景也是不同的。
(1)列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会比较慢,所以它更加适合实现如 “新鲜事” 或 “日志” 这样很少访问中间元素的应用。
(2)有序集合类型是使用散列表和跳跃表(Skip list)实现的,所以即使读取位于中间部分的数据速度也是超快的(时间复杂度是 O(log(N)))。
(3)列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)。
(4)有序集合要比列表类型更耗费内存。
2.5.2 命令
(1)增加元素
ZADD key score member [score member ...]
ZADD 命令用来向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。ZADD 命令的返回值是新加入到集合中的元素个数(不包含之前已经存在的元素)。
更多命令详见Redis有序集合操作命令.菜鸟教程