一、安装Redis
体验 Redis 需要使用 Linux 或者 Mac 环境,如果是 Windows 可以考虑使用虚拟机。主要方式有四种:
- 使用 Docker 安装。
- 通过 Github 源码编译。
- 直接安装 apt-get install(Ubuntu)、yum install(RedHat) 或者 brew install(Mac)。
- 如果读者懒于安装操作,也可以使用网页版的 Web Redis 直接体验。
1.1 Docker 方式
# 拉取 redis 镜像
> docker pull redis
# 运行 redis 容器
> docker run --name myredis -d -p6379:6379 redis
# 执行容器中的 redis-cli,可以直接使用命令行操作 redis
> docker exec -it myredis redis-cli
1.2 Github 源码编译方式
# 下载源码
> git clone --branch 2.8 --depth 1 [email protected]:antirez/redis.git
> cd redis
# 编译
> make
> cd src
# 运行服务器,daemonize表示在后台运行
> ./redis-server --daemonize yes
# 运行命令行
> ./redis-cli
1.3 直接安装方式
# mac
> brew install redis
# ubuntu
> apt-get install redis
# redhat
> yum install redis
# 运行客户端
> redis-cli
或参考菜鸟教程:Redis 安装
二、Redis简介
Remote Dictionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String)、 哈希(Hash),、列表(list)、 集合(sets) 和 有序集合(sorted sets)等类型。
Redis 是完全开源免费的,是一个高性能的key-value数据库。Redis 与其他 key - value 缓存产品有以下三个特点:
1、Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2、Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3、Redis支持数据的备份,即master-slave模式的数据备份。
三、Redis 优势
1、性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
2、丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
3、原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
4、丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
四、Redis与其他key-value存储有什么不同?
1、Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
2、Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
五、Redis 数据类型
Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及zset(sorted set:有序集合)。
5.1 String(字符串)
String 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。String 类型是二进制安全的。意思是 Redis 的 String 可以包含任何数据。比如jpg图片或者序列化的对象。String 类型是 Redis 最基本的数据类型,String 类型的值最大能存储 512MB。
5.1.1键值对
实例
127.0.0.1:6379> set name "AlanChen"
OK
127.0.0.1:6379> get name
"AlanChen"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> get name
(nil)
在以上实例中我们使用了 Redis 的 set 和 get 命令。键为 name,对应的值为 AlanChen。
字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。我们将用户信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程。
Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
5.1.2批量键值对
可以批量对多个字符串进行读写,节省网络耗时开销。
实例
127.0.0.1:6379> mset name1 AlanChen name2 Kyra
OK
127.0.0.1:6379> mget name1 name2
1) "AlanChen"
2) "Kyra"
5.1.3过期和 set 命令扩展
可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。不过这个「自动删除」的机制是比较复杂的。
实例
127.0.0.1:6379> set name "AlanChen"
OK
127.0.0.1:6379> get name
"AlanChen"
127.0.0.1:6379> expire name 5 # 5s 后过期
(integer) 1
... #wait for 5s
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> setex name 5 "AlanChen" #5s 后过期,等价于 set+expire
OK
127.0.0.1:6379> get name
"AlanChen"
... #wait for 5s
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> setnx name "AlanChen" #如果 name 不存在就执行 set 创建
(integer) 1
127.0.0.1:6379> get name
"AlanChen"
127.0.0.1:6379> setnx name "AC" #因为 name 已经存在,所以 set 创建不成功
(integer) 0
127.0.0.1:6379> get name #没有改变
"AlanChen"
5.1.4 计数
如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错。
实例
127.0.0.1:6379> set age 30
OK
127.0.0.1:6379> incr age
(integer) 31
127.0.0.1:6379> incrby age 5
(integer) 36
127.0.0.1:6379> incrby age -5
(integer) 31
5.2 Hash(哈希)
Redis Hash 是一个键值(key=>value)对集合。Redis Hash 是一个 String 类型的 field 和 value 的映射表,Hash 特别适合用于存储对象。
实例
127.0.0.1:6379> hmset Student name "AlanChen" age 18
OK
127.0.0.1:6379> hgetall Student
1) "name"
2) "AlanChen"
3) "age"
4) "18"
127.0.0.1:6379> hget Student name
"AlanChen"
127.0.0.1:6379> hlen Student
(integer) 2
实例中我们使用了 Redis hmset, hget 命令,hmset 设置了三个 field=>value 对, hget 获取对应 field 对应的 value。每个 Hash 可以存储 232 -1 键值对(40多亿)。
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的hash结构取而代之。当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。
hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。
5.3 List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
5.3.1 基本操作
实例
127.0.0.1:6379> rpush nameList AlanChen
(integer) 1
127.0.0.1:6379> rpush nameList Kyra
(integer) 2
127.0.0.1:6379> rpush nameList AC
(integer) 3
127.0.0.1:6379> lrange nameList 0 1
1) "AlanChen"
2) "Kyra"
列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
5.3.2 先进先出:队列
127.0.0.1:6379> rpush userNames AlanChen Kyra AC
(integer) 3
127.0.0.1:6379> llen userNames
(integer) 3
127.0.0.1:6379> lpop userNames
"AlanChen"
127.0.0.1:6379> lpop userNames
"Kyra"
127.0.0.1:6379> lpop userNames
"AC"
127.0.0.1:6379> lpop userNames
(nil)
5.3.3 先进后出:栈
127.0.0.1:6379> del userNames
(integer) 0
127.0.0.1:6379> rpush userNames AlanChen Kyra AC
(integer) 3
127.0.0.1:6379> rpop userNames
"AC"
127.0.0.1:6379> rpop userNames
"Kyra"
127.0.0.1:6379> rpop userNames
"AlanChen"
127.0.0.1:6379> rpop userNames
(nil)
5.3.4 lindex 、 trim
lindex 相当于 Java 链表的get(int index)方法,它需要对链表进行遍历,性能随着参数index增大而变差。
ltrim 跟的两个参数start_index和end_index定义了一个区间,在这个区间内的值,ltrim 要保留,区间之外统统砍掉。我们可以通过ltrim来实现一个定长的链表,这一点非常有用。
index 可以为负数,index=-1表示倒数第一个元素,同样index=-2表示倒数第二个元素。
> rpush books python java golang
(integer) 3
> lindex books 1 # O(n) 慎用
"java"
> lrange books 0 -1 # 获取所有元素,O(n) 慎用
1) "python"
2) "java"
3) "golang"
> ltrim books 1 -1 # O(n) 慎用
OK
> lrange books 0 -1
1) "java"
2) "golang"
> ltrim books 1 0 # 这其实是清空了整个列表,因为区间范围长度为负
OK
> llen books
(integer) 0
5.3.5 linked list & quickest
如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
5.4 Set(集合)
Redis 的 Set 是 String 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
Set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。
实例
127.0.0.1:6379> sadd nameSet AlanChen
(integer) 1
127.0.0.1:6379> sadd nameSet AlanChen #重复
(integer) 0
127.0.0.1:6379> sadd nameSet AC Kyra
(integer) 2
127.0.0.1:6379> members nameSet # 注意顺序,和插入的并不一致,因为 set 是无序的
1) "AC"
2) "AlanChen"
3) "Kyra"
127.0.0.1:6379> sismember nameSet AC # 查询某个 value 是否存在,相当于 contains(o)
(integer) 1
127.0.0.1:6379> sismember nameSet Tom
(integer) 0
127.0.0.1:6379> scare nameSet # 获取长度相当于 count()
(integer) 3
127.0.0.1:6379> stop nameSet # 弹出一个
"Kyra"
5.5 zset(sorted set:有序集合)
Redis zset 和 Set 一样也是String类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。
zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。
zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。
实例
> zadd books 9.0 "think in java"
(integer) 1
> zadd books 8.9 "java concurrency"
(integer) 1
> zadd books 8.6 "java cookbook"
(integer) 1
> zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围
1) "java cookbook"
2) "java concurrency"
3) "think in java"
> zrevrange books 0 -1 # 按 score 逆序列出,参数区间为排名范围
1) "think in java"
2) "java concurrency"
3) "java cookbook"
> zcard books # 相当于 count()
(integer) 3
> zscore books "java concurrency" # 获取指定 value 的 score
"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
> zrank books "java concurrency" # 排名
(integer) 1
> zrangebyscore books 0 8.91 # 根据分值区间遍历 zset
1) "java cookbook"
2) "java concurrency"
> zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
> zrem books "java concurrency" # 删除 value
(integer) 1
> zrange books 0 -1
1) "java cookbook"
2) "think in java"
六、容器型数据结构的通用规则
list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
1、create if not exists
如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。
2、drop if no elements
如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。
七、过期时间
Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。
还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。
127.0.0.1:6379> set name alanchen
OK
127.0.0.1:6379> tel name #查看过期的剩余时间 -1 表示不过期
(integer) -1
127.0.0.1:6379> expire name 20 # 设置过期时间为20s
(integer) 1
127.0.0.1:6379> tel name #查看过期的剩余时间
(integer) 16
127.0.0.1:6379> set name AC # 重新设置name
OK
127.0.0.1:6379> tel name # name的过期时间消失
(integer) -1
八、各个数据类型应用场景
类型 | 简介 | 特性 | 场景 |
---|---|---|---|
String(字符串) | 二进制安全 | 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M | - |
Hash(字典) | 键值对集合,即编程语言中的Map类型 | 适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去) | 存储、读取、修改用户属性 |
List(列表) | 链表(双向链表) | 增删快,提供了操作某一段元素的API | 1、最新消息排行等功能(比如朋友圈的时间线) 2、消息队列 |
Set(集合) | 哈希表实现,元素不重复 | 1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作 | 1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐 |
Sorted Set(有序集合) | 将Set中的元素增加一个权重参数score,元素按score有序排列 | 数据插入集合时,已经进行天然排序 | 1、排行榜 2、带权重的消息队列 |
九、Redis & 多个数据库
Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。
Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。
每个数据库对外都是一个从0开始的递增数字命名,Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库,如要选择1号数据库:
redis> SELECT 1
OK
redis [1] > GET foo
(nil)
然而这些以数字命名的数据库又与我们理解的数据库有所区别。首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据。另外Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL命令可以清空一个Redis实例中所有数据库中的数据。
综上所述,这些数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。
十、Redis 发布订阅
Redis 发布订阅
十一、Redis 事务
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
1、批量操作在发送 EXEC 命令前被放入队列缓存。
2、收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
3、在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:开始事务、命令入队、执行事务。
实例
以下是一个事务的例子, 它先以 multi 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user-name "AlanChen"
QUEUED
127.0.0.1:6379> get user-name
QUEUED
127.0.0.1:6379> hmset User name "AC" age 10
QUEUED
127.0.0.1:6379> sadd names "Tom"
QUEUED
127.0.0.1:6379> sadd names "Kyra"
QUEUED
127.0.0.1:6379> exec
1) OK
2) "AlanChen"
3) OK
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> get user-name
"AlanChen"
127.0.0.1:6379> smembers names
1) "Tom"
2) "Kyra"