1 Redis概述
1.1前言
Redis是一个开源、支持网络、基于内存亦可持久化的日志型、键值对存储数据库。使用ANSI C编写。并提供多种语言的API。
其开发由VMware主持,是最流行的键值对存储数据库之中的一个。
Redis的一大特点就是速度异常快。官方发布的性能測试结果显示,每秒钟能够达到10万次的操作。
1.2安装和验证
在Redis的官网上。我们能够方便地下载Redis的各种版本号,其官网下载地址为:。
我们下载了redis的稳定版redis-2.8.9.tar.gz。
我们依次运行下面命令:
$ tar xzf redis-2.8.9.tar.gz
$ cd redis-2.8.9
$ make
在运行完以上命令后。会在同级文件夹下生成src文件夹。
再运行命令:
$ src/redis-server
就启动好了Redis。
高速验证服务是否启动成功能够运行下面命令:
$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"
2 Redis数据结构
Redis以键值的形式存储我们的数据。
键
Redis key值是二进制安全的。这意味着能够用不论什么二进制序列作为key值,比如字符串、或者一个JPEG的文件。
特殊地,空字符串是一个有效的key值。
另外。对于我们的系统应用,假设多个系统公用一个redis实例。为了避免键值冲突。一个解决的办法,就是在key中包括系统名称。比如有两个用户账号信息的key,分别为:system1.account_info.123456,system2.account_info.123456。
当然,后边我们会提到。通过database来区分key的空间。也是一个不错的方案。
值
redis提供了五种数据类型:String(字符串)、list(双向链表)、set(集合)、zset(有序集合)和hash(哈希类型)。
String(字符串):最常见的值,比如“aaa”、“{no:'1234',name:'张三'}”等等。
redis支持对其包含set、get、incr、append、strlen等操作。
list(双向链表):数组,多用于1个key相应多个value的场景。redis支持对其进行Lpop、Lset、Rpush等操作。
set(集合):也是一个key相应多个valude的场景。
但对其的操作。主要是和集合相关的。比如sadd、smove、sdiff、sunion等。
zset(有序集合):存储元素和set相近。可是内部以一个score来排序我们放进去的数据。
redis对其支持的操作有ZADD、ZRANGE、ZREVRANGE等操作。
hash(哈希):值又是一个key-value键值对的集合。假设整个redis相当于一个java里的HashMap的话,类型为hash的redis存储值又是一个HashMap。对其的操作包含HGET、HDEL、HSET、HKEYS等。
3 怎样使用Redis
现阶段,我们能够通过两种方式来使用redis:命令方式、client方式。
3.1 Redis命令
參考下列网址的说明 https://redis.readthedocs.org/en/latest/
3.2 Java client
Jedis是Redis首推的javaclient开发包。
该项目在git的源码在:https://github.com/xetorthio/jedis。
Jedis主要功能是对redis的全部命令操作进行封装。供java开发者调用。Jedis处理我们的每一个命令调用步骤例如以下:
a. 依据提供的ip、port、password连接redisserver。并持有连接。
b. 接收各个命令及其參数。
c. 对參数按utf8格式编码成byte[];
d. 将byte[]组装成符合redis协议格式的顺序,并加入redis格式要求的一些分隔符;比如:将byte[]形式的參数1{ 0x01, 0x02,0x03, 0x04 }和參数2{ 0x05, 0x06, 0x07, 0x08 }之间用“\r”和“\n”分开。(很多其它地关于redis协议的内容,请关注4.3.1Redis协议)
e. 通过发送TCP请求(socket)。将组装后的redis协议内容发送到redisserver运行。
f. 接收redis返回的符合redis协议的命令运行结果。通过utf8格式将byte[]转成str,解析出响应字符串。作为命令的运行结果返回给用户;
g. 假设须要,关闭连接。
在Jedis中,Jedis.class是提供给开发者使用的API类。Jedis.class继承自BinaryJedis.class。
前者接收明文的參数,比如“aa”,后者接收byte[]形式的參数。
假设我们调用Jedis.class中的Api。依据上述过程,我们能够看到。明码的參数会被转成byte[]形式的參数,终于调用BinaryJedis.class中的api完毕我们的命令运行。
面向redis管理的操作封装类包含:Client,Connection,Protocol。
鉴于Jedis的project代码比較简单,并且有非常多的范例和測试代码,在此仅仅是简单地说明一下project的包结构以及查找案例的方法。
redis.clients.jedis.tests.benchmark
正如包名一样,此包以下的代码为client的演示样例代码。假设刚接触jedis和redis,则能够直接改动当中的ip和port等,体验一下redis。
redis.clients.jedis.tests.commands
该包以下是对jedis的全部commond的单元測试案例。其測试的代码比較简洁,假设大家对jedis的某个命令使用不太明白,在此处搜索其用法。应该是一个不错的选择。
很多其它关于jedis的细节。我们能够直接看Jedis的源代码。
3.3 和Redis通信
接下来,我们看看假设哪天我们认为jedis不好用了,我们想自己写一套client。我们应该怎么来和redis通信交互。
首先,redis和外部通信。仅仅支持tcp协议,port默觉得6379。
其次,假设想要redis能解析你发给它的命令和參数,我们的命令和參数必须符合redis协议。
另外,redis回复给我们的响应信息。也是依照redis协议来组装。接下来,我们具体看看redis协议是怎么回事。
3.3.1 Redis协议
在这个协议中。全部发送至Redis server的參数都是二进制安全(binary safe)的。
我们先看一个实际的样例:
我们想发送一个set(“mykey”, “myvalue”)的命令给redis,那么这条命令终于会被转换成符合redis协议的形式(真正传给redis的。是byte[]型数据,这里仅仅是为了便于说明和看清问题,因而用没有转换)。其内容例如以下:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
我们能够对比协议的说明来解读下上述命令。
协议的格式为:
*<number of arguments> CR LF
$<number of bytes of argument 1> CR LF
<argument data> CR LF
...
$<number of bytes of argument N> CR LF
<argument data> CR LF
我们结合协议的格式解读一下上边实际的样例:
a. CR表示\r,LF表示\n;每一个Redis命令或者client和server之间传输的数据都以\r\n (CRLF)结束;
b. 第一个以*号開始的数字表示參数的个数;当中,我们的命令名称set是第一个參数;
c. 各个參数以$開始来表示。
d. 第一个$后紧接着的内容为第一个參数的长度;
e. 第一个$后的CRLF后的内容(”set”)为第一个參数的内容;
f. 以此内推,第二个參数的长度为5。值为mykey
g. 第三个參数的长度为7,值为myvalue
h. 不要忘了,最后一个參数后还要紧跟一个CRLF作为该数据结束的标志
另外,对于回复。redis也有一些规定,我们能够參照附属资料《redis大全》通信协议.回复、状态回复等章节的相关说明。
4 Redis事务
Redis通过MULTI、DISCARD、EXEC和WATCH四个命令来实现事务功能。
4.1Hello world
一个简单地使用事务的样例例如以下所看到的:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++""Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
4.2 開始事务MULTI
MULTI命令用于開始一个事务。
该命令的效果,即是将服务端上记录的client状态改为事务状态。
该命令返回“OK”。
4.3 加入要运行的命令
Redis服务端上的client状态改动为事务状态后。一个明显的差别就是:全部发送给服务端运行的命令(除了用于事务处理的四个命令,MULTI、DISCARD、EXEC和WATCH),不是立即运行以及返回运行结果。取而代之的是。这些命令会被redis放到一个命令队列中,并返回client“QUEUED”。
Redis上的事务命令队列是一个数组,每一个数组项包括三个属性:
1. 要运行的命令(cmd)。
2. 命令的參数(argv);
3. 參数的个数(argc)。
4.4 运行事务EXEC
假设client正处于事务状态,当client再发送EXEC命令到服务端时,Redis服务端的运行步骤例如以下:
1. 服务端会从client所保存事务命令队列中。以FIFO的顺序取出命令运行,而且把每条命令运行的结果保存到一个FIFO的回复队列中;
2.当事务命令队列中的所有命令都被运行完了以后。EXEC命令会将回复队列中的结果所有返回给client。
4.5 取消事务
DISCARD 命令用于取消一个事务,它清空client的整个事务队列,然后将client从事务状态调整回非事务状态,最后返回字符串OK给client,说明事务已被取消。
之前已经提到过。在client输入了EXEC命令开启一个事务后。和事务相关的MULTI,EXEC,DISCARD和WATCH四个命令不会被放到client的事务命令队列中。
那么假设在client已经开启一个事务的情况下。client输入上述四个命令,redis会怎么处理呢?我们下边来看看。
MULTI
Redis 的事务是不可嵌套的, 当client已经处于事务状态。 而client又再向server发送 MULTI 时, server仅仅是简单地向client发送一个错误, 然后继续等待其它命令的入队。 MULTI 命令的发送不会造成整个事务失败。 也不会改动事务队列中已有的数据。
WATCH
仅仅能在client进入事务状态之前运行。 在事务状态下发送 WATCH 命令会引发一个错误, 但它不会造成整个事务失败。 也不会改动事务队列中已有的数据(和前面处理 MULTI 的情况一样)。
当然,最后,EXEC会运行事务队列中的所有命令;DISCARD会清空事务队列中的命令,并改动client状态。
4.6带WATCH的事务
WATCH命令用于在事务開始之前监视随意数量的键:当调用EXEC命令运行事务时,假设随意一个被监视的键已经被其它client改动了,那么整个事务不再运行,直接返回失败。
下面演示样例展示了一个运行失败的事务样例:
redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC
(nil)
注意最后EXEC命令返回的不再是SET命令运行的结果。
4.7 WATCH命令的实现和触发
在每一个代表数据库的redis.h/redisDb 结构类型中,都保存了一个 watched_keys 字典。字典的键是这个数据库被监视的键,而字典的值则是一个链表。链表中保存了全部监视这个键的client。
比方说,下面字典就展示了一个 watched_keys 字典的样例:
当中。键 key1 正在被 client2 、 client5 和 client1 三个客户端监视, 其它一些键也分别被其它别的客户端监视着。
WATCH 命令的作用, 就是将当前client和要监视的键在 watched_keys 中进行关联。
举个样例。假设当前client为client10086,那么当client运行WATCH key1 key2时。前面展示的watched_keys 将被改动成这个样子:
通过watched_keys字典,假设程序想检查某个键是否被监视,那么它仅仅要检查字典中是否存在这个键就可以。假设程序要获取监视某个键的全部client。那么仅仅要取出键的值(一个链表),然后对链表进行遍历就可以。
在不论什么对数据库键空间(keyspace)进行改动的命令成功运行之后(比方FLUSHDB、SET、DEL、LPUSH、SADD、ZREM。诸如此类),multi.c/touchWatchedKey函数都会被调用——它检查数据库的watched_keys字典。看是否有client在监视已经被命令改动的键,假设有的话。程序将全部监视这个/这些被改动键的client的REDIS_DIRTY_CAS选项打开。
当client发送EXEC命令、触发事务运行时,server会对client的状态进行检查:
假设client的REDIS_DIRTY_CAS选项已经被打开,那么说明被client监视的键至少有一个已经被改动了,事务的安全性已经被破坏。server会放弃运行这个事务,直接向client返回空回复,表示事务运行失败。
假设REDIS_DIRTY_CAS选项没有被打开。那么说明全部监视键都安全。server正式运行事务。
4.8 事务状态和非事务状态下运行命令
不管在事务状态下, 还是在非事务状态下。 Redis 命令都由同一个函数运行, 所以它们共享非常多server的一般设置, 比方 AOF 的配置、RDB 的配置,以及内存限制,等等。
只是事务中的命令和普通命令在运行上还是有一点差别的,当中最重要的两点是:
1.非事务状态下的命令以单个命令为单位运行,前一个命令和后一个命令的client不一定是同一个;而事务状态则是以一个事务为单位,运行事务队列中的全部命令:除非当前事务运行完成。否则server不会中断事务。也不会运行其它client的其它命令。
2.在非事务状态下,运行命令所得的结果会马上被返回给client;而事务则是将全部命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给client。
4.9 很多其它的Redis事务说明
官方说明
依据Redis官方文档,redis事务有下面两个重要的保证:
1. 全部的事务命令队列中的命令会被顺序地运行;而且,在一个client的事务正在运行时,其它的client的请求都将不会运行。
2. 无论事务命令队列中是否有命令。Redis都是具有原子性的。
也就是说。EXEC命令都将运行队列中的全部命令。
基于此,发生下面错误时,redis的处理例如以下:
a.假设在MULTI命令运行之前。client的连接在断开了。则什么也不会运行。
b. 假设EXEC命令已经调用,而client的连接断开了,则全部的命令都将运行;
c. 假设事务命令队列中已经存在了一些待运行的命令。此时发生一些停机、断电等操作,则redis会按情况处理:
内存模式:假设Redis没有採取不论什么持久化机制。那么重新启动之后的数据库总是空白的,所以数据总是一致的。
RDB模式:在运行事务时,Redis不会中断事务去运行保存RDB的工作,仅仅有在事务运行之后,保存RDB的工作才有可能開始。
所以当RDB模式下的Redisserver进程在事务中途被杀死时,事务内运行的命令,无论成功了多少,都不会被保存到RDB文件中。
恢复数据库须要使用现有的RDB文件。而这个RDB文件的数据保存的是近期一次的数据库快照(snapshot)。所以它的数据可能不是最新的,但仅仅要RDB文件本身没有由于其它问题而出错,那么还原后的数据库就是一致的。
AOF模式:由于保存AOF文件的工作在后台线程进行,所以即使是在事务运行的中途。保存AOF文件的工作也能够继续进行,因此,依据事务语句是否被写入并保存到AOF文件,有下面两种情况发生:
假设事务语句未写入到AOF文件,或AOF未被SYNC调用保存到磁盘,那么当进程被杀死之后,Redis能够依据近期一次成功保存到磁盘的AOF文件来还原数据库,仅仅要AOF文件本身没有由于其它问题而出错,那么还原后的数据库总是一致的,但当中的数据不一定是最新的。
假设事务的部分语句被写入到AOF文件,并且AOF文件被成功保存,那么不完整的事务运行信息就会遗留在AOF文件中。当重新启动Redis时。程序会检測到AOF文件并不完整。Redis会退出,并报告错误。须要使用redis-check-aof工具将部分成功的事务命令移除之后。才干再次启动server。还原之后的数据总是一致的。并且数据也是最新的(直到事务运行之前为止)。
处理错误
在操作事务时。我们常常会发生下面两种错误:
1.在运行了MULTI命令,再往事务命令队列中加入命令时,可能会出现一些错误。比如加入的命令的參数个数不正确错误,甚至内存溢出等系统级错误;
2.在运行EXEC命令时,出现的一些错误。比如我们使用对Sting类型值的命令。但实际上对应key上存储的值是List。
对于第一种错误,client会明显地收到非“QUEUED”回复。此时,client最应该撤销该事务。另外,在redis2.6.5以后,假设client不做不论什么处理,服务端也记住了这样的错误。而且在运行EXEC命令时,返回错误信息。而且不会运行事务命令队列中的命令。
对于另外一种错误,剩下的其它命令会继续运行;而且在EXEC命令的返回值中,我们能够看到响应的错误信息。
持久性(Durability)补充
由于事务只是是用队列包裹起了一组Redis命令,并没有提供不论什么额外的持久性功能。所以事务的持久性由Redis所使用的持久化模式决定:
在单纯的内存模式下,事务肯定是不持久的。
在RDB模式下,server可能在事务运行之后、RDB文件更新之前的这段时间失败,所以RDB模式下的Redis事务也是不持久的。
在AOF的“总是SYNC”模式下,事务的每条命令在运行成功之后。都会马上调用fsync或fdatasync将事务数据写入到AOF文件。可是,这样的保存是由后台线程进行的。主线程不会堵塞直到保存成功,所以从命令运行成功到数据保存到硬盘之间。还是有一段很小的间隔,所以这样的模式下的事务也是不持久的。
其它AOF模式也和“总是SYNC”模式类似。所以它们都是不持久的。
5 订阅与公布
Redis通过PUBLISH、SUBSCRIBE等命令实现了订阅与公布模式,这个功能提供两种信息机制,各自是订阅/公布到频道和订阅/公布到模式。
频道,指详细要公布或订阅的消息的标示,比如news .sports。
模式,指能够匹配多个频道的表达式,比如news.*匹配频道news.sports,news.education等。
Redis眼下仅仅支持在线订阅。不支持durable式订阅。
5.1Hello world
先看一个简单订阅者的样例:
redis> psubscribe news.* tweet.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe" # 返回值的类型:显示订阅成功
2) "news.*" # 订阅的模式
3) (integer) 1 # 眼下已订阅的模式的数量
1) "psubscribe"
2) "tweet.*"
3) (integer) 2
1) "pmessage" # 返回值的类型:信息
2) "news.*" # 信息匹配的模式
3) "news.it" # 信息本身的目标频道
4) "Google buy Motorola" # 信息的内容
当然。在用户A订阅后,还要等用户B发送消息。我们再看一个公布者的样例:
redis> publish msg "good morning"
(integer) 1
公布消息后,将返回订阅者的数量。
5.2Redis的命令
Redis的订阅公布功能非常easy,我们这里简单地罗列一下各个命令的使用方法和说明,方便大家查阅。
PSUBSCRIBE pattern [pattern ...]
订阅一个或多个模式或频道。多个频道或模式之间用空格隔开。
PUBLISH channel message
将信息message发送到指定的一个频道channel。
PUBSUB <subcommand> [argument [argument ...]]
PUBSUB 是一个查看订阅与公布系统状态的内省命令。它由数个不同格式的子命令组成。
PUBSUB CHANNELS [pattern]
列出当前的活跃频道。活跃频道指的是那些至少有一个订阅者的频道。订阅模式的client不计算在内。
pattern 參数是可选的:
假设不给出pattern 參数,那么列出订阅与公布系统中的全部活跃频道。
假设给出pattern 參数,那么仅仅列出和给定模式pattern 相匹配的那些活跃频道。
PUBSUB NUMSUB [channel-1 ... channel-N]
返回给定频道的订阅者数量,订阅模式的client不计算在内。
PUBSUB NUMPAT
返回订阅模式的数量。注意,这个命令返回的不是订阅模式的client的数量。而是client订阅的全部模式的数量总和。
PUNSUBSCRIBE [pattern [pattern ...]]
指示client退订全部给定模式。假设没有模式被指定,也即是,一个无參数的PUNSUBSCRIBE调用被运行,那么client使用PSUBSCRIBE
命令订阅的全部模式都会被退订。在这样的情况下,命令会返回一个信息,告知client全部被退订的模式。
SUBSCRIBE channel [channel ...]
订阅给定的一个或多个频道的信息。
UNSUBSCRIBE [channel [channel ...]]
指示client退订给定的频道。假设没有频道被指定,那么client使用SUBSCRIBE命令订阅的全部频道都会被退订。在这样的情况下。命令会返回一个信息,告知client全部被退订的频道。
5.3订阅与公布使用补充说明
在实际使用redis-cli測试订阅与公布过程中,发现有些问题:
作为订阅者者(subscribe)的窗体,在订阅了模式或频道后,面临一个尴尬的问题:不能退出(或者说,没找到)订阅收听模式。
例如以下图所看到的:
也就是说,这个时候。我想取消订阅(unsubscribe)时,没有了输入命令的地方。
在jedis(redis的java客户端)中,找到了有关订阅与公布的測试代码redis.clients.jedis.tests.commands.PublishSubscribeCommandsTest。其逻辑还是比較简单。最终松了一口气,看到了怎么实际玩这个功能。
6 Redis的内存结构(database)
Redis数据库是真正存储数据的地方。当然,数据库本身也是存储在内存中的。
Databased的数据结构伪代码例如以下:
typedef struct redisDb {
// 保存着数据库以整数表示的号码
int id;
// 保存着数据库中的全部键值对数据
// 这个属性也被称为键空间(key space)
dict *dict;
// 保存着键的过期信息
dict *expires;
// 实现列表堵塞原语,如 BLPOP
// 在列表类型一章有具体的讨论
dict *blocking_keys;
dict *ready_keys;
// 用于实现 WATCH 命令
// 在事务章节有具体的讨论
dict *watched_keys;
} redisDb;
一个数据库,在内存中的数据结构例如以下图所看到的:
Database的内容要点包含:
1.数据库主要由 dict 和 expires 两个字典构成,当中 dict 保存键值对,而 expires 则保存键的过期时间。
2.数据库的键总是一个字符串对象,而值能够是随意一种 Redis 数据类型,包含字符串、哈希、集合、列表和有序集。
3.expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。
4.Redis 使用惰性删除和定期删除两种策略来删除过期的键。
a.更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。
b.当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。
c.当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到全部附属节点。
d.附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,这样能够保证主节点和附属节点的数据总是一致的。
数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。
它们的收缩策略是:当节点的填充百分比不足 10% 时,将可用节点数量降低至大于等于当前已用节点数量。
7 集群简单介绍
Redis集群能够实如今多个redis节点之间进行数据共享。
对于多个键的redis命令。不支持在集群环境里执行。(稍后说明为什么多个键的不行)
Redis集群带来的优点:
将数据自己主动切分到了多个节点上。
当集群中的一部分节点无法进行通讯时,仍然能够继续处理命令请求。
7.1 集群部署和验证
Redis集群部署大致分为下面步骤:
1.为每一个redis节点改动生成redis.conf配置文件:
port7000 //节点端口
cluster-enabledyes //集群开关
cluster-config-file nodes.conf //指定节点id的存储文件
cluster-node-timeout 5000 //集群中节点超时时间
appendonlyyes //aof文件读写模式
当中。
nodes.conf会在创建集群的过程中自己主动生成,并存储集群环境中各个node的id;该id是redis实例的唯一标示。在实例的整个生命周期内有效;
cluster-node-timeout会在后边一节提到其功能,主要用于节点间发现自己或别人有没有已经“不在”了。
2. 通过命令redis-server./redis.conf启动全部的节点实例;
3. 通过一个ruby的redis-trib工具脚本创建集群。
a. server上须要ruby的执行环境;
b. 工具的使用命令为:./redis-trib.rbcreate --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003127.0.0.1:7004 127.0.0.1:7005。命令解释为:将最后的6个server上的redis实例,创建成每一个master有1个从节点的集群环境。
c. redis-trib会尽量地分配主从节点在不同的ip机器上。
4. 正常情况下,此时会得到一个redis-trib工具为我们分配好的集群环境方案,假设我们认同。则输入“yes”。
5. 最后。出现例如以下日志信息时,表示集群环境搭建成功:
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
集群环境的验证:
使用redis自带的client工具redis-cli。依次输入下面命令:
$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to
redis-cli -c -p 7000命令表示:使用集群环境。且连接7000port的redis实例。
通过运行每一个命令的提示信息,我们能够看到集群环境已经生效了。
7.2Redis集群内部原理
数据共享
新的redis版本号,採用数据分片(sharding)而非一致性哈希来实现:一个Redis集群包括16384个哈希槽(hash slot),数据库中的每一个键都属于这16384个哈希槽的当中一个。集群使用公式CRC16(key)%16384来计算键key属于哪个槽,当中CRC16(key)语句用于计算键key的CRC16校验和。
集群中的每一个节点负责处理一部分哈希槽。举个样例。一个集群能够有三个哈希槽,当中:
a.节点A负责处理0号至5500号哈希槽。
b.节点B负责处理5501号至11000号哈希槽。
c.节点C负责处理11001号至16384号哈希槽。
这样的将哈希槽分布到不同节点的做法使得用户能够非常easy地向集群中加入或者删除节点。比方说:
d.假设用户将新节点D加入到集群中,那么集群仅仅须要将节点A、B、C中的某些槽移动到节点D就能够了。
e.与此类似,假设用户要从集群中移除节点A,那么集群仅仅须要将节点A中的全部哈希槽移动到节点B和节点C,然后再移除空白(不包括不论什么哈希槽)的节点A就能够了。
由于将一个哈希槽从一个节点移动到还有一个节点不会造成节点堵塞,所以不管是加入新节点还是移除已存在节点,又或者改变某个节点包括的哈希槽数量,都不会造成集群下线。
主从复制
为了使得集群在一部分节点下线或者无法与集群的大多数节点进行通讯的情况下,仍然能够正常运作,Redis集群对节点使用了主从复制功能:集群中的每一个节点都有1个至N个复制品(replica),当中一个复制品为主节点(master),而其余的N-1个复制品为从节点(slave)。
在之前列举的节点A、B、C的样例中,假设节点B下线了,那么集群将无法正常执行,由于集群找不到节点来处理5501号至11000号的哈希槽。
还有一方面,假如在创建集群的时候(或者至少在节点B下线之前),我们为主节点B加入了从节点B1。那么当主节点B下线的时候。集群就会将B1设置为新的主节点,并让它取代下线的主节点B,继续处理5501号至11000号的哈希槽,这样集群就不会由于主节点B的下线而无法正常运作了。
只是假设节点B和B1都下线的话。Redis集群还是会停止运作。
一致性保证
Redis集群不保证数据的强一致性(strong consistency):在特定条件下,Redis集群可能会丢失已经被运行过的写命令。
使用异步复制(asynchronous replication)是Redis集群可能会丢失写命令的当中一个原因。考虑下面这个写命令的样例:
a.client向主节点B发送一条写命令。
b.主节点B运行写命令,并向client返回命令回复。
c.主节点B将刚刚运行的写命令复制给它的从节点B1、B2和B3。
Note:假设真的有必要的话,Redis集群可能会在将来提供同步地(synchronou)运行写命令的方法。
Redis集群第二种可能会丢失命令的情况是:集群出现网络分裂(network partition)。而且一个client与至少包含一个主节点在内的少数(minority)实例被孤立。
举个样例,如果集群包括A、B、C、A1、B1、C1六个节点,当中A、B、C为主节点,而A1、B1、C1分别为三个主节点的从节点。另外另一个clientZ1。
如果集群中发生网络分裂,那么集群可能会分裂为双方,大多数(majority)的一方包括节点A、C、A1、B1和C1。而少数(minority)的一方则包括节点B和clientZ1。
在网络分裂期间。主节点B仍然会接受Z1发送的写命令:
a.假设网络分裂出现的时间非常短,那么集群会继续正常执行;
b.可是,假设网络分裂出现的时间足够长,使得大多数一方将从节点B1设置为新的主节点,并使用B1来取代原来的主节点B,那么Z1发送给主节点B的写命令将丢失。
注意,在网络分裂出现期间,clientZ1能够向主节点B发送写命令的最大时间是有限制的。这一时间限制称为节点超时时间(nodetimeout),是Redis集群的一个重要的配置选项:
c.对于大多数一方来说,假设一个主节点未能在节点超时时间所设定的时限内又一次联系上集群。那么集群会将这个主节点视为下线,并使用从节点来取代这个主节点继续工作。
d.对于少数一方。假设一个主节点未能在节点超时时间所设定的时限内又一次联系上集群,那么它将停止处理写命令,并向client报告错误。