Redis 作为一门主流技术,缓存应用场景非常多,很多大中小厂的项目中都会使用redis作为缓存层使用。
但是Redis作为缓存,也会面临各种使用问题,比如数据一致性,缓存穿透,缓存击穿,缓存雪崩,数据倾斜(热点key(hot key),大key(big key),在集群中的数据倾斜),redis集群脑裂等常见的缓存使用问题。
下边我们就来依次解析一下这些常见问题的生成原因和解决方案:
数据一致性
Redis作为缓存层,它是为了高并发场景下,可以提供高并发量的访问和更新数据的一种技术。当用户面对不同的操作数据场景下,我们应该对Redis做出什么要求呢?
场景:
查询数据:
当用户对数据进行查询时,用户会先到Redis中进行查询,如果Redis中不存在,会请求数据库,并且写到Redis中,然后返回给用户数据。
新增数据:
当用户进行从插入数据时,直接写入到数据库中,不经过缓存,也不会新增到缓存中,只有查询数据时,如果缓存没有,才会进行新增。
更新数据:
当用户对数据进行更改的时候,不但要对数据库中的数据进行更改,还要对缓存进行更新,不然就会出现脏读的现象。
那么,如何保证在更改数据的时候,不会出现缓存和数据库中的数据不一致的现象,导致脏读呢?
数据一致性的四种可能处理逻辑:
我们按照常规逻辑来想,无非就四种情况:
- 更新缓存,更新数据库
- 更新数据库,更新缓存
- 更新数据库,删除缓存
- 删除缓存,更新数据库
1)更新数据库,更新缓存
如果我成功更新了缓存,但是在执行更新数据库的那一步,服务器突然宕机了,那么此时,我的缓存中是最新的数据,而数据库中是旧的数据。
脏数据就因此诞生了,并且如果我缓存的信息(是单独某张表的),而且这张表也在其他表的关联查询中,那么其他表关联查询出来的数据也是脏数据,结果就是直接会产生一系列的问题。
2)更新数据库,更新缓存
只有等到缓存更新之后,才能访问到正确的信息。那么在缓存没过期的时间段内,所看到的都是脏数据。
以上两个策略只要执行第二步时失败了,就必然会产生脏数据。
3)删除缓存,更新数据库
这种方式在没有高并发的情况下,是可能保持数据一致性的。
如果只有第一步执行成功,而第二步失败,那么只有缓存中的数据被删除了,但是数据库没有更新,那么在下一次进行查询的时候,查不到缓存,只能重新查询数据库,构建缓存,这样其实也是相对做到了数据一致性。
4)更新数据库,删除缓存
如果更新数据库成功了,而删除缓存失败了,那么数据库中就会是新数据,而缓存中是旧数据,数据就出现了不一致情况。
3,4两个策略在读写并发的情况下,还是会出现数据不一致的情况。
3策略
4策略
但是此处达成这个数据不一致性的条件明显会比起其他的方式更为困难 :
时刻1:读请求的时候,缓存正好过期
时刻2:读请求在写请求更新数据库之前查询数据库,
时刻3:写请求,在更新数据库之后,要在读请求成功写入缓存前,先执行删除缓存操作。
延迟双删
以上四种方式无论选择那种方式,如果实在多服务或时并发的情况下,其实都是有可能产生数据不一致性的。
为了解决这个存在的问题有以下方式:
先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。进行两次删除,且中间需要延迟一段时间
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
和3策略比起来,其实就是加了一个“延时+删除缓存”的操作,因为读写并发场景下,可能会导致构建旧缓存的操作在update之后,所以加个延时时间,确保删除操作,能够在构建旧缓存之后完成,延时时间以读取+构建缓存的时间作为参考,加上几百ms。
这样只有在延时的时间内,会读取到脏数据,但是最终一致性是得到了保证。对于用户来说,是完全可以接受的。
延迟双删的优化(消息队列删除缓存)
延迟双删要求更新操作延迟一段时间,但是这会降低代码的性能,减少了接口的吞吐量,对代码也有一定的侵入性。
而且都无法保证两者都能一次性操作成功,所以我们最好的办法就是重试,这个重试并不是立即重试,因为缓存和数据库可能因为网络或者其它原因停止服务了,立即重试成功率极低,而且重试会占用线程资源,显然不合理,所以我们需要采用异步重试机制。
异步重试我们可以使用消息队列来完成,因为消息队列可以保证消息的可靠性,消息不会丢失,也可以保证正确消费,当且仅当消息消费成功后才会将消息从消息队列中删除。
优点1:可以大幅减少接口的延迟返回的问题
优点2:MQ本身有重试机制,无需人工去写重试代码
优点3:解耦,把查询Mysql和同步Redis完全分离,互不干扰
延迟双删的再次优化(canal订阅日志)
即使把删除缓存的工作交给了消息队列,但是操作消息队列还是对代码有一定的侵入性。因为有的时候更新数据库并不需要修改缓存,这样维护起来还是比较麻烦的,不能做到适配。
以mysql为例,在数据库一条记录发生变更时就会生成一条binlog日志,我们可以订阅这种消息,拿到具体的数据,然后根据日志消息更新缓存,订阅日志目前比较流行的就是阿里开源的canal,那么我们的架构就变为如下形式。
订阅数据库变更日志,当数据库发生变更时,我们可以拿到具体操作的数据,然后再去根据具体的数据,去删除对应的缓存。
当然Canal 也是要配合消息队列一起来使用的,因为其Canal本身是没有数据处理能力的。
这个方式算的上彻底解耦了,应用程序代码无需再管消息队列方面发送失败问题,全交由 Canal来发送。
但是需要注意的是,canal对数据库会有性能上的影响,具体用哪种操作来保证双写一致性,还是要看具体的业务逻辑和整体的性能需求。
缓存穿透
问题的发生现象:
是指查询一个根本不存在的数据,缓存层和存储层都不会命中,于是这个请求就可以随意访问数据库,这个就是缓存穿透,缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。
通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。
问题的发生原因:
造成缓存穿透的基本原因有两个。
第一,自身业务代码或者数据出现问题,比如,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。
第二,一些恶意攻击、爬虫等造成大量空命中。
问题的解决方法:
- 缓存空对象
当存储层不命中,到数据库查发现也没有命中,那么仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消前面所说的数据一致性方案处理。
- 布隆过滤器拦截
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。
因为布隆过滤器是通过哈希映射来进行判断的,不存储真正的值,所以必定存在哈希碰撞,这样以来删除对于布隆过滤器来说是个难题。虽然可以通过定期重建布隆过滤器或者使用计数布隆过滤器来实现删除操作,但是前者实时性并不好,后者会占用更大的空间。
缓存击穿
问题的发生现象:
缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接大并发量请求数据库,就像在一个完好无损的桶上凿开了一个洞。
问题的发生原因:
大多数情况是因为秒杀或者热点新闻,或者特殊的在极短时间内,有某个热度极高的数据被不断请求。
问题的解决方法:
缓存击穿的话,设置热点数据永远不过期,或者加上互斥锁就能搞定了。
-
使用互斥锁(mutex key):
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
-
key永远不过期:
这里的“永远不过期”包含两层意思:
(1) 从redis上看,不设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
缓存雪崩
问题的发生现象:
由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,比如同一时间缓存数据大面积失效,那一瞬间Redis跟没有一样,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
缓存雪崩的英文原意是stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。
问题的发生原因:
原因一:缓存中大量的数据同时过期
原因二:redis 服务挂了,缓存层瘫痪
问题的解决方法:
缓存中大量的数据同时过期场景
方案一:请求限流并降级(补救兜底方案)
请求限流,就是限制前端请求每秒请求量,使得数据库能承受前端全部请求。比如前端允许每秒访问1000次,其中900请求缓存,100次才请求数据库。一旦发生雪崩,数据库每秒请求激增到1000次,此时启动请求限流,在前端入口只允许每秒请求100次,过多的请求直接拒绝。
对redis所在的服务器进行指标监控,比如QPS、CPU使用率、内存使用率等,如果发现redis服务宕机,而数据库请求压力倍增,此时可以启动请求限流
方案二:缓存失效时间分散(防止同时过期)
将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
Redis宕机场景
这个问题其实就是保证Redis的高可用
方案一:哨兵机制或者集群cluster
保证缓存层服务高可用性。和飞机都有多个引擎一样,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。Sentinel和 Redis Cluster都实现了高可用。
数据倾斜
热点key(hot key)
问题的发生现象:
和缓存击穿稍许不同,缓存击穿是因为热点key的过期时间到期,导致请求直接越过了缓存层打到了DB上,导致DB宕机。
缓存热点是指大部分甚至所有的业务请求都命中同一份缓存数据,给缓存服务器带来了巨大压力,甚至超过了单机的承载上限,导致服务器宕机。
请求分片集中,超过单Server的性能极限。在服务端读数据进行访问时,往往会对数据进行分片切分,此过程中会在某一主机Server上对相应的Key进行访问,当访问超过Server极限时,就会导致热点Key问题的产生。
1、流量集中,达到物理网卡上限。
2、请求过多,缓存分片服务被打垮。
3、DB击穿,引起业务雪崩。
问题的发生原因:
热点问题产生的原因大致有以下两种:
用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)。
在日常工作生活中一些突发的事件,例如:双十一期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击浏览或者购买时,会形成一个较大的需求量,这种情况下就会造成热点问题。同理,被大量刊发、浏览的热点新闻、热点评论、明星直播等,这些典型的读多写少的场景也会产生热点问题。
问题的解决方法:
解决热点key问题可以从两个方面来综合考虑
1.发现热点key
1)预估发现
针对业务提前预估出访问频繁的热点key,例如秒杀商品业务中,秒杀的商品都是热点key。
当然并非所有的业务都容易预估出热点key,可能出现漏掉或者预估错误的情况。
2)客户端发现
客户端其实是距离key"最近"的地方,因为Redis命令就是从客户端发出的,以Jedis为例,可以在核心命令入口,使用这个Google Guava中的AtomicLongMap进行记录,如下所示。
使用客户端进行热点key的统计非常容易实现,但是同时问题也非常多:
(1) 无法预知key的个数,存在内存泄露的危险。
(2) 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
(3) 规模化汇总实现比较复杂。
3)Redis发现(命令)
monitor命令
monitor命令可以监控到Redis执行的所有命令,利用monitor的结果就可以统计出一段时间内的热点key排行榜,命令排行榜,客户端分布等数据。
Facebook开源的redis-faina正是利用上述原理使用Python语言实现的,例如下面获取最近10万条命令的热点key、热点命令、耗时分布等数据。为了减少网络开销以及加快输出缓冲区的消费速度,monitor尽可能在本机执行。
此种方法会有两个问题:
1、monitor命令在高并发条件下,内存暴增同时会影响Redis的性能,所以此种方法适合在短时间内使用。
2、只能统计一个Redis节点的热点key,对于Redis集群需要进行汇总统计。
可以参考的框架:Facebook开源的redis-faina正是利用上述原理使用Python语言实现的
hotkeys命令
Redis在4.0.3中为redis-cli提供了–hotkeys,用于找到热点key。
如果有错误,需要先把内存淘汰策略设置为allkeys-lfu或者volatile-lfu,否则会返回错误。
但是如果键值较多,执行会较慢,和热点的概念的有点背道而驰,同时热度定义的不够准确。
4)抓取TCP包发现
Redis客户端使用TCP协议与服务端进行交互,通信协议采用的是RESP。如果站在机器的角度,可以通过对机器上所有Redis端口的TCP数据包进行抓取完成热点key的统计
此种方法对于Redis客户端和服务端来说毫无侵入,是比较完美的方案,但是依然存在3个问题:
(1) 需要一定的开发成本
(2) 对于高流量的机器抓包,对机器网络可能会有干扰,同时抓包时候会有丢包的可能性。
(3) 维护成本过高。
对于成本问题,有一些开源方案实现了该功能,例如ELK(ElasticSearch Logstash Kibana)体系下的packetbeat[2] 插件,可以实现对Redis、MySQL等众多主流服务的数据包抓取、分析、报表展示
2.解决热点key
1)使用二级缓存
可以使用 guava-cache或hcache,发现热点key之后,将这些热点key加载到JVM中作为本地缓存,并且设置一个失效时间。将首先检查该数据是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的服务器,有效的保护了缓存服务器。
缺点:实时感知最新的缓存数据有点麻烦,会产生数据不一致的情况。我们可以设置一个比较短的过期时间,采用被动更新。当然,也可以用监控机制,如果感知到数据已经发生了变化,及时更新本地缓存。
2)key分散
将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上。
注意:缓存一般都会设置过期时间,为了避免缓存的集中失效,我们对缓存的过期时间尽量不要一样,可以在预设的基础上增加一个随机数。
大key(big key)
问题的发生现象:
1、内存空间不均匀.(平衡):例如在Redis Cluster中,bigkey 会造成节点的内存空间使用不均匀。
2、超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
3、CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
4、网络拥塞:每次获取bigkey产生的网络流量较大
假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB 的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。
如果这个bigkey存在但是几乎不被访问,那么只有内存空间不均匀的问题存在,相对于另外两个问题没有那么重要紧急,但是如果bigkey是一个热点key(频繁访问),那么其带来的危害不可想象,所以在实际开发和运维时一定要密切关注bigkey的存在。
问题的发生原因:
1、redis数据结构使用不恰当
将Redis用在并不适合其能力的场景,造成Key的value过大,如使用String类型的Key存放大体积二进制文件型数据。
2、未及时清理垃圾数据
没有对无效数据进行定期清理,造成如HASH类型Key中的成员持续不断的增加。即一直往value塞数据,却没有删除机制,value只会越来越大。
3、对业务预估不准确
业务上线前规划设计考虑不足没有对Key中的成员进行合理的拆分,造成个别Key中的成员数量过多。
4、明星、网红的粉丝列表、某条热点新闻的评论列表
假设我们使用List数据结构保存某个明星/网红的粉丝,或者保存热点新闻的评论列表,因为粉丝数量巨大,热点新闻因为点击率、评论数会很多,这样List集合中存放的元素就会很多,可能导致value过大,进而产生Big Key问题。
问题的解决方法:
发现bigkey:
-
redis-cli --bigkeys可以命令统计bigkey的分布。
但是在生产环境中,开发和运维人员更希望自己可以定义bigkey的大小,而且更希望找到真正的bigkey都有哪些,这样才可以去定位、解决、优化问题。
判断一个key是否为bigkey,只需要执行debug object key查看serializedlength属性即可,它表示 key对应的value序列化之后的字节数。
-
scan + debug object
如果是要遍历多个,则尽量不要使用keys的命令,可以使用scan的命令来减少压力。
Redis 从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决 keys命令可能带来的阻塞问题,但是要真正实现keys的功能,需要执行多次scan。可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:
scan cursor [match pattern] [count number]
cursor :是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
Match pattern :是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。
Count number :是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。
除了scan 以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似,请自行参考Redis官网。
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan 的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。
debug object key
根据传入的对象(Key的名称)来对Key进行分析并返回大量数据,其中serializedlength的值为该Key的序列化长度,需要注意的是,Key的序列化长度并不等同于它在内存空间中的真实长度,此外,debug object属于调试命令,运行代价较大,并且在其运行时,进入Redis的其余请求将会被阻塞直到其执行完毕。
如果键值个数比较多,scan + debug object会比较慢,可以利用Pipeline机制完成。对于元素个数较多的数据结构,debug object执行速度比较慢,存在阻塞Redis的可能,所以如果有从节点,可以考虑在从节点上执行。
- 第三方工具
利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
https://github.com/sripathikrishnan/redis-rdb-tools
解决bigkey:
-
使用合理的数据结构
-
对大Key进行清理
对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,通过UNLINK,你可以安全的删除大Key甚至特大Key。
- 拆分
对 big key 存储的数据 (big value)进行拆分,变成value1,value2… valueN等等。
例如big value 是个大json 通过 mset 的方式,将这个 key 的内容打散到各个实例中,或者一个hash,每个field代表一个具体属性,通过hget、hmget获取部分value,hset、hmset来更新部分属性。
例如big value 是个大list,可以拆成将list拆成。= list_1, list_2, list3, …listN
其他数据类型同理。
key推荐值:
单个key的value小于10KB
对于集合类型的key,建议元素数量小于1000
在集群中的数据倾斜
背景
对于分布式系统而言,整个集群处理请求的效率和存储容量,往往取决于集群中响应最慢或存储增长最快的节点。所以在系统设计和容量规划时,我们尽量保障集群中各节点的“数据和请求分布均衡“。但在实际生产系统中,出现数据容量和请求倾斜(类似Data Skew)问题是比较常见的。
什么是集群中的数据倾斜?
单台机器的硬件配置有上限制约,一般我们会采用分布式架构将多台机器组成一个集群,下图的集群就是由三台Redis单机组成。客户端通过一定的路由策略,将读写请求转发到具体的实例上。
由于业务数据特殊性,按照指定的分片规则,可能导致不同的实例上数据分布不均匀,大量的数据集中到了一台或者几台机器节点上计算,从而导致这些节点负载多大,而其他节点处于空闲等待中,导致最终整体效率低下。
数据倾斜主要分为两类:
- 数据存储容量倾斜,数据存储总是落到集群中少数节点;(bigKey)
- qps请求倾斜,qps总是落到少数节点。(hot key)
导致Redis集群倾斜的常见原因
-
一般是系统设计时,键空间(keyspace)设计不合理:
-
系统存在大的集合key(hash,set,list等),导致大key所在节点的容量和QPS过载,集群出现qps和容量倾斜;
-
DBA在规划集群或扩容不当,导致数据槽(slot)数分配不均匀,导致容量和请求qps倾斜;
-
系统大量使用Keys hash tags, 可能导致某些数据槽位的key数量多,集群集群出现qps和容量倾斜;
-
工程师执行monitor这类命令,导致当前节点client输出缓冲区增大;used_memory_rss被撑大;导致节点内存容量增大,出现容量倾斜;
如何有效避免Redis集群倾斜问题
-
避免出现hotkey 和 bigkey
-
redis集群部署和扩缩容处理,保证数据槽位分配平均;
-
系统设计角度应避免使用keys hash tag(某类key分配同一个分片),使用scan扫描keyspace是否有使用hash tags的,或使用monitor,vc-redis-sniffer工具分析倾斜节点,是否大理包含有hash tag的key;
-
日常运维和系统中应避免直接使用keys,monitor等命令,导致输出缓冲区堆积;这类命令建议作rename处理;
-
合量配置normal的client output buffer, 建议设置10mb,slave限制为1GB按需要临时调整(警示:和业务确认调整再修改,避免业务出错)
在实际生产业务场景中,大规模集群很难做到集群的完全均衡,只是尽量保证不出现严重倾斜问题。
redis脑裂
所谓的脑裂,就是指在有主从集群中,同时出现了两个主节点,它们都能接收写请求。
而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
数据丢失一定是发生了脑裂吗?
那可能你想问了,数据丢失一定是发生了脑裂吗?如何判断发生了脑裂?
数据丢失不一定是发生了脑裂,最常见的原因是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。
如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断,也就是计算 master_repl_offset 和 slave_repl_offset 的差值。如果从库上的 slave_repl_offset 小于原主库的 master_repl_offset,那么,我们就可以认定数据丢失是由数据同步未完成导致的。
而判断是否发生了脑裂,可以采取排查客户端的操作日志的方式。
通过看日志能够发现,在主从切换后的一段时间内,会有客户端仍然在和原主库通信,并没有和升级的新主库进行交互,这就相当于主从集群中同时有了两个主库。根据这个迹象,我们就可以判断可能是发生了脑裂。
单机主从架构脑裂
现在假设:有三台服务器一台主服务器,两台从服务器,还有一个哨兵。
-
基于上边的环境,这时候网络环境发生了波动导致了sentinel没有能够心跳感知到master,但是哨兵与slave之间通讯正常。所以通过选举的方式提升了一个salve为新master。如果恰好此时server1仍然连接的是旧的master,而server2连接到了新的master上。数据就不一致了,哨兵恢复对老master节点的感知后,会将其降级为slave节点,然后从新maste同步数据(full resynchronization),导致脑裂期间老master写入的数据丢失。
-
基于setNX指令的分布式锁,可能会拿到相同的锁;
-
基于incr生成的全局唯一id,也可能出现重复。
解决方案
通过配置参数
min-replicas-to-write x
min-replicas-max-lag y
第一个参数表示最少的salve节点为z个
第二个参数表示数据复制和同步的延迟不能超过y秒
配置了这两个参数:
这两个配置项组合后的要求是,主库连接的从库中至少有 x 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 y 秒,否则,主库就不会再接收客户端的请求了。
这样一来,即使原主库是假故障,它在假故障期间也无法响应哨兵发出的心跳测试,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了,这样可以避免大量数据丢失。
集群脑裂
Redis集群的脑裂一般是不存在的,因为Redis集群中存在着过半选举机制,而且当集群16384个槽任何一个没有指派到节点时整个集群不可用(可用配置文件更改)。
所以我们在构建Redis集群时,应该让集群 Master 节点个数最少为 3 个,且集群可用节点个数为奇数。
不过脑裂问题不是是可以完全避免,只要是分布式系统,必然就会一定的几率出现这个问题,CAP的理论就决定了。