在上一篇文章中,主要学习了一下Redis的5种数据结构的底层实现原理,在这一篇中,将介绍Redis的持久化方式,与Memcached的区别,Redis3.0的集群部署以及广泛的应用场景。
Redis持久化方式
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
快照保存过程:
1. redis调用fork,现在有了子进程和父进程。
2. 父进程继续处理client请求,子进程负责将内存内容写入到临时文件。由于os的写时复制机制(copy on write)父子进程会共享相同的物理页面,当父进程处理写请求时os会为父进程要修改的页面创建副本,而不是写共享的页面。所以子进程的地址空间内的数据是fork时刻整个数据库的一个快照。
3. 当子进程将快照写入临时文件完毕后,用临时文件替换原来的快照文件,然后子进程退出(fork一个进程入内在也被复制了,即内存会是原来的两倍)。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
用图来表示为:
AOF:配置相关配置文件,写操作会记录到相应的文件中,配置多长时间存储到硬盘:
AOF在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
RDB快照存储: 快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名 为dump.rdb。可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置:
save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000 #60秒内容如超过10000个key被修改,则发起快照保存
也可以进行人工save rdb。
RDB与AOF的对比:
1. 相比于AOF机制,如果数据集很大,则RDB故障恢复的时间较短,启动效率较高,而AOF需要执行其日志,在数据量大地情况下,启动效率会低。
2. RDB这种持久化方式可能会丢失部分数据,一旦系统在定时持久化之前出现宕机,那么,距上一次持久化到现在的没来得及写入磁盘的数据都将会丢失。而AOF可以提高较高的一致性。
3.由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。造成其性能降低。
Redis与Memcached的区别
1. redis与memcached相比,比仅支持简单的key-value数据类型,同时还提供list,set,zset,hash等数据结构的存储;
redis支持数据的备份,即master-slave模式的数据备份;
2. redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用等等
3. 性能对比:由于Redis只使用单核(单线程),而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程。redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架。
4 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
5. 集群管理的区别
Redis Cluster分区的机制
槽(slot)概念
Redis Cluster中有一个16384长度的槽的概念,他们的编号为0、1、2、3……16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。
Redis Cluster怎么知道哪些槽是由哪些节点负责的呢?某个Master又怎么知道某个槽自己是不是拥有呢?
实现方式是:每个Master节点维护着一个(16384除8)个字节的位序列,Master节点用bit来标识对于某个槽自己是否拥有。拥有就将对应的位置1。
同时:集群同时还维护着槽到集群节点的映射,是由长度为16384类型为节点的数组实现的,槽编号为数组的下标,数组内容为集群节点,这样就可以很快地通过槽编号找到负责这个槽的master节点。位序列这个结构很精巧,即不浪费存储空间,操作起来又很便捷。
键空间分布基本算法
这里讲的是Redis Cluster如何将键空间分布在不同的节点的,键空间意为Redis Cluster所拥有用户所有数据集合的键的取值范围,这个范围叫做键空间。提到空间分布,必然会想到哈希算法,没错,通过哈希算法再加上取模运算可以将一个值固定地映射到某个区间,在这里,这个区间叫做slots,区间由连续的slot组成。在Redis Cluster中,我们拥有16384个slot,这个数是固定的,我们存储在Redis Cluster中的所有的键都会被映射到这些slot中。简单来说就是和常规的hash算法一样,也是hash取模。
客户端访问
GET msg
-MOVED 254 127.0.0.1:6381
表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果。这里明确一下,没有代理的Redis Cluster可能会导致客户端两次连接急群中的节点才能找到正确的服务,推荐客户端缓存连接,这样最坏的情况是两次往返通信。
重新分片(Resharding)
重新分片意为槽到集群节点的映射关系要改变,不变的是键到槽的映射关系,因此当重新分片的时候,如果槽中有键,那么键也是要被移动到新的节点的。
槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得Redis Cluster不必宕机的情况下可以执行槽的迁移。下面这张图描述了我们迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态。
MIGRATING状态(迁移状态)
本例中MIGRATING状态是发生在MasterA节点中的一种槽的状态,预备迁移槽的时候槽的状态首先会变为MIGRATING状态,这种状态的槽会实际产生什么影响呢?当客户端请求的某个Key所属的槽处于MIGRATING状态的时候,影响有下面几条:
- 如果Key存在则成功处理
- 如果Key不存在,则返回客户端ASK,仅当这次请求会转向另一个节点,并不会刷新客户端中node的映射关系,也就是说下次该客户端请求该Key的时候,还会选择MasterA节点
- 如果Key包含多个命令,如果都存在则成功处理,如果都不存在,则返回客户端ASK,如果一部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的Key都迁移完毕的时候客户端重试请求的时候回得到ASK,然后经过一次重定向就可以获取这批键
IMPORTING状态(输入状态)
本例中的IMPORTING状态是发生在MasterB节点中的一种槽的状态,预备将槽从MasterA节点迁移到MasterB节点的时候,槽的状态会首先变为IMPORTING。IMPORTING状态的槽对客户端的行为有下面一些影响:
- 正常命令会被MOVED重定向,如果是ASKING命令则命令会被执行,从而Key没有在老的节点已经被迁移到新的节点的情况可以被顺利处理;
- 如果Key不存在则新建;
- 没有ASKING的请求和正常请求一样被MOVED,这保证客户端node映射关系出错的情况下不会发生写错;
Redis事务实现
Redis
作为一个内存型数据库,同样支持传统数据库的事务特性。这篇文章会从源代码角度来分析Redis
中事务的实现原理。事务的概念:Redis
事务提供了一种将多个命令请求打包,然后一次性、按照顺序地执行多个命令的机制,并且在事务执行的期间,服务器不会中断事务而去执行其他不在事务中的命令请求,它会把事务中所有的命令都执行完毕才会去执行其他的命令。Redis
中提供了multi
、discard
、exec
、watch
、unwatch
这几个命令来实现事务的功能。Redis
的事务始于multi
命令,之后跟着要在事务中执行的命令,终于exec
命令或者discard
命令。加入事务中的所有命令会原子的执行,中间不会穿插执行其他没有加入事务的命令。multi
命令告诉Redis
客户端要开始一个事物,然后Redis
会返回一个OK
,接下来所有的命令Redis
都不会立即执行,只会返回QUEUED
结果,直到遇到了exec
命令才会去执行之前的所有的命令,或者遇到了discard
命令,会抛弃执行之前加入事务的命令。
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get gender
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Slogen
QUEUED
127.0.0.1:6379> set gender male
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> mget name gender
1) "Slogen"
2) "male"
watch
命令是
Redis
提供的一个乐观锁,可以在
exec
执行之前,监视任意数量的数据库
key
,并在
exec
命令执行的时候,检测被监视的
key
是否至少有一个已经被修改,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name slogen
QUEUED
127.0.0.1:6379> set gender male
QUEUED
127.0.0.1:6379> get name
QUEUED
这个时候
client
还没有执行
exec
命令,接下来在
client2
下执行下面命令修改
name
:
127.0.0.1:6379> set name rio
OK
127.0.0.1:6379> get name
"rio"
接下来在
client1
下执行
exec
命令:
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get name
"rio"
从执行结果可以看到,在
client1
中执行
exec
命令的时候,
Redis
会检测到
name
字段已经被其他客户端修改了,所以拒绝执行事务中所有的命令,直接返回
nil
表示执行失败。这个时候获取到的
name
的值还是在
client2
中设置的
rio
。