一、redis 基础

1.1 导言

若是你历来没使用过 Redis 数据库,那你确定会问,为何咱们要学 Redis数据库,我只使用 MySQL 或 Oracle 就够了。其实 Redis 虽叫数据库,可又不是传统意义上的关系型数据库,Redis 是一个高性能的 Key-Value 数据库。javascript

首先咱们先来说一下 Redis 的历史。Redis 实际上是做者 Salvatore Sanfilippo 为了解决实际问题而创造出来的。当时做者 Salvatore 有这么一个需求,就是多个网站不断向服务器发送页面,而服务器须要为每一个网站保存必定数量的最新页面记录,同时经过网页将数据实时给用户看到。可是不管 Salvatore 如何优化,都很难在关系数据库里让小虚拟机处理大负荷的负载。最终他打算本身写一个内存数据库,能对列表的两端执行常数时间复杂度的弹出和推入操做,并加上子进程的持久化操做,因而 Redis 就诞生了。php

到了今天,Redis 已经进入了成熟期。数以千计的开发者都在开发和使用这个数据库,Redis 拥有很是完善的文档。我记得第一次使用 Redis,是为了在保存有数十百万用户的关系数据库里对某个条件进行查询。你们知道,要想在几百万用户中找到某条数据,是很难经过关系数据库在十几秒查询到的。因而我选择了 Redis,在不断优化后每次操做能够控制在 1 秒钟甚至更短,带给我至关大的震撼。css

本教程不但教给你一些基本的使用,同时也会根据我多年总结的技巧解决平常生产环境上优化和排错的问题。特别是后期的数据库优化和集群的讲解,但愿对各位进行 Redis 开发有必定帮助。前端

1.2 认识 Redis

在 Redis 以前,不少互联网公司会使用 MySQL + Memcached 架构,这个架构虽然适合于海量数据存储,但随着业务的增长,会出现不少问题,例如,MySQL 数据库常常拆表,致使 Memcached 也不断扩容;同步问题;命中率低,致使直接穿透 Memcached 进入 DB 查询,DB 资源池是有限的,进而宕机。这些问题都会致使Memcached其实并很差用。java

Redis 就在这种时代背景中产生,你会发现 Memcached 遇到的问题都被 Redis 给解决了。若是你用过 Memcached,你就会感觉到 Redis 绝对不是简单的 Key-Value 数据,还有 List、Set、哈希等各类数据类型的存储,同时支持冷热备份和主从复制,不但解决了数据库的容错,还能轻易地将数据分布到多个 Redis 实例中。node

1. Redis 特性

那么 Redis 有哪些具体特性呢?大体可分为以下八大特性。mysql

  • 速度极快。官方给出的数据是 10 万次 ops 的读写,这主要归功于这些数据都存在于内存中。因为 Redis 是开源的,当你打开源代码,就会发现 Redis 都是用 C 语言写的,C 语言是最接近计算机语言的代码,并且只有区区 5 万行,保证了 Redis 的速度。同时一个 Redis 只是一个单线程,其真正的缘由仍是由于单线程在内存中是效率最高的。ios

  • 持久化。Redis 的持久化能够保证将内存中的数据每隔一段时间就保存于磁盘中,重启的时候会再次加载到内存。持久化方式是 RDB 和 AOF。nginx

  • 支持多种数据结构。分别支持哈希、集合、BitMaps,还有位图(多用于活跃用户数等统计)、HyperLogLog(超小内存惟一值计数,因为只有 12K,是有必定偏差范围的)、GEO(地理信息定位)。git

  • 支持多种编程语言。支持 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 实现起来显然更为轻松。

enter image description here

  • 计数器

如转发数、评论数,有了原子递增(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,执行命令”这一阶段。同时客户端超时不必定是慢查询,但慢查询倒是客户端超时的一个缘由。最后,执行完后返回。

enter image description here

请你们认识好这个模型,在实际开发中,咱们须要对异常进行判断,这时,该流程就显得很是重要。

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。以下图所示:

enter image description here

  • 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 在内存中的第一个二进制是多少。

enter image description here

如上图所示,上面的英文字母在内存里都是 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。

订阅者只要订阅这个频道,就能够实时得到信息,见下图。

enter image description here

说明:当发布者发送一条消息到 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 消息队列是抢的模式,就是只有一个用户能收到,谁网速快,谁人品好,谁就能得到那条消息,做为开发者咱们须要了解需求,若是只须要一个消息订阅者收到,那么就能够使用消息队列,以下图所示。

enter image description here

这种场景好比抢红包、谁中奖这种模式,就能够使用消息队列了。

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 须要恢复时,就是从硬盘再到内存的过程。

enter image description here

由上图可知,持久化就是把内存中的数据保存到硬盘中的过程。,由于 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(同步)

即其余的命令都要排队。

enter image description here

若是有 1000 万条数据,执行 save 命令,Redis 就会对 1000 万条数据打包,而这个时候是同步的,Redis 就会进入阻塞状态。这也是 save 的缺点,接下来咱们讲异步命令 bgsave。

2. bgsave(异步命令)

即返回 OK,后台会新开一个线程去执行。

enter image description here

上图可知,客户端会在 Redis 发出 bgsave 命令,另外开一个进程调用 fork() 方法,这个进程同时会建立 RDB 文件,也就是咱们上面提到的自动触发 RDB 文件机制。这样,你在父进程里操道别的命令,就不会受影响了。

3. 自动触发 RDB 文件

快照方式虽然是 Redis 自动的,可是若是 Redis 服务器挂掉后,那么最近写入的,是不会被拷入快照中的。因此 RDB 存在两方面的缺点。

  • 耗时耗性能:Redis 写 RDB 文件是 dump 操做,因此须要的时间是 O(n),须要全部命令都执行一遍,很是耗时耗性能。

  • 容易丢失数据和不可控制:若是咱们在某个时间 T1 内写多个命令,这个时候 T2 时间执行 RDB 文件操做,T3 时间又执行多个命令,T4 时间就会出现宕机了。那么 T3 到 T4 的数据就会丢失。

enter image description here

虽然 RDB 有缺陷,可是依然在生产环境中会使用,RDB 适合冷备,就是当用户数据不高的时候,好比在午夜时分就可以使用 RDB 备份。其余时候咱们一般使用 AOF 日志备份。

5.3 日志 AOF

AOF(Append-only File)是用日志方式,通俗点讲就是当写一条命令的时候,如 Set 某个值,Redis 就会去日志里写一条 Set 某个值的语句。以下图:

enter image description here

当服务器宕机后,Redis 就会调用 AOF 日志文件,而且这个过程通常是实时的,不须要时间消耗。

enter image description here

上图表示客户端向 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 上一次启动和重写的尺寸(字节)

enter image description here

由上图可知,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,有时启动的时候会出现警告的状况,这个时候须要配置这个命令。

enter image description here

  • 硬盘

    开销:因为大量的 AOF 和 RDB 文件写入,致使硬盘开销大,建议使用 iostat、iotop 分析硬盘状态。

    优化:

  • 不要和高硬盘负载部署到一块儿,好比存储服务、消息队列等等;

  • 配置文件中的 no-appendfsync-on-rewrite 设置成 yes;

  • 当写入量很大的时候,建议更换 SSD 硬盘;

  • 单机多实例持久化文件考虑硬盘分配分盘。

3. AOF 追加阻塞

咱们若是使用 AOF 策略,一般就会使用每秒刷盘的策略(everysec),主线程会阻塞,直到同步完成。首先咱们知道主线程是很是宝贵的资源,其次咱们每秒刷盘实际上未必是 1 秒,多是 2 秒的数据。

enter image description here

咱们如何定位 AOF 阻塞?

  • 经过 Redis 日志

enter image description here

上图能够看到, Redis 日志会出现上述的语句,告诉你异步 IO 同步时间太长,你的硬盘是否有问题,同时会拖慢 Redis。

  • 固然除了上述的问题,你还能够用 Redis 的 info 方式来肯定问题。
info rersistence // 直接在命令中打这个命令便可。 ... aof_delayed_fsync : 100 // 会记录你发生阻塞的次数,每一次加 1 ... ... 

可是这个命令没法看到当前的问题,由于它是历史的累计值。

固然你还能够使用 Liunx 命令 top。

enter image description here

上图能看到 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 什么是主从复制

enter image description here

如上图所示,咱们将 Redis 服务器做为 Master 主库,另一台做为 Slave 从库,主库只是负责写数据,每次有数据更新的时候,Redis 会将数据同步到其余的从库中,而从库只是负责读数据。

固然你还能够根据业务需求,增长更多的从库,以下图所示,红色的 Redis 为主库,蓝色的是三台从库。

enter image description here

不少小伙伴都以为这没什么好说的,但在这里我仍是想说这么作的两大好处。

  • 实现了读写分离,读写分离不只能够提升服务器的负载能力,同时可根据需求的变化,改变从库的数量,第一张图中只有一个从库,你还能够像第二张图那样增长至两个、三个……你以为这个优势怎么样?

  • 数据备份了多份,若是一台机器宕机,你能够从其余机器上快速恢复。但须要注意的是一台主库能够拥有多个从库,但一个从库却只能隶属于一个主库。

6.2 复制的配置

接下来,咱们讲一下主从复制的做用。首先请看下面这张图。

enter image description here

如该图所示,Master 属于主节点,Slave 属于从节点。咱们在主机上执行 set 和 incr 命令,在从库中经过 get 获得这些数据,这正好说明 Redis 复制是自动完成的。当 Master 节点宕机之后,Slave节点能够支援主节点。

除了一主一从,Redis 还支持一主多从,这样咱们就能够得到更多高可用的可能性。好比,当一个主机和一个从机都宕机了,那么还有几台从机能够作备份;再好比,当 Master 节点的流量超过最大值的时候,能够经过从机对流量进行分流。

总而言之,主从复制的做用主要是提供数据副本及扩展 Redis 读的性能。

接下来,咱们再来配置一下主从架构,这是为一些刚开始学习的朋友准备的,经过实际的配置,以便于你们更好的理解。

  • 安装两台服务器的实例,关于如何安装 Redis,我在第一篇文章已经讲到。我将主库端口设置为6379,从库设置为 6380。bind 都设置为 127.0.0.1。

enter image description here

  • 在 Slave 实例中咱们增长了 slaveof 127.0.0.1 6379 的配置,从库配置相同。以下图所示。

enter image description here

配置后,须要启动这两个实例,如输出下面内容,说明主从复制的架构已经配置成功。

enter image description here

这里惟一须要说明的是,主库和从库的端口号不能相同,不然不可能同时启动。

6.3 全量复制和部分复制

在讲解全量复制和部分复制以前,咱们先来说一下,runid 和偏移量的概念。

什么是 runid,每次 Redis 启动的时候,Redis 就会有一个运行的 ID,这个 ID 只在 Redis 运行的时候才有,若是关闭 runid 就不存在了。runid 的做用是一个标识,若是主库去复制从库的数据,就须要根据这个 runid 去复制。

enter image description here

上图所示,经过 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。

enter image description here

上图所示,咱们首先运行 redis-cli -p 6379 info replication,就能在里面找到 master_repl_offset:1865,而后执行一次 set 操做,再运行 redis-cli -p 6379 info replication,此时就能看到偏移值变成了 1950。若是在其余从库中运行,也会变成 1950,偏移量是记录部分复制的依据。你们只要了解便可,生产环境中不怎么关心这个值。

1. 全量复制

咱们来看下 Redis 全量复制的流程图。

enter image description here

如图所示:

第一步,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 出现抖动(至关于断开链接)的时候,能够有一些机制将复制的损失下降到最低。

enter image description here

如图所示:

第一步,若是打算抖动(链接断开 connection lost);

第二步,Master 仍是会写 repl_back_buffer(复制缓冲区);

第三步,Slave 会继续尝试链接主机;

第四步,Slave 会把本身当前 runid 和偏移量传输给主机 Master,而且执行 pysnc 命令同步;

第五步,若是 Master 发现你的偏移量在缓冲区的范围内,就会返回 continue 命令;

第六步,同步了 offset 的部分数据,因此部分复制的基础就是偏移量 offset。

经过部分复制,能够有效的减小全量复制的开销。

6.4 故障如何处理

讲解完主从复制和部分复制,咱们来说一下 Redis 复制的故障如何处理。在开发运维的时候,故障是不可避免的。若是采用单机 Redis ,一旦经过短信或者电话告知你错误时,你多会一脸茫然,这种故障一般发生在半夜。

但若是咱们使用了自动故障转移,那效果就不一样了。即便半夜出现故障,你仍是能够好好休息,等早上起来再去完成故障排除。

主要考虑主机 Master 宕机、从机 Slave 宕机两种状况。

enter image description here

上图所示,一主二从的结构,若是某一台从机宕机了。这个时候咱们须要把客户端传到宕机的请求改为第一个 Slave 从机就能够了。基本不会有什么问题。

但若是是 Master 宕机,Master 就会和两个 Slave 从机断掉,这个时候该怎么办呢?以下图所示。

enter image description here

当 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 从机读数据。

enter image description here

虽然读写有优点,可以让读这部分分配给各个 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 从机都要作一次全量复制。这将引发单节点和单机器的复制风暴,开销会很是大。

enter image description here

  • 单节点复制风暴。当主节点重启,多从节点会复制。这个时候须要更换复制拓扑。上图就是改变拓扑结构的问题,经过在 Slave 下再分从机,能够有效的减小主机 Master 的压力。

enter image description here

  • 单机器的复制风暴。如上图,若是每一个 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 从机。

enter image description here

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。以下图所示。

enter image description here

第二步,当客户端找到 Sentinel-2 节点的时候,Client 会经过 get-master-addr-by-name 命令获取 masterName,这个时候,Sentinel-2 会获取真正的名称和地址。以下图所示。

enter image description here

第三步,Client 获取到 Master 节点的时候,还会发出 role 或 role replication 命令,验证是否是 Master 节点,Sentinel-2 会返回这个节点信息加以验证。以下图所示。

enter image description here

第四步,若是 Sentinel 感知到 Master 宕机了,这时 Sentinel 集群应该是最早知道的。客户端和 Sentinel 集群之间实际上是发布订阅,客户端 Client 去订阅某个 Sentinel 的频道,若是哪一个 Sentinel 发现 Master 发生了变化,Client 是会被通知到这个信息的,以下图所示。可是要注意这个不是代理模式

enter image description here

总结一下,以上四步就是客户端和 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. 高可用读写分离

你们知道,从节点是高可用的基础,它的扩展功能是读的能力。咱们先来看下面这张高可用读写分离使用以前的图。

enter image description here

如上图所示,Sentinel 实际上是对 Master 作故障转移,对 Slave 只有下线的操做。Sentinel 集群是不会对 Slave 作故障转移的。那么咱们应该怎么作呢?

其实咱们须要使用一个客户端去监控 Slave,和 Master 相似。主要运用如下命令。

  • switch-master:这个命令用来切换主节点,从节点晋升为主节点的操做;
  • covert-to-slave:切换从节点,原来主节点须要降为从节点的时候使用该命令。
  • sdown:这个命令在主观下线时使用。

咱们再来看看使用高可用读写分离以后的图。

enter image description here

如图所示,和第一张不一样的是,咱们把 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. 顺序分布

顺序分布就是把一整块数据分散到不少机器中,以下图所示。

enter image description here

顺序分布通常都是平均分配的。

2. 哈希分区

以下图所示,1~100这整块数字,经过 hash 的函数,取余产生的数。这样能够保证这串数字充分的打散,也保证了均匀的分配到各台机器上。

enter image description here

二者的区别,请见下表。

分布 特色 特色
顺序分布 数据分散容易倾斜、键值业务相关、可顺序访问、支持批量 Big Table、HBase
哈希分布 数据分散度高、键值分布业务无关、支持批量、没法顺序访问 一致性哈希、redis cluster、其余缓存

由上表可知,哈希分布和顺序分布只是场景上的适用。哈希分布不能顺序访问,好比你想访问1~100,哈希分布只能遍历所有数据,同时哈希分布由于作了 hash 后致使与业务数据无关了。而顺序分布是会致使数据倾斜的,主要是访问的倾斜。每次点击会重点访问某台机器,这就致使最后数据都到这台机器上了,这就是顺序分布最大的缺点。其余的特色见表可知。

但哈希分布实际上是有个问题的,当咱们要扩容机器的时候,专业上称之为“节点伸缩”,这个时候,由于是哈希算法,会致使数据迁移。在节点取余的时候,迁移数量和添加的节点数是有关的,这个时候建议使用翻倍扩容。

这里须要说明的是节点取余究竟是什么。

好比以前是三个节点,那么如今加一个节点,就是四个节点。这个时候哈希算法就是从3的取余变成了4的取余,这个时候,原来的数字所在的位置确定是须要发生变化的,总体的数据基本上都是作了漂移。

总体的数据漂移实际上是有问题的,对数据库的性能,硬件上都是考验,因此为了减小总体的数据漂移,咱们就须要对哈希算法有个一致性哈希算法

3. 一致性哈希

enter image description here

上图就是一个一致性哈希的原理解析。假设咱们有 n1~n4 这四台机器,咱们对每一台机器分配一个惟一 token,每次有数据(图中黄色表明数据),一致性哈希算法规定每次都顺时针漂移数据,也就是图中黄色的数据都指向 n3。这个时候咱们须要增长一个节点 n5,在 n2 和 n3 之间,数据仍是会发生漂移,可是这个时候你是否注意到,其实只有 n2~n3 这部分的数据被漂移,其余的数据都是不会变的,这样就实现了部分漂移,而没有对全部数据进行漂移的弊端了。

最后咱们来介绍另一个哈希算法,就是 Redis Cluster 的哈希算法 —— 虚拟槽分区。咱们经过下面这张图来解释。

enter image description here

如上图所示,咱们知道槽的范围是0~16383,若是此时有五个节点,key 会经过 CRC16 哈希算法,对16383取余,而后保存到 Redis Cluster 里,Redis Cluster 会根据数据判断是不是这个虚拟槽里的数据,若是不是这个槽范围的,因为 Redis Cluster 数据是共享的,因而就会告知数据应该存到那台机器上。

若是你对虚拟槽理解还有问题,这是正常的。下面咱们将经过讲解 Redis Cluster 的基本架构、安装使用让你真正认识虚拟槽的概念。

8.3 基本架构

先来看看,什么是分布式架构。分布式架构是一种彼此通信的架构,经过每一个节点之间负责对应的槽,每一个节点都负责读写。以下图

enter image description here

那么 Redis Cluster 是怎样的架构呢?实际上咱们能够经过安装来了解 Redis Cluster 的架构。了解Redis Cluster 架构,咱们从了解节点、meet操做、指派槽、复制四个知识点开始。

1. 节点

Redis Cluster 是有不少节点的,每一个节点负责读和写。对某个节点进行配置文件设置,以下所示。

cluster-enabled:yes // 集群模式来启动 

2. meet 操做

咱们知道 Redis Cluster 是经过节点完成数据交换的,meet 操做则是这个过程的基础。

enter image description here

上图所示,A 和 C 之间有 meet 操做通信数据,A 和 B 之间也有 meet 操做通信,那么 B 和 C 之间也就能够相互通讯。只要某个节点和另外的节点可以正常读写,那么任何节点之间其实也是能够相互进行数据交换的。

能够说,全部节点均可以共享消息。

3. 指派槽

咱们只有对节点进行指派某个槽,每一个 key 算出来的哈希值是否在某个槽内,它才能正常的读写。

enter image description here

如上图所示,当咱们的 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 的架构。可是生产环境中不使用。
  • 官方工具安装是很是简单和高效的。其实在生产环境中会使用脚本,会更加简单高效准确。通常生产环境都会使用。
  • 其余部署还能够经过可视化部署,可是只存在少数企业中,大部分仍是在使用官方工具安装的。

论如下七个问题。

  1. 缓存收益与成本的问题
  2. 缓存更新的策略
  3. 缓存颗粒的控制
  4. 缓存穿透的优化
  5. 无底洞问题的优化
  6. 缓存雪崩的优化
  7. 热点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 缓存穿透问题

首先你们看下下面这张图。

enter image description here

当请求发送给服务器的时候,缓存找不到,而后都堆到数据库里。这个时候,缓存至关于穿透了,不起做用了。

缘由有两点:

  • 业务代码自身的问题。不少实际开发的时候,若是是一个不熟练的程序员,因为缺少必要的大数据的意识,不少代码在第一次写的时候是 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 无底洞问题优化

无底洞问题就是即便加机器,性能却没有提高,反而下降了。到底这是怎么回事呢?先看下面的图。

enter image description here

当客户端增长一个缓存的时候,只须要 mget 一次,可是若是增长到三台缓存,这个时候则须要 mget 三次了,每增长一台,客户端都须要作一次新的 mget,给服务器形成性能上的压力。

同时,mget 须要等待最慢的一台机器操做完成才能算是完成了 mget 操做。这仍是并行的设计,若是是串行的设计就更加慢了。

经过上面这个实例能够总结出:更多的机器!=更高的性能

可是并非没办法,通常在优化 IO 的时候能够采用如下几个方法。

  • 命令的优化。例如慢查下 keys、hgetall bigkey。
  • 咱们须要减小网络通信的次数。这个优化在实际应用中使用次数是最多的,咱们尽可能减小通信次数。
  • 下降接入成本。好比使用客户端长链接或者链接池、NIO 等等。

1. 四种批量优化的方法

四种方法主要是:串行 mget、串行 IO、并行 IO、hash_tag。

  • 串行 mget

以下图所示,串行 mget 就是根据 Redis 增长的台数,来 mget 屡次网络时间。

enter image description here

  • 串行 IO

以下图所示,根据 key 的增长,先在客户端组装成各类 subkeys,而后一次性根据 pipeline 方式进行传输,这样能有效的减小网络时间。

enter image description here

  • 并行 IO

以下图所示,在串行 IO 的基础上,再根据并行打包,把请求一次性的传给 Redis 集群。

enter image description here

  • hash_tag

以下图所示,用最极端的方式进行哈希传送给 Redis 集群。

enter image description here

总之,实际使用过程当中,咱们根据特定的业务场景,选定对应的批量优化方式,能够有效的优化。

9.7 热点 Key 重建优化

咱们知道,使用缓存,若是获取不到,才会去数据库里获取。可是若是是热点 key,访问量很是的大,数据库在重建缓存的时候,会出现不少线程同时重建的状况。

enter image description here

如上图,就是由于高并发致使的大量热点的 key 在重建还没完成的时候,不断被重建缓存的过程,因为大量线程都去作重建缓存工做,致使服务器拖慢的状况。只有最后一个是重建完成,命中缓存。

为了解决以上的问题,咱们着重研究了三个目标和两个解决方案。

三个目标为:

  • 减小重建缓存的次数;
  • 数据尽量保持一致;
  • 减小潜在的风险。

两个解决方案为

  • 互斥锁
  • 永不过时

咱们根据三个目标,解释一下两个解决方案。

1. 互斥锁(mutex key)

由下图所示,第一次获取缓存的时候,加一个锁,而后查询数据库,接着是重建缓存。这个时候,另一个请求又过来获取缓存,发现有个锁,这个时候就去等待,以后都是一次等待的过程,直到重建完成之后,锁解除后再次获取缓存命中。

enter image description here

那么这个过程是怎么作到的呢?请见下面代码演示。

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 添加逻辑过时时间,当发现超过逻辑过时时间后,会使用单独的线程去构建缓存。这样的好处就是不须要线程的等待过程。见下图。

enter image description here

如上图所示,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 的重建问题,但缺点是不保证一致性,逻辑过时时间增长了维护成本和内存成本。

相关文章
相关标签/搜索