基于内存的NoSQL数据库。提供五种数据结构的存储。字符串、列表、集合、有序集合、散列表。Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
Redis 适用场景
缓存
适用 Redis 作为缓存,将热点数据放到内存中。
消息队列
Redis 的 List 类型是双向链表,很适合用于消息队列。
计数器
Redis 这种内存数据库才能支持计数器的频繁读写操作。
好友关系
使用 set 类型的交集很容易就可以知道两个用户的共同好友。
数据库
单纯作为一个读取快的数据库
数据类型
String
我们来看Redis中是如何定义字符串的
struct sdshdr {
//字符长度,记录buf数组中已使用的字节数量
unsigned int len;
//当前可用空间,记录buf数组中未使用的字节数量
unsigned int free;
//具体存放字符的buf
char buf[];
};
C语言中的字符表示,并不能满足Redis对字符串在安全性、效率以及功能方面的要求,具体体现在一下几点:
- 获取字符串长度的时间复杂度
在C语言中,要获取一个字符串的长度要对字符串进行遍历,其时间复杂度为O(N),而SDS本身记录了字符串的长度即len属性,所以获取一个字符串的长度的实践复杂度为O(1),特别是Redis的使用环境中存在大量、频繁的字符串操作,如果每次都调用strlen将会严重影响系统性能。 - 缓冲区溢出
在C语言中,我们要对一个字符串进行连接操作是,很容易造成缓冲区溢出,而redis设置free变量会在连接操作的时候自动检查空间是否足够,不够空间系统会自动分配 -
内存分配与释放
(1) 空间预分配:
当对字符串进行拼接操作时,Redis不仅分配给满足拼接操作所必要的空间,通常还会额外分配一定量的空间供下次拼接操作使用,避免每次拼接操作进行过多的内存重分配。
(2) 分配原则:
如果操作后的字符串长度 < 1MB ,则len的长度和free的长度一样,也就是会额外的分配一倍的空间(具体为什么这么设定还有待考究)
如果操作后的字符串长度 >= 1MB,则Redis会分配额外的1MB未使用空间
(3) 惰性空间释放:
在对字符串进行缩减操作时,Redis不会立即回收缩减掉的部分空间,而是使用free字段记录下来,供下次使用,同时,Redis也提供了相应的API,可以在需要的时候释放掉这些空间,以免造成内存浪费。
> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)
List
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
"item2"
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
set
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> smembers set-key
1) "item"
2) "item3"
hash
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0
> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
"value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
zset
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
过期时间对于清理缓存数据非常有用。
发布与订阅
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
发布与订阅实际上是观察者模式,订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
(1)发送消息
Redis采用PUBLISH命令发送消息,其返回值为接收到该消息的订阅者的数量。
(2)订阅某个频道
Redis采用SUBSCRIBE命令订阅某个频道,其返回值包括客户端订阅的频道,目前已订阅的频道数量,以及接收到的消息,其中subscribe表示已经成功订阅了某个频道。
(3)模式匹配
模式匹配功能允许客户端订阅符合某个模式的频道,Redis采用PSUBSCRIBE订阅符合某个模式所有频道,用“”表示模式,“”可以被任意值代替。
(4)取消订阅
Redis采用UNSUBSCRIBE和PUNSUBSCRIBE命令取消订阅,其返回值与订阅类似。
由于Redis的订阅操作是阻塞式的,因此一旦客户端订阅了某个频道或模式,就将会一直处于订阅状态直到退出。在SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE和PUNSUBSCRIBE命令中,其返回值都包含了该客户端当前订阅的频道和模式的数量,当这个数量变为0时,该客户端会自动退出订阅状态。
发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下:
- 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多;
- 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。
Redis发布订阅与ActiveMQ的比较
(1)ActiveMQ支持多种消息协议,包括AMQP,MQTT,Stomp等,并且支持JMS规范,但Redis没有提供对这些协议的支持;
(2)ActiveMQ提供持久化功能,但Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失;
(3)ActiveMQ提供了消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis没有提供消息传输保障。
总之,ActiveMQ所提供的功能远比Redis发布订阅要复杂,毕竟Redis不是专门做发布订阅的,但是如果系统中已经有了Redis,并且需要基本的发布订阅功能,就没有必要再安装ActiveMQ了,因为可能ActiveMQ提供的功能大部分都用不到,而Redis的发布订阅机制就能满足需求。
redis事务
Redis事务允许在一次单独的步骤中执行一组命令,MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础。
事务特性:
- Redis会将一个事务中的所有命令序列化,然后按顺序执行。不可能在一个事务的执行过程中插入其他命令。保证隔离性。
- Redis的一个事务要么全部执行要么一点都不会执行,保证原子性
- Redis借助AOF来将事务操作也持久化到日志文件中,这样就算在执行事务的中途宕机,重启的时候也会先去日志中查找上次运行的事务情况,使用redis-check-aof工具将会从日志文件中删除执行不完全的事务。从2.2版本开始,除了上述两项保证之外,Redis还能够以乐观锁的形式提供更多的保证,这种形式非常类似于“检查再设置”(CAS:Check And Set)操作。
事务命令:
- MULTI
用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令原子化地执行这个命令序列。这个命令的返回值总是OK。 - EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。当使用WATCH命令时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。当使用WATCH命令时,如果事务执行中止,那么EXEC命令就会返回一个Null值。 - WATCH
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。
这个命令的运行格式如下所示:
WATCH key [key ...]
这个命令的返回值总是OK。
对于每个键来说,时间复杂度总是O(1)。 - DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。这个命令的返回值总是OK。 - UNWATCH
清除所有先前为一个事务监控的键。如果你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令。时间复杂度总是O(1)。这个命令的返回值总是OK。
命令运行流程
使用MULTI命令便可以进入一个Redis事务。此时,用户可以发出多个Redis命令。Redis会将这些命令放入队列,而不是执行这些命令。一旦调用EXEC命令,那么Redis就会执行事务中的所有命令。相反,调用DISCARD命令将会清除事务队列,然后退出事务。
事务内部错误处理
在redis2.6.5之后处理输入事务命令过程中出现语法错误等问题时的解决方案是,保留错误命令,不会中断事务操作,在用EXEC命令提交命令时,将所有命令交给服务器运行,运行过程中,即使遇到错误命令,其他命令也会继续执行。所以Redis不支持回滚,主要的原因是,命令失败基本是命令语句错误,也就是程序员开发时候犯的错误,而此错误在生产环境下是不大可能的,所以redis没必要支持回滚。
事务丢弃
使用DISCARD命令之后,终止事务运行。(在EXEC之前)
通过CAS实现乐观锁
Redis使用WATCH命令实现事务的“检查再设置”(CAS)行为。WATCH命令可以被调用多次。当调用EXEC命令时,所有的键都会变为未受监控的状态。
持久化
RDB快照
Redis支持将当前数据的快照存成一个二进制数据文件的持久化机制,即RDB快照。但是一个持续写入的数据库如何生成快照呢?Redis借助了fork命令的copy on write机制。在生成快照时,将当前进程fork出一个子进程,然后在子进程中循环所有的数据,将数据写成为RDB文件。子进程先将数据写到临时文件中(后缀不为RDB),写完后通过操作系统rename命令(原子命令)将临时文件后缀名改为.RDB。这样在任何时候出现故障,RDB文件都是可用的。
我们可以通过Redis的save指令来配置RDB快照生成的时机,比如配置10分钟就生成快照,也可以配置有1000次写入就生成快照,也可以多个规则一起实施。这些规则的定义就在Redis的配置文件中,你也可以通过Redis的CONFIG SET命令在Redis运行时设置规则,不需要重启Redis。
将快照复制到其它服务器从而创建具有相同数据的服务器副本。
AOF
AOF日志的全称是append only file,它是一个追加写入的日志文件。与一般数据库的binlog不同的是,AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令。并且只有那些会导致数据发生修改的命令才会追加到AOF文件。
AOF同步频率
可以手动设置AOF缓存写入到文件的同步频率
always 选项会严重减低服务器的性能;everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
AOF文件会越来越大,所以Redis又提供了一个功能,叫做AOF rewrite。
- REWRITE:在主线程中重写AOF,会阻塞工作线程,在生产环境中很少使用,处于废弃状态;
- BGREWRITE: 在后台(子进程)重写AOF,不会阻塞工作线程,能正常服务,此方法最常用
AOF BGREWRITE实现方式:
Server收到BGREWRITE命令或者系统触发AOF重写时,主进程Fork一个子进程并进行AOF重写,重写(复制)原AOF文件中的部分信息(不是全部),主进程异步等待子进程结束(信号量),此时主进程能正常接收处理用户请求,用户请求会修改数据库里数据,会使得当前数据库的数据跟重写后AOF里不一致,需要有种机制保证数据的一致性。当前的做法是在重写 AOF 期间系统会新开一块内存用于缓存重写期间收到的命令(命令会被执行再缓存),在重写完成以后再将缓存中的命令追加到新的AOF。在处理命令时既要将命令追加到 aof_buf(原AOF的buffer),也要追加到重写AOF Buffer(重写的AOF buffer)。最后再将重写AOF Buffer写入到新的AOF中。以上buf操作都是主线程来执行,子线程只负责复制原AOF的工作。
AOF BGREWRITE存在的问题:
- 重写AOF Buffer占用内存
- 主线程在将AOF Buffer写入到新AOF过程中会阻塞,影响用户请求的运行。在缓存量大时会发生比较严重的问题。
解决方案:
官方的解决方案:
主进程跟子进程通过管道通信,主进程实时将新写入的数据发送给子进程,子进程从管道读出数据缓存在buffer中(之前是主进程写入buffer,现在是由子线程写),然后子进程负责将buffer写入新的AOF新的解决方案:
AOF重写期间,主进程创建一个新的aof_buf(两个aof_buf),两个aof_buf文件同时追加新写入的命令。当主进程收到子进程重写AOF文件完成后:
- 停止向旧的aof_buf,AOF文件追加命令;
- 删除旧的的appendonly.aof.last文件;
- 交换两个aof_buf,AOF文件指针;
- 回收旧的aof_buf,AOF文件;
- 重命令子进程生成的AOF文件为appendonly.aof.last;
新的解决方案的优点在于,可以减少更多的双buffer写入文件操作,旧的方案解决了主线程阻塞问题,新方案在此基础上还解决了内存占用问题。
redis主从复制
- redis的复制功能是支持多个数据库之间的数据同步。一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
- 通过redis的复制功能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。
主从复制流程
- 当一个从数据库启动时,会向主数据库发送sync命令,
- 主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来
- 当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。
- 从数据库收到后,会载入快照文件并执行收到的缓存的命令。
redis2.8之前的版本:当主从数据库同步的时候从数据库因为网络原因断开重连后会重新执行上述操作,不支持断点续传。redis2.8之后支持断点续传。
主从复制的设置
主服务器(master)
port 6000 //设置主服务器的对外端口
requirepass 123456 //设置密码 要么不设置密码,要么主从全部设置密码
在配置redis复制功能的时候如果主数据库设置了密码,需要在从数据的配置文件中通过masterauth参数设置主数据库的密码,这样从数据库在连接主数据库时就会自动使用auth命令认证了。相当于做了一个免密码登录。
从服务器slave1
port 6001
slaveof 127.0.0.1 6000 //通过使用 slaveof host port
masterauth 123456 //从服务器的密码
requirepass 123456 //主服务器的密码
从服务器slave2
port 6002
slaveof 127.0.0.1 6000 //通过使用 slaveof host port
masterauth 123456 //
requirepass 123456
之所以从服务器和主服务器的密码设置成一样是因为做主从交换,密码一致会更方便。
redis-server redis.conf //启动主机master
redis-server redis1.conf //启动从机slave1
redis-server redis2.conf //启动从机slave2
接下来验证一下
master上:
[root@localhost master]# redis-cli -p 6000
127.0.0.1:6000> auth 123456
OK
127.0.0.1:6000> set test chenqm
OK
slave1上
[root@localhost slave2]# redis-cli -p 6001
127.0.0.1:6001> auth 123456
OK
127.0.0.1:6001> get test
"chenqm"
slave2上
[root@localhost slave2]# redis-cli -p 6002
127.0.0.1:6002> auth 123456
OK
127.0.0.1:6002> get test
"chenqm"
同步成功,此时读写分离也实现了。
注意
如果你使用主从复制,那么要确保你的master激活了持久化,或者确保它不会在当掉后自动重启。原因:slave是master的完整备份,因此如果master通过一个空数据集重启,slave也会被清掉。
sentinel(哨兵)
万一主机挂了怎么办,这是个麻烦事情,所以redis提供了一个sentinel(哨兵),以此来实现主从切换的功能,类似与zookeeper.
//我们配置两个sentinel进程:
vim sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6000 2//这个后面的数字2,是指当有两个及以上的sentinel服务检测到master宕机,才会去执行主从切换的功能。
sentinel auth-pass mymaster 123456
vim sentinel.conf
port 26479
sentinel monitor mymaster 127.0.0.1 6000 2
sentinel auth-pass mymaster 123456
redis-server sentinel.conf --sentinel //启动sentinel服务
//查看日志
[7014] 11 Jan 19:42:30.918 # +monitor master mymaster 127.0.0.1 6000 quorum 2
[7014] 11 Jan 19:42:30.923 * +slave slave 127.0.0.1:6002 127.0.0.1 6002 @ mymaster 127.0.0.1 6000
[7014] 11 Jan 19:42:30.925 * +slave slave 127.0.0.1:6001 127.0.0.1 6002 @ mymaster 127.0.0.1 6000
//现在我们kill master
kill -9 6960
//观察日志:
[7014] 11 Jan 19:43:41.463 # +sdown master mymaster 127.0.0.1 6000
[7014] 11 Jan 19:46:42.379 # +switch-master mymaster 127.0.0.1 6000 127.0.0.1 6001
master切换了,当6000端口的这个服务重启的时候,他会变成6001端口服务的slave。因为sentinel在切换master的时候,会把对应的sentinel.conf和redis.conf文件的配置修改。期间我们还需要关注的一个问题:sentinel服务本身也不是万能的,也会宕机,所以我们还得部署sentinel集群,像我这样多启动几个sentinel。
除了监控和自动故障转移功能,Sentinel还有如下功能:当被监控的某个 Redis 服务器出现问题时, Redis Sentinel 可以向系统管理员发送通知, 也可以通过 API 向其他程序发送通知。
主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
处理故障
当主服务器出现故障时,Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器,B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C,并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。
分片
Redis 的分片承担着两个主要目标:
- 允许使用很多电脑的内存总和来支持更大的数据库。没有分片,你就被局限于单机能支持的内存容量。
- 允许伸缩计算能力到多核或多服务器,伸缩网络带宽到多服务器或多网络适配器。
分片算法
- 有很多不同的分片标准(criteria)。最简单的执行分片的方式之一是范围分片(range partitioning),通过映射对象的范围到指定的 Redis 实例来完成分片。例如,我可以假设用户从 ID 0 到 ID 10000 进入实例 R0,用户从 ID 10001 到 ID 20000 进入实例 R1,等等。这套办法行得通,并且事实上在实践中被采用,然而,这有一个缺点,就是需要一个映射范围到实例的表格。这张表需要管理,不同类型的对象都需要一个表。
- 哈希分片(hash partitioning)。这种模式适用于任何键。使用一个哈希函数(例如,crc32 哈希函数) 将键名转换为一个数字。例如,如果键是 foobar,crc32(foobar)将会输出类似于 93024922 的东西。对这个数据进行取模运算,以将其转换为一个 0 到 16383 之间的数字,16384个哈希槽(slot),16384个slot这样这个数字就可以映射到我的 4 台 Redis 实例之一,4台redis分别按一定比例分摊这16384个slot。这可以让在集群中添加和移除节点非常容易。从一个节点向另一个节点移动哈希槽并不需要停止操作,所以添加和移除节点,或者改变节点持有 的哈希槽百分比,都不需要任何停机时间(downtime)。
consistence hash 哈希一致性
一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
- 平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
- 单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
- 分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
- 负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:
环形Hash空间
按照常用的hash算法来将对应的key哈希到一个具有232次方个桶的空间中,即0~(232)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图
现在我们将object1、object2、object3、object4四个数据对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;
再采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
-
节点(机器)的删除
以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:
-
节点(机器)的添加
如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:
通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
虚拟节点
一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。即有可能很多数据都在某些节点集中分部,没有平均分布。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。
“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一个实际节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。
对象从hash到虚拟节点到实际节点的转换如下图:
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
Redis集群搭建
使用redis-server + .conf搭建
port 7000 //端口7000,7002,7003
bind 10.93.84.53 //默认ip为127.0.0.1 需要改为其他节点机器可访问的ip 否则创建集群时无法访问对应的端口,无法创建集群
daemonize yes //redis后台运行
pidfile ./redis_7000.pid //pidfile文件对应7000,7001,7002
cluster-enabled yes //开启集群 把注释#去掉
cluster-config-file nodes_7000.conf //集群的配置 配置文件首次启动自动生成 7000,7001,7002
cluster-node-timeout 15000 //请求超时 默认15秒,可自行设置
appendonly yes //aof日志开启 有需要就开启,它会每次写操作都记录一条日志
分别启动6个redis实例
./bin/redis-server cluster/conf/7000.conf
./bin/redis-server cluster/conf/7001.conf
./bin/redis-server cluster/conf/7002.conf
./bin/redis-server cluster/conf/7003.conf
./bin/redis-server cluster/conf/7004.conf
./bin/redis-server cluster/conf/7005.conf
使用redis-trib.rb搭建
Redis 3.0以上的集群方式是通过Redis安装目录下的bin/redis-trib.rb脚本搭建。这个脚本是用Ruby编写的,所以需要安装ruby环境。
ruby ./bin/redis-trib.rb create --replicas 1 10.93.84.53:7000 10.93.84.53:7001 10.93.84.53:7002 10.93.84.53:7003 10.93.84.53:7004 10.93.84.53:7005
--replicas 1表示为集群的master节点创建1个副本。那么6个实例里,有三个master,有三个是slave。
查看集群状态
./bin/redis-cli -h 10.93.84.53 -p 7000 -c //登录集群,-c标识以集群方式登录
10.93.84.53:7000> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:8
cluster_my_epoch:8
cluster_stats_messages_sent:215
cluster_stats_messages_received:215
10.93.84.53:7000> cluster nodes
942c9f97dc68198c39f425d13df0d8e3c40c5a58 10.93.84.53:7004 slave 5ac973bceab0d486c497345fe884ff54d1bb225a 0 1507806791940 5 connected
5ac973bceab0d486c497345fe884ff54d1bb225a 10.93.84.53:7001 master - 0 1507806788937 2 connected 5461-10922
a92a81532b63652bbd862be6f19a9bd8832e5e05 10.93.84.53:7005 slave cc46a4a1c0ec3f621b6b5405c6c10b7cffe73932 0 1507806790939 6 connected
cc46a4a1c0ec3f621b6b5405c6c10b7cffe73932 10.93.84.53:7002 master - 0 1507806789937 3 connected 10923-16383
6346ae8c7af7949658619fcf4021cc7aca454819 10.93.84.53:7000 myself,slave 92f62ec93a0550d962f81213ca7e9b3c9c996afd 0 0 1 connected
92f62ec93a0550d962f81213ca7e9b3c9c996afd 10.93.84.53:7003 master - 0 1507806792941 8 connected 0-5460
分片定位方式
- 客户端分片(Client side partitioning)意味着,客户端直接选择正确的节点来写入和读取指定键。许多 Redis 客户端实现了客户端分片。
- 代理协助分片(Proxy assisted partitioning)意味着,我们的客户端发送请求到一个可以理解 Redis 协议的代理上,而不是直接发送请求到 Redis 实例上。代理会根据配置好的分片模式,来保证转发我们的请求到正确的 Redis 实例,并返回响应给客户端。Redis 和 Memcached 的代理 Twemproxy 实现了代理协助的分片。
- 客户端分片、查询路由(Query routing)意味着,你可以发送你的查询到一个随机实例,这个实例会保证转发你的查询到正确的节点。Redis 集群在客户端的帮助下,实现了查询路由的一种混合形式 (请求不是直接从 Redis 实例转发到另一个,而是客户端收到重定向到正确的节点)。
集群通信
每个 Redis 集群节点需要两个 TCP 连接打开。正常的 TCP 端口用来服务客户端,称作命令端口,例如 6379。加 10000 的端口用作数据端口,服务集群总线(bus),使用点到点通信,ping-pong机制实现信道保活。命令端口和集群总线端口的偏移量一直固定为 10000。集群总线端口用于错误检测,配置更新,故障转移授权,节点之间通信等操作。
节点的 fail 是通过集群中超过半数的节点检测失效时才生效.如果集群任意 master 挂掉,且当前 master 没有 slave.集群进入 fail 状态。如果集群超过半数以上 master 挂掉,无论是否有 slave ,集群进入 fail 状态。当集群不可用时,所有对集群的操作做都不可用。
分片的缺点
- 涉及多个键的操作通常不支持(例如,你不能对映射在两个不同 Redis 实例上的键执行交集)。涉及多个键的事务不能使用。
- 数据持久化和备份变得复杂
- 数据的添加删除变得复杂
Twemproxy
Twemproxy 是 Twitter 开发的一个支持 Memcached ASCII 和 Redis 协议的代理。它是单线程的,由 C 语言编写,运行非常的快。他是基于 Apache 2.0 许可的开源项目。内部处理是无状态的,它本身可以很轻松地集群,这样可避免单点压力或故障。
数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
redis事件
文件事件:
服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件。文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事务处理器。文件事件处理器由四个部分组成:套接字、I/O多路复用程序、文件事件分派器以及事件处理器
时间事件:
又分为两类:定时事件是让一段程序在指定的时间之内执行一次;周期性时间是让一段程序每隔指定时间就执行一次。时间事件都放在一个无序链表中,时间事件执行器运行时,遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
redis与memcached对比
Redis数据类型多
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
redis支持数据持久化
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
redis支持分布式
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。用第三方代理工具Twemproxy都可以实现。
内存管理机制
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换swap到磁盘。而 Memcached 的数据则会一直在内存中。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
性能对比
由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。