1.1 导言
如果你从来没使用过 Redis 数据库,那你肯定会问,为什么我们要学 Redis数据库,我只使用 MySQL 或 Oracle 就够了。其实 Redis 虽叫数据库,可又不是传统意义上的关系型数据库,Redis 是一个高性能的 Key-Value 数据库。
首先我们先来讲一下 Redis 的历史。Redis 其实是作者 Salvatore Sanfilippo 为了解决实际问题而创造出来的。当时作者 Salvatore 有这么一个需求,就是多个网站不断向服务器发送页面,而服务器需要为每个网站保存一定数量的最新页面记录,同时通过网页将数据实时给用户看到。但是无论 Salvatore 如何优化,都很难在关系数据库里让小虚拟机处理大负荷的负载。最终他打算自己写一个内存数据库,能对列表的两端执行常数时间复杂度的弹出和推入操作,并加上子进程的持久化操作,于是 Redis 就诞生了。
到了今天,Redis 已经进入了成熟期。数以千计的开发者都在开发和使用这个数据库,Redis 拥有非常完善的文档。我记得第一次使用 Redis,是为了在保存有数十百万用户的关系数据库里对某个条件进行查询。大家知道,要想在几百万用户中找到某条数据,是很难通过关系数据库在十几秒查询到的。于是我选择了 Redis,在不断优化后每次操作可以控制在 1 秒钟甚至更短,带给我相当大的震撼。
本教程不但教给你一些基本的使用,同时也会根据我多年总结的技巧解决日常生产环境上优化和排错的问题。特别是后期的数据库优化和集群的讲解,希望对各位进行 Redis 开发有一定帮助。
1.2 认识 Redis
在 Redis 之前,很多互联网公司会使用 MySQL + Memcached 架构,这个架构虽然适合于海量数据存储,但随着业务的增加,会出现很多问题,例如,MySQL 数据库经常拆表,导致 Memcached 也不断扩容;同步问题;命中率低,导致直接穿透 Memcached 进入 DB 查询,DB 资源池是有限的,进而宕机。这些问题都会导致Memcached其实并不好用。
Redis 就在这种时代背景中产生,你会发现 Memcached 遇到的问题都被 Redis 给解决了。如果你用过 Memcached,你就会感受到 Redis 绝对不是简单的 Key-Value 数据,还有 List、Set、哈希等各种数据类型的存储,同时支持冷热备份和主从复制,不但解决了数据库的容错,还能轻易地将数据分布到多个 Redis 实例中。
1. Redis 特性
那么 Redis 有哪些具体特性呢?大致可分为如下八大特性。
速度极快。官方给出的数据是 10 万次 ops 的读写,这主要归功于这些数据都存在于内存中。由于 Redis 是开源的,当你打开源代码,就会发现 Redis 都是用 C 语言写的,C 语言是最接近计算机语言的代码,而且只有区区 5 万行,保证了 Redis 的速度。同时一个 Redis 只是一个单线程,其真正的原因还是因为单线程在内存中是效率最高的。
持久化。Redis 的持久化可以保证将内存中的数据每隔一段时间就保存于磁盘中,重启的时候会再次加载到内存。持久化方式是 RDB 和 AOF。
支持多种数据结构。分别支持哈希、集合、BitMaps,还有位图(多用于活跃用户数等统计)、HyperLogLog(超小内存唯一值计数,由于只有 12K,是有一定误差范围的)、GEO(地理信息定位)。
支持多种编程语言。支持 Java、PHP、Python、Ruby、Lua、Node.js。
功能丰富。如发布订阅、Lua 脚本、事务、Pipeline(管道,即当指令到达一定数量后,客户端才会执行)。
简单。不依赖外部库、单线程、只有 23000 行 Code。
主从复制。主节点的数据做副本,这是做高可用的基石。
高可用和分布式。Redis-Sentinel(v2.8)支持高可用,Redis-Cluster(v3.0)支持分布式。
2. Redis 场景
Redis 最大的作用是增加你原来的访问性能问题,试想如果项目已经搭建好,这个项目一般是不太可能更换的。但是 Redis 独特的存在是只需要增加一层,把常用的数据存放在 Redis 即可。你在开发环境中使用 Redis 功能,但却不需要转到 Redis。
无论是什么架构,你都可以将 Redis 融入项目中来,这可以解决很多关系数据库无法解决的问题。比如,现有数据库处理缓慢的任务,或者在原有的基础上开发新的功能,都可以使用 Redis。接下来,我们一起看看 Redis 的典型使用场景。
- 缓存系统
这是 Redis 使用最多的场景。Redis 能够替代 Memcached,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据。
毫无疑问,Redis 缓存使用的方式与 Memcached 相同。网络中总是能够看到这个技术更新换代,Redis 的原生命令,尽管简单却功能强大,把它们加以组合,能完成的功能是无法想象的。当然,你可以专门编写代码来完成所有这些操作,但 Redis 实现起来显然更为轻松。
- 计数器
如转发数、评论数,有了原子递增(Atomic Increment),你可以放心的加上各种计数,用 GETSET 重置,或者是让它们过期。目前新浪是号称史上最大的 Redis 集群。
比如,你想计算出最近用户在页面间停顿不超过 30 秒的页面浏览量,当计数达到比如 10 时,就可以显示提示。再比如,如果想知道什么时候*一个 IP 地址,INCRBY 命令让这些变得很容易,通过原子递增保持计数;GETSET 用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
- 消息队列系统
虽然 Kafka 更强,但是简单的可以使用 Redis。运行稳定并且快速,支持模式匹配,能够实时订阅与取消频道。
Redis 还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。你也可以做些更有趣的事情,比如一个旋转更新的 RSS Feed 队列。
- 排行榜及相关问题
排行榜实际就是一种有序集合。对于 Redis 来说,如果你要在几百万个用户中找到排名,其他数据库查询是非常慢的,因为每过几分钟,就会有几百万个不同的数据产生变化,但是 Redis 却可以轻松解决。
排行榜(Leader Board)按照得分进行排序。ZADD 命令可以直接实现这个功能,而 ZREVRANGE 命令可以用来按照得分获取前 100 名的用户,ZRANK 可以用来获取用户排名,非常直接而且操作容易。
- 社交网络
Redis 可以非常好地与社交网络相结合,如新浪微博、Twiter 等,比如 QQ 和用户交互的时候,用户和状态消息将会聚焦很多有用的信息,很多交互如实时聊天就是通过 Redis 来实现的。
- 按照用户投票和时间排序
Reddit 的排行榜,得分会随着时间变化。LPUSH 和 LTRIM 命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD 命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
- 过期项目处理
通过 Unix 时间作为关键字,用来保持列表能够按时间排序。对 current_time 和 time_to_live 进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用 ZRANGE...WITHSCORES 进行查询,删除过期的条目。
- 实时系统
使用位图来做布隆过滤器,例如实现垃圾邮件过滤系统的开发变得非常容易。
综上所述, Redis 的应用是非常广泛的,而且在实际使用中是非常有价值的。你可以让网站向 100 万用户推荐新闻、可以实时显示最新的项目列表、在游戏中实时获得排名、获得全球排名,等等。Redis 的出现,解决了传统关系数据库的短板,让开发变得更加简单和高效,大大提高了开发效率,也在用户体验上获得更加实时的体验。随着 Redis 的使用越来越广泛,将会有更多的开发者加入 Redis 的使用和开发上来。
1.3 小结
最后我们回顾下本文所讲述的内容。
首先,介绍了 Redis 主要是用于缓存系统的,不同于一般关系数据库。
其次,我们介绍了 Redis 的八大特性。通过这八大特性,我们可以把经常变化的数据放在 Redis 数据库中,并设置过期时间,到达时间 Redis 就会自动删除;还可以缓解服务器压力,如我们日常发微博,先会保存在 Redis 数据库中,然后等数据库压力比较小的时候保存进关系数据库中。
2.1 内部实现
接下来,我们简单了解下 Redis 的内部实现。Redis 内部会封装一个 redisObject 实例。由这个 redisObject 来表示所有的 key 和 value 。redisObject 所包含的字段中,最主要的是 type 和 encoding。
type 代表一个 value 对象具体是何种数据类型,包括 String 、Hash 、List 、Set 和 Sorted set 等数据类型。
encoding 则表示不同数据类型在 Redis 内部的存储方式,包括 Raw 、Int 、Ziplist 、LinkedList 、HashMap 和 Intset 存储方式。
上面说得比较抽象,为了帮助大家理解,我举个例子,比如 type 为 String 代表的是 value 存储了一个字符串,那么对应的 encoding 可以是 Int 或者 Raw 。如果是 Int 则代表实际 Redis 内部是按数值类型存储的,比如你要存储 “1234” 、“2018” 等字符串。
还有一个特别的内部字段,vm 字段。这个字段默认是关闭的,只有打开了 Redis 的虚拟内存功能,才能真正分配内存。有同学要问了,这个字段有什么用呢?因为 Redis 是 Key/Value 存储,是非常浪费内存的,这些内存成本主要是为了给 Redis 上面的 7 种数据类型提供统一管理接口。
2.2 单线程
我们再来看下为什么 Redis 中单线程快。很多程序员应该深有体会,其实其他很多语言单线程是非常慢的,但是为什么 Redis 的单线程快呢?
我觉得最大的原因是纯内存存储。正因为这个是主要原因,所以后面两个原因显得有点不太重要,即非阻塞 IO 和避免线程切换和竞态消耗。
你要清楚,首先 Redis 一次只运行一条命令。其次我们应该减少长命令,哪些是长命令呢?如 KEYS 、 FLUSHALL 、FLUSHDB 、Slow Lua Script 、MULTI/EXEC 、Operate Big Value( Collection )。最后说明一点,其实 Redis 不只是单线程,如果你去读源码,你就会发现有些指令绝不是单线程能够做的。如 Fysnc File Descriptor 、Close File Descriptor 等。
2.3 7 种数据类型的使用
1. 字符串 String
Redis 的 key 没什么好说,值得一提的就是 value 的五种数据类型。分别是字符串类型、数字、二进制、和 JSON 类型的数据。
那我们在实际生产环境哪些场景中使用它们呢?如缓存、计数器( 每次加 1 的计数 )、分布式锁等场景都能看到。
接着我将列出与该数据类型相关的命令及使用说明。
GET 、SET 和 DEL 。这是 Redis 最简单的命令,如果你要得到一个 value 的值,只需要 GET key ,就 ok 了。如果你要设置某个 key 的值,那就 SET key value ,搞定。
INCR 、DECR 、INCRBY 和 DECRBY 。
INCR key :就是 key 自增 1,不存在,自增后 get(key)=1 ;
DECR key :就是 key 自减 1,不存在,自减后返回 -1 ;
INCRBY key k :自增 k ,不存在,则返回 k ;
-
DECRBY key k :自减 k 。
实际使用:如果你想访问某个主页的访问量,那可以用 INCR id:pageview 。
我们实际开发中,常常会使用下面的伪代码。
public VideoInfo get(long id){
String redisKey = redisPrefix + id;
VideoInfo videoInfo = redis.get(redisKey);
if(videoInfo == null){
videoInfo = mysql.get(id);
if(videoInfo != null){
// 序列化
redis.set(redisKey, serialize(videoInfo));
}
}
return videoInfo;
}
SET 、SETNX 和 SET xx 。
SET key value :不管 key 是否存在,都设置;
SETNX key value :key 不存在,才设置( 相当于 add );
-
SET key value xx :key 存在,才设置( 相当于 update )。
实际操作,见如下代码。
exists php --> 0
set php good -->OK
setnx php bad -->0
set php best xx -->ok
exists lua --> 0
set lua hehe xx -->(nil)
MGET 、MSET 。
MGET key1 key2 key3 …:批量获取 key,原子操作;
-
MSET key1 val2 key2 val2 key3 val3:批量设置 key-value。
实际开发的过程中,我们通常使用 MGET,因为 MGET 只有 1 次网络时间和 n 次命令时间。但是如果你使用 GET 的话,就是 n 次网络时间和 n 次命令时间。
使用 MGET 效率更高。
GETSET 、APPEND 和 STRLEN。
GETSET key newvalue:set key newvalue 并返回旧的 value,旧的 value 会被删除;
APPEND key value :将 value 追加到旧的 value 上;
STRLEN key :返回字符串的长度( 注意中文)。
INCRBYFLOAT 、GETRANGE 和 SETRANGE。
INCRBYFLOAT key 3.5:在 key 上追加对应的值 3.5;
GETRANGE key start end :获取字符串指定下标所有的值;
SETRANGE key index value :设置指定下标所有对应的值。
2. 哈希 Hash
说到 Hash,就要说到为什么我们要使用 Hash。我们在使用字符串的数据类型的时候,如果我们存储的是个对象,比如某个图书馆的会员,里面存储着会员的姓名、年龄、身份证信息、地址、借阅书籍、借阅时间……一系列的属性。
如果我们用 String 来存储的话,那我们就需要每次都序列化这个字符串,每次只要一修改某个属性,我们就要把一串属性都覆盖一遍。这样是不是非常麻烦?
这个时候,哈希就应运而生了。Hash 相当于 value 是一个 Map ,里面的所有属性我们都可以单独新增、修改或者删除,而不需要像字符串那样全部覆盖操作。
常用的命令有 HGET 、HSET 和 HGETALL。
- HGET key field:获取存储在哈希表中指定字段的值;
- HSET key field value:将哈希表 key 中的字段 field 的值设为 value ;
- HGETALL key:获取在哈希表中指定 key 的所有字段和值,生产环境不常用。
3. 列表 List
List 是一种简单的字符串的集合,是有顺序的。在实际生产环境中,我们时常会使用它。比如当我们需要获取某个数据的列表(例如粉丝列表)。
由于 Redis 的 List 是链表结构,我们可以非常轻松地实现消息排行等功能,还能用于消息队列等功。
常用的命令有 LPUSH 、RPUSH 、LPOP 、RPOP 和 LRANGE。
- LPUSH key value1 [value2] :将一个或多个值插入到列表头部;
- RPUSH key value1 [value2] :在列表中添加一个或多个值;
- LPOP key :移出并获取列表的第一个元素;
- RPOP key :移除并获取列表最后一个元素;
- LRANGE key start stop :获取列表指定范围内的元素。
4. 集合 Set
Set 和 List 最大的不同是无序,Set 是没有顺序的。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
常用命令有 SADD 、SCARD 、SNENVERS 和 SPOP。
- SADD key member1:向集合添加一个或多个成员;
- SCARD key:获取集合的成员数;
- SMEMBERS key:返回集合中的所有成员;
- SPOP key:移除并返回集合中的一个随机元素。
5. Sorted Set 有序集合
Sorted Set 和 Set 最大的不同是前者是自动排序的,而后者是无序的。如果你需要一个有序的,但是不重复的数据结构,就可以使用 Sorted Set 。
常用的命令有 ZADD 、ZRANGE 、ZREM 和 ZCARD。
- ZADD key score1 member1:向有序集合添加一个或多个成员,或者更新已存在成员的分数;
- ZRANGE key start stop:通过索引区间返回有序集合成指定区间内的成员;
- ZREM key member:移除有序集合中的一个或多个成员;
- ZCARD key:获取有序集合的成员数。
6. Pub/Sub 发布订阅
即发布(Publish)与订阅(Subscribe)。在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当 key 的值进行了消息发布后,所有订阅它的客户端都会收到相应的消息,这类似于 QQ 和微信。
常用的命令有 PSUBSCRIBE 、PUBSUB 、PUBLISH 和 SUBSCRIBE。
- PSUBSCRIBE pattern:订阅一个或多个符合给定模式的频道;
- PUBSUB subcommand :查看订阅与发布系统状态;
- PUBLISH channel message:将信息发送到指定的频道;
- SUBSCRIBE channel:订阅给定的一个或多个频道的信息;
- UNSUBSCRIBE [channel [channel …]]:指退订给定的频道。
7. Transactions 事务
我们一般认为 NoSQL 数据库都没有事务,恐怕要让你失望了。Redis 就支持事务,但并不是我们一般意义上的事务,如果你执行 exec 命令,途中断电或者服务器挂掉了,我们还是会发现 Redis 里一部分插入了,一部分未插入。
不过 Redis 提供了 WATCH 命令,我们可以对某个 key 来 watch 一下,然后再执行 Transactions 。如果这个被 Watch 的值进行了修改,那么这个 Transactions 会发现并拒绝执行。
redis 127.0.0.1:6381> MULTI
OK
redis 127.0.0.1:6381> SET book-name "JAVA Programming Mastering Series"
QUEUED
redis 127.0.0.1:6381> GET book-name
QUEUED
redis 127.0.0.1:6381> SADD tag "java" "Programming" "Mastering Series"
QUEUED
redis 127.0.0.1:6381> SMEMBERS tag
QUEUED
redis 127.0.0.1:6381> EXEC
1) OK
2) "JAVA Programming Mastering Series"
3) (integer) 3
4) 1) "java"
2) "Programming"
3) "Mastering Series"
常用命令有 MULTI 、EXEC 和 DISCARD。
- MULTI:标记一个事务块的开始;
- EXEC:执行所有事务块内的命令;
- DISCARD:取消事务,放弃执行事务块内的所有命令;
- UNWATCH:取消 WATCH 命令对所有 key 的监视;
- WATCH key:监视 key,如果在事务执行之前 key 被其他命令所改动,那么事务将被打断。
Redis 作为一个数据库,很多开发者还可以单独使用它。事实上,更多时候 Redis 是在数据库和代码中间作为一个中间件使用,如果你发现目前的数据库出现瓶颈,那么就可以通过 Redis 来优化。
4.1 慢查询
1. 生命周期
慢查询的生命周期,可参见下图。首先客户端发送命令给 Redis,Redis 需要对慢查询排队处理,这里需要说明的是慢查询发生在第三阶段,也就是下图的“3,执行命令”这一阶段。同时客户端超时不一定是慢查询,但慢查询却是客户端超时的一个原因。最后,执行完后返回。
请大家认识好这个模型,在实际开发中,我们需要对异常进行判断,这时,该流程就显得非常重要。
2. 如何配置慢查询
所谓慢查询指的是内部执行时间超过某个指定时限的查询,而控制该指定时限的就是 Redis 配置文件中的配置项 slowlog-log-slower-than。除 slowlog-log-slower-than 外,在配置文件中还有另外一个参数与慢查询日志有关,那就是 slowlog-max-len,该配置项控制了 Redis 系统最多能够维护多少条慢查询。下面让我们一个一个地讲解。
- slowlog-max-len:服务器使用先进先出的方式保存多条慢查询日志。
当服务器储存的慢查询日志数量等于 slowlog-max-len 选项的值时, 服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除。
什么意思呢?首先 Redis 会配置一个 slowlog-log-slower-than = 10000,slowlog-max-len = 100,就是说 10000 微秒(1 秒等于 1,000,000 微秒)后是慢查询,然后就把它放到队列(内存)中,并且从 100 开始一直到 1。如下图所示:
- slowlog-log-slower-than:该选项指定执行时间超过多少微秒。
例如,如果这个选项的值为 1000,那么执行时间超过 1000 微秒的命令就会被记录到慢查询日志。如果这个选项的值为 5000,那么执行时间超过 5000 微秒的命令就会被记录到慢查询日志。以此类推。
redis> CONFIG SET slowlog-log-slower-than 0
OK
redis> CONFIG SET slowlog-max-len 10
OK
上面的代码表示,CONFIG_SET 命令将 slowlog-log-slower-than 选项的值设为 0 微秒,这样 Redis 执行的全部命令都会被记录进去,然后将 slowlog-max-len 选项的值设为 10,让服务器最多只保存 10 条慢查询日志。
然后我们发送几个请求:
redis> SET msg "welcome my city"
OK
redis> SET number 12345
OK
redis> SET database "redis"
OK
SLOWLOG GET 命令就可以查看慢日志,如下。
redis> SLOWLOG GET
1) 1) (integer) 4 # 日志的唯一标识符(uid)
2) (integer) 1338784447 # 命令执行时的 UNIX 时间戳
3) (integer) 12 # 命令执行的时长,以微秒计算
4) 1) "SET" # 命令以及命令参数
2) "database"
3) "redis"
2) 1) (integer) 3
2) (integer) 1372181139
3) (integer) 10
4) 1) "SET"
2) "number"
3) "12345"
...
3. 慢查询的默认值
我们在使用 Redis 时,可以设置慢查询的默认值。有以下两种方式。
方式一,请见下面代码。
// 不推荐
config get slowlog-max-len = 128 // 设置 slowlog-max-len 为 128,保存数据最多 128 条
config get slowlog-log-slower-than = 10000// 表示超时到 10000 微秒会被记录到日志上
上面的配置表示修改配置文件后需要重启,这里不推荐使用此方式。
方式二,动态配置。配置方式,请见下面两条命令。推荐使用该方式。
// 推荐使用动态配置
config set slowlog-max-len 1000 // 设置 slowlog-max-len
为 1000,保存数据最多 1000
条,不能太小,也不能太大
config set slowlog-log-slower-than 10000 // 表示超时到 10000 微秒会被记录到日志上
4. 慢查询的命令
- slowlog get[n]:获取慢查询队列;
- slowlog len:获取慢查询队列的长度;
- slowlog reset:清空慢查询队列。
对于慢查询,我们在实际使用的时候需要注意以下几点。
- slowlog-log-slower-than 不要太大,默认是 10ms,实际使用中也只是 1ms 或者 2ms,必须根据 QPS 的大小来设定;
- slowlog-max-len 不要太小,通常是 1000。默认是 128。因为存在内存中,如果设置过小,会导致之前的慢查询丢失,故建议改成 1000;
- 定期对慢查询进行持久化。
4.2 PipeLine 流水线
1. 概念
如果想同时使用 HSET 或者 MSET,那有没有 HMSET 呢?实际上是没有的,但是我们可以使用 PipeLine 流水线功能。
如果使用 n 次网络请求加 n 次命令就很麻烦,但是如果使用 PipeLine 的 1 次网络请求加 n 次命令,就可以节约网络带宽和访问时间。但是 PipeLine 每次条数要控制。
2. 如何使用 PipeLine
这里我们介绍在 Jedis 中使用 PipeLine,我们通过使用 PipeLine 和不使用 PipeLine 作对比,你就能清晰地感受 PipeLine 的方便了。
第一种方式用 HSET,就是我们不用 PipeLine 的时候,代码如下。
Jedis jedis = new Jedis("127.0.0.1",6379); // new 一个 Redis,它提供 IP 和端口号的参数
for(int i=0;i<10000;i++){
jedis.hset("keyvalue:"+i,"keyfield"+i);
} // 测试后发现,1w 的 HSET 需要 50s
第二种使用 PipeLine 方式,代码如下。
Jedis jedis = new Jedis("127.0.0.1",6379);// new 一个 Redis,它提供 IP 和端口号的参数
for(int i=0;i<100;i++){
Pipeline pipeline = jedis.pipelined();// 此句就是使用 pipelined 方法,激活
pipeline
for(int j=i*100;j<(i+1)*100;j++){
pipeline.hset("key"+j,"keyfield"+j,"keyvalue"+j);
}
pipeline.syncAndReturnAll();//结束的时候必须要加的
} // 测试发现,1w 的 HSET 只需要 0.7s
从上面的代码你就能看到,如果你不使用 PipeLine,这个速度是非常慢的,需要 50 秒;但是如果使用 PipeLine,那么只需要 0.7 秒就能循环出来了。
这里需要注意以下两点:
- 每次 PipeLine 携带的数据量;
- PipeLine 一次只能在一个 Redis 节点上。在后面的集群部分,我也会进行说明。
4.3 BitMap 位图
接下来我们来讲位图功能。我们知道,如果你要 Set 一个值,比如 one,那么 one 这个值存放在内存中其实是以二进制的方式,那么我们通过 getbit one 0,就能查询到这个 one 在内存中的第一个二进制是多少。
如上图所示,上面的英文字母在内存里都是 0 和 1,那么当我们 getbit one 0 的时候,我们取的就是这个内存中二进制的第一个值,得到的就是 0。
使用 BitMap,有如下五个命令。
- SETBIT
setbit key offset value //给位图指定索引设置值
例如,setbit book1:books:2017-01-10 1 1,即将 book1 的 2017 年 1 月 10 日的第一个位图值改为 1,返回结果是之前的值。
- GETBIT
getbit key offset //获取位图对应的值
例如,getbit book1:books:2017-01-10 1 8,和 SET 类似。
- BITCOUNT
bitcount key [start end] // 获取位图指定范围值为 1 的个数
例如,bitcount book1:books:2017-01-10 1 3,返回 3。
- BITOP
bitop op destkey key [key...]
上面代码表示,进行多个 Bitmap 的 and、or、not、xor 操作并保存到 destkey 中。
- BITOPS
bitops key targetBit [start] [end] // 计算指定位图范围第一个偏移量对应的值等于 targetBit 的位置
例如,bitops book1:books:2017-01-10 1 2 5,后面三个数表示 2 到 5 的位置里为 1 的位置。
比较 SET 和 BitMap,很多是使用在统计上的,可见下面两个示例。
示例一,如果你有 1 个亿的用户,每天 5 千万的独立访问。
数据类型 | 空间占用 | 存储用户量 | 全部内存占用 |
---|---|---|---|
set | 32位 | 50,000,000 | 50,000,000*32 =200M |
bitmap | 1位 | 100,000,000 | 100,000,000*1=12.5M |
如果你用 SET,一天是 200 M,那么一年就是 72G,但如果你用 BitMap,一年只有 4.5G,你觉得哪个好呢?
示例二,但是如果你只有 10万独立用户呢?
数据类型 | 空间占用 | 存储用户量 | 全部内存占用 |
---|---|---|---|
set | 32位 | 1,000,000 | 1,000,000*32 =4M |
bitmap | 1位 | 100,000,000 | 100,000,000*1=12.5M |
通过上述表格可知,这个时候就要用 SET 了。
所以,BitMap 不一定好,BitMap 是针对大数据量设计的。我们需要根据需求来区分使用,如果数据量非常大,可以考虑,只有在用户量非常大的时候,才会使用。
4.4 HyperLogLog
这种算法又叫:极小空间完成独立数量统计。我们日常是不会使用 HyperLogLog 算法的,只有当统计数据量非常大的时候,才会使用它。很多小伙伴在研究 HyperLogLog 的时候,就会发现 HyperLogLog 本质上还是字符串 String。你不信可以使用 type 命令,就知道 HyperLogLog 返回的是字符串。
常用的 HyperLogLog 命令有以下三个。
- pfadd key element ...:向HyperLogLog 添加元素;
- pfcount key ...:计算HyperLogLog 的独立总数;
- pfmerge key1 key2 ...:合并多个 HyperLogLog。
4.5 发布订阅
有的小伙伴会问什么是发布订阅,工业生产设计之前是根据非定时的监听设计,定时器会定时在内存中进行监听,如果有改变,再发送,其实不算是实时的。
同时,这种工业设计其实是非常复杂的。很多实时的发布依赖于 Redis 做事件消息推送。发布订阅大大简化了设计流程,而且性能也比较可观。
Redis 在 2.0 之后实现了事件推送的 Pub/Sub 命令。Pub 就是发布者 Publisher,Sub 就是订阅者 Subcriber。
订阅者只要订阅这个频道,就可以实时获得信息,见下图。
说明:当发布者发送一条消息到 Redis Server 后,只要订阅者订阅了该频道就可以接收到这样的信息。同时,订阅者可以订阅不同的频道。
使用发布订阅,有如下四个命令。
- PUBLISH,发布。
publish channel message //发布命令
publish youku:tv "welcome back!" // 返回有几个订阅者。
- SUBSCRIBE,订阅。
subcribe [channel] // 订阅一个或者多个
subcribe youku:tv // 订阅优酷并返回信息。
- UNSUBSCRIBE,取消订阅。
unsubcribe [channel] // 取消订阅一个或者多个
unsubcribe youku:tv // 取消订阅优酷并返回信息。
-
其他命令
如 PSUBSCRIBE(订阅模式)、PUNSUBSCRIBE(退订指定模式)、PUBSUB CHANNELS(列出至少一个订阅者频道)
补充:消息队列。
Redis 消息队列是抢的模式,就是只有一个用户能收到,谁网速快,谁人品好,谁就能获得那条消息,作为开发者我们需要了解需求,如果只需要一个消息订阅者收到,那么就可以使用消息队列,如下图所示。
这种场景比如抢红包、谁中奖这种模式,就可以使用消息队列了。
4.6 GEO 地理位置存储
Redis 3.2 以后才出现 GEO 的功能。GEO 是使用 ZSET 来实现的。最常用的功能就是微信里的附近的人和摇一摇的功能,还有美团通过本地的坐标自动识别周围的餐馆等功能。
使用 GEO,有如下四个命令。
- GEOADD,增加。
geo add 经度 维度 标识 ...
例如,geo add cities:location 117.20 40.11 beijing。
- GEOPOS,获取地理信息。
geopos key 标识 ...
例如,geopos cities:locations beijing,返回经度和纬度。
- GEODIST,计算距离。
geodist key 标识1 标识2 ... [unit] // unit 表示单位
例如,geodist cities:locations beijing tianjing km,计算北京到天津的距离
- GEORADIUS,它是非常复杂的,如果你有兴趣可以访问如下网址找到详细内容。
5.1 持久化的概念及其作用
首先我们来看下什么是 Redis 的持久化。Redis 的所有数据都保持在内存中,对数据的更新将异步保存到磁盘上。如果 Redis 需要恢复时,就是从硬盘再到内存的过程。
由上图可知,持久化就是把内存中的数据保存到硬盘中的过程。,因为 Redis 本身就在内存中运行的;当突然断电或者死机后,我们可以从硬盘中拷贝数据到内存,这是整个过程。
这就是 Redis 持久化的用处。持久化是为了让硬盘成为备份,以保证数据库数据的完整。
持久化有哪些方式呢?你会看到网上和书上有很多分类,但是其实大致只分成两种。
- 快照 RDB
- 日志 AOF
5.2 快照 RDB
顾名思义,快照就是拍个照片,做个备份,而这种备份是 Redis 自动完成的。
这个功能是 Redis 内置的一个持久化方式,即使你不设置,它也会通过配置文件自动做快照持久化的操作。你可以通过配置信息,设置每过多长时间超过多少条数据做一次快照。具体的操作如下:
save 500 100 // 每过 500 秒超过 100 个 key 被修改就执行快照
我们再来看看它们的运行状况。
Redis 调用 fork() 进程,同时拥有父进程和子进程;
子进程将数据都写到一个临时 RDB 文件之中;
当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换旧的 RDB 文件。
这个过程使 Redis 可以从写时复制中得到备份。
接下来我们讲解三种 RDB 的触发机制:save( 同步 )、bgsave( 异步 )和自动触发 RDB 文件。
1. save(同步)
即其他的命令都要排队。
如果有 1000 万条数据,执行 save 命令,Redis 就会对 1000 万条数据打包,而这个时候是同步的,Redis 就会进入阻塞状态。这也是 save 的缺点,接下来我们讲异步命令 bgsave。
2. bgsave(异步命令)
即返回 OK,后台会新开一个线程去执行。
上图可知,客户端会在 Redis 发出 bgsave 命令,另外开一个进程调用 fork() 方法,这个进程同时会创建 RDB 文件,也就是我们上面提到的自动触发 RDB 文件机制。这样,你在父进程里操作别的命令,就不会受影响了。
3. 自动触发 RDB 文件
快照方式虽然是 Redis 自动的,但是如果 Redis 服务器挂掉后,那么最近写入的,是不会被拷入快照中的。所以 RDB 存在两方面的缺点。
耗时耗性能:Redis 写 RDB 文件是 dump 操作,所以需要的时间是 O(n),需要所有命令都执行一遍,非常耗时耗性能。
容易丢失数据和不可控制:如果我们在某个时间 T1 内写多个命令,这个时候 T2 时间执行 RDB 文件操作,T3 时间又执行多个命令,T4 时间就会出现宕机了。那么 T3 到 T4 的数据就会丢失。
虽然 RDB 有缺陷,但是依然在生产环境中会使用,RDB 适合冷备,就是当用户数据不高的时候,比如在午夜时分就可使用 RDB 备份。其他时候我们通常使用 AOF 日志备份。
5.3 日志 AOF
AOF(Append-only File)是用日志方式,通俗点讲就是当写一条命令的时候,如 Set 某个值,Redis 就会去日志里写一条 Set 某个值的语句。如下图:
当服务器宕机后,Redis 就会调用 AOF 日志文件,并且这个过程一般是实时的,不需要时间消耗。
上图表示客户端向 AOF 文件写入的时候,是会通过缓冲的,缓冲是系统机制,是为了提高文件的写入效率。
AOF 三种策略分别是 always 、everysec 和 no。
- always
客户端是不会直接把命令写入 AOF 文件的,Liunx 系统会有一个缓冲机制,把一部分命令打包再同步到 AOF 文件,从而提高效率。
但是如果你使用的是 always 命令,就表示每条命令都写入 AOF 文件中,这样是为了保证每条命令都不丢失。
- everysec
即每秒策略,简而言之,就是说每一秒的缓冲区的数据都会刷新到硬盘当中。但是它不像 always 那样,每条数据都会写入硬盘中,如果硬盘发生故障有可能丢失 1 秒的数据。
- no
这个 no 的配置相当于把控制权给了操作系统,操作系统来决定什么时候刷新数据到硬盘,以及不需要我们考虑哪种情况。
命令 | always | everysec | no |
---|---|---|---|
优点 | 不丢失数据 | 每秒一次同步,丢一秒数据 | 不需要管 |
缺点 | IO 开销大,一般 SATA 盘只有 TPS | 丢一秒数据 | 不可控制 |
关于 AOF,我们补充一点。对于 AOF 操作,Redis 在写入的时候,会压缩命令。它既可以减少硬盘的占用量,同时可以提高恢复硬盘的速度。例如如下表格。
原生 AOF | AOF 复写 |
---|---|
set hello a1 | set hello a3 |
set hello a2 | |
set hello a3 | |
incr counter | set counter 2 |
incr counter | |
rpush hello a | rpush hello a b c |
rpush hello b | |
rpush hello c |
从上表可以看到,set hello 有三个值,但是 a1 和 a2 是无效的,最终 AOF 会自动 set 最后一个 a3 的值;incr counter 两次,AOF 自动识别 2 次;rpush 三个值,rpush 会自动简写为一条数据。
针对上面的 Redis 的 AOF 复写。Redis 提供了两种命令。这两种命令是 bgrewriteaof 和 AOF 重写配置。bgrewriteaof 类似 RDB 中的 bgsave 命令,它还是 fork() 子进程,然后完成 AOF 的过程。
AOF 重写配置包含两个配置命令,见如下表格。
配置名 | 含义 |
---|---|
auto-aof-rewrite-min-size | AOF 文件重写最小的尺寸 |
auto-aof-rewrite-percentage | AOF 文件增长率 |
auto-aof-rewrite-min-size 表示配置最小尺寸,超过这个尺寸就进行重写。
auto-aof-rewrite-percentage ,这里说的是 AOF 文件增长比例,指当前 AOF 文件比上次重写的增长比例大小。AOF 重写即 AOF 文件在一定大小之后,重新将整个内存写到 AOF 文件当中,以反映最新的状态(相当于 bgsave)。这样就避免了 AOF 文件过大而实际内存数据小的问题(频繁修改数据问题)。
接下来看下统计配置,如下表所示,有了它,就可以对上面的配置命令进行控制。
配置名 | 含义 |
---|---|
aof-current-size | AOF 当前尺寸(字节) |
aof-base-size | AOF 上一次启动和重写的尺寸(字节) |
由上图可知,bgrewriteaof 命令发出后,Redis 会在父进程中 fork 一个子进程,同时父进程会分别对旧的 AOF 文件和新的 AOF 文件发出 aof_buf 和 aof_rewrite_buf 命令,同时子进程写入新的 AOF 文件,并通知父进程,最后 Redis 使用 aof_rewrite_buf 命令写入新的 AOF 文件。
实际配置过程如下。
appendonly yes // appendonly
默认是 no
appendfilename "append only - ${port}.aof" //设置 AOF 名字
appendfsync everysec // 每秒同步
dir /diskpath // 新建一个目录
no-appendfsync-on-rewrite yes // 为了减少磁盘压力,AOF 性能上需要权衡。默认是 no,不会丢失数据,但是延迟会比较高。为了减低延迟,一般我们设置成 yes,这样可能丢数据
5.4 Redis 持久化开发运维时遇到的问题
问题可总结为四种,即 fork 操作、进程外的开销和优化、AOF 追加阻塞和单机多实例部署。
1. fork 操作
fork 操作包括以下三种:
- 同步操作,即 bgsave 时是否进行同步;
- 与内存量息息相关:内存越大,耗时越长;
- info:lastest_fork_usec,持久化操作。
改善 fork 的方式有以下四种:
- 使用物理机或支持高效 fork 的虚拟技术;
- 控制 Redis 实例最大可用内存 maxmemory;
- 合理配置 Liunx 系统内存分配策略:vm.overcommit_memory=1;
- 降低频率,如延长 AOF 重写 RDB,不必要的全量复制。
2. 子进程的开销以及优化
这里主要指 CPU、内存、硬盘三者的开销与优化。
-
CPU
开销:AOF 和 RDB 生成,属于 CPU 密集型,对 CPU 是巨大开销;
优化:不做 CPU 绑定,不与 CPU 密集型部署。
-
内存
开销:需要通过 fork 来消耗内存的,如 copy-on-write。
优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled,有时启动的时候会出现警告的情况,这个时候需要配置这个命令。
-
硬盘
开销:由于大量的 AOF 和 RDB 文件写入,导致硬盘开销大,建议使用 iostat、iotop 分析硬盘状态。
优化:
不要和高硬盘负载部署到一起,比如存储服务、消息队列等等;
配置文件中的 no-appendfsync-on-rewrite 设置成 yes;
当写入量很大的时候,建议更换 SSD 硬盘;
单机多实例持久化文件考虑硬盘分配分盘。
3. AOF 追加阻塞
我们如果使用 AOF 策略,通常就会使用每秒刷盘的策略(everysec),主线程会阻塞,直到同步完成。首先我们知道主线程是非常宝贵的资源,其次我们每秒刷盘实际上未必是 1 秒,可能是 2 秒的数据。
我们如何定位 AOF 阻塞?
- 通过 Redis 日志
上图可以看到, Redis 日志会出现上述的语句,告诉你异步 IO 同步时间太长,你的硬盘是否有问题,同时会拖慢 Redis。
- 当然除了上述的问题,你还可以用 Redis 的 info 方式来确定问题。
info rersistence // 直接在命令中打这个命令即可。
...
aof_delayed_fsync : 100 // 会记录你发生阻塞的次数,每一次加 1
...
...
但是这个命令无法看到当前的问题,因为它是历史的累计值。
当然你还可以使用 Liunx 命令 top。
上图能看到 wa 值,wa 值是表示 IO 瓶颈,如果超过 50%,就表示 IO 出现阻塞了。
本文,我们首先讲了持久化的概念,持久化的方式有 RDB 和 AOF 两种。RDB 主要是 save(同步)、bgsave(异步)、自动触发 RDB 文件三种触发方式;AOF 则是 always、everysec、no 三种方式,同时我们补充了 AOF 重写的命令和如何配置。
最后我们讨论了 Redis 持久化开发运维时遇到的问题,主要有 fork 操作、进程外的开销和优化、AOF 追加阻塞、单机多实例部署四种问题。
这要求我们在做 RDB 和 AOF 备份时,要注意到这些问题。特别是大数据,需要监控 Redis 是否阻塞、开销是否过大等。
要实现分布式数据库的更大的存储容量和高并发访问量,我们会将原来集中式数据库的数据分别存储到其他多个网络节点上。Redis 为了解决这个单一节点的问题,也会把数据复制多个副本部署到其他节点上进行复制,实现 Redis的高可用,实现对数据的冗余备份,从而保证数据和服务的高可用。
应该说,Redis 复制是高可用的基石。没有 Redis 复制,也就不可能实现高可用。然而,Redis 复制这块经常出现开发和运维故障,在我们排错之前,需要对 Redis 有一个清晰的认识。
下面我们将从以下五个方面全面认识 Redis 复制。
- 什么是主从复制
- 复制的配置
- 全量复制和部分复制
- 故障如何处理
- 开发和运维遇到的常见问题
在此之前,我们需要明确将 Redis 应用到工程项目中时,只用一台 Redis 显然是万万不能的,为什么不能呢?主要概括为以下三个原因。
第一,机器故障。我们部署到一台 Redis 服务器,当发生机器故障时,需要迁移到另外一台服务器并且要保证数据是同步的。而数据是最重要的,如果你不在乎,基本上也就不会使用 Redis 了。
第二,容量瓶颈。当我们有需求需要扩容 Redis 内存时,从 16G 的内存升到 64G,单机肯定是满足不了。当然,你可以重新买个 128G 的新机器。但是我们非要这么做吗?我将会在后续讲到。
第三,QPS 瓶颈。Redis 号称支持10 万 QPS,当业务需要 100 万 QPS 时,我们该怎么做呢?这时就用到了 Redis 复制。本文讲解的主要内容便是 Redis 复制(Replication)。
6.1 什么是主从复制
如上图所示,我们将 Redis 服务器作为 Master 主库,另外一台作为 Slave 从库,主库只是负责写数据,每次有数据更新的时候,Redis 会将数据同步到其他的从库中,而从库只是负责读数据。
当然你还可以根据业务需求,增加更多的从库,如下图所示,红色的 Redis 为主库,蓝色的是三台从库。
很多小伙伴都觉得这没什么好说的,但在这里我还是想说这么做的两大好处。
实现了读写分离,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量,第一张图中只有一个从库,你还可以像第二张图那样增加至两个、三个……你觉得这个优点怎么样?
数据备份了多份,如果一台机器宕机,你可以从其他机器上快速恢复。但需要注意的是一台主库可以拥有多个从库,但一个从库却只能隶属于一个主库。
6.2 复制的配置
接下来,我们讲一下主从复制的作用。首先请看下面这张图。
如该图所示,Master 属于主节点,Slave 属于从节点。我们在主机上执行 set 和 incr 命令,在从库中通过 get 得到这些数据,这正好说明 Redis 复制是自动完成的。当 Master 节点宕机以后,Slave节点可以支援主节点。
除了一主一从,Redis 还支持一主多从,这样我们就可以获得更多高可用的可能性。比如,当一个主机和一个从机都宕机了,那么还有几台从机可以做备份;再比如,当 Master 节点的流量超过最大值的时候,可以通过从机对流量进行分流。
总而言之,主从复制的作用主要是提供数据副本及扩展 Redis 读的性能。
接下来,我们再来配置一下主从架构,这是为一些刚开始学习的朋友准备的,通过实际的配置,以便于大家更好的理解。
- 安装两台服务器的实例,关于如何安装 Redis,我在第一篇文章已经讲到。我将主库端口设置为6379,从库设置为 6380。bind 都设置为 127.0.0.1。
- 在 Slave 实例中我们增加了 slaveof 127.0.0.1 6379 的配置,从库配置相同。如下图所示。
配置后,需要启动这两个实例,如输出下面内容,说明主从复制的架构已经配置成功。
这里唯一需要说明的是,主库和从库的端口号不能相同,否则不可能同时启动。
6.3 全量复制和部分复制
在讲解全量复制和部分复制之前,我们先来讲一下,runid 和偏移量的概念。
什么是 runid,每次 Redis 启动的时候,Redis 就会有一个运行的 ID,这个 ID 只在 Redis 运行的时候才有,如果关闭 runid 就不存在了。runid 的作用是一个标识,如果主库去复制从库的数据,就需要根据这个 runid 去复制。
上图所示,通过 redis-cli -p 6379 info server | grep run 显示出 6379 端口的 runid 和6380端口的 runid。
很多 Redis 第一次启动的时候,压根就不知道其他机器上面的数据,这个时候我们就需要全量复制。
什么是偏移量呢?偏移量就是记录到底写了多少数据,比如在 6379 端口的 Redis 中,执行 set k1 v1 这个命令就是写入一个字节。这时它同步给 6380 端口,6380 也会记录偏移量。
主 Redis 每次向从 Redis 传播 N 个字节的数据时,都会在自己的复制偏移量上加 N; 同理,从 Redis 每次接收到 N 个字节时,也会在自己的复制偏移量上加 N。
上图所示,我们首先运行 redis-cli -p 6379 info replication,就能在里面找到 master_repl_offset:1865,然后执行一次 set 操作,再运行 redis-cli -p 6379 info replication,此时就能看到偏移值变成了 1950。如果在其他从库中运行,也会变成 1950,偏移量是记录部分复制的依据。大家只要了解即可,生产环境中不怎么关心这个值。
1. 全量复制
我们来看下 Redis 全量复制的流程图。
如图所示:
第一步,Redis 内部会发出一个同步命令,刚开始是 Psync 命令,Psync ? -1 表示要求 Master 主机同步数据;
第二步,Master 会向从机发送 runid 和 offset,因为 Slave 并没有对应的 offset,所以是全量复制;
第三步,通过指令 save masterInfo,从机 Slave 会保存 Master 的基本信息;
第四步,Master 执行 bgsave 命令(持久化命令),对于一个快照来说,怎么快怎么来。实际上 Master 主机里有 repl_back_buffer(复制缓冲区);
第五步,通过指令 send RDB 发送 RDB 文件;
第六步,发送缓冲区数据;
第七步,刷新旧的数据;
第八步,加载 RDB 文件和缓冲区数据的加载。
这就是建立全量数据副本的过程。希望大家结合着图示能够了解 Redis 进行全量复制的流程,在大脑里有一个印象。
那么全量复制需要哪些开销呢?主要有以下几项。
- bgsave 时间;
- RDB 文件网络传输时间;
- 从节点清空数据的时间;
- 从节点加载 RDB 的时间;
- AOF 重写的时间(这里需要说明一下,RDB 全量复制完加载 RDB,如果 AOF 开启的话,就会出现 AOF 重写来保证是最新的)。
2. 部分复制
部分复制是 Redis 2.8 以后出现的,之所以要加入部分复制,是因为全量复制会产生很多问题,比如像上面的时间开销大、无法隔离等问题, Redis 希望能够在 Master 出现抖动(相当于断开连接)的时候,可以有一些机制将复制的损失降低到最低。
如图所示:
第一步,如果打算抖动(连接断开 connection lost);
第二步,Master 还是会写 repl_back_buffer(复制缓冲区);
第三步,Slave 会继续尝试连接主机;
第四步,Slave 会把自己当前 runid 和偏移量传输给主机 Master,并且执行 pysnc 命令同步;
第五步,如果 Master 发现你的偏移量在缓冲区的范围内,就会返回 continue 命令;
第六步,同步了 offset 的部分数据,所以部分复制的基础就是偏移量 offset。
通过部分复制,可以有效的减少全量复制的开销。
6.4 故障如何处理
讲解完主从复制和部分复制,我们来讲一下 Redis 复制的故障如何处理。在开发运维的时候,故障是不可避免的。如果采用单机 Redis ,一旦通过短信或者电话告知你错误时,你多会一脸茫然,这种故障通常发生在半夜。
但如果我们使用了自动故障转移,那效果就不同了。即使半夜出现故障,你还是可以好好休息,等早上起来再去完成故障排除。
主要考虑主机 Master 宕机、从机 Slave 宕机两种情况。
上图所示,一主二从的结构,如果某一台从机宕机了。这个时候我们需要把客户端传到宕机的请求改成第一个 Slave 从机就可以了。基本不会有什么问题。
但如果是 Master 宕机,Master 就会和两个 Slave 从机断掉,这个时候该怎么办呢?如下图所示。
当 Master 宕机,原来从 Master 读的客户端会通过 slaveof no one 读写 Slave 从机,发生该命令的 Slave 从机会变成 Master 主机,而另一台从机会执行 slaveof new master 命令,让另外一台 Slave 知道哪台变成 Master 并且与它发生同步。
上述两种情况其实并不是自动的,那我们如何让这两种情况变成自动化呢,Redis Sentinel 相关知识会在下一篇讲解。Redis Sentinel可以真正实现高可用,它会自动设置好哪台是 Master,当 Master 宕机,就会自动把某一台 Slave 从机变成 Master,并且告诉所有 Slave 从机哪台变成了 Master 并自动连接。
6.5 开发和运维中的问题
我将从下面四点来说明:读写分离、主从配置不一致、规避全量复制、规避复制风暴。
1. 读写分离
读流量分摊到从节点。这是个非常好的特性,如果一个业务只需要读数据,那么我们只需要连一台 Slave 从机读数据。
虽然读写有优势,能够让读这部分分配给各个 Slave 从机,如果不够,直接加 Slave 机器就好了。但是也会出现以下问题。
- 复制数据延迟。可能会出现 Slave 延迟导致读写不一致等问题,当然你也可以使用监控偏移量 offset,如果 offset 超出范围就切换到 Master 上。
- 读到过期数据。Redis 采用懒惰性策略和采样式策略,懒惰性策略指的是 Redis 操作 key,它才去看 key 有没有过期数据。采样式策略是指定期会去采样,如果是过期的,就自动删除。当过期数量非常多的时候,我的采样速度比不上逻辑数据变化的速度,Slave 没有删除权限,只有 Master 有,这个时候就会出现过期数据。但如果你用 Redis 3.2 以上版本时,就没有这个问题了。
- 从节点故障。怎么对发生故障的从节点进行迁移。
2. 配置不一致
主机和从机不同,经常导致主机和从机的配置不同,并带来下列两种问题。
数据丢失:主机和从机有时候会发生配置不一致的情况,例如 maxmemory 不一致,如果主机配置 maxmemory 为 8G,从机 Slave 设置为 4G,这个时候是可以用的,而且还不会报错。但是如果要做高可用,让从节点变成主节点的时候,就会发现数据已经丢失了,而且无法挽回。
数据结构优化参数导致不一致:hash-max-ziplist-enties 参数,如果主机对这些优化参数设置了,从机 Slave 却没有优化,就会发生数据不一致的情况。
3. 规避全量复制
全量复制指的是当 Slave 从机断掉并重启后,runid 产生变化而导致需要在 Master 主机里拷贝全部数据。这种拷贝全部数据的过程非常耗资源。
全量复制是不可避免的,例如第一次的全量复制是不可避免的,这时我们需要选择小主节点,且maxmemory 值不要过大,这样就会比较快。同时选择在低峰值的时候做全量复制。
造成全量复制的原因之一是主从机的运行 runid 不匹配。解释一下,主节点如果重启,runid 将会发生变化。如果从节点监控到 runid 不是同一个,它就会认为你的节点不安全。当发生故障转移的时候,如果主节点发生故障,那么从机就会变成主节点。我们会在后面讲解哨兵和集群再详细解释。
造成全量复制的第二个原因是复制缓冲区空间不足,比如默认值 1M,可以部分复制。但如果缓存区不够大的话,首先需要网络中断,部分复制就无法满足。其次需要增大复制缓冲区配置(rel_backlog_size),对网络的缓冲增强。默认是 1M,我们实际会设置成 10M 左右。
4. 规避复制风暴
当一个主机下面挂了很多个 Slave 从机的时候,主机 Master 挂了。这时 Master 主机重启后,因为 runid 发生了变化,所有的 Slave 从机都要做一次全量复制。这将引起单节点和单机器的复制风暴,开销会非常大。
- 单节点复制风暴。当主节点重启,多从节点会复制。这个时候需要更换复制拓扑。上图就是改变拓扑结构的问题,通过在 Slave 下再分从机,可以有效的减少主机 Master 的压力。
- 单机器的复制风暴。如上图,如果每个 Master 主机只有一台 Slave 从机,那么当机器宕机以后,会产生大量全量复制。这是非常危险的情况,带宽马上会被占用,会导致不可用。这个问题在实际运维中必须注意。在这种情况下,建议将单机器改成 Redis Sentinel。这样可以自动将从机变成主机 Master。
Redis Sentinel 正是为了解决这样的问题而被开发的。Redis Sentinel 是一个分布式的架构,每一个 Sentinel 节点会对数据节点和其余 Sentinel 节点进行监控,当发现某个节点无法到达的时候,会自动标识该节点。如果这个节点是主节点,那么它会和其他 Sentinel 节点“协商”,大部分节点都认为主节点无法到达的时候,它们会选举一个 Sentinel 节点来完成自动故障转移,同时会告知 Redis 的应用方。
由于这个过程是自动化的,不需要人工参与,大大提高了 Redis 的高可用性。
接下来,我们将从实现流程、安装配置、客户端连接、实现原理、常见开发运维问题这五个方面来探讨。
7.1 实现流程
如下图所示,Sentinel 集群会监控每一个 Slave 和 Master。客户端不再直接从 Redis 获取信息,而是通过 Sentinel 集群来获取信息。
再看下面这张图,当 Master 宕机了,Sentinel 监控到 Master 有问题,就会和其他 Sentinel 进行选举,选举出一个 Sentinel 作为领导,然后选出一个 Slave 作为 Master,并通知其他 Slave。上图 Slave1 变成了 Master,如果原来的 Master 又连上了,也会变成 Slave 从机。
7.2 安装与配置
我们将从以下两个方面讲解如何安装和配置主从节点和 Sentinel 节点。
- 如何配置开启主从节点;
- 如何开启 Sentinel 监控主节点。
1. 开启主从节点
Sentinel 对主节点和从节点的配置是不同的,需要分别配置,我们分开来讲解。
- 主节点配置
我们在命令行使用下面的命令进行主节点的启动。
redis-server redis-7000.conf
启动完成以后,我们参考下面的配置进行参数的设置。
port 7000
daemonize yes // 守护进程
pidfile /var/run/redis-7000.pid // 给出 pid
logfile “7000.log” // 日志查询
dir "/opt/redis/data" // 工作目录
- 从节点配置
我们在命令行使用下面的命令进行从节点的启动。
redis-server redis-7001.conf
redis-server redis-7002.conf
启动完成以后,和主节点配置一样,配置下面的参数。这个时候要注意,我们需要分别对 Slave 节点的每台机器进行配置。
Slave1 的配置如下。
port 7001
daemonize yes // 守护进程
pidfile /var/run/redis-7001.pid // 给出 pid
logfile “7001.log” // 日志查询
dir "/opt/redis/data" // 工作目录
slaveof 127.0.0.1 7000
Slave2 的配置如下。
port 7002
daemonize yes // 守护进程
pidfile /var/run/redis-7002.pid // 给出 pid
logfile “7002.log” // 日志查询
dir "/opt/redis/data" // 工作目录
slaveof 127.0.0.1 7000
2. Sentinel 监控主要配置
开启了主从节点以后,我们需要对 Sentinel 进行监控上的配置,见下面的配置参数。
port 端口号
dir "/opt/redis/data/"
logfile "端口号.log"
sentinel monitor mymaster 127.0.0.1 7000 2
sentinel down-after-millseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
由于需要配置多台 Sentinel,从上面配置信息可以看到,除了修改端口号,其他配置都是相同的。重点看最后四个配置,这四个配置是 Sentinel 的核心配置。我们分别来解释一下这四个配置,斜杠后面的文字解释了该参数的意义。
sentinel monitor mymaster 127.0.0.1 7000 2 // 监控的主节点的名字、IP 和端口,最后一个 2 表示有 2 台 Sentinel 发现有问题时,就会发生故障转移;
sentinel down-after-millseconds mymaster 30000 // 这个是超时的时间。打个比方,当你去 ping 一个机器的时候,多长时间后仍 ping 不通,那么就认为它是有问题;
sentinel parallel-syncs mymaster 1 // 指出 Sentinel 属于并发还是串行。1 代表每次只能复制一个,可以减轻 Master 的压力;
sentinel failover-timeout mymaster 180000 // 表示故障转移的时间。
7.3 Sentinel 客户端原理
我们配置高可用的时候,如果只是配置服务端的高可用是不够的。如果客户端感知不到服务端的高可用,是不会起作用的。所以,我们不但要让服务端高可用,还要让客户端也是高可用的。
我们先来看下客户端基本原理。
第一步,客户端 Client 需要遍历 Sentinel 节点集合,找到一个可用的 Sentinel 节点,同时需要获取 Master 主机的 masterName。如下图所示。
第二步,当客户端找到 Sentinel-2 节点的时候,Client 会通过 get-master-addr-by-name 命令获取 masterName,这个时候,Sentinel-2 会获取真正的名称和地址。如下图所示。
第三步,Client 获取到 Master 节点的时候,还会发出 role 或 role replication 命令,验证是不是 Master 节点,Sentinel-2 会返回这个节点信息加以验证。如下图所示。
第四步,如果 Sentinel 感知到 Master 宕机了,这时 Sentinel 集群应该是最先知道的。客户端和 Sentinel 集群之间其实是发布订阅,客户端 Client 去订阅某个 Sentinel 的频道,如果哪个 Sentinel 发现 Master 发生了变化,Client 是会被通知到这个信息的,如下图所示。但是要注意这个不是代理模式。
总结一下,以上四步就是客户端和 Sentinel 集群的基本原理,任何客户端原理都是按照这个流程做的,只是内部的封装不同而已。
1. Jedis
我们先通过使用率最高的 Java 的客户端 Jedis 讲起。
如何通过代码实现 Sentinel 的访问,让我们来看看代码如何连接 Sentinel 的资源池。
JedisSentinelPool sentinelPool = new JedisSentinelPool(masterName,sentinelSet,poolConfig,timeout); //内部的本质还是去连接 Master 主机,参数 masterName 表示 Master 名称,sentinelSet
表示 Sentinel 集合,后面依次是 poolConfig 配置和超时时间
Jedis jedis = null;
try{
//获得
redisSentinelPool 资源
jedis = redisSentinelPool.getResource();
//Jedis 相关的命令
}catch (Exception e){
logger.error(e.getMessage(),e);
}finally{
if(jedis!=null){
jedis.close(); // Jedis 归还
}
}
2. redis-py
接下来我们再来看下如何使用 Python 连 Redis 客户端的 Sentinel,和 Jedis 一样,我们将直接给出连接 Sentinel 的代码。
from redis.sentinel import Sentinel
sentinel = Sentinel([('localhost',26379),('localhost',26380),('localhost',26381)],socket_time=0.1) // 获取可用的 Sentinel,并设置超时时间。
sentinel.discover_master('mymaster') // 获取 Master 地址
>>> ('127.0.0.1',7000)
sentinel.discover_slaves('mymaster') //获取 Slave 地址
>>> [('127.0.0.1',7001),('127.0.0.1',7002)]
7.4 Sentinel 实现原理
讲完了 Sentinel 的代码实现,很多人还不懂 Sentinel 的原理。接下来我们就讲解下它的实现原理,主要分为以下三个步骤。
检测问题,主要讲的是三个定时任务,这三个内部的执行任务可以保证出现问题马上让 Sentinel 知道。
发现问题,主要讲的是主观下线和客观下线。当有一台 Sentinel 机器发现问题时,将对它主观下线,但是当多个 Sentinel 都发现有问题的时候,才会出现客观下线。
找到解决问题的人,主要讲的是领导者选举,如何在 Sentinel 内部多台节点中进行领导者选举,选出一个领导者。
解决问题,主要讲得是如何进行故障转移。
我们分开进行阐述。
1. 三个定时任务
首先要讲的是内部 Sentinel 会执行以下三个定时任务。
- 每 10 秒每个 Sentinel 对 Master 和 Slave 执行一次 Info Replication。
- 每 2 秒每个 Sentinel 通过 Master 节点的 Channel 交换信息(Pub/Sub)。
- 每 1 秒每个 Sentinel 对其他 Sentinel 和 Redis 执行 Ping。
在这里一一解释下。
第一个定时任务,指的是 Redis Sentinel 可以对 Redis 节点做失败判断和故障转移,在 Redis 内部有三个定时任务作为基础,来 Info Replication 发现 Slave 节点,这个命令可以确定主从关系。
第二个定时任务,类似于发布订阅,Sentinel 会对主从关系进行判定,通过 _sentinel_:hello 频道交互。了解主从关系有助于更好地自动化操作 Redis。然后 Sentinel 会告知系统消息给其他 Sentinel 节点,最终达到共识,同时 Sentinel 节点能够互相感知到对方。
第三个定时任务,指的是对每个节点和其他 Sentinel 进行心跳检测,它是失败判定的依据。
2. 主观下线和客观下线
我们先来回顾一下 Sentinel 的配置。
sentinel monitor mymaster 127.0.0.1 6379 3 //如不懂意思,请参见上面对 Sentinel 进行配置的说明;3 这个配置请记住,我将在后面讲解。
sentinel down-after-milliseconds mymaster 3000 //Sentinel 会 Ping 每个节点,如果超过 30 秒,依然没有恢复的话,做下线的判断。
那么什么是主观下线呢?
每个 Sentinel 节点对 Redis 节点失败存在“偏见”。之所以是偏见,只是因为某一台机器 30 秒内没有得到回复。
那么如何做到客观下线呢?
这个时候需要所有 Sentinel 节点都发现它 30 秒内无回复,才会达到共识。
3. 领导者选举
Sentinel 集群会采用领导者选举的方式,完成 Sentinel 节点的故障转移。通过 sentinel is-master-down-by-addr 命令都希望成为领导者。
领导者选举的步骤请见下:
每个做主观下线的 Sentinel 节点向其他节点发送命令,要求将它设置为领导者;
收到命令的 Sentinel 节点,如果没有同意通过其他 Sentinel 节点发送的命令,那么将同意该要求,否则就会拒绝。
如果 Sentinel 节点发现自己的票数已经超过 Sentinel 半数同时也超过 Sentinel monitor mymaster 127.0.0.1 6379 3 中的 3 个的时候,那么它将成为领导者;
如果有多个 Sentinel 节点成为领导者,那么将等待一段时间后重新选举。
这里需要解释一下为什么要重新选举。因为如果有多个领导者,那么哪个节点能覆盖更多的节点,才会成为真正的领导者,盲目成为领导者,只会让 Sentinel 效率低下,只有不断确认保证最优的选举,才是高效的,当然这个过程是需要消耗时间的。
4. 故障转移
故障转移主要包括以下四个步骤:
从 Slave 节点中选出一个“合适的”节点作为新节点;
对上面的节点执行 slaveof no one 命令让其成为 Master节点;
向剩余的 Salve 节点发送命令,让它们成为新的 Master 节点的 Slave节点,复制规则和同步参数;
将原来 Master 节点更新配置为 Slave 节点,并保持其“关注”。当其恢复后命令它去复制新的 Master 节点。
通过以上四步,就能获得“Master 断掉 -> 选出新的 Master -> 同步 -> 旧 Master 恢复后成为 Slave,同时同步新的 Master数据”这样一整套的流程。
5. 如何选择“合适的”Slave 节点
Redis 内部其实是有一个优先级配置的,在配置文件中 slave-priority 这个参数是 Salve 节点的优先级配置,如果存在则返回,如果不存在则继续。
当上面这个优先级不满足的时候,Redis 还会选择复制偏移量最大的 Slave 节点,如果存在则返回,如果不存在则继续。之所以选择偏移量最大,这是因为偏移量越小,和 Master 的数据越接近,现在 Master挂掉了,说明这个偏移量小的机器数据也可能存在问题,这就是为什么要选偏移量最大的 Slave 的原因。
如果发现偏移量都一样,这个时候 Redis 会默认选择 runid 最小的节点。
7.5 常见的开发运维的问题
对于 Sentinel 来说,日常是不需要做太多运维工作的,主要说的是以下两点。
节点运维:偏运维,例如对 Master 和 Slave 节点的上下限进行操作。
高可用读写分离:偏开发,开发人员会思考是否能有更加好用的方式。例如用高可用的读写分离。
我们分别来讲解一下这两个问题。
1. 节点运维
节点运维包括主节点、从节点和 Sentinel 节点的运维。
首先是机器下线问题,如过保等情况。
其次是机器性能不足问题,如 CPU、内存、硬盘、网络等硬件。
最后是节点自身故障,如服务器不稳定,可能因为系统、硬件等未知原因,这个时候我们只能对它进行下线,转移至其他机器上。
- 主节点
主节点的节点运维,主要是通过以下命令,对主节点做故障转移。
sentinel failover <masterName>
- 从节点
对于从节点,我们要区别是永久下线还是临时下线。例如是否做一些清理工作(如 RDB、AOF 文件的清理),但要考虑一下读写分离的情况。
我们再来看一下节点上线,我们需要把某台 Slave 晋升为主节点,就需要 Sentinel failover 进行替换;对于从节点的上线,我们只需要执行 slaveof 就可以了,Sentinel 节点会根据命令自动感知;对于 Sentinel 节点,我们只需要参考其他 Sentinel 节点启动就可以。
2. 高可用读写分离
大家知道,从节点是高可用的基础,它的扩展功能是读的能力。我们先来看下面这张高可用读写分离使用之前的图。
如上图所示,Sentinel 其实是对 Master 做故障转移,对 Slave 只有下线的操作。Sentinel 集群是不会对 Slave 做故障转移的。那么我们应该怎么做呢?
其实我们需要使用一个客户端去监控 Slave,和 Master 类似。主要运用以下命令。
- switch-master:这个命令用来切换主节点,从节点晋升为主节点的操作;
- covert-to-slave:切换从节点,原来主节点需要降为从节点的时候使用该命令。
- sdown:这个命令在主观下线时使用。
我们再来看看使用高可用读写分离之后的图。
如图所示,和第一张不同的是,我们把 Slave 的机器全部做到同一个资源池中,让客户端每次都是访问这个 Slave 资源池。
但是这种高可用读写分离在实际应用场景中很少使用,主要因为这种高可用读写分离太过复杂,配置参数比较多。那怎么办呢?
当我们在实际运维中真正需要可扩展的时候,Redis 其实给我们提供了集群的模式
我们在使用 Redis 的时候,经常是会遇到一些问题。比如高可用问题、容量问题、并发性能问题等。于是开发者考虑能不能像服务器一样,当一台机器不够的时候,我们用多台机器形成 Redis Cluster 集群呢?
在 Redis 团队的努力下,终于做出了一套解决方案。这套解决方案有以下特点:
- 去中心化。Redis Cluster 增加了1000个节点,性能随着节点而线性扩展。
- 管理简单方便。可根据实际情况去掉节点或者增加节点,移动分槽等。
- 官方推荐。
- 容易上手。
根据以上 Redis 集群的特点,我们将从以下七个方面对 Redis Cluster 进行讲解:
- 为什么要有集群;
- 如何进行数据分布;
- 如何搭建集群;
- 如何进行集群的伸缩;
- 如何使用客户端去连接 redis-cluter;
- 理解集群原理;
- 常见的开发运维的问题。
8.1 为什么要有集群
首先是并发量,一般 QPS 到10万每秒已经非常牛了,随着公司业务的发展,或者当需要离散计算的时候,需要用到中间件缓存的时候,业务需要100万每秒。这个时候,我们就需要使用分布式了。
其次是数据量,一般一个 Redis 的内存大约是16G~256G,假设我们在一台主从机器上配置了200G内存,但是业务需求是需要500G的时候,我们首先不会通过升级硬件,而是通过分布式。
我们对并发量大和数据量剧增的时候,采取的最常用的手段就是加机器,对数据进行分区。做一个形象的比喻,我们的数据量相当于货物,当货物只有很少一部分的时候,我们可以使用驴来拉货;当货物多起来了的时候,已经超出驴能拉的范围,我们可以使用大象来拉货;当货物更多的时候,已经没有更强壮的动物了,这个时候我们可以考虑使用多只大象来拉货。
分布式就是一种采用某种规则对多台机器管理的方式。采用分布式我们就是为了节省费用。
8.2 如何进行数据分布
什么是数据分布?数据分布有两种方式,顺序分区和哈希分区。
1. 顺序分布
顺序分布就是把一整块数据分散到很多机器中,如下图所示。
顺序分布一般都是平均分配的。
2. 哈希分区
如下图所示,1~100这整块数字,通过 hash 的函数,取余产生的数。这样可以保证这串数字充分的打散,也保证了均匀的分配到各台机器上。
两者的区别,请见下表。
分布 | 特点 | 特点 |
---|---|---|
顺序分布 | 数据分散容易倾斜、键值业务相关、可顺序访问、支持批量 | Big Table、HBase |
哈希分布 | 数据分散度高、键值分布业务无关、支持批量、无法顺序访问 | 一致性哈希、redis cluster、其他缓存 |
由上表可知,哈希分布和顺序分布只是场景上的适用。哈希分布不能顺序访问,比如你想访问1~100,哈希分布只能遍历全部数据,同时哈希分布因为做了 hash 后导致与业务数据无关了。而顺序分布是会导致数据倾斜的,主要是访问的倾斜。每次点击会重点访问某台机器,这就导致最后数据都到这台机器上了,这就是顺序分布最大的缺点。其他的特点见表可知。
但哈希分布其实是有个问题的,当我们要扩容机器的时候,专业上称之为“节点伸缩”,这个时候,因为是哈希算法,会导致数据迁移。在节点取余的时候,迁移数量和添加的节点数是有关的,这个时候建议使用翻倍扩容。
这里需要说明的是节点取余到底是什么。
比如之前是三个节点,那么现在加一个节点,就是四个节点。这个时候哈希算法就是从3的取余变成了4的取余,这个时候,原来的数字所在的位置肯定是需要发生变化的,整体的数据基本上都是做了漂移。
整体的数据漂移其实是有问题的,对数据库的性能,硬件上都是考验,所以为了减少整体的数据漂移,我们就需要对哈希算法有个一致性哈希算法。
3. 一致性哈希
上图就是一个一致性哈希的原理解析。假设我们有 n1~n4 这四台机器,我们对每一台机器分配一个唯一 token,每次有数据(图中黄色代表数据),一致性哈希算法规定每次都顺时针漂移数据,也就是图中黄色的数据都指向 n3。这个时候我们需要增加一个节点 n5,在 n2 和 n3 之间,数据还是会发生漂移,但是这个时候你是否注意到,其实只有 n2~n3 这部分的数据被漂移,其他的数据都是不会变的,这样就实现了部分漂移,而没有对所有数据进行漂移的弊端了。
最后我们来介绍另外一个哈希算法,就是 Redis Cluster 的哈希算法 —— 虚拟槽分区。我们通过下面这张图来解释。
如上图所示,我们知道槽的范围是0~16383,如果此时有五个节点,key 会通过 CRC16 哈希算法,对16383取余,然后保存到 Redis Cluster 里,Redis Cluster 会根据数据判断是否是这个虚拟槽里的数据,如果不是这个槽范围的,由于 Redis Cluster 数据是共享的,于是就会告知数据应该存到那台机器上。
如果你对虚拟槽理解还有问题,这是正常的。下面我们将通过讲解 Redis Cluster 的基本架构、安装使用让你真正认识虚拟槽的概念。
8.3 基本架构
先来看看,什么是分布式架构。分布式架构是一种彼此通讯的架构,通过每个节点之间负责对应的槽,每个节点都负责读写。如下图
那么 Redis Cluster 是怎样的架构呢?实际上我们可以通过安装来了解 Redis Cluster 的架构。了解Redis Cluster 架构,我们从了解节点、meet操作、指派槽、复制四个知识点开始。
1. 节点
Redis Cluster 是有很多节点的,每个节点负责读和写。对某个节点进行配置文件设置,如下所示。
cluster-enabled:yes // 集群模式来启动
2. meet 操作
我们知道 Redis Cluster 是通过节点完成数据交换的,meet 操作则是这个过程的基础。
上图所示,A 和 C 之间有 meet 操作通讯数据,A 和 B 之间也有 meet 操作通讯,那么 B 和 C 之间也就可以相互通信。只要某个节点和另外的节点能够正常读写,那么任何节点之间其实也是可以相互进行数据交换的。
可以说,所有节点都可以共享消息。
3. 指派槽
我们只有对节点进行指派某个槽,每个 key 算出来的哈希值是否在某个槽内,它才能正常的读写。
如上图所示,当我们的 Redis Cluster 有三台机器的时候,把0~16383的槽平均分配给每台机器(Redis Cluster 制定给每个槽分配16384个槽,也就是0~16383)。每当 key 访问过来,Redis Cluster 会计算哈希值是否在这个区间里。它们彼此都知道对应的槽在哪台机器上。
客户端只需要计算一个 key 的哈希值,然后传给 Redis Cluster 就可以了。
4. 复制
Redis Cluster 是一个主从复制的过程。所以在这里就不解释了。
8.4 安装 Redis Cluster
安装 Redis Cluster 有两种方式,一种是原生安装,一种是官方工具安装。
1. 原生安装
原生安装首先是通过配置开启节点;其次是通过 meet,实现节点间相互通讯;再次是指配槽,只有通过指配槽才能实现客户端数据的基本访问;最后是主从配置,才能够实现故障转移。
- 配置开启 Redis
代码如下。
port 端口
daemonize yes // 守护进程方式启动
dir "/opt/redis/data/" // 目录
dbfilename "dump-3339.rdb" // 端口来区分
logfile "3339.log" //日志文件,使用端口区分
cluster-enabled yes //代表开启cluster
cluster-config-file nodes-3339.conf //clstuer开启各个节点的配置。
配置完以后可以使用下面的命令,分别开启每一个 cluster:
redis-server redis-端口.conf
但是以上命令是相互独立的。下面我们进行 meet 操作:
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7001
根据上面的命令自动感知两台,然后分别用上面的命令都和7000这个端口进行 meet 操作即可全部实现 meet 配置。
- Cluster 节点主要配置
配置代码如下。
cluster-enabled yes // 使用cluster
cluster-node-timeout 15000 // 默认超时配置
cluster-config-file "nodes.conf" //集群节点,端口来区分
cluster-require-full-coverage no //如果有一个节点坏掉了,对外就不提供服务了。默认是yes,必须配置为no。
- 分配槽
我们需要为每个端口配置对于的分配槽,代码如下。
redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {10923...16383}
根据有多少个端口,就分配好对应的槽就可以了。记住这个范围是0~16383。
- 设置主从
为了完成故障转移,就必须进行主从配置,代码如下。
redis-cli -h 127.0.0.1 -p 7001 cluster replicate ${nodeid-7000}
这里注意 nodeid 和我们单机讲解的 runid 是不同的,runid 重启以后是会改变的,但是 nodeid 却不会。关于如何获取 nodeid,我们将会在后续进行讲解。
2. 官方工具安装
Redis 官方给我们提供了官方安装工具,主要是通过 Ruby 安装。
- 安装 Ruby
下载对应的 Ruby,代码如下。
wget https://cache.ruby-lang.org/ruby/2.3/ruby-2.3.1.tar.gz
安装 Ruby,代码如下。
tar -xvf ruby-2.3.1.tar.gz
./configure -prefix=/usr/local/ruby //编译
make
make install //安装
cd /usr/local/ruby
cp bin/ruby /usr/local/bin //拷贝到此路径
cp bin/gem /usr/local/bin //拷贝到此路径
- 安装 Rubygem Redis
下载 Ruby 的 gem,代码如下。
wget http://rubygems.org/downloads/redis-3.3.0.gem
安装,代码如下。
gem install -l redis-3.3.0.gem
gem list --check redis gem
- 安装 redis-trib.rb
代码如下。
cp ${redis_home}/src/redis-trib.rb /usr/local/bin
通过复制就可以把 redis-trib.rb 文件拷贝到 bin 文件下,就可以使用了。
8.5 总结
最后,对本文的内容做下总结,主要有以下三点。
- 使用原生命令安装,理解 Redis Cluster 的架构。但是生产环境中不使用。
- 官方工具安装是非常简单和高效的。其实在生产环境中会使用脚本,会更加简单高效准确。一般生产环境都会使用。
- 其他部署还可以通过可视化部署,但是只存在少数企业中,大部分还是在使用官方工具安装的。
论以下七个问题。
- 缓存收益与成本的问题
- 缓存更新的策略
- 缓存颗粒的控制
- 缓存穿透的优化
- 无底洞问题的优化
- 缓存雪崩的优化
- 热点key的重建优化
9.1 缓存收益与成本的问题
关于缓存收益与成本主要分为三个方面的讲解,第一个是什么是收益;第二个是什么是成本;第三个是有哪些使用场景。
1. 收益
主要有以下两大收益。
- 加速读写:通过缓存加速读写,如 CPU L1/L2/L3 的缓存、Linux Page Cache 的读写、游览器缓存、Ehchache 缓存数据库结果。
- 降低后端负载:后端服务器通过前端缓存来降低负载,业务端使用 Redis 来降低后端 MySQL 等数据库的负载。
2. 成本
产生的成本主要有以下三项。
- 数据不一致:这是因为缓存层和数据层有时间窗口是不一致的,这和更新策略有关的。
- 代码维护成本:这里多了一层缓存逻辑,顾会增加成本。
- 运维费用的成本:如 Redis Cluster,甚至是现在最流行的各种云,都是费用的成本了。
3. 使用场景
使用场景主要有以下三种。
- 降低后端负载:这是对高消耗的 SQL,join 结果集和分组统计结果缓存。
- 加速请求响应:这是利用 Redis 或者 Memcache 优化 IO 响应时间。
- 大量写合并为批量写:比如一些计数器先 Redis 累加以后再批量写入数据库。
9.2 缓存的更新策略
主要有以下三种策略。
- LRU、LFU、FIFO 算法策略。例如 maxmemory-policy,这是最大内存的策略,当 maxmemory 最大时,会优先删除过期数据。我们在控制最大内存,让它帮我们去删除数据。
- 过期时间剔除,例如 expire。设置过期时间可以保证其性能,如果用户更新了重要信息,应该怎么办。所以这个时候就不适用了。
- 主动更新,例如开发控制生命周期。
这三个策略中,一致性最好的就是主动更新。能够根据代码实时的更新数据,但是维护成本也是最高的;算法剔除和超时剔除一致性都做的不够好,但是维护成本却非常低。
根据缓存的使用场景,我们会采用不同的更新策略。
实际开发中我给大家以下两个建议。
- 低一致性:最大内存和淘汰策略,数据库有些数据是不需要马上更新的,这个时候就可以用低一致性来操作。
- 高一致性:超时剔除和主动更新的结合,最大内存和淘汰策略兜底。你没办法确保内存不会增加,从而使服务不可用了。
9.3 缓存粒度问题
我们知道,用户第一次访问客户端,客户端访问 Redis 肯定是没有的,这个时候只能从数据库 DB 那里获取信息,代码如下。
select * from t_teacher where id= {id}
在 Redis 设置用户信息缓存,代码如下。
set teacher:{id} select * from t_teacher where id= {id}
这个时候我们来看看缓存粒度问题。
因为我们要更新全部属性。到底我们是采用 select * 还是仅仅只是更新你需要更新的那些字段呢?如下两段代码。
set key1 = ? from select * from t_teacher
set key1 = ? from select key1 from t_teacher
缓存粒度控制可以从以下三个角度来观察,通过这三点来决定如何选择。
- 通用性:全量属性更好。上面一个对比 * 和某个字段的查询,最好是通过全量属性,这样的话,select * 具有很好的通用性,因为如果你 select 某个字段的话,未来如果一旦业务改变,代码也要随之改变才可以。
- 占用空间:部分属性会更好。因为这样占用的空间是最小的。
- 代码维护上:表面上全量属性会更好。我们真的需要全量吗?其实我们在使用缓存的时候,优先考虑的是内存而不单单只是保证代码的扩展性。
9.4 缓存穿透问题
首先大家看下下面这张图。
当请求发送给服务器的时候,缓存找不到,然后都堆到数据库里。这个时候,缓存相当于穿透了,不起作用了。
原因有两点:
- 业务代码自身的问题。很多实际开发的时候,如果是一个不熟练的程序员,由于缺乏必要的大数据的意识,很多代码在第一次写的时候是 OK 的,但是当需要修改业务代码的时候,常常会出现问题。
- 恶意攻击和爬虫问题。网络上充斥着各种攻击和各种爬虫模仿着人为请求来访问你的数据。如果恶意访问穿透你的数据库,将会导致你的服务器瞬间产生大量的请求导致服务中止。
那我们去如何发现这些问题呢?
- 业务的相应时间:一般请求的时间都是稳定的,但是如果出现类似穿透现象,必然在短时间内有一个体现。
- 业务本身的问题。产品的功能出现问题。
- 对缓存层命中数、存储层的命中数这些值的采集。
1. 解决方案一:缓存空对象
当缓存中不存在,访问数据库的时候,又找不到数据,需要设置给 cache 的值为 null,这样下次再次访问该 id 的时候,就会直接访问缓存中的 null 了。
但是可能存在的两个问题。首先是需要更多的键,但是如果这个量非常大的话,对业务也是有影响的,所以需要设置过期时间;其次是缓存层和存储层数据“短期”不一致。当缓存层过期时间到了以后,可能会产生和存储层数据不一致的情况。这个时候需要使用一些消息队列等方式,来确保这个值的一致性。
下面的代码用 Java 来实现简单的缓存空对象。
public String getCacheThrough(String key){
String cacheValue = cache.get(key);
if(StringUtils.isBlank(cacheValue)){ // 如存储数据为空
String storageValue = storage.get(key);
cache.set(key,storageValue);//需要设置一个过期时间
if(StringUtils.isBlank(strageValue){
cache.expire(key.60*10);
}
return storageValue;
}else{
return cacheValue;
}
}
2. 解决方案二:布隆过滤器拦截
布隆过滤器,实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
类似于一个字典,你查词典的时候不需要把所有单词都翻一遍,而是通过目录的方式,用户通过检索的形式在极小内存中可以找到对应的内容。
虽然布隆过滤器可以通过极小的内存来存储,但是免不了需要一部分代码来维护这个布隆过滤器,并且经常需要根据规则来调整,在选取是否使用布隆过滤器,还需要通过场景来选取。
9.5 无底洞问题优化
无底洞问题就是即使加机器,性能却没有提升,反而降低了。到底这是怎么回事呢?先看下面的图。
当客户端增加一个缓存的时候,只需要 mget 一次,但是如果增加到三台缓存,这个时候则需要 mget 三次了,每增加一台,客户端都需要做一次新的 mget,给服务器造成性能上的压力。
同时,mget 需要等待最慢的一台机器操作完成才能算是完成了 mget 操作。这还是并行的设计,如果是串行的设计就更加慢了。
通过上面这个实例可以总结出:更多的机器!=更高的性能
但是并不是没办法,一般在优化 IO 的时候可以采用以下几个方法。
- 命令的优化。例如慢查下 keys、hgetall bigkey。
- 我们需要减少网络通讯的次数。这个优化在实际应用中使用次数是最多的,我们尽量减少通讯次数。
- 降低接入成本。比如使用客户端长连接或者连接池、NIO 等等。
1. 四种批量优化的方法
四种方法主要是:串行 mget、串行 IO、并行 IO、hash_tag。
- 串行 mget
如下图所示,串行 mget 就是根据 Redis 增加的台数,来 mget 多次网络时间。
- 串行 IO
如下图所示,根据 key 的增加,先在客户端组装成各种 subkeys,然后一次性根据 pipeline 方式进行传输,这样能有效的减少网络时间。
- 并行 IO
如下图所示,在串行 IO 的基础上,再根据并行打包,把请求一次性的传给 Redis 集群。
- hash_tag
如下图所示,用最极端的方式进行哈希传送给 Redis 集群。
总之,实际使用过程中,我们根据特定的业务场景,选定对应的批量优化方式,可以有效的优化。
9.7 热点 Key 重建优化
我们知道,使用缓存,如果获取不到,才会去数据库里获取。但是如果是热点 key,访问量非常的大,数据库在重建缓存的时候,会出现很多线程同时重建的情况。
如上图,就是因为高并发导致的大量热点的 key 在重建还没完成的时候,不断被重建缓存的过程,由于大量线程都去做重建缓存工作,导致服务器拖慢的情况。只有最后一个是重建完成,命中缓存。
为了解决以上的问题,我们着重研究了三个目标和两个解决方案。
三个目标为:
- 减少重建缓存的次数;
- 数据尽可能保持一致;
- 减少潜在的风险。
两个解决方案为
- 互斥锁
- 永不过期
我们根据三个目标,解释一下两个解决方案。
1. 互斥锁(mutex key)
由下图所示,第一次获取缓存的时候,加一个锁,然后查询数据库,接着是重建缓存。这个时候,另外一个请求又过来获取缓存,发现有个锁,这个时候就去等待,之后都是一次等待的过程,直到重建完成以后,锁解除后再次获取缓存命中。
那么这个过程是怎么做到的呢?请见下面代码演示。
public String getKey(String key){
String value = redis.get(key);
if(value == null){
String mutexKey = "mutex:key:"+key; //设置互斥锁的key
if(redis.set(mutexKey,"1","ex 180","nx")){ //给这个key上一把锁,ex表示只有一个线程能执行,过期时间为180秒
value = db.get(key);
redis.set(key,value);
redis.delete(mutexKety);
}else{
// 其他的线程休息100毫秒后重试
Thread.sleep(100);
getKey(key);
}
}
return value;
}
但是互斥锁也有一定的问题,就是大量线程在等待的问题。下面我们就来讲一下永远不过期。
2. 永远不过期
首先在缓存层面,并没有设置过期时间(过期时间使用 expire 命令)。但是功能层面,我们为每个 value 添加逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。这样的好处就是不需要线程的等待过程。见下图。
如上图所示,T1 时间无需等待,直接输出,到 T2 的时候,发现 value 已经到了过期时间,于是就开始构建缓存,还是输出旧值。到了 T3 已经是旧值,直到 T4 时间,构建缓存已经完成,直接输出新值。
这样就避免了上面互斥锁大量线程等待的问题。具体实现伪代码如下:
public String getKey(final String key){
V v = redis.get(key);
String value = v.getValue();
long logicTimeout = v.getLogicTimeout();
if(logicTimeout >= System.currentTimeMillis()){
String mutexKey = "mutex:key:"+key; //设置互斥锁的key
if(redis.set(mutexKey,"1","ex 180","nx")){ //给这个key上一把锁,ex表示只有一个线程能执行,过期时间为180秒
threadPool.execute(new Runable(){
public void run(){
String dbValue = db.getKey(key);
redis.set(key,(dbValue,newLogicTimeout));//缓存重建,需要一个新的过期时间
redis.delete(keyMutex); //删除互斥锁
}
};
}
}
}
互斥锁的优点是思路非常简单,具有一致性,其缺点是代码复杂度高,存在死锁的可能性。
永不过期的优点是基本杜绝 key 的重建问题,但缺点是不保证一致性,逻辑过期时间增加了维护成本和内存成本。