在上一篇文章中,主要学习了一下Redis的5种数据结构的底层实现原理,在这一篇中,将介绍Redis的持久化方式,与Memcached的区别,Redis3.0的集群部署以及广泛的应用场景。
Redis持久化方式
redis默认创建16个数据库,通过select语句可以切换数据库。我们知道,Redis是一个内存数据库,在内存中以K-V形式来存储数据,但是它可以配置持久化选项来进行持久化,这也是Redis和Memecached的区别之一。Redis提供了两种持久化方式:
一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF持久化(原理是将Reids的操作日志以追加的方式写入文件)。
两种的区别有:
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的对比:
- 相比于AOF机制,如果数据集很大,则RDB故障恢复的时间较短,启动效率较高,而AOF需要执行其日志,在数据量大地情况下,启动效率会低。
- RDB这种持久化方式可能会丢失部分数据,一旦系统在定时持久化之前出现宕机,那么,距上一次持久化到现在的没来得及写入磁盘的数据都将会丢失。而AOF可以提高较高的一致性。
3.由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。造成其性能降低。
Redis与Memcached的区别
Memcached也是常用的NoSQL数据库,与Redis很类似,接下来我们就来比较一下这两种数据库的差别。
- 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与Memcached区别。具体可以参考这篇文章,尤其是两者在内存和网络IO模型的解释。
Redis Cluster分区的机制
Redis Cluster本身提供了自动将数据分散到Redis Cluster不同节点的能力,分区实现的关键点问题包括:如何将数据自动地打散到不同的节点,使得不同节点的存储数据相对均匀;如何保证客户端能够访问到正确的节点和数据;如何保证重新分片的过程中不影响正常服务。这篇文章通过了解这些问题来认识Redis Cluster分区实现原理。
Redis Cluster是由多个同时服务于一个数据集合的Redis实例组成的整体,对于用户来说,用户只关注这个数据集合,而整个数据集合的某个数据子集存储在哪个节点对于用户来说是
透明
的。Redis Cluster具有分布式系统的特点,也具有分布式系统如何实现高可用性与数据一致性的难点,由多个Redis实例组成的Redis Cluster结构通常如下:
部署多个节点,可以分布式存储数据,节点之间互相连接,Redis的每个节点复杂部分数据的存储,同时每个节点为了保证数据的高可用性,又会利用master-slaver结构来保证某个节点宕机后不丢失数据。(其中通过主从节点实现故障转移与故障恢复,也master选举等过程可以参考zookeeper的实现方式)。
关于Redis的容错机制,先不在这讨论,可以参考本博客后边的系列:Zookeeper系列。
下边主要分析一下Redis Cluser分区的实现原理
Redis Cluster分区实现原理主要关注三个问题,1)数据是如何被自动分散到不同的节点的;2)客户端是如何能够正确找到节点的;3)键空间迁移过程是怎么样的?其次是一些实现小细节,通过了解这些问题更好地了这些问题从而更好地认识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取模。
客户端访问
Redis Cluster并不会代理查询,那么如果客户端访问了一个此master节点的key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息
1
2
3 1GET msg
2-MOVED 254 127.0.0.1:6381
3
表示客户端想要的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
命令,会抛弃执行之前加入事务的命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1127.0.0.1:6379> get name
2(nil)
3127.0.0.1:6379> get gender
4(nil)
5127.0.0.1:6379> multi
6OK
7127.0.0.1:6379> set name Slogen
8QUEUED
9127.0.0.1:6379> set gender male
10QUEUED
11127.0.0.1:6379> exec
121) OK
132) OK
14127.0.0.1:6379> mget name gender
151) "Slogen"
162) "male"
17
1
2 1 watch
2
命令是Redis
提供的一个乐观锁,可以在exec
执行之前,监视任意数量的数据库key
,并在exec
命令执行的时候,检测被监视的key
是否至少有一个已经被修改,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
1
2
3
4
5
6
7
8
9
10
11
12
13 1127.0.0.1:6379> get name
2(nil)
3127.0.0.1:6379> watch name
4OK
5127.0.0.1:6379> multi
6OK
7127.0.0.1:6379> set name slogen
8QUEUED
9127.0.0.1:6379> set gender male
10QUEUED
11127.0.0.1:6379> get name
12QUEUED
13
这个时候client
还没有执行exec
命令,接下来在client2
下执行下面命令修改name
:
1
2
3
4
5 1127.0.0.1:6379> set name rio
2OK
3127.0.0.1:6379> get name
4"rio"
5
接下来在client1
下执行exec
命令:
1
2
3
4
5 1127.0.0.1:6379> exec
2(nil)
3127.0.0.1:6379> get name
4"rio"
5
从执行结果可以看到,在client1
中执行exec
命令的时候,Redis
会检测到name
字段已经被其他客户端修改了,所以拒绝执行事务中所有的命令,直接返回nil
表示执行失败。这个时候获取到的name
的值还是在client2
中设置的rio
。
ps:在本篇博客中,主要分析了Redis的持久化,集群分区,事务及与Memcached的比较。在下一篇,我们将重点分析一下Redis的缓存淘汰机制,以及其经典的应用场景,了解其应用非常重要。