一、读写分离的问题

图片

1.数据复制的延迟  

读写分离时,master会异步的将数据复制到slave,如果这是slave发生阻塞,则会延迟master数据的写命令,造成数据不一致的情况  

解决方法:可以对slave的偏移量值进行监控,如果发现某台slave的偏移量有问题,则将数据读取操作切换到master,但本身这个监控开销比较高,所以关于这个问题,大部分的情况是可以直接使用而不去考虑的。

  2.读到过期的数据 

产生原因:  

redis的从库是无法主动的删除已经过期的key的,所以如果做了读写分离,就很有可能在从库读到脏数据

例子重现

主Redis

setex test 20 1
+OK
get test
$1
1
ttl test
:18

从Redis

get test
$1
1
ttl test
:7

以上都没问题,然而过几秒再看从Redis

ttl test
:-1
get test
$1
1

test这个key已经过期了,然而还是可以获取到test的值。

在使用Redis做锁的时候,如果直接取读从库的值,这就有大问题了。 

二、为什么从库不删除数据?

redis删除过期数据有以下几个策略:  

  1. 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key,很明显,这是被动的!  
  2. 定期删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 redis 会定期主动淘汰一批已过期的key。(在第二节中会具体说明)  
  3. 主动删除:当前已用内存超过maxMemory限定时,触发主动清理策略。主动设置的前提是设置了maxMemory的值

    int expireIfNeeded(redisDb *db, robj *key) { 
     time_t when = getExpire(db,key); 
     
     if (when < 0) return 0; /* No expire for this key */ 
     
     /* Don't expire anything while loading. It will be done later. */ 
     if (server.loading) return 0; 
     
     /* If we are running in the context of a slave, return ASAP: 
     * the slave key expiration is controlled by the master that will 
     * send us synthesized DEL operations for expired keys. 
     * 
     * Still we try to return the right information to the caller, 
     * that is, 0 if we think the key should be still valid, 1 if 
     * we think the key is expired at this time. */ 
     if (server.masterhost != NULL) { 
     return time(NULL) > when; 
     } 
     
     /* Return when this key has not expired */ 
     if (time(NULL) <= when) return 0; 
     
     /* Delete the key */ 
     server.stat_expiredkeys++; 
     propagateExpire(db,key); 
     return dbDelete(db,key); 
    }

通过以上源码发现,4行:没有设置超时时间,则不删;7行:在”loading”时不删;16行:非主库不删;21行未到期不删。25行同步从库和文件。

所以说,在从库执行主动删除操作,或者通过惰性删除的方式触发删除key的操作,最终都不会执行成功。原因就在上面的第16行代码。 

解决方法:  
1)通过一个程序循环便利所有的key,例如scan  
2)通过ttl判断  
3)升级到reidis3.2 

主从配置不一致

这个问题一般很少见,但如果有,就会发生很多诡异的问题

例如:

1.maxmemory配置不一致:这个会导致数据的丢失  

原因:例如master配置4G,slave配置2G,这个时候主从复制可以成功,但,如果在进行某一次全量复制的时候,slave拿到master的RDB加载数据时发现自身的2G内存不够用,这时就会触发slave的maxmemory策略,将数据进行淘汰。更可怕的是,在高可用的集群环境下,如果我们将这台slave升级成master的时候,就会发现数据已经丢失了。 

2.数据结构优化参数不一致(例如hash-max-ziplist-entries):这个就会导致内存不一致  

原因:例如在master上对这个参数进行了优化,而在slave没有配置,就会造成主从节点内存不一致的诡异问题。

三、规避全量复制

redis复制有全量复制和部分复制两种,而全量复制的开销是很大的。那么我们如何尽量去规避全量复制。

1.第一次全量复制

某一台slave第一次去挂到master上时,是不可避免要进行一次全量复制的,那么,我们如何去想办法降低开销呢?  
方案1:小主节点,例如我们把redis分成2G一个节点,这样一来,会加速RDB的生成和同步,同时还可以降低我们fork子进程的开销(master会fork一个子进程来生成同步需要的RDB文件,而fork是要拷贝内存快的,如果主节点内存太大,fork的开销就大)。  

方案2:既然第一次不可以避免,那我们可以选在集群低峰的时间(凌晨)进行slave的挂载。

2.节点RunID不匹配

例如我们主节点重启(RunID发生变化),对于slave来说,它会保存之前master节点的RunID,如果它发现了此时master的RunID发生变化,那它会认为这是master过来的数据可能是不安全的,就会采取一次全量复制  

解决办法:对于这类问题,我们只有是做一些故障转移的手段,例如master发生故障宕掉,我们选举一台slave晋升为master(哨兵或集群) 

3.复制积压缓冲区不足  

master生成RDB同步到slave,slave加载RDB这段时间里,master的所有写命令都会保存到一个复制缓冲队列里(如果主从直接网络抖动,进行部分复制也是走这个逻辑),待slave加载完RDB后,拿offset的值到这个队列里判断,如果在这个队列中,则把这个队列从offset到末尾全部同步过来,这个队列的默认值为1M。而如果发现offset不在这个队列,就会产生全量复制。  

解决办法:增大复制缓冲区的配置 rel_backlog_size 默认1M,我们可以设置大一些,从而来加大我们offset的命中率。这个值,我们可以假设,一般我们网络故障时间一般是分钟级别,那我们可以根据我们当前的QPS来算一下每分钟可以写入多少字节,再乘以我们可能发生故障的分钟就可以得到我们这个理想的值。 

四、规避复制风暴

什么是复制风暴?举例:我们master重启,其master下的所有slave检测到RunID发生变化,导致所有从节点向主节点做全量复制。尽管redis对这个问题做了优化,即只生成一份RDB文件,但需要多次传输,仍然开销很大。  

1.单主节点复制风暴:主节点重启,多从节点全量复制

解决:更换复制拓扑如下图:

图片

  1.我们将原来master与slave中间加一个或多个slave,再在slave上加若干个slave,这样可以分担所有slave对master复制的压力。(这种架构还是有问题:读写分离的时候,slave1也发生了故障,怎么去处理?) 

 2.如果只是实现高可用,而不做读写分离,那当master宕机,直接晋升一台slave即可。 

2.单机器复制风暴:机器宕机后的大量全量复制,

如下图:

图片

当machine-A这个机器宕机重启,会导致该机器所有master下的所有slave同时产生复制。(灾难)

解决:  

1.主节点分散多机器(将master分散到不同机器上部署)  

2.还有我们可以采用高可用手段(slave晋升master)就不会有类似问题了。 
 

常见的数据库集群架构如何?

答: 一主多从,主从同步,读写分离。
图片
 如上图:  
(1)一个主库提供写服务  
(2)多个从库提供读服务,可以增加从库提升读性能  
(3)主从之间同步数据

画外音:任何方案不要忘了本心,加从库的本心,是提升读性能。 

为什么会出现不一致?

答:主从同步有时延,这个时延期间读从库,可能读到不一致的数据。

图片

 如上图:  
(1)服务发起了一个写请求  
(2)服务又发起了一个读请求,此时同步未完成,读到一个不一致的脏数据  (3)数据库主从同步最后才完成

画外音:任何数据冗余,必将引发一致性问题。

如何避免这种主从延时导致的不一致

?答:常见的方法有这么几种。
方案一:忽略任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。
画外音:如果业务能接受,最推崇此法。 如果业务能够接受,别把系统架构搞得太复杂。 

方案二:强制读主

图片

 如上图:  
(1)使用一个高可用主库提供数据库服务 
 
(2)读和写都落到主库上 

(3)采用缓存来提升系统读性能

这是很常见的微服务架构,可以避免数据库主从一致性问题。 

方案三:选择性读主
强制读主过于粗暴,毕竟只有少量写请求,很短时间,可能读取到脏数据。 

有没有可能实现,只有这一段时间,可能读到从库脏数据的读请求读主,平时读从呢?

 可以利用一个缓存记录必须读主的数据。

图片
 如上图,当写请求发生时:

(1)写主库

(2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”

画外音:key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。 
 
图片

如上图,当读请求发生时:

这是要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果, 
 
(1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询  

(2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询以此,保证读到的一定不是不一致的脏数据。

 总结数据库主库和从库不一致,常见有这么几种优化方案:
(1)业务可以接受,系统不优化
(2)强制读主,高可用主库,用缓存提高读性能
(3)在cache里记录哪些记录发生过写请求,来路由读主还是读从

哨兵是什么?

哨兵是Redis高可用(HA)的解决方案.它是一种特殊的模式.只不过它不对外提供服务.

组成:
Sentinel是一个系统(或者说是集群)它是由一个(多个实例)组成,采用哨兵集群的原因是防止哨兵的误判.(单个哨兵的话会由于主节点过大或者由于网络原因造成误判)

为什么需要Redis哨兵?

Redis有主从模式(客户端写操作发送到主节点,读操作发送到从节点),如果这时候主节点出现故障,不能对外提供服务了.这个时候我们需要做哪几个操作才能正常的对外提供不间断服务.

1.我们不知什么主节点挂了(最主要的问题)

2.手工把从库设置为主库

3.客户端还需要修改新的主库地址

Redis如何提供无间断对外提供服务?

首先我们了解下Redis哨兵三个定时任务

1.监控(主节点是否下线)

2.选主(选择一个新的主节点)

3.通知(通知客户端和其他从节点,哨兵)

监控(三个定时任务)

INFO命令

第一个定时任务: 每10s每个哨兵对master和slave节点发送INFO命令(获取最新的拓扑结构)

image.png

订阅频道

第二个定时任务 每2秒每个sentinel通过master节点的channel交换信息

图片

每个哨兵节点岁会向频道发送对于(__sentinel__: hello)判断信息
也会订阅其他哨兵节点对于主节点的判断

PING命令(检测)

每隔1秒, 每个Sentinel节点会向主节点、 从节点、 其余Sentinel节点发送一条ping命令做一次心跳检测, 来确认这些节点当前是否可达

image.png

Redis判断主节点下线(主观 客观)

Redis 主节点下线的过程分为 主观下线,客观下线
在这里如果你的哨兵只有一个会直接下线就行故障转移,如果是多个会进行询问后(多数哨兵就决定下线)
才会就行故障转移。正常情况下都会有多个哨兵。
主节点下线步骤:
1.当哨兵PING命令发现,有节点响应超时,会把节点标记为客观下线.
2.如果客观下线节点为主节点时,会询问其他哨兵,当其他哨兵也判定客观下线(超过半数哨兵认为客观下线)
3.哨兵系统会选择一个哨兵把其中的一个从节点升级为主节点,并通知客户端.


smilesnake
298 声望4 粉丝