主从复制
主从复制是指将一台Redis服务器的数据,复制到其他的Redis服务器,前者称为主节点,后者称为从节点。
数据的复制是单向的,只能从主节点到从节点。
主节点以写为主,从节点以读为主。
- 默认情况下,每台Redis服务器都是主节点。
- 一个主节点可以有多个从节点
- 一个从节点只能有一个主节点
为什么要主从复制
- 主从复制实现了数据的热备份
- 故障恢复,当主节点出现问题,服务可以由从节点提供
- 负载均衡,尤其是在写少读多的场景下,通过多个节点分担读负载
- 主从复制是高可用的基石
主从复制的核心机制
- Redis采用异步方式复制数据到slave节点,Redis2.8开始,slave node会周期性地确认自己每次复制的数据量
- 一个master node可以配置多个slave node
- slave可以连接其他的slave
- slave在复制的时候,不会阻塞master正常工作
- slave在复制的时候,不会阻塞自己的查询操作,复制完成后加载数据时会暂停对外服务
全量复制|增量复制
完整同步:
当一个Redis服务器接收到replicaof命令,开始对另一个服务器进行复制的时候,主服务器会进行如下操作:
- 主服务器bgsave生成一个RDB,缓存区存储bgsave命令之后的写命令
- 主通过套接字传RDB给从
- 从接收载入RDB文件
- 主再把缓存区的写命令发送给从
主机中的所有信息和数据,都会自动被从机备份保存。
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步
核心原理
Slave启动成功后,会发送一个psync同步命令给master。
如果这是slave初次连接到master,会触发一次full resynchronization 全量复制。master启动一个后台线程,生成RDB快照,并且将新收到的写命令缓存在内存中。
master会将RDB发送给slave,slave会先写入本地磁盘,然后再从磁盘加载到内存中,接着master会将内存中的缓存的写命令发送到slave,slave也会同步这些数据。
如果slave和master断开连接会自动重连,连接之后master仅会复制给slave部分缺少的数据。
断点续传
从Redis2.8开始,就支持主从复制的断点续传,如果主从复制过程中,断开了连接,可以接着上次复制的地方继续复制下去。
master会在内存中维护一个backlog,master和slave都会保存一个replica offset还有一个master run id, offset就是保存在backlog中的,如果master和slave网络连接断掉了,slave会让master从上次replica offset开始继续复制,如果没有找到对应的offset,那么就会执行一次 resynchronization。
无磁盘化复制
master在内存中创建RDB,然后发送给slave,不会在自己本地落地磁盘了。只需要在配置文件中开启repl-diskless-sync yes
如果主机宕机了
主机宕机,群龙无首,如果本台服务器想当老大,可以执行如下命令:
redis> slaveof no one
使用整个命令让自己变成主机,其他从机还是要手动选择它作为老大。
但如果这个时候,老大又好了,,,,老大也不是老大了,还得手动改回去
哨兵模式
哨兵模式就是自动选择老大的模式。
Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
Redis哨兵主备切换的数据丢失问题
导致数据丢失的两种情况
主备切换的过程,可能导致数据丢失:
- 异步复制导致的数据丢失
因为master-slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这部分数据就丢失了 - 脑裂导致的数据丢失
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但实际上master还运行着。哨兵认为master宕机,重新选了一个,集群中有两个master。
虽然某slave变成master,但可能客户端还没来得及切换到新的master,还继续向旧master写数据。旧master恢复的时候,会作为slave,自己的数据被清空,新master没有后面客户端写入的数据,所以这部分数据丢失了。
数据丢失的解决方案
进行如下配置:
min-slaves-to-write 1
min-slaves-max-lag 10表示要求至少有1个slave,数据复制和同步的延迟不能超过10秒
一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,master就不会再接收任何请求了
减少异步复制数据的丢失:
有了min-slaves-max-lag
配置,就可以确保说,一旦slave复制数据和ack延时过长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于数据未同步到slave导致的数据丢失降低的可控范围内。
减少脑裂的数据丢失:
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多丢失10秒数据。
启动Sentinel
用户需要在配置文件中指定想要被Sentinel监视的主服务器,并且Sentinel也需要在配置文件中写入信息以记录主从服务器的状态。
$> redis-sentinel sentinel.conf
//配置文件路径看具体的,也可能是/etc/sentinel.conf
Sentinel配置文件需要包含以下选项:
sentinel monitor
- master-name: 指定主服务器的名字
- quorum: 判断这个主服务器下线所需的Sentinel数量
当Sentinel开始监视一个主服务器之后,就会去获取被监视主服务器的从服务器名单,并根据名单对各个从服务器实施监视,整个过程是完全自动的,所以用户只需输入待监视主服务器的地址就可以了。
Sentinel会对每个被监视的主从服务器实施心跳检测,记录各个服务器的在线状态、响应速度等信息,当Sentinel发现被监视的主服务器进入下线状态时,就会开始故障转移。
redis-sentinel是一个运行在特殊模式下的Redis服务器,
也可以使用:redis-server sentinel.conf --sentinel
去启动一个sentinel一个Sentinel可以监视多个主服务器,
在配置文中指定多个 sentinel monitor选项,
给不同的主机设置不同的名字
新主机挑选规则
设置从服务器优先级:
配置文件里的replica-priority
- 默认值100,值越小优先级越高。
- 0表示永远不会被选为主服务器。
新主机的挑选规则:
先剔除不符合条件的从服务器
- 否决已经下线、长时间没用回复心跳检测疑似下线的服务器
- 否决长时间没有与主机通信,数据状态过时的服务器
- 否决优先级为0的服务器
根据以下规则,在剩余的当中选
- 优先级最高的从服务器
- 同等优先级复制偏移量最大的服务器
- 以上都相同选运行ID最小的
Sentinel网络
只选择一个哨兵对Redis服务进行监控,可能会出现问题,可以使用多个哨兵进行监控,各个哨兵之间还可以互相监控,这样就形成了多哨兵模式。
假设主服务器宕机,哨兵1检查到了这个结果,但他不会马上进行failover
(故障转移)过程,仅仅只是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover(故障转移)操作。切换成功后,就会通知发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
当Sentinel网络中的其中一个Sentinel认为某个主服务器已经下线时,它会将这个主服务器标记为主观下线,然后询问其他的Sentinel是否也将这个主服务器标记成了主观下线(sdown)。
当这个同意主机下线的数量达到配置中设置的quorum
指定的数量时,将主服务器标记为客观下线(odown)。
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过Redis的pub/sub系统实现的。
每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他哨兵的存在。
每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello channel里发送一个消息,内容是自己的host、ip、runid还有对这个master的监控配置。
每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__hello channel,然后去感知到同样在监听这个master+slaves的哨兵的存在。
每个哨兵还会跟其他哨兵交换对master的监控配置,互相进程监控配置的同步。
Sentinel管理命令
获取所有被监视主服务器的信息
redis> sentinel masters
获取指定被监视主服务器的信息
redis> sentinel master
获取被监视主服务器的从服务器信息
redis> sentinel slaves
获取其他Sentinel的相关信息
redis> sentinel sentinels
重置主服务器状态
sentinel reset
强制执行故障转移
sentinel failover
检查可用Sentinel的数量
sentinel ckquorum
集群 ✍
Redis集群是Redis 3.0版本开始正式引入的功能,带来了在线扩展Redis系统读写性能的能力。
基本特性
- 提供主从复制功能、Sentinel功能,部分master不可用还是可以继续工作
- 分片和重分片,Redis集群会将整个数据空间划分为16384个槽(slot)来实现数据分片,集群中的各个主节点会分别负责处理其中的一部分
- 集群采用无代理模式,客户端发送的命令会直接交给节点执行
Redis cluster直接集成了replication和sentinel的功能,所以具有高可用性,主备替换原理也基本上是一致的。
在Redis cluster架构下,每个Redis要开放两个端口号,比如一个是6379,另一个是加1万的端口号,16379。
16379端口号是用来进行节点间通信的,也就是cluster bus的东西。
cluster bus的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制的协议,gossip
协议,用于节点间高效数据交换。
搭建集群
搭建集群有两种方法:
- 使用源码附带的集群自动搭建程序
- 配置文件手动搭建集群
F1 自动搭建
create-cluster程序位于源码的utils/create-cluster/create-cluster位置
通过start命令创建6个节点:
使用create命令把6个节点组合成一个集群,包括3个主节点和3个从节点
create命令会根据现有节点制定出一个响应的角色和槽分配计划,会询问一下你的意见。
这里是30001、30002、30003被设置为主节点,分别负责槽0 ~5460、5461 ~ 10922、10923 ~ 16383,30004、30005、30006分别设置为以上3个节点的从节点。
成功构建集群后,就可以使用客户端来连接和使用集群。
关闭集群,清理节点信息:
F2 手动搭建
搭建一个3个主节点和3个从节点组成的Redis集群。
创建6个文件夹,用于存放相应节点数据和配置文件。
redis.conf里的内容:
cluster-enabled yes
port 30001
执行命令:redis-cli --cluster create 127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 --cluster-replicas 1
这里注意,redis版本要5.0.0以上才能使用 --cluster
这样就成功了,打印的信息可以看到具体的分配情况。
如果想重新搭建集群,先将下图30001~30006的server的PID给kill
然后再将一个个节点重新redis-server redis.conf
启动
散列标签
可以通过散列标签将原本不属于同一个槽的键放到相同的槽里。
(ps: 集群客户端命令需要加-c)
使用散列标签,该功能会找出键中第一个被大括号包围并且非空的字符串子串,然后根据子串计算出该键所属的槽。即使两个键本来不属于一个槽,只要拥有相同的被包围子串,就可以把它们放到同一个槽中。
虽然从逻辑上说,把user::101和{user}::101看作同一个键,但散列标签只是Redis集群对键名的一种特殊解释,这两个键在实际中不相同,它们同时存在于数据库,由上面的图也可以看出来。
节点间的内部通信机制
基本通信原理
集群元数据的维护有两种方式:集中式、Goosip协议。
Redis cluster节点间采用gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其他节点,让其他节点进行元数据的变更。
goosip好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力,不好的在于元数据的更新有延迟。
gossip协议
gossip协议包含多种消息,包含ping、pong、meet、fail等等
- meet :某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信。
- ping :每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据。
- pong :返回ping和meet,包含自己的状态和其他信息,也用于信息广播和更新。
- fail :某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点某节点宕机。
分布式寻址算法
- hash算法 (大量缓存重建)
- 一致性hash算法(自动缓存迁移) + 虚拟节点(自动负载均衡)
- Redis cluster 的 hash slot算法
hash算法
给一个key,计算hash值,对节点数取模,打在对应的master节点上。
问题是如果某台机器宕机了,取模无法拿到想要的数据,因为节点数变了。
一致性hash算法
将整个hash值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织。
将各个master节点(使用服务器的ip或主机名)进行hash。这样就能确定每个节点在hash环上的位置。
给一个key,计算hash值,确定在环上的位置,从次位置沿环顺时针走,遇到的第一个master节点就是key所在的位置。
该方法如果有一个节点挂了,受影响的也仅仅只是此节点到环空间前一个节点之间的数据,其他都不受影响。增加一个节点也是这样。
但是当节点太少的时候,节点分布不均匀会造成缓存热点,所以引入了虚拟节点机制,对每个节点计算多个hash,每个计算结果都放置一个虚拟节点。这样就可以实现数据均匀分布,负载均衡。
Redis cluster的 hash slot算法
Redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。
Redis cluster每个master都会持有部分slot,hash slot让节点的增加移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。移动hash slot的成本是很低的,任何一台机器宕机对其他机器都不影响,因为key找的是hash slot,不是机器。
Redis cluster的高可用原理
判断节点宕机
和哨兵模式一样有主观宕机和客观宕机,在cluster-node-timeout内,某节点一直没有返回pong,被认为pfail(主观宕机)
如果一个节点认为某节点宕机,会在gossip ping消息中,ping给其他节点,如果超过半数节点都认为宕机,就会判断为客观宕机、从节点过滤
对于宕机的master,从其所有的slave node中,选一个切换为master node,如果salve和master断开连接的时间超过了
cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成master。从节点选举
每个从节点,根据自己对master复制数据的offset,来设置一个选举时间,offset越大的从节点,选举时间越靠前,优先进行选举。
所有的master节点开始slave选举投票,要给进行选举的slave进行投票,如果大部分master都投票给了某个从节点,那么选举通过,那个从节点可以切换成master。从节点执行主备切换,从节点切换为主节点。
缓存穿透和雪崩 ✍
缓存雪崩
比如某平台每天的高峰期,每秒10000个请求,缓存在高峰期可以抗住每秒8000个请求,但是缓存发生了全盘宕机,所有的请求都给了数据库,数据库挂了。
这就是缓存雪崩。
缓存雪崩的解决方案:
- 事前:Redis高可用,主从+哨兵,Redis cluster,避免全盘崩溃
- 事中:本地ehcache缓存+hystrix限流&降级,避免MySQL被打死
- 事后:Redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据
【encache】
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。【Hystrix】
hystrix是高可用性保障的一个框架,可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。
Hystrix通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延,同时Hystrix还提供故障时的fallback降级机制。
用户发送一个请求,系统收到请求后,先查本地ehcache缓存,如果没有查到再查Redis。如果ehcache都没有,再查数据库,将数据库的结果写入ehcache和Redis中。
组件限流,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求可以走降级,可以返回一些默认的值,或者友情提示,或者空值。
- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过
- 只要数据库不死,对用户来说还是有部分请求可以被处理的
- 只要还有部分请求不死,系统就没死,对用户来说就是多刷几次
缓存穿透
对于某系统,一秒6000个请求,结果5000个都是恶意攻击。5000个攻击缓存中查不到,数据库中也查不到。
每次请求都直奔数据库,这种恶意的攻击场景的缓存穿透就会直接把数据库给打死。
解决方式:每次系统从数据库中只要没查到,就写一个空值到缓存中,然后设置一个过期时间,这样的话,下次有相同的key来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
缓存击穿就是说某个key非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库。
解决方案如下:
- 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于Redis、Zookeeper等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
缓存和数据库的双写一致性✍
如果系统不是严格要求缓存数据库必须保持一致性的话,可以读请求和写请求串行化,串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低。
Cache Aside Pattern
最经典的缓存+数据库读写的模式,就是Cache Aside Pattern。
- 读的时候先读缓存,缓存没有去数据库读,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存(不是更新!因为有时候缓存不仅只是数据库中直接取出来的值,可能需要缓存的是两个数据的计算值)
缓存不一致解决方案
- 先更新数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决方案:先删除缓存,再更新数据库。
- 但是如果先删除了缓存,再修改数据库时一个请求过来,去读缓存,发现缓存空,去读数据库,查到了旧数据,放到了缓存中,然后后面数据库完成了更新,又不一致了。
解决方案:这里我暂时也无法给出准确答案。并发场景下如果要更新数据库,可以强制删除缓存重复更新请求几次?或者更新数据的时候给数据加个标识,如果发现在缓存里找不到数据,就删除缓存重新更新数据库。(未必正确)