古之立大事者,不惟有超世之才,亦必有坚韧不拔之志。——苏轼
缓存
缓存是高并发场景下提高热点数据访问性能的一个有效手段,在开发项目时会经常使用到。缓存的类型分为:本地缓存、分布式缓存和多级缓存。
本地缓存就是在进程的内存中进行缓存,比如我们的JVM堆中,可以用LRUMap来实现,也可以使用Ehcache这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
分布式缓存可以很好得解决这个问题。分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。
Redis是非关系型数据库,虽然也是数据库,但是它是保存在内存中的,所以读写速度非常快,经常被用于缓存。此外Redis也经常被用于分布式锁。
为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
保证高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数据缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
很常用的一个场景就是电商,一个商品的信息一天之内都不会改变,那么每个用户从数据库中查询都要比如800ms,那么此时如果用缓存,在第一次加载之后把数据放入到缓存中,这样后面其他用户查询的时候直接查缓存即可,性能可以提升非常多。
保证高并发
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中 去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
比如某个商品在中午有大量的访问请求,比如100万个,每秒4000个,MySQL是没办法承接这么大并发访问量的。可以分开访问,让3000个走缓存,剩下的1000个直接访问数据库。
为什么内存可以承受3000个访问量?——内存读写能力很强,别说4000/s,4万/s都可以。但是数据库别超过2000/s。
怎么保证redis是高并发以及高可用的?
总的回答
redis高并发:主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS。
redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最多只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。
redis高可用:如果你做主从架构部署,其实就是加上哨兵就可以实现,任何一个实例宕机,自动会进行主备切换。
详细介绍
经典问题是:如何保证Redis的高并发和高可用?redis的主从复制原理能介绍一下么?redis的哨兵原理能介绍一下么?
其实问这个问题,主要是考考你,redis单机能承载多高并发?如果单机扛不住如何扩容抗更多的并发?redis会不会挂?既然redis会挂那怎么保证redis是高可用的?
这里先介绍如何保证高可用高并发,哨兵放到了Redis章节里面去看。
redis高并发:主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,虽然单机最多只能承载几万QPS(也因此,不能让所有请求一把全都打到单机Redis上),但是如果用多从用来查询数据,多个从实例可以提供每秒10万+的QPS。所以说,redis不能支持高并发的原因,就两个字:单机
redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最多只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。
redis高可用:如果你做主从架构部署,其实就是加上哨兵就可以了,就可以实现,任何一个实例宕机,自动会进行主备切换。
实际上,Redis在使用中经常需要做到读写分离。因为redis缓存读的操作远远比写操作多。多到什么程度?可能写请求也就一秒钟几千,一两千。但是大量的请求都是读,一秒钟二十万次读。
所以要做读写分离,也就是做主从架构,master redis(主)把数据都复制到slave redis(从)上,从而可以支持10万+读QPS。
过程如下图所示:
所有读的操作都走slave redis,写操作才走master redis。这个就是redis的redis replication基本原理
,本质就是复制数据的操作。
redis replication核心机制
- redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量
- 一个master node可以配置多个slave node
- slave node也可以连接其他的slave node
- slave node做复制的时候,不会block master node的正常工作
- slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
- slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量
需要注意,如果开启了主从架构,那么master必须开启持久化,不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了。如果master没开持久化,那么重启之后数据拷贝,slave数据也会丢失。
此外,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的。
redis replication的完整流运行程和原理的深入剖析
主要对redis replication的复制的完整流程、数据同步相关核心机制、全量复制流程、增量复制流程、heartbeat、异步复制进行介绍。
redis replication复制完整流程
- slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始(master host和ip是从哪儿来的?redis.conf里面的slaveof配置的!)
- slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接
- slave node发送ping命令给master node
- 口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证
- master node第一次执行全量复制,将所有数据发给slave node
- master node后续持续将写命令,异步复制给slave node
数据同步相关的核心机制
指的就是第一次slave连接msater的时候,执行的全量复制,那个过程里面你的一些细节的机制
- master和slave都会维护一个offset
master会在自身不断累加offset,slave也会在自身不断累加offset。
slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset。
这个倒不是说特定就用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况
- backlog
master node有一个backlog,默认是1MB大小
master node给slave node复制数据时,也会将数据在backlog中同步写一份
backlog主要是用来做全量复制中断候的增量复制的
- master run id
通过info server
,可以看到master run id。
如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制。
为什么会不靠谱?
举个例子:
一开始master和slave都有100万条数据,我们现在master数据出了问题,我们想切换回之前版本的数据,那么一般用RDB恢复。RDB可能是20个小时之前的数据,那么此时slave中的数据一开始是和master同步的,而且如果slave也用offset等信息来要求数据同步,那么也是有问题的,需要把已经用RDB回复的数据再全量复制到slave中。
所以,master在使用RDB恢复数据之后,master的run id会变化,此时slave通过检测run id,发现run id变化了,就会触发全量复制,master把数据再复制到slave,这样slave中的数据也是正确的了。
如下图:
如果需要不更改run id重启redis,可以使用redis-cli debug reload命令
- psync
从节点使用psync从master node进行复制,psync runid offset
master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制
全量复制
当master node想要把数据全部复制给slave,那么进行全量复制。
- master执行
bgsave
,在本地生成一份rdb快照文件 - master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(
repl-timeout
),那么slave node就会认为复制失败,可以适当调节大这个参数 - 对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
- master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node
client-output-buffer-limit slave
256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败- slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
- 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF
rdb生成、rdb通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间
如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟
增量复制
- 如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制
- master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB
- msater就是根据slave发送的psync中的offset来从backlog中获取数据的
heartbeat
主从节点互相都会发送heartbeat信息
master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat
异步复制
master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node
redis主从复制原理、断点续传、无磁盘化复制、过期key处理
主从复制原理和步骤
当启动一个slave node的时候,它会发送一个Ping命令给master node看看两者能不能互通。
如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization(全量复制)
开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
如下图所示:
主从复制断点续传
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制
但是如果没有找到对应的offset,那么就会执行一次resynchronization。
无磁盘化复制
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync
repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来
过期key处理
slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
redis主从架构下如何才能做到99.99%的高可用性?
不可用的系统
正常情况,用户访问我们的系统(比如是电商系统),没问题。
但是可能会出现的情况是:后台系统宕机了、或者JVM进程OOM了、或者CPU打满了,不工作了、系统磁盘塞满了……总之导致系统挂掉了,不能提供服务了,那么这时候如果只抢修机器,这段时间系统没法访问,会造成巨大损失。——这是不可用的系统。
可用系统
和上面一样,系统崩了,但是如果我们系统设计的比较好,可以做到快速的主从切换。
一个基于哨兵的高可用性系统如下图所示:
注意这里的哨兵sentinal node,监控master有没有挂掉。
有关哨兵sentinal 到后面详细讲解。
这样我们的系统不可用的时间能够大大缩短。
99.99%可用?(高可用)
实际上这个道理理解起来很简单,如果我们的系统能够保证全年99.99%的时间都是可用的,我们的系统就是有高可用性的。与之对比的是,如果这个时间只有80%左右,那么这会是一个低可用的系统。
不可用系统
我们已经做了读写分离,读操作都交给了slave节点。当slave挂掉的时候,不会影响整个的可用性,还有其他的slave在提供相同数据下的相同的对外查询服务。
但是如果master挂掉了,就没法写数据了!或者说,master挂掉了,整个系统就相当于不可用了,缓存不能用了,大量访问会涌入到mysql中。
Redis
首先来看Redis相关知识图谱:
Redis为什么快?
三点原因:
- 采用了多路复用io阻塞机制
- 数据结构简单,操作节省时间
- 运行在内存中,速度当然快
为什么Redis用单线程还能很快
单线程的Redis在瓶颈是cpu的io时(这不是大多数应用的实际应用场景),确实速度会比多线程慢。但是,我们实际应用场景中很少会遇到瓶颈是CPU的io的情况,这时候单线程优势就凸显出来了。
实现很简单!性能又不会比多线程差,并且,单线程确实不用处理上下文的切换,cpu利用率会比多线程高,这时候采用单线程实现是一种很划算的做法。
为什么要用 redis 而不用 map/guava 做缓存?
之前有提到,缓存实际上分为本地缓存和分布式缓存两种。Redis属于分布式缓存,那么本地缓存如果能解决问题,为什么要用分布式缓存呢?——答案很明显,本地缓存不够用。下面分析一下。
以 Java 为例,使用自带的 LRUMap 或者 guava 实现的是本地缓存,主要的特点是 轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致 性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
Redis的功能总结
Bitmap位图是支持按bit位来存储信息,可以用来实现BloomFilter;HyperLogLog提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计UV;Geospatial 可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。这三个其实也可以算作一种数据结构。
pub/sub 功能是订阅发布功能,可以用作简单的消息队列。
Pipeline可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
Redis 支持提交 Lua 脚本来执行一系列的功能。
最后一个功能是事务,但 Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
Redis和memcached的区别
现在公司一般都用Redis实现缓存,因为Redis的优势更多也更实用。
主要有四点:
- redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供 list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache把数据全部存在内存之中。所以Redis不仅仅可以用作缓存,也可以用做NoSQL数据库。
- 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
- Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。这样做的原因有2个:一个是因为采用了异步非阻塞的事件处理机制;另一个是缓存数据都是内存操作,IO时间不会太长,单线程可以避免上下文切换产生的代价。
其他区别可以参考这张表格:
redis常见数据结构以及使用场景分析
1.String
常用命令:set,get,decr,incr,mget等
string类型是Redis中最常使用的类型,内部的实现是通过SDS(SimpleDynamicString)来存储的。SDS类似于Java中的ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
String最大可以存储512M,它是线程安全的,所以可以包含任何内容(比如jpg格式图片,序列化对象等)
Redis有原子性,让我们不用考虑并发。比如记录某网站用户访问次数,web只需要拼接用户id和当前日期,两者一起作为key。每次用户访问它的时候执行incr即可。
2.Hash
常用命令: hget,hset,hgetall 等。
hash类型在Redis中有ziplist和hashtable两种实现。当Hash表中所有的key和value字符串长度都小于64字节且键值对数量小于512个时,使用压缩表来节省空间;超过时,转为使用hashtable
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。
比如下面我就用 hash 类型存放了我本人的一些信息:
1 | key = JavaUser666666 |
一个hash可以存储多个组的键值对。实际中我们常用JSON文件存储用户信息,所以用hash存储JSON文件再合适不过了!
3.List
常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是链表,Redis list 的应用场景非常多,也是Redis重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。有ziplist压缩列表和linkedlist双链表实现。ziplist是存储在一段连续的内存上,存储效率高,但是它不利于修改操作,适用于数据较少的情况;linkedlist在插入节点上复杂度很低,但它的内存开销很大,每个节点的地址不连续,容易产生内存碎片。此外在3.2版本后增加了quicklist,结合了两者的优点,quicklist本身是一个双向无环链表,它的每一个节点都是一个ziplist。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功 能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
在实践中可以看到,list的插入顺序实际上是类似栈的,后进先出。所以list可以轻松实现新闻更新、排行更新等功能
4.Set
常用命令: sadd,spop,smembers,sunion 等
set 类型的内部实现可以是 intset 或者 hashtable,当集合中元素小于 512 且所有的数据都是数值类型时,才会使用 intset,否则会使用 hashtable。
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
sinterstore key1 key2 key3 将交集存在key1内
如果向Set中插入重复的元素,会返回0,插入失败。而且插入多个元素之后我们会发现set是无序的。
5.Sorted Set
常用命令: zadd,zrange,zrem,zcard等
sortedset是有序集合,有序集合的实现可以是ziplist或者是skiplist跳表。有序集合的编码转换条件与hash和list有些不同,当有序集合中元素数量小于128个并且所有元素长度都小于 64 字节时会使用 ziplist,否则会转换成 skiplist。
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维 度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
redis设置过期时间(redis的过期策略)
碰到这个问题,简而言之的回答,就是围绕两点:定期删除+惰性删除。
我往Redis里写的数据怎么没了?
缓存是基于内存的,大小是有限的,如果存储容量太大了,redis会把多余的干掉。类似,redis中可以设置数据的过期时间。
我的数据明明已经过期了,为什么还占着内存?
原因:Redis的过期策略删除方式是定期删除和惰性删除相结合的,你设置的过期的Redis没有被Redis的过期策略删除掉,只有系统再次扫描的时候才会将其删除。
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库, 这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统 的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
定期删除+惰性删除。
通过名字大概就能猜出这两个删除方式的意思了。
定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删 除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所 有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除:定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这是在访问时触发被动删除。这就是所谓的惰性删除,也是够懒的哈!
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查, 也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢?
使用redis 内存淘汰机制。
redis内存淘汰机制
如果redis的内存占用过多,那么之后会进行内存淘汰机制来让其他数据可以进入到redis中。下面介绍一下redis的淘汰机制。
提示:Redis的内存分配是使用jemalloc进行分配。jemalloc将内存空间划分为小、大、巨大三个范围,并在范围中划分了小的内存块,当存储数据时,选择大小最合适的内存块进行分配,有利于减小内存碎片。
一个问题:MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
redis 提供 6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选近少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除近少使用的key(这个是常用的).
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
Redis保证高可用的重要组件——哨兵
哨兵基本概念
sentinal,即哨兵。
哨兵是redis集群架构中非常重要的一个组件,主要功能如下
- 集群监控,负责监控redis master和slave进程是否正常工作
- 消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移,如果master node挂掉了,会自动转移到slave node上
- 配置中心,如果故障转移发生了,通知client客户端新的master地址
哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作
- 故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了
目前采用的是sentinal 2版本,sentinal 2相对于sentinal 1来说,重写了很多代码,主要是让故障转移的机制和算法变得更加健壮和简单
哨兵核心知识
- 哨兵至少需要3个实例,来保证自己的健壮性
- 哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性
- 对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练
为什么哨兵集群只有2个节点无法正常工作?
如题,在实际中,哨兵集群必须部署2个以上的节点才可以。下面进行分析:
如果哨兵集群仅仅部署了个2个哨兵实例,quorum=1。下图有一个Master(M1),一个Slave(R1)
1 | +----+ +----+ |
Configuration: quorum = 1
master宕机,s1和s2中只要有1个哨兵认为master宕机就可以进行切换,同时s1和s2中会选举出一个哨兵来执行故障转移.
如果这个时候,只有master(M1)挂掉了,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移
但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行
看一下经典的3节点哨兵集群
+----+
| M1 |
| S1 |
+----+
|+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+
Configuration: quorum = 2,majority
如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和S3可以一致认为master宕机,然后选举出一个来执行故障转移
同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移
redis哨兵主备切换的数据丢失问题:异步复制、集群脑裂
这小节主要解决两个问题:1、两种数据丢失的情况;2、解决异步复制和脑裂导致的数据丢失
Q:为什么会出现数据丢失?
A:一共有两种情况可能导致数据丢失:异步复制 和 集群脑裂。
异步复制:在Client往Master node写数据之后,只要master自己没问题,它就会返回值给Client,然后异步把新写的数据同步给slave node。但是因为主从之间数据复制是异步的,所以可能master没有来得及把数据复制给slave node,master就挂了,那么数据本身还是在master上的。此时哨兵监测到了master挂了,把slave提升成master node,后面读写都基于这个新的master node。但是这样就使得新的master node没有之前丢失的数据。
具体过程如下图:
集群脑裂:整个集群中还是只有一个master node。本来正常运行的系统因为网络故障而形成了网络分区,导致哨兵访问不到master,以为master挂了,则把slave node提升成了master。但是原本的master其实没有挂掉,这就导致了有两个master,搞成了多个数据节点,形成了脑裂。而且可能client只能和master连接从而一段时间之内还只往master里面写数据。而当我们要做恢复的时候,把master切换成了slave,会把新的master(原先是slave)节点的数据拷贝过去,导致master原本的数据也丢了。
如下图:
具体文字如下:
1、两种数据丢失的情况
主备切换的过程,可能会导致数据丢失
- 异步复制导致的数据丢失
因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了
- 脑裂导致的数据丢失
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着
此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master
这个时候,集群里就会有两个master,也就是所谓的脑裂
此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了
因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据
2、解决异步复制和脑裂导致的数据丢失
用两个参数解决:
1 | min-slaves-to-write 1 |
要求至少有1个slave,数据复制和同步的延迟不能超过10秒
如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致的数据丢失
- 减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内
如下图所示:
- 减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求
这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失
上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求
因此在脑裂场景下,最多就丢失10秒的数据
如下图所示:
redis哨兵的多个核心底层原理的深入解析(包含slave选举算法)
sdown和odown转换机制
哨兵需要判断master有没有宕机,master一般会有两种宕机状态:主管宕机(sdown)和客观宕机(odown)。
sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds
(在哨兵的配置文件中配置的)指定的毫秒数之后,就主观认为master宕机
sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机
哨兵和slave集群的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统(消息发布和订阅系统)实现的,每个哨兵都会往__sentinel__:hello
这个channel里发送一个消息,其他哨兵都会定时查看(或者说消费)这个channel里的消息,所以当某个哨兵发生变化的时候,其他哨兵都可以很快感知到这个变化。
每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello
channel里发送一个消息,内容是自己的host、ip和run id还有对这个master的监控配置
每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello
channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在,也是通过这个途径,各个哨兵之间会交换对master的监控配置,互相进行监控配置的同步。
slave配置的自动纠正
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上。
slave->master的选举算法(重要)
很多时候为了保证系统仍然可用,需要在master挂掉的情况下把slave提为master,而slave节点经常有很多,这就涉及到了具体选哪一个slave作为新的master的问题了。
在选举的具体过程中,会考虑slave的一些信息:
- 跟master断开连接的时长
- slave优先级
- 复制offset
- run id
如果一个slave跟master断开连接已经超过了down-after-milliseconds
的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master(这一步可以认为相当于先做一个过滤)
计算公式:
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
接下来会对slave进行排序
- 按照slave优先级进行排序,
slave priority
(这其实也是一个配置项,默认值是100)越低,优先级就越高 - 如果slave priority相同,那么看replica offset(当前slave复制了master的数据量),哪个slave复制了越多的数据,offset越靠后,优先级就越高
- 如果上面两个条件都相同,那么选择一个run id比较小的那个slave
quorum和majority
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵都认为了sdown,然后才可以转换成odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换
但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换
configuration epoch
其实是指配置的版本号。哨兵会对一套redis master+slave进行监控,有相应的监控的配置。
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号
configuration传播
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制
这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的
其他的哨兵都是根据版本号的大小来更新自己的master配置的,只要作用就是在故障切换之前要拿到master相关的配置的版本号,切换之后要根据版本号把最新的配置传播给其他的哨兵。
Redis持久化(怎么保证redis挂掉之后再重启数据可以进行恢复)
Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。
redis持久化的意义,在于故障恢复。比如你部署了一个redis,作为cache缓存,当然也可以保存一些较为重要的数据。如果没有持久化的话,redis遇到灾难性故障的时候,就会丢失所有的数据。
如果通过持久化将数据搞一份儿在磁盘上去,然后定期比如说同步和备份到一些云存储服务上去,那么就可以保证数据不丢失全部,还是可以恢复一部分数据回来的。
Redis的一种持久 化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF).这两种方法各有千秋。
Redis提供了RDB和AOF两种持久化方式,RDB是把内存中的数据集以快照形式写入磁盘,实际操作是通过fork子进程执行,采用二进制压缩存储;AOF是以文本日志的形式记录Redis处理的每一个写入或删除操作。
RDB和AOF原理参考图:
快照(snapshotting)持久化(RDB)
RDB:保存某个时间点的全量数据快照。
简单来说,RDB模式就是让Redis每隔一段时间(几分钟、几个小时、几天)保存一次当前Redis里面所有的数据。
RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行 备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性 能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
1 | save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令 创建快照。 |
RDB需要手动触发,一般用BGSAVE
,可以fork,即派生出一个子进程来创建RDB文件,不阻塞服务器进程。
RDB优点:
- RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,然后可以根据配置,把每个一段时间就把当前最新的RDB存储的数据发送到目标服务器。以预定好的备份策略来定期备份redis中的数据
- RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
- 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速
说明:主要针对RDB三点优势进行详细说明。
- 为什么RDB更适合做冷备?——实际上,RDB可以做冷备(生成多个文件,每个文件都代表了某一个时刻的完整数据快照),但AOF也可以做冷备(只有一个文件,但可以每隔一定时间,去copy一份这个文件出来)。那么RDB做冷备的优势在哪儿?第一点:RDB是由Redis去控制固定时长生成快照文件的,比较方便。而AOF需要自己去写一些脚本才能完成这个事情,需要各种定时。第二点:RDB数据做冷备,在最坏的情况下,数据恢复的速度比AOF快(AOF存储日志,恢复的时候要执行所有指令才能恢复,而RDB只是数据文件,恢复的时候只需要加载到内存中即可)。
- 为什么RDB性能更高?——因为RDB每次只写Redis内存,只是在一定的时候,才会将数据写入磁盘中;而AOF每次都要写文件,虽然可以快速写入os cache中,但还是开销比直接写内存大,也就是比RDB略慢
- 为什么RDB更快速?——参考第一点的第二部分,即:RDB数据做冷备,在最坏的情况下,数据恢复的速度比AOF快(AOF存储日志,恢复的时候要执行所有指令才能恢复,而RDB只是数据文件,恢复的时候只需要加载到内存中即可
综合上述优点,RDB适合做冷备份。
RDB缺点:
- 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据。也就是说,RDB比较容易丢数据,这也是RDB最大缺点,导致RDB不适合做第一优先的回复方案,如果你依赖RDB做第一优先恢复方案,会导致数据丢失比较多。
- RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。所以,一般不要让RDB的间隔时间太长,否则每次生成的RDB文件太大了,对redis本身性能可能有影响。
RDB数据丢失问题如下图所示:
AOF(append-only file)持久化
AOF会稍微复杂一点,简单来说,Redis每次被写入数据的时候,都会写入到磁盘的AOF文件里。注意这个过程会经过Linux OS Cache,因为现代操作系统中,写文件不是直接写磁盘的,会先写到os cache,然后过一段时间后再从os cache写到disk file.实际中一般每隔一秒,调用一次操作系统FSYNC操作,强制将os cache中的数据刷入磁盘文件中。
但是,因为AOF文件只有一个,所以AOF文件会越写越大。不能让redis内存中的数据无限增长而让AOF无限增大。解决方法就是定期rewrite,一般用LRU算法。
AOF 对日志文件的写入操作使用的追加模式,AOF文件里面保存的是写的指令,所以会不断膨胀。有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启 AOF(append only file)方式的持久化,可以通过appendonly
参数开启:
1 | appendonly yes |
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的 保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
1 | appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no #让操作系统决定何时进行同 |
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能 几乎没受到任何影响。而且这样即使出现系统崩溃,用户多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操 作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的大写入速度。
AOF优点:
- AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,保证os cache中的数据写入磁盘中,即使redis进程挂了,只最多丢失1秒钟的数据
- AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
- AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
- AOF可读性高(如果打开RDB文件,发现全是乱码,没法读。但AOF保存的数据是可读的),适合保存增量数据,数据不易丢失。也就是说,日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
AOF缺点:
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
- AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的。注意,如果你要保证一条数据都不丢,也是可以的,AOF的fsync设置成每写一条数据就做一次fsync。但是,这样也可以说完蛋了,因为redis的QPS会大降。
- 以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
- 总的来说,AOF唯一比较大的缺点,其实是做数据恢复的时候,会比较慢,还有做冷备,定期的备份,不太方便,可能要自己手写很复杂的脚本才能实现,不适合做冷备。
AOF重写(rewrite)
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读 入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期 间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容 追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作。
rewrite过程如下图所示,rewrite的目标就是让AOF中存储的文件大小不超过设定容量,这过程可能需要重新定义一个AOF等操作。
AOF和RDB如何选择
一言蔽之,就是不要只选择一种,而是两种结合。
- 不要仅仅使用RDB,因为那样会导致你丢失很多数据
- 也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
- 综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复
Redis事务
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然 后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令 请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性 (Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务 也具有持久性(Durability)。
Redis4.0对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble
开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整
聊聊redis cluster集群模式的原理吗?
早前,redis如果要搞几个节点,每个节点存储一部分的数据,得借助一些中间件来实现,比如codis,或者twemproxy。你会读写这些中间件,然后这些中间件负责将你的数据分布式地存储在多台机器上的redis实例中。
但是最近几年,redis在不断发展,不断更新版本,现在redis默认支持集群模式( redis cluster),你可以做到在多台机器上,部署多个redis实例,每个实例存储一部分的数据,同时每个redis主实例可以挂载redis从实例,从而可以做到当redis主实例挂掉之后可以切换到从实例顶上。
所以,现在redis最新版本大家用得都是redis cluster,也就是redis原生支持的redis集群模式,那么面试官肯定会就redis cluster给你几个连环炮。要是你没用过redis cluster,正常,以前很多人用codis之类的客户端来支持集群。但是,起码你得研究一下redis cluster吧!!
如何在保持读写分离+高可用的架构下,还能横向扩容支撑1T+海量数据
实际上,redis cluster的读写不是分离的,都是在master节点上做的,但是先讲原始情况,读写分离的情况,后面再扩展。
原本的架构中,master被一个哨兵监听、master负责写操作,slave负责承载读操作、每次写入新数据都让master和salve同步。
但是这样的架构有一个很大的问题,就是当数据量非常大之后,怎么办?我们不能让数据量超过master的物理上限,但是如果master只有32G内存,我们要存储1T的数据,怎么办???用LRU类似算法进行淘汰?那系统就出现了很大的瓶颈了!——或许看出来了,问题在于只有一个master
上述情景如下图所示:
那么,怎么才能够突破单机瓶颈,让redis支撑海量数据?
解决方法,就是用多个master,做成redis cluster即可。
支撑N个redis master node,每个master node都可以挂载多个slave node
有多个master,一个负责一部分写请求,这样就可以支持redis的横向扩容了。读写分离的架构,对于每个master来说,写就写到master,然后读就从mater对应的slave去读。
redis通过master扩容来支持1T+数据量的原理如下图所示:
高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master
一言蔽之:redis cluster(多master + 读写分离 + 高可用)
我们只要基于redis cluster去搭建redis集群即可,不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用
redis cluster vs. replication + sentinal
有关 redis cluster vs. replication + sentinal,即两者该如何选择的问题
如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了
replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了。
redis cluster,主要是针对海量数据+高并发+高可用的场景,重点是海量数据,如果你的数据量很大,那么建议就用redis cluster。
数据分布算法
主要介绍数据分布算法,包括 hash(最老最挫) -> 一致性hash算法(使用memcached的需要好好研究) -> redis cluster的hash slot算法。
用不同的算法,主要解决在多个master节点的时候,数据如何分布到这些节点上去的问题。
回顾redis cluster
redis cluster主要有两个特点:
- 自动将数据进行分片,每个master上放一部分数据
- 提供内置的高可用支持,部分master不可用时,还是可以继续工作的
在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379(四位数端口),另外一个就是加10000的端口号,比如16379(五位数端口)
16379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线。cluster bus的通信(用二进制协议来通信,可以减少节点间通信的时候数据交换的量,减少网络带宽消耗)。用来进行故障检测,配置更新,故障转移授权。
cluster bus用了另外一种二进制的协议,主要用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
哈希算法
这种算法很老,也比较挫,但是现在很少有人用哈希算法用在缓存,一般分库分表会使用哈希算法。
哈希算法就是在一个key来了之后,对节点数量取模,然后得到结果一定不大于节点数量。比如有3个master,对3取模,结果只能是0,1,2,然后存储到对应的master节点即可。
但是有一个很大的弊端,当一个master挂掉之后,会有1/n的数据丢失掉,要对剩下的机器取模,再分布。而对于高并发场景来说,这样不可接受,因为比如1/3不走缓存走数据库,那么数据库无法接受。所以,最大的问题是,只要一个master宕机,那么大量的数据需要重新计算写入缓存,风险很大。
如下图所示:
注意,上面不是1/3失效,而是几乎全部失效了。因为一个master失效了,再来的数据会根据2取模,这样很可能覆盖原来的数据,导致大部分请求都无法拿到请求,大量流量会涌入数据库中。
一致性hash算法(自动缓存迁移)+虚拟节点(自动负载均衡)
所谓的一致性hash,就是搞了个圆环,key来了之后放到圆环对应的点上(每个店都有一个对应的hash值)。key落在圆环上以后,会顺时针旋转去寻找距离自己最近的一个节点。
一致性哈希算法可以保证某个master宕机之后,只有master上的数据受到影响,也就是1/n。对比一下之前哈希算法,丢失的是几乎100%的数据!
一致性hash算法搞出来一个圆环,还挺有创意的。
但是可能造成缓存的热点问题,即大部分数据都堆积到了一个节点上。
解决方法:搞出来虚拟节点,给每个master都做了虚拟节点,这样在每个区间内,大量的数据都可以分布到不同的虚拟节点内,而不是按照顺时针顺序去走全都打入一个节点。
加入虚拟节点之后如下图所示:
hash slot算法(redis用的方法)
redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot,然后把hash slot放到对应的节点上,这样很随机。
redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去,移动hash slot的成本是非常低的。
而客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现。
假设此时有一个master宕机了,那么其他节点不会被影响,因为key找的是hash slot,不是机器。而且就算宕机了,还是对16384取模,还是能找到一个位置。效果和一致性哈希圆环是一样的。
实际上,redis发现了某个机器宕机之后,会以最快的速度把宕机的机器上的hash slot发送到其他没挂的节点,然后再来新hash slot,找到原本的slot即可,不会丢。
尽量让hash slot在各个机器上均匀分布。
redis cluster的核心原理分析
redis cluster的核心原理,主要包含gossip通信、jedis smart定位、主备切换。
节点间的内部通信机制
基础通信原理
(1)redis cluster节点间采取gossip协议进行通信
跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的
维护集群的元数据用的,两种,一种为集中式,一种叫做gossip
集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中(比如非常常用的zookeeper),其他节点读取的时候立即就可以感知到; 缺点在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。读数据都是从zookeeper里面读。
集中式存储和维护如下图:
gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点:元数据更新有延时,可能导致集群的一些操作会有一些滞后。Redis就是使用gossip协议的。
gossip原理维护元数据的原理如下图所示:
我们刚才做reshard,去做另外一个操作,会发现说,configuration error,达成一致
(2)10000端口
每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口
每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong
(3)交换的信息
故障信息,节点的增加和移除,hash slot信息,等等
gossip协议
gossip协议包含多种消息,包括ping,pong,meet,fail,等等
meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
在执行redis-trib.rb add-node
的时候,其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群
ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了
ping消息探入
ping很频繁,而且要携带一些元数据,所以可能会加重网络负担
每个节点每秒会执行10次ping,每次会选择5个最久没有通信的其他节点
当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了
比如说,两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。
所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率
每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
面向集群的jedis内部实现原理
等下开发都会基于jedis,也就是说redis的java client客户端,redis cluster,jedis cluster api
jedis cluster api与redis cluster集群交互的一些基本原理
基于重定向的客户端
redis-cli -c,自动重定向
(1)请求重定向
客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot。如果这个hash slot在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向
cluster keyslot mykey,可以查看一个key对应的hash slot是什么
用redis-cli的时候,可以加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令
(2)计算hash slot
计算hash slot的算法,就是根据key计算CRC16值,然后对16384取模,拿到对应的hash slot
用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和set mykey2:{100}
(3)hash slot查找
节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上
smart jedis
(1)什么是smart jedis
基于重定向的客户端,很消耗网络IO,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点
所以大部分的客户端,比如java redis客户端,就是jedis,都是smart的
本地维护一份hashslot -> node的映射表,缓存,大部分情况下,直接走本地缓存就可以找到hashslot -> node,不需要通过节点进行moved重定向
(2)JedisCluster的工作原理
在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot -> node映射表,同时为每个节点创建一个JedisPool连接池
每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后在本地映射表找到对应的节点
如果那个node正好还是持有那个hashslot,那么就ok; 如果说进行了reshard这样的操作,可能hashslot已经不在那个node上了,就会返回moved
如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新本地的hashslot -> node映射表缓存
重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException
jedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新hash slot,频繁ping节点检查活跃,导致大量网络IO开销
jedis最新版本,对于这些过度的hash slot更新和ping,都进行了优化,避免了类似问题
(3)hashslot迁移和ask重定向
如果hash slot正在迁移,那么会返回ask重定向给jedis
jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存
已经可以确定说,hashslot已经迁移完了,moved是会更新本地hashslot->node映射表缓存的
高可用性与主备切换原理(重要)
redis cluster的高可用的原理,几乎跟哨兵是类似的
判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是pfail(哨兵的sdown),主观宕机
如果多个节点都认为另外一个节点宕机了,那么就是fail(哨兵的odown),客观宕机,跟哨兵的原理几乎一样,sdown,odown
在cluster-node-timeout
内,某个节点一直没有返回pong,那么就被认为pfail
如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail
从节点过滤
对宕机的master node,从其所有的slave node中,选择一个切换成master node
检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成master
这个也是跟哨兵是一样的,从节点超时过滤的步骤
从节点选举
哨兵:对所有从节点进行排序,slave priority,offset,run id
每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举
所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master
从节点执行主备切换,从节点切换为主节点
与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能。
使用Redis的缺陷和可能碰到的问题
不考虑使用Redis过程中可能碰到的问题是不行的,因为实际中技术都有优劣存在。
其实这是问到缓存必问的,因为缓存雪崩和穿透,那是缓存最大的两个问题,要么不出现,一旦出现就是致命性的问题。所以面试官一定会问你。
缓存雪崩及解决方案
简而言之就是,如果缓存全部宕机,那么大量用户请求一下子全都落在了MySQL数据库上,那么MySQL承受不住,会挂掉。而且一重启就挂,一重启就挂!一个例子,就是曾经用户千万级的互联网公司,缓存全部宕机,从下午持续到凌晨三四点,公司损失几千万。
缓存雪崩现象图示:
解决方案主要分三个部分:事前、事中、事后。
事前:让redis高可用,可以用主从+哨兵,做成redis cluster(之前讲过它包含哨兵和replication两个功能)形式,即一个master带一些slave,这样master挂了之后可以把slave提上来,避免全盘崩溃。(当然这样做还是可能出问题的,可能所有redis集群全部崩溃)。发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:可以在本地增加一个ehcache的缓存(但是面对真正的雪崩,是杯水车薪) + hystrix限流&降级,避免MySQL崩掉。所有的数据必须先经过限流组件,你可以限制每秒只能通过多少数据,比如一共5000个,你限制一次只能通过2000个,那么只能通过2000个。剩下的3000个怎么办?走降级——限流组件发现有3000个请求没法通过自己,会调用降级组件,返回一些默认的值或者空白的值。这样如果缓存挂了,但是数据库可能有限度地处理数据,只要数据库不死,你的系统就不会死,只是处理得慢一些,但是比整个挂掉要强得多。
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
总的来说,事前靠高可用、事中靠本地缓存+限流(确保库不能死)、事后靠redis(Redis一定要开持久化)
主要就是事前、事中、事后三个方式来解决缓存雪崩的问题。
如下图:
缓存穿透及解决方案
穿透就是指大量的请求在数据库中是查不到数据的。
一般是黑客故意去请求缓存中不存在的数据,(因为黑客是随意编造的数据来发的请求,数据库中根本没有,而且缓存里面也不会有,那么缓存就帮不了忙,而且每次都是这样,都只会去数据库里面找数据)。这个时候,导致所有的请求都落到数据库上,造成数据库短时间内承受大量 请求而崩掉。
如下图所示:
可以用两种解决方法。
- 第一种:每次系统A只要从数据库里没查到,就写一个空值到缓存里(VALUE统一附上UNKNOW值),这样下次黑客发过来请求的时候,哪怕是假数据,缓存中也已经保存了,可以返回一个UNKNOW!这种方法简而言之,就是空对象缓存。
- 第二种:如果恶意攻击非常多,可以考虑使用布隆过滤器,可以快速判断请求是否存在于我们的缓存中。
缓存与数据库双写不一致
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如 何解决一致性问题?这个是实际生产中很经常会遇到的问题。
这个问题要好好分析一下。
先介绍经典的缓存+数据库读写模式,然后分析如何解决缓存和数据库双写不一致的问题。
缓存+数据库读写模式:cache aside pattern
cache aside pattern是最经典的缓存+数据库读写的模式。
cache aside pattern简单来说,就是两点:
- 读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
- 更新的时候,先删除缓存,然后再更新数据库
如下图所示:
优先去读redis,如果能读到,就返回数据。如果没在redis读到,则去读MySQL,并且之后把数据写入到redis里面去,同时返回数据。
如果要更新数据,先把redis里面的缓存删掉,再更新。
为什么是删除缓存,而不是更新缓存呢?
原因很简单,很多时候,复杂点的缓存的场景,缓存有的时候不简单是数据库中直接取出来的值,而是放了很久都不会被访问的值。如果你为了这个很久不被访问的值去做计算(或者叫做更新),是很不划算的,不如直接删除来得快。换句话说,你不常用这个缓存的话,没必要总是把缓存计算。
商品详情页的系统,修改库存,只是修改了某个表的某些字段,但是要真正把这个影响的最终的库存计算出来,可能还需要从其他表查询一些数据,然后进行一些复杂的运算,才能最终计算出现在最新的库存是多少,然后才能将库存更新到缓存中去。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值的。
但是,更新缓存的代价是很高的。是不是每次修改数据库的时候,都一定要将其对应的缓存去跟新一份?也许有的场景是这样的,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁地更新缓存。
但是问题在于,这个缓存到底会不会被频繁访问到???
举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次; 但是这个缓存在1分钟内就被读取了1次,有大量的冷数据。(数据访问其实遵循28法则,黄金法则,20%的数据,占用了80%的访问量)
实际上,如果你只是删除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。每次数据过来,就只是删除缓存,然后修改数据库,如果这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被重新计算的,所以可以设计成用缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
类似mybatis,hibernate的懒加载思想,用得时候才加载。
举一个懒加载的例子:查询一个部门,部门带了一个员工的list,没有必要说每次查询部门,都里面的1000个员工的数据也同时查出来啊,因为80%的情况,查这个部门,就只是要访问这个部门的信息就可以了。只有极少数的情况,先查部门,同时要访问里面的员工。按照lazy的思想,只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工。
具体解决方案
最初级的缓存不一致问题以及解决方案
问题:先修改数据库,成功了,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致。
如下图所示:
解决思路:非常简单,就是cache aside pattern模式,一定要先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。
因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。如下图:
比较复杂的数据不一致问题分析
比较复杂的情况就是,数据发生了变更,哪怕你先删除了缓存,然后修改数据库,也会发生数据不一致的情况。
一个请求过来,要修改数据库的库存。此时按照cache aside pattern模式,会先去把缓存给删了,然后尝试修改数据库的库存数量。重点是,当修改还没有完成的时候,同时来了一个读请求,这个读请求会直接去读缓存,发现此时缓存是空的,那么这个请求就会发送请求给这个服务的其他接口,要把数据库中的数据拿出来放到缓存里面去(完全符合cache aside pattern的流程做的,异常原因是读写同时发生了),此时把原先的库存写到了缓存里,返回的就是老的库存数据。而在这个读操作完成之后,MySQL才完成修改库存的写操作,这就造成了现在数据库的数据和缓存中的数据不一致的情况……有点悲催。
如下图所示:
等等,我们其实可以在这里分析一下,为什么会发生这种复杂度,读写同时发生的情况?
可以说,这个情况的发生背景是上亿流量高并发场景,缓存才会出现这个问题。
因为只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题,但是如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么一般不会出现这种复杂的数据不一致的场景。
但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。高并发了以后,问题是很多的!
解决思路:数据库与缓存更新与读取操作进行异步串行化。(简略介绍)
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,因为如果你要保证数据库和缓存完全一致,要做读请求和写请求的串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
Redis并发竞争Key(缓存击穿)
redis的并发竞争问题是什么?如何解决这个问题?了解Redis事务的CAS方案吗?
这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。
解决方法:分布式锁+加时间戳判断数据版本。可以基于zookeeper(ZK)做分布式锁。然后不要让旧版本覆盖掉新版本。
需要提一嘴,如果你的系统不太可能出现这样的问题,那么其实没必要做这个锁,但是如果可能出现这样的情况,即请求可能一股脑过来,每个实例都可能拿到一个请求,分别更新数据,那么就需要有分布式锁。
先尝试获取分布式锁,如果能够获得,那么其他系统此时跑过来,也尝试获取分布式锁,那么它是获取不到锁的
如下图所示:
而且redis自己就有天然解决这个问题的CAS类的乐观锁方案。
公司生产环境的redis集群的部署架构是什么样的?
生产环境中的redis是怎么部署的?你的redis是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上redis给几个G的内存?设置了哪些参数?压测后你们redis集群承载多少QPS?
redis cluster,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。
机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给redis进程的是10g内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。
5台机器对外提供读写,一共有50g内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。
目前高峰期每秒就是3500左右的请求量
比如很多大型的互联网公司,其实基础架构的team,会负责缓存集群的运维。