黑马程序员技术交流社区

标题: 【成都校区】redis缓存 [打印本页]

作者: 丨灬勿忘初心灬    时间: 2019-7-18 18:08
标题: 【成都校区】redis缓存
Redis支持的数据类型
字符串(String)

string是redis最基本的类型,一个键最大能存储512MB。

列表(Lists)

Redis的列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

并且,有redis中的list在底层实现并不是数组,而是双向链表,也就是说对于一个具有上百万个元素的lists来说,在头部和尾部插入一个新元素,其时间复杂度是常数级别的,比如用LPUSH在10个元素的lists头部插入新元素,和在上千万元素的lists头部插入新元素的速度应该是相同的。

lists的常用操作包括LPUSH、RPUSH、LRANGE等。我们可以用LPUSH在lists的左侧插入一个新元素,用RPUSH在lists的右侧插入一个新元素,用LRANGE命令从lists中指定一个范围来提取元素。我们来看几个例子:

//新建一个list叫做mylist,并在列表头部插入元素"1"
127.0.0.1:6379> lpush mylist "1"
//返回当前mylist中的元素个数
(integer) 1
//在mylist右侧插入元素"2"
127.0.0.1:6379> rpush mylist "2"
(integer) 2
//在mylist左侧插入元素"0"
127.0.0.1:6379> lpush mylist "0"
(integer) 3
//列出mylist中从编号0到编号1的元素
127.0.0.1:6379> lrange mylist 0 1
1) "0"
2) "1"
//列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "0"
2) "1"
3) "2"

Lists的应用相当广泛,随便举几个例子:

    我们可以利用lists来实现一个消息队列,而且可以利用链表来确保先后顺序。
    利用LRANGE还可以很方便的实现分页功能。

集合(Sets)

set 是无序集合,最大可以包含(2 的 32 次方-1)个元素。set 的是通过 hash table 实现的,所以添加,删除,查找的复杂度都是 O(1)。hash table 会随着添加或者删除自动的调整大小。需要注意的是调整 hash table 大小时候需要同步(获取写锁)会阻塞其他读写操作。可能不久后就会改用跳表(skip list)来实现。跳表已经在 sorted sets 中使用了。关于 set 集合类型除了基本的添加删除操作,其它有用的操作还包含集合的取并集(union),交集(intersection) ,差集(difference)。通过这些操作可以很容易的实现 SNS 中的好友推荐和 blog 的 tag 功能

//向集合myset中加入一个新元素"one"
127.0.0.1:6379> sadd myset "one"
(integer) 1
127.0.0.1:6379> sadd myset "two"
(integer) 1
//列出集合myset中的所有元素
127.0.0.1:6379> smembers myset
1) "one"
2) "two"
//判断元素1是否在集合myset中,返回1表示存在
127.0.0.1:6379> sismember myset "one"
(integer) 1
//判断元素3是否在集合myset中,返回0表示不存在
127.0.0.1:6379> sismember myset "three"
(integer) 0
//新建一个新的集合yourset
127.0.0.1:6379> sadd yourset "1"
(integer) 1
127.0.0.1:6379> sadd yourset "2"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "1"
2) "2"
//对两个集合求并集
127.0.0.1:6379> sunion myset yourset
1) "1"
2) "one"
3) "2"
4) "two"

对于集合的使用,也有一些常见的方式,比如,QQ有一个社交功能叫做“好友标签”,大家可以给你的好友贴标签,比如“大美女”、“土豪”、“欧巴”等等,这时就可以使用redis的集合来实现,把每一个用户的标签都存储在一个集合之中。
有序集合(sorted sets)

sorted sets是有序集合(sorted sets)的意思,有序集合中的每个元素都关联一个序号(score),这便是排序的依据。

很多时候,我们都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的,比如zrange、zadd、zrevrange、zrangebyscore等等。

//新增一个有序集合myzset,并加入一个元素baidu.com,给它赋予的序号是1:
127.0.0.1:6379> zadd myzset 1 baidu.com
(integer) 1
//向myzset中新增一个元素360.com,赋予它的序号是3
127.0.0.1:6379> zadd myzset 3 360.com
(integer) 1
//向myzset中新增一个元素google.com,赋予它的序号是2
127.0.0.1:6379> zadd myzset 2 google.com
(integer) 1
//列出myzset的所有元素,同时列出其序号,可以看出myzset已经是有序的了。
127.0.0.1:6379> zrange myzset 0 -1 with scores
1) "baidu.com"
2) "1"
3) "google.com"
4) "2"
5) "360.com"
6) "3"
//只列出myzset的元素
127.0.0.1:6379> zrange myzset 0 -1
1) "baidu.com"
2) "google.com"
3) "360.com"

哈希(hashes)

hashes存的是字符串和字符串值之间的映射,比如一个用户要存储其全名、姓氏、年龄等等,就很适合使用哈希。

//建立哈希,并赋值
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
OK
//列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
//更改哈希中的某一个值
127.0.0.1:6379> HSET user:001 password 12345
(integer) 0
//再次列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"

Redis 数据持久化

由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了。

Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。

由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB文件持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
RDB

RDB持久化既可以通过save和BGSAVE命令进行手动持久化,也可以根据服务器配置选项定期执行,其实定期执行的进程也是执行的BGSAVE命令。

SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何客户端命令请求,所以一般不推荐使用SAVE命令。

BGSAVE不会直接阻塞服务器进程,相反它会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理请求。处理流程图如下:

image.png-31.5kB

Redis也允许用户通过save选项设置多个保存条件,只要其中任意一个条件被满足,Redis服务器就会执行BGSAVE命令。假设,我们有下面的配置:

save 900 1
save 300 10
save 60 10000

根据上面的配置,只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:

    服务器在900秒之内,对数据库进行了至少1次修改。
    服务器在300秒之内,对数据库进行了至少10次修改。
    服务器在60秒之内,对数据库进行了至少10000次修改。

Redis服务器会维持着一个dirty计数器以及一个lastsave属性:

    dirty计数器记录了距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改(包括写入、删除、更新等操作)
    lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会去执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
AOF

AOF,英文是Append Only File,即只允许追加不允许改写的文件。

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。在服务器重启时,Redis服务器会载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。

image.png-25.4kB
image.png-204.4kB
image.png-32.3kB

我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。如果有写操作(如SET等),redis就会被追加到AOF文件的末尾。

默认的AOF持久化策略是每秒钟fsync一次(fsync是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。

如果在追加日志时,恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整,也没有关系,redis提供了redis-check-aof工具,可以用来进行日志修复。

因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。

Redis在对AOF文件重写时, 并不需要对现有的AOF文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。比如前面说的,调用了100次INCR指令,Redis并不会遍历分析这100条指令,而是简单地读取了当前数据库中该键对应的值,然后使用一条set指令将这个值写入到新的AOF文件中。这就是AOF重写功能的实现原理。

AOF方式的另一个好处,我们通过一个“场景再现”来说明。某同学在操作redis时,不小心执行了FLUSHALL,导致redis内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要redis配置了AOF持久化方式,且AOF文件还没有被重写(rewrite),我们就可以用最快的速度暂停redis并编辑AOF文件,将最后一行的FLUSHALL命令删除,然后重启redis,就可以恢复redis的所有数据到FLUSHALL之前的状态了。是不是很神奇,这就是AOF持久化方式的好处之一。但是如果AOF文件已经被重写了,那就无法通过这种方法来恢复数据了。

虽然优点多多,但AOF方式也同样存在缺陷,比如在同样数据规模的情况下,AOF文件要比RDB文件的体积大。而且,AOF方式的恢复速度也要慢于RDB方式。

AOF也是通过Fork一个子进程实现的。
Redis主从同步

像MySQL一样,Redis是支持主从同步的,而且也支持一主多从以及多级从结构。

主从结构,一是为了纯粹的冗余备份,二是为了提升读性能,比如很消耗性能的SORT就可以用从服务器承担。

主从架构中,就可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样就可以提供主服务器的处理性能。

Redis的复制功能分为初次同步和命令传播两个操作:
初次同步(静态复制)

    从服务器向主服务器发送 PSYNC命令。
    收到PSYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
    当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
    主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器当前所处的状态。
    主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。

image.png-22.7kB

另外,要说的一点是,即使有多个从服务器同时发来SYNC指令,主服务器也只会执行一次BGSAVE,然后把持久化好的RDB文件发给多个下游。因为这个过程对从服务器和主服务器都是一个非常消耗资源的过程:

    主服务器需要执行BGSAVE命令来生成RDB文件,这个生成操作会耗费主服务器大量的CPU、内存和磁盘I/O资源。
    主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的事件产生影响。
    接收到RDB文件的从服务器需要载入主服务器发过来的RDB文件,并且在载入期间,从服务器会因为阻塞而无法处理命令请求。

命令传播(动态复制)

在进行完初次同步时,主服务器会将所有的更新操作命令,发送给从服务器执行,让从服务器执行相同的更新命令,从而实现主从服务器的同步。
部分重同步的实现

在Redis2.8版本之前,如果从服务器与主服务器因某些原因断开连接的话,都会进行一次主从之间的全量的数据同步;而在2.8版本之后,redis支持了效率更高的增量同步策略,这大大降低了连接断开的恢复成本,这就是部分重同步功能。

部分重同步功能由以下三个部分构成:

    主服务器的复制偏移量和从服务器的复制偏移量。
    主服务器的复制积压缓冲区。
    主服务器的运行ID

执行复制的双方 —— 主服务和从服务器会分别维护一个复制偏移量:

    主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
    从服务器每次收到主服务器传播带来的N个字节的数据时,就将自己的复制偏移量的值加上N。

通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态,以及自己落后了多少偏移量。

复制积压缓冲区:

复制积压缓冲区是由主服务器维护的一个固定长度先进先出队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面。

image.png-75.1kB

当复制积压缓存区满时,按照先进先出原则,会将先入队列的数据弹出栈,以腾出空间给最新的数据入栈。因此,主服务器的复制积压缓冲区里面只会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。具体如下图:

image.png-52.8kB

当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,如果offset之后的数据仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作,根据复制积压缓冲区里记录的每个字节复制偏移量计算出指定部分发送给从服务器;如果offset之后的数据已经不再复制积压缓冲区里面,则主服务器将对从服务器执行完整重同步操作。

服务器运行ID:

每个Redis服务器,不论是主服务器还是从服务器,都会有自己的运行ID,当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。断线重连后,从服务器将向当前连接的主服务器发送之前保存的运行ID;主服务器接收到从服务器发送过来的断线前从服务器复制来源的服务器Id时,会和自己的运行ID比较,相同则继续进行下面的offset判断。不相同,则说明之前从服务器不是同步的自己,所以传过来的offset对自己来说无用,所以主服务器将对从服务器执行完整重同步操作。
哨兵模式(Sentinel)

哨兵(Sentinel)本质上也是一个Redis服务,但它的作用不是存储,而是监视集群中多台Redis机器(包括主从)的健康状况。并在被监视的主服务器进入下线状态时,自动将下线的主服务器下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求,其他从服务器也将改为同步复制新的主服务器。(自动故障转移)

每个哨兵(Sentinel)会向其他哨兵(sentinel)、master、slave定时发送消息,以确认对方是否“活着”,如果发现对象在指定时间(可配置)内未响应,则暂时认为对方已挂(就是所谓的“主观宕机”)

当一个哨兵将主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了线下状态(可以是主观下线或者客户下线)。当这个哨兵从其他哨兵那里接收到足够数量的已下线判断之后,这个哨兵就会将主服务器判定为客观下线,并对主服务器进行故障转移操作。

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个哨兵会进行协商,选举出一个领头哨兵,并由这个哨兵对下线主服务器进行故障转移操作。
领头哨兵选举过程

    所有在线的Sentinel都有被选为领头Sentinel的资格。
    每次进行领头Sentinel选举之后,无论选举成功是否,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器。
    在一个配置纪元里面,所有Sentinel都有且只有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个设置纪元里面就不能再更改。
    每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部令头部Sentinel。
    Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel都将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。
    目标Sentinel在收到源Sentinel要求将自己设置为局部领头Sentinel时,无论是否将该源Sentinel设置为局部领头Sentinel,都会向源Sentinel返回一条命令回复,回复的消息体中会包含目标Sentinel认可的局部领头Sentinel的运行Id和配置纪元。
    源sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中的配置纪元是否与自己的配置纪元相同,如果相同的话,那么源Sentinel继续取出运行Id和自己的运行Id比较,一致则认为目标Sentinel已经将自己设置为局部领头Sentinel。
    如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么Sentinel就成了领头Sentinel。
    因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里,只会出现一个领头Sentinel。
    如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

故障转移

在选举产生领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:

    从所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
    其他从服务器改为复制新的主服务器。
    将已下线的主服务器配置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

新的主服务器选举过程:

    领头Sentinel剔除所有处于下线或者断线状态的从服务器,保证剩余的从服务器都是正常在线的。
    领头Sentinel将根据服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器作为主服务器。
    如果有多个具有相同最高优先级的从服务器,那么领头Sentinel将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器作为主服务器。(复制偏移量越大,保存的数据就越新)。
    如果有多个优先级最高,复制偏移量最大的从服务器,那么领头Sentinel将按照运行ID对这些从服务器进行排序,并选出其中运行Id最小的从服务器。






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2