一文吃透 Redis:主从复制 / 哨兵 / 集群模式 + 缓存 + 分布式锁_redis raft
目录
一,主从复制
1,配置主从复制
2,断开主从复制
3,拓扑
一主一从结构
一主多从结构
树形结构
4,原理
PSYNC工作流程
全量复制的流程
部分复制流程
实时复制
5,总结
二,哨兵(Sentinel)
1,相关名词解释
2,哨兵自动恢复主节点故障
3,哨兵机制的特点
4,主从切换的具体流程
5,总结
三,集群
1,哈希求余
2,一致性哈希算法
3,哈希槽分区算法
4,故障处理
故障判定
故障迁移
四,Redis典型应用——缓存
1,使用redis作为数据库的缓存
2,如何知道redis中应该存储哪些数据?
3,缓存预热,缓存穿透,缓存雪崩和缓存击穿
五,分布式锁
1,引入过期时间
2,引入校验id
3,引入lua脚本
4,引入看门狗(Watch Dog)
5,redlock算法
在分布式系统中,为了解决单点问题,通常会把redis服务部署到多个服务器上,满足故障恢复和负载均衡等请求。
所谓的单点问题,就是某个服务器程序,只有一个节点(只有一个物理服务器,来部署这个服务器程序)。
存在的问题:
-
可用性问题:如果这个机器挂了,那么部署的redis服务就挂了,服务就中断了。
-
性能/支持的并发量有限:毕竟一台主机的硬件资源(比如CPU,硬盘,网络带宽等等)是有限的,一个请求就要消耗一定量的硬件资源。一旦并发量太高,如果请求超过主机所能提供这些资源,那么可能就会出现异常,甚至主机直接宕机了。
在分布式系统中,往往希望有多个服务器来部署redis服务,从而构成一个redis集群,此时就可以让这个集群给分布式系统中的其他服务,提供更稳定/更高效的数据存储功能 。
主要存在以下几种部署方式:
-
主从模式
-
主从+哨兵模式
-
集群模式
一,主从复制
1,配置主从复制
主从模式,即在若干个redis节点中,有的是主节点,有的是从节点 。
redis主从模式中,从节点上的数据,不允许修改,只允许读取数据。要想修改数据,只能访问主节点。
假设有三个物理服务器(三个节点),都部署了redis服务,此时就可以把其中一个看作是主节点,其余两个看作是从节点。从节点上的数据要跟随主节点的数据而变化,从节点的数据要和主节点保持一致。
本来,在主节点上保存了一堆数据,现在引入从节点,就是要把主节点上的数据复制出来,放到从节点中。后续,主节点这里对数据有任何修改,都会把这样的修改同步给从节点。
总结:主从模式,主要是针对读操作进行可用性和并发量的提高,而对于写操作来说,无论是可用性还是并发量,都很依赖与主节点,但是主节点又不能搞多个。
如何在一个云服务器上部署多个redis服务,按照主从模式,实现类似于分布系统的效果?(ubutun20.04)
我们可以在一个云服务器主机上,运行多个redis-server进程,我们只需保证这几个服务的端口号不同即可。本来redis-server的默认端口号是6379,此时新启动的redis-server端口号就不能是6379了。
在这里我们创建两个从节点,端口号是6380和6381,而主节点就是原先的redis-server,端口号为默认的6379。
可以通过修改配置文件来改变:
1,首先需要将配置文件拷贝两份
cp /etc/redis/redis.conf slave1.conf
cp /etc/redis/redis.conf slave2.conf
2,然后修改拷贝后的两个配置文件中的选项
修改端口号——port 6380 port6381
将该服务设置为后台进程——daemonize yes
只需修改这两个选项即可。
3,启动这两个redis-server(从节点)
redis-server slave1.conf
redis-server slave2.conf
4,现在我们只是有了多个redis-server,还没有构成主从结构。
要想配置成主从结构,就需要使用slaveof。有如下三个方法:
-
在配置文件中加入
slaveof{masterHost} {masterPort}
,之后随redis启动生效。 -
在redis-server启动命令中加入--slaveof{masterHost}{masterPort}生效。
-
直接使用redis命令:slaveof{masterHost}{masterPort}生效。
其中{masterHost}和{masterPort}分别指主节点的ip和端口。
5,在这里我们选择通过修改配置文件的方式来构成主从结构
在slave1.conf和slave2.conf配置文件中加上slaveof 127.0.0.1 6379即可,以6379的redis-server作为主节点。配置文件修改完毕之后,需要我们重新启动redis-server,配置文件才会生效,所以此时需要重启端口号为6380和6381的redis-server。
由于我们启动redis-server服务时,是使用redis-server port
这样的方式启动的,所以在重启的时候,需要使用kill -9
来杀掉该服务进程,然后再通过redis-server port重启即可。
而如果我们是使用service redis-server start
来启动服务器,就需要使用service redis-server stop
来终止程序。此时如果使用kill -9来终止服务进程,kill掉之后,这个redis-server进程能自动启动 。相当于有一个进程来专门监控指定服务器的运行状态,如果服务器挂了,会立即重新启动。
因此怎么启动的redis-server,就必须搭配对应的方式进行终止。
6,再次启动redis-server即可实现主从模式
2,断开主从复制
使用命令slaveof no one可以断开主从关系,这只是暂时的,当redis-server重启之后,主从关系就会恢复,所以要想一直断开主从关系,还是需要修改配置文件。
3,拓扑
我们的redis-server服务器可能有很多个节点,如何组织这些节点?
一主一从结构
如果其他客户端想要读取数据,从主节点A或 从节点B读取都可以,两个服务器上的数据是一致的。
如果是写数据请求,那么只能由主节点A来完成。
如果此时写请求太多,也会给主节点造成 一定的压力。我们可以通过关闭主节点的AOF,毕竟写内存比写磁盘快,只打开从节点的AOF。但是这种设定,有一个严重缺陷,主节点一旦挂了,不能立即重启,因为主节点这边没有使用AOF保存数据,如果重启了,那么数据就会丢失,进一步的主从同步,也就使从节点上数据也丢失了。改进办法是,当主节点挂了,先从从节点那里获取AOF文件,再启动,这样就可以保证数据没有问题了。
一主多从结构
同理,如果是读请求,可以访问主节点,也可以访问从节点。
如果是写请求 ,是能访问主节点,主节点上的数据发生变化,就会把改变的数据同步给其他从节点。
这个结构存在的问题:同步操作是需要消耗网络带宽的,如果子节点太多,就会大大消耗主节点主机的硬件资源。
树形结构
主节点将数据同步给从节点B和从节点C,再由从节点B将数据同步给从节点D和从节点E。
此时主节点A就不需要太高的网络带宽,但此时数据同步的延时会更长。
4,原理
同步数据的命令:PSYNC replicationid offset
,这个命令是从节点执行的。
其中replicationid是复制id,是主节点生成的,主节点在启动的时候就会生成。
当从节点和主节点建立了复制关系,就会从主节点上 获取到这个id,这个id就表明了当前从节点是从哪个主节点上获取的数据。
在主节点和从节点上一般都有两个replicationid:replicationid和replicationid2。其中replicationid2其备份的作用,比如下面的例子。
现在有一个主节点A和一个从节点B,A在启动的时候会生成一个replicationid,之后B获取到A的replicationid。如果A和B通信过程中出现了一些网络抖动,B可能会认为A挂了,此时从节点B就会晋升为主节点,并给自己生成一个replicationid,此时B的replicationid2就保存了之前获取到的A的replicationid。等到网络稳定,B还可以根据这个replicationid2再次与节点A建立主从关系。(但是主从复制这种方法,当主节点挂了,一般从节点是不会晋升为主节点的)
这个再次建立主从关系的过程,一般是需要手动完成的。当然,在下面的哨兵机制部分,可以自动完成这个过程。
offset表示偏移量
主节点和从节点都会维护这个偏移量(一个整数)。
主节点的偏移量:主节点可能会收到很多的修改操作的命令,每个命令都选哟占据几个字节。主节点会把这些命令的字节数进行累加,这个数字就是主节点的偏移量。
从节点的偏移量:描述了当前数据同步的进度,从节点会每秒上报自身的偏移量给主节点。
如果从节点和主节点的偏移量一致,就表述数据 同步完成了。
综上,replicationid描述了从哪个主节点同步数据,offset表示同步数据的进度。如果两个机器的replicationid和 offset一样,就表示这两个机器上的数据一致。
replicationid和offset就共同描述了一个\"数据集合\"。
PSYNC工作流程
PSYNC可以从主节点获取全量数据,也可以获取部分数据。
主要看offset的值,如果设为-1,表示全量获取,如果设置为具体的整数,则表示从当前整数位置来进行获取。
当然,从节点想要全量的获取数据,还是增量的获取数据,同时也取决于主节点,主节点会自行判定,看当前是否方便给部分数据,如果不方便,会直接给 全量数据。
全量复制的流程
在一个从节点与主节点第一次建立主从关系的时候,就会涉及到数据的同步,而此时一般就是进行全量复制。
在进行全量复制的时候,主节点要进行生成rdb文件的操作,然后再把该文件通过网络传输发送给从节点,从节点也是先将该rdb文件保存,然后读取该文件来获取数据。
而redis也支持\"无硬盘模式\"(diskless):主节点生成的rdb的二进制数据,不直接保存到文件中,而是直接通过网络传输发送给从节点 ,省去了读写硬盘的操作,而从节点现在也可以省去这个操作了,直接将收到数据加载到内存中......
但是,即使引入了\"无硬盘模式\",对全量复制的效率提升不是很大,因为全量复制整个操作是比较重量的,数据规模较大。相比于网络传输,读写硬盘操作算是快的了。所以减少了对硬盘的操作,但网络传输无法省去,意味着对整个操作的效率提升不大。
部分复制流程
在主节点与从节点连接的过程中,可能由于网络抖动等原因,主从节点的连接断开,此时重新建立连接并进行数据同步的时候,使用的就是部分复制。
实时复制
全量复制:主要适用于从节点刚连上主节点进行数据初始化的工作。
部分复制:是全量复制的一种特殊情况,属于全量复制的一种优化。
实时复制:在从节点与主节点建立好连接,并且完成数据同步之后,此时主节点可能会受到源源不断的请求,其中就会包含修改数据的请求,主节点的数据就会发生变化,此时就要把数据也同步给从节点。从节点和主节点之间建立有Tcp连接,当主节点收到修改数据的请求,就会通过该Tcp连接,将这个请求发送给从节点,从节点再根据这些请求修改数据即可。
所以在实时复制的时候,我们需要保证主节点与从节点的Tcp连接处于可用状态。在这里,使用心跳包机制来实现。
心跳包机制:
主节点:默认每隔10s,向从节点发送一个ping命令,从节点收到后会返回一个\"pong\"。
从节点:默认每隔1s,向主节点发送一个特定的请求,告诉主节点当前的同步进度(offset),之后主节点也会给出一个响应。
如果,达到某个阈值,还没有收到响应,那么就认为这个主节点/从节点存在问题,判断下线了。
5,总结
主从复制解决的问题:单点问题
单点问题:单个redis节点,可用性不高,性能有限。
主从复制的特点:
1,主节点可以用来读写,从节点只能用来读,可以减少主节点的访问压力。
2,主从复制存在多种拓扑结构:可以在适当的场景使用适当的拓扑结构,比如一主多从的结构,同步操作快,但是消耗的网络资源多,因为主节点要通过网络和所有从节点实现同步。而树形结构,主节点消耗的网络资源减少了,但是如果树的层级太高,会造成数据同步的延迟增长加。
3,复制分为全量复制,部分复制和实时复制。
4,通过心跳机制保证主节点和从节点的正常通信和数据一致。
主从复制的缺点:
1,从节点多了,数据复制的延时就会非常明显。
2,如果主节点挂了,从节点不会晋升为主节点,需要通过人工干预的方式恢复。
二,哨兵(Sentinel)
主从复制最大的问题,还是在主机点上,如果主节点挂了,从节点不会晋升为主节点,需要通过人工干预的方式恢复。
因此Redis哨兵机制,就是为了解决上述问题,自动的对挂了的主节点进行替换,也就是将上述手动的过程改成自动。
哨兵机制是通过启动一个不同的进程来体现的,它和redis-servver服务器进程不是 同一个进程 。
1,相关名词解释
名词
逻辑结构
物理结构
主节点
Redis服务
一个独立的redis-server进程
从节点
Redis服务
一个独立的redis-server进程
Redis数据节点
主从节点
主节点和从节点的进程能
哨兵节点
监控Redis数据节点的节点
一个独立的redis-sentinel进程
哨兵节点集合
若干哨兵节点构成的整体
若干redis-sentinel进程
Redis哨兵(Sentinel)
Redis提供的高可用方案
哨兵节点集合和Redis主从节点
应用方
一个或多个客户端
一个或多个连接Redis的进程
2,哨兵自动恢复主节点故障
如果一个哨兵节点发现主节点挂了,为了防止预判,还需多个哨兵节点共同认为这件事情。
如果主节点确实挂了,这些哨兵节点会选出一个作为leader,由这个哨兵节点负责从剩下的从节点中选一个出来,作为主节点。选出新的主节点之后,哨兵节点就会控制该节点执行slaveof no one,并且通知其他从节点,修改slaveof到新的主节点之上。哨兵节点会自动通知客户端程序,告知新的主节点是谁,并且此后客户端再进行写操作时,就会访问新的主节点了。
3,哨兵机制的特点
监控:Sentinel节点会定期检查redis数据节点,使用心跳包机制。
故障转移:实现从节点晋升为主节点,并维护好正确的主从关系。
通知:Sentinel会将故障转移的结果通知给应用方。
4,主从切换的具体流程
重点,面试题:
1,主观下线:哨兵节点通过心跳包进制,判读redis主节点服务器是否正常工作,如果没有沙鸥到响应,该哨兵节点就会认为该主节点下线了。
2,客观下线:当多个哨兵节点都认为主节点挂了之后,此时这个主节点就是主观下线了。
3,再从多个哨兵节点中,选举出一个leader节点,由这个leader节点负责从剩下的节点中选出一个作为主节点。
4,leader挑选完毕后,此时需要从剩下的从节点中选一个当作新的主节点。
-
首先会看这些从节点的优先级,每个 redis数据节点,都会有自己的配置文件,配置文件中就有一个优先级的设置。leader会选择优先级高的作为新的主节点。
-
如果数据节点的优先级都一样,那么就比较这些数据节点的offset,offset表示从节点与原来主节点进行数据同步的进度,offset越大,说明这个节点与原来主节点的数据相似度最高,此时就会选择这个节点作为新的主节点。
-
如果前面两个条件都一样,此时其实意味着从这些节点中任选一个都行。那么再看这些节点的runid(一串数字),每个redis数据节点启动时都会生成一个随机的runid,此时比较runid的大小。让runid更小的作为新的主节点。
-
指定好新的主节点之后,此时leader就会控制这个主节点执行slaveof no one,让这个节点成为主节点(master)。再控制其他从节点,执行slaveof,让这些从节点,以新的mater作为主节点。
5,总结
哨兵节点不能只有一个,因为哨兵节点挂了也会影响 系统的运作。
哨兵节点最好是奇数个,方便选举leader,得票数更容易超过一半。
哨兵+主从复制解决的问题是\"提高可用性\",极端情况下写操作的数据丢失无法解决。
哨兵+主从复制不能提高数据的存储容量 ,当数据接近或者几乎超过机器的物理内存时,这样的结构就难以胜任了,而接下来的redis集群,就是解决存储容量问题的有效方案。
三,集群
广义上的集群,是指多个机器,构成的分布式系统,就可以称为一个集群,所以前面的主从复制和哨兵模式也可以看作是一个集群。
而侠义上的集群,是redis提供的集群模式。这个集群模式之下,主要是解决存储空间不足的问题。
在redis哨兵模式中,本质还是redis数据节点存储数据,其中就要求主节点/从节点来存储数据的全集。为了提高 数据存储的容量,这时就引入多台机器,每台机器只存储一部分数据。
假设有1TB的数据需要存储: 拿两台机器来存储 ,每台机器需要存储512GB。
拿三台机器来存储,每台机器需要存储300多GB。
拿四台机器来存储,每台机器需要存储 256GB......
但是还存在一个问题,如果使用三台机器来存储1TB的数据,如果某个机器挂了怎么办,所以我们还需要为每个机器 再分配几个从节点。
这三组机器的数据都是不同的,每个 slave都是mster的备份,当master挂了,slave就会晋升为master。
如上图所示,每个虚线框就可以看作是一个分片(Sharding)。
重点面试题:
三种主流的分片算法:哈希求余,一致性哈希算法,哈希槽分区算法。
1,哈希求余
借助hash函数,把一个key映射成为 一个数字,在对数组长度(这里就是分片的个数)进行求余,就可以得到这个key是在哪个分片中。
比如现在有3个分片,编号为0,1,2
此时就可以针对要查询的数据(或插入的数据)计算hash值(比如可以使用md5算法),再把这个值余上分片的个数,此时就会得到一个数字,这个数字就表示这个数据在哪个分片中。
但是当总体的数据增长时,就需要扩容,引入更多的分片,此时分片的个数就变了。
如果发现某个数据在扩容之后,不该待在当前的分片中,那么就需要重新分配数据(数据搬运)。这里涉及到的数据搬运不仅仅是主节点进行,从节点也需要进行。
这种方式开销极大,往往不能再生产环境上操作的,搬运成本比较大。
数据搬运成本大的原因:这种哈希求余的方式,导致数据是交替出现的,比如100出现在0号分片,101出现在1号分片,102出现在2号分片,103又是0号分片 。这就导致在扩容之后,分片个数增长,就会有 大量的数据需要进行搬运。
2,一致性哈希算法
这种方式可以降低上述的\"搬运开销\"。
key映射到分片序号的过程不再是简单的求余了,而是改成以下过程:
第一步:把0~2^32-1这个数据空间,映射到一个圆环上,按照顺时针方向增长。
第二步:假设分成3个分片,将分片放到对应的位置上
每个分片就会对应一个值,比如0号分片对应的值就是0
第三步:假定有一个key,计算得到的hash值为H,如何计算这个key是在哪个分区?此时H会在圆环上的某个位置,从这个位置开始,顺时针向下找,找到的第一个分片,就是这个key所属 的分片。
这就相当于,N个分片,把整个圆环分成N个区域,key的hash值落在哪个区域,它就属于哪个分片。
因此在 一致性哈希这样的设定下,把数据交替出现,改进成了连续出现。
在这种情况下,如何进行扩容,假设新增一个分片。
如下图所示,在圆环上找一个位置,设为3号分片的位置。该部分本来是0号分片上的,这样一来,只需将0号分片上的这段数据搬运到3号分片上即可,其他分片上的数据不需要搬运。
这种搬运的成本是有的,但是比之前哈希求余的方式低了不少。
这种方式,虽然搬运的成本降低了,但是也导致了各个分片上的数据量不均匀,称作数据倾斜。
3,哈希槽分区算法
这是Redis真正采用的分片算法。
哈希槽计算公式:hsah_slot=hash(key)%16384,一共有16384个槽。
假设现在有3个分片,一种分配方式如下:
-
0号分片:[0,5461],共5462个槽位。
-
1号分片:[5462,10923],共5462个槽位。
-
2号分片:[5463,16384],共5460个槽位。
这里只是分片的一种,分片可以很灵活。每个分片持有的槽位号:可以是连续的,也可以是不连续的。
此处 ,每个分片都会使用一个位图结构,来表示该分片有多少槽位号,16384个bit位,用每一位的0/1表示是否持有这个槽位。
现在假设要新增一个分片,那么此时可以从0号分片,1号分片,2号分片上分别截取一部分出来,放到新的分片上,这样就可以解决数据倾斜的问题。
4,故障处理
如果某个主节点挂了,此时就会把该主节点旗下的某一个从节点提拔为主节点,保证我们的redis能够正常工作。
故障判定
识别某个节点是否挂了。
-
节点A给节点B发送ping包,B就会给A返回一个pong包。ping和pong除了携带message type属性之外,其他部分都是一样的。还会包含集群的配置信息(该节点的id,该节点属于哪个分片,该节点是主节点还是从节点,从属于哪个主节点,持有哪些slot槽位)。
-
每个节点,每秒钟都会给一些 随机的节点发送ping包,而不是给所有节点都发送。这样设定设为了在节点很多的时候,心跳包也会非常多。
-
当节点A向节点B发送ping包后,B不能如期回应的时候,此时A就会尝试重置和B的TCP连接,看能否连接成功,如果连接失败,就会认为B节点下线了,A就会把B设为PFAIL状态(相当于主观下线)。
-
A判定B为PFAIL后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态。(每个 节点都会维护一个自己的下线列表,由于视角不同,每个节点的下线列表也就不同)。
-
此时A发现很多节点也认为B为PFAIL,并且数目超过集群节点个数的一半,那么A就会把B标记为PFAIL(相当于客观下线),并且把这个消息同步给其他节点(其他节点收到后,也会把B标记为PFAI)。
以下三种情况会出现集群宕机:
-
某个分片,所有的主节点和从节点都挂了。
-
某个分片,主节点挂了,没有从节点。
-
整个集群超过一半的主节点挂了。
故障迁移
还是上述的例子。
如果B是从节点挂了,那么就不需要进行故障迁移,毕竟从节点挂了,还可以通过访问同一个 分片内的主节点或者其他从节点来获取数据。
如果B是主节点,就会由B的从节点(比如C和D)发生故障迁移。重新挑选一个主节点,代替之前主节点的位置。
具体过程如下:
-
从节点需要判断自己是否具有参选资格,如果主节点和从节点太久没有进行通信(此时认为主节点和从节点的数据差异太大了),就失去竞选资格。
-
具有资格的节点,比如C和D,就会先休眠一段时间,休眠时间=500ms基础时间+【0,500ms】随机事件+排名*1000ms。offset值越大,排名就越靠前(越小)。offset表示主节点和从节点数据同步的进度。
-
比如C的休眠时间到了,C就会给集群中其他所有节点,进行拉票操作。但是只有主节点才有投票资格。
-
主节点就会把自己的票投给C节点(每个主节点只有一票),当C收到的票数超过主节点数目的一半,C就会晋升为主节点(C会自己执行slaveof no one,并且让D执行slaveof D)。
-
同时,C还会把自己称为主节点的消息,同步给集群中的其他节点,大家也都会更新自己保存的集群信息结构。
总之,哪个节点会成为主节点,就看哪个节点先被唤醒,哪个节点的休眠时间短,大概率就是新的主节点。
如果两个节点被唤醒的时间是差不多的,那么此时就各凭本事了,取决于网络延迟,线程调度等等因素。
上述选举的过程,称为Raft算法。
四,Redis典型应用——缓存
Redis最主要的三个用途:
-
存储数据(内存数据库)
-
缓存
-
消息队列
1,使用redis作为数据库的缓存
在一个网站中,通常会使用关系型数据库(如MySQL)来存储数据,关系型数据库虽然强大,但是有一个很大的缺陷,就是性能不高。(换言之 ,进行一次 查询操作消耗的系统资源较多)。
为什么说关系型数据库的性能不高?
数据库是把数据存储在硬盘上的,硬盘的IO速度并不快,尤其是随机访问。
如果查询不能命中索引,就需要进行表的遍历,这就会大大增加 IO的次数。
关系型数据库对SQL的操作会进行一系列的解析,校验,优化工作。
如果是一些复杂查询,比如联合查询,需要进行笛卡尔积的操作,效率更是降低很多。
因为MySQL等数据库 ,效率比较低,所以承担的并发量就有限了,一旦请求量多了,数据库的压力就很大,甚至很容易就宕机了。对于服务器的每一个请求,都要消耗一定的硬件资源(CPU,内存,硬盘,网络带宽等等),任意一种资源的消耗超出了机器能提供的上限,机器就很容易出故障。
如何提高MySQL能承担的并发量?
-
开源:引入更多的机器,构成数据集群。
-
节流:引入缓存,将一些热点数据保存到缓存中。后续在查询数据的时候,如果数据库中已经存在了,就不再访问MySQL了。
2,如何知道redis中应该存储哪些数据?
也就是怎么获得热点数据。
这涉及到缓存的两种更新策略:1,定期生成 2,实时生成
1,定期生成
将访问的数据 ,以日志的 形式记录下来。接下来就可以针对这些日志进行统计了,统计这一天/一周/一个月,数据出现的频率,然后再按照降序排序,取出前20%的数据数据,这些数据 就是热点数据。
优点:上述过程,实际上实现起来比较简单,过程更可控,缓存中的数据是比较扶额和预期的,方便排查问题。
缺点:实时性不够。如果出现一些突发事件,有些本来不是热词的内容成了热词,这就可能会给后面的数据库带来较大的压力。
2,实时生成
-
如果在redis中查到了数据,就直接返回。
-
如果没有查到,就从数据库查,同时把查到的数据写入redis中。
这里就会有一个问题,如果不停的向redis中写入数据,就会使redis的内存占用越来越高,逐渐达到内存上限。
此时如果继续向redis中写入数据,就会出现问题,为了解决这个问题,redis就引入了\"内存淘汰策略\"。
经典面试题:
FIFO(First In First Out)先进先出:把缓存中存在时间最久的(也就是先来的数据)淘汰掉。
LRU(Least Recently Used)淘汰最近未使用的:记录每个key的最近访问时间,把最近访问时间最老的key淘汰掉
LFU(Least Frequently Used)淘汰访问次数最少的:记录每个key最近一段时间的访问次数,把访问次数最少的淘汰掉。
Random 随机淘汰:从所有的key中随机抽取一个淘汰掉。
3,缓存预热,缓存穿透,缓存雪崩和缓存击穿
缓存预热(Cache preheating)
缓存中的数据,有两种更新策略:1,定期生成 2,实时生成
-
如果使定期生成,就不涉及到预热。
-
如果是实时生成,在redis服务首次接入之后,服务器里是没有数据的,此时客户端的所有请求就都会打给MySQL,如果请求量太多,可能就会导致MySQL服务挂了。随着时间的推移,reids中的数据越来越多,MySQl承担的压力也就越来越小了。
缓存预热,就是为了解决上述问题。把定期生成和实时生成相结合,先通过离线的方式,通过一些统计的途径,先找到一批热点数据,导入到redis中。此时导入的这批热点数据就能帮MySQL分担一些压力了。随着时间的推移,使用新的热点数据来淘汰旧的热点数据。
在刚开始架构演进的时候,没有缓存,此时要加入缓存,就要进行缓存预热。还有当服务器进行重启的时候,我们要保证重启之后缓存中是否有数据以及 这里的数据 是否是热点数据,这也涉及到缓存预热。
缓存穿透(Cache penetration)
在一次查询的过程中,如果要查询的某个key,在redis中没有,在MySQL中也没有。也就意味着此时这个key是不会被放到redis中,那么下次访问依然会访问数据库,这就会导致数据库承担的请求太多,压力很大。这种情况称为缓存穿透。
出现这种情况可能的原因:
-
业务设计不合理:比如缺少必要的参数检验环节,导致非法到的key也被进行查询了。
-
开发/运维误操作:不小心把部分数据从数据库中删除。
-
黑客恶意攻击。
解决方案:
-
如果发现这个key,在redis和MySQL中都不存在在,仍然写入redis,将value设成一个非法值(比如\"\")。再应用层程序可以检查出这是一个非法的key。
-
还可以引入布隆过滤器,每次查询redis/MySQL之前,都先判断一下key是否在布隆过滤器上存在。布隆过滤器本质是结合了hash+bitmap,以较小的空间开销,以较快的访问速度,实现针对key是否存在的判定。
缓存雪崩(Cache avalanche)
由于在短时间内,redis上大规模的key失效,导致缓存命中率陡然下降,并且MySQL压力迅速上升,甚至导致MySQL直接宕机。
可能的原因:
-
redis直接挂了,redis宕机/redis集群模式下很多节点宕机。(这是最主要的)
-
redis正常工作,但是可能之前短时间内设置了很多key给redis,并且设置的过期时间是相同的。在给redis里设置key作为缓存的时候,有的时候为了考虑时效性,就会设置过期时间(和redis的内存淘汰机制是配合使用的)。
解决方法:
-
加强监控报警,加强redis集群可用性的保证。
-
不给key设置过期时间,或者在设置过期时间的时候,添加随机因子(避免同一时刻过期 )。
缓存击穿(Cache breakdown)
相当于缓存雪崩的特殊情况,针对热点key,突然过期了,导致大量的请求访问到数据库上,导致数据库宕机了。
解决方案:
-
基于统计的方式发现热点key,并设置为永不过期。这种方案往往需要服务器做出较大的调整。比如把当前访问哪些key的日志记录下来,接到一个消息队列中,再通过一些计算,将结果再返回给我们的服务器。
-
进行必要的降级服务。例如访问数据库的时候,使用分布式锁,限制同时请求数据库的并发数。
五,分布式锁
在一个分布式系统中,会涉及到多个节点访问同一个公共资源的问题,此时就需要通过 锁 来做互斥控制,避免出现类似于 线程安全的问题。而C++中的std::mutex,这样的锁只能在当前进程中生效。
而在分布式系统中,是有很多进程的(每个服务器,都是独立的进程)。因此,之前的锁就难以对现在分布式系统中的多个进程之前产生制约。分布式系统中,多个进程之间的执行顺序也是不确定的。
此时就需要引入\"分布式锁\",来解决上述 问题。
所谓的分布式锁,也是一个/一组单独的服务器程序,给其他的服务器提供\"加锁\"这样的服务。redis是一种典型的可以是实现分布式锁的方案,但不是唯一的一种。
买票服务器在进行买票的过程中,就需要先加锁,就是往redis上尝试设置一个特殊的key-value,完成买票后,就会把这个key-value删掉。其他服务器在买票的过程中,也会去尝试设置这个key-value,如果发现key-value已经存在,就认为加锁失败(是放弃还是阻塞,就看具体的实现策略了)。
这个加锁过程其实就对标redis中的一个命令setnx key val,这个命令如果key不存在才会设置,如果key存在就会执行出错,同时解锁过程也对标redis中的del key命令。
1,引入过期时间
问题1:某个服务器加锁成功了(setnx成功),如果该服务器执行后续逻辑的过程中,程序崩溃了,此时还没有执行到解锁操作。这种情况就会导致redis上的key无人删除,也就导致其他服务器无法获取到锁了。
解决办法:在加锁过程中,给这个key设置一个过期时间,set ex nx这样的命令来完成设置,时间到了,redis服务器会自动删除这个key,这是其他服务器就可以获取到锁了。
注意:在设置过期时间的时候,智能使用set nx ex这样的方式设置,不能使用set nx ,exprie这两个命令来设置。因为redis上多个命令之间,是无法保证原子性的,此时就可能出现,这两个命令,一个执行成功,一个执行失败。相比之下,使用一条命令设置,是更加稳妥的。
2,引入校验id
问题2:所谓的加锁,就是给redis上设置一个key-val,所谓的解锁,就是给redis上的key-val删除掉。锁,就可以认为是redis上的一个普通键值对。可能会出现服务器1执行了加锁,而服务器2误执行了解锁。因此就可能给我们的系统带来严重的问题。(比如票数超卖)
为了解决这个问题,就引入了校验机制。
-
给服务器编号,每个服务器都有自己的身份标识。
-
进行加锁的时候,设置key-val。key对应着服务器要访问的资源,val表示服务器的编号。
-
在解锁的时候,先查询这个锁对应的服务器编号,然后判定这个编号和执行解锁的服务器的编号是否一致,如果是,才能真正执行del;否则,执行失败。
3,引入lua脚本
对于问题2,我们引入了校验id,但是还存在问题。就是在解锁的时候,需要两步操作,先获取到key对应的val,在执行del,此处是两步操作(不是原子的),就可能会出现问题。
一个服务器内部,也可能是多线程的,此时,就可能服务器A的两个线程都在执行解锁操作,首先进行id校验,都通过了,然后开始执行del命令,del就会被重复执行。
这看起来没有什么问题,但是如果此时一个线程执行完了del,又有一个服务器B来进行加锁(set nx ex),加锁成功,之后服务器A的另一个线程执行del,就会把服务器B的锁给解掉。
归根节点,是因为get 和 del这两个命令不是原子的,此时可以引入事务,将这两个操作打包成一个事务,使在执行get 和 del之间不会执行其他操作(避免插队)。
使用事务,能解决上述问题,但是在实践中,往往推荐使用更好的方案——lua脚本。
redis执行lua脚本的过程 ,也是原子的,相当于执行一条命令一样。
在redis官方文档中,也明确说明了,lua就属于事务的替代方案。
4,引入看门狗(Watch Dog)
在前面提到过,服务器在进行加锁的时候,要给key设置一个过期时间。
-
这个过期时间,如果设置的太短,就可能在服务器的业务逻辑还未执行完,锁就释放了。
-
如果设置的太长,也会导致\"锁释放不及时\"的问题。
这里更好的方式是\"动态续约\"。
初始情况下,设置一个过期时间(比如设置1s),就提前在还剩300ms的时候(不一定是300ms,数值可以灵活调整),如果当前任务还未执行完,就把过期时间再续上1s。等到时间又快到了,任务还未执行完,就再续。
这样设置也有一个好处:如果服务器中途崩溃了,也就没人续约了,此时,锁就可以再较短的时间内被释放。
服务器进行\"动态续约\"往往是需要有一个专门的线程来完成这个事情,这个线程就叫做\"看门狗\"。
5,redlock算法
使用redis作为分布式锁,redis本身是有可能挂了的。
要想保证redis的高可用,可以使用主从复制,哨兵,集群模式等方案。这里使用哨兵机制最合适。
进行加锁操作,就是把key设置到设置到主节点上,如果主节点挂了,有哨兵节点会把从节点升级为主节点,进一步保证刚才的锁可用。
但是主节点和从节点的数据同步是有延迟的,可能主节点收到了加锁的请求(set nx ex),还没来得及推送给从节点,主节点就挂了。即使从节点升级成为了主节点,但是刚才加锁的对应的数据是不存在的。
此时就需要使用 redlock算法。(redis作者给出的一种方案)核心思想:冗余,少数服从多数。
此时加锁,就是按照一定的顺序,针对这些redis都进行加锁操作。如果某个主节点挂了(加不上锁),没关系,继续给下一个主节点加锁。如果加锁成功的主节点个数超过总结点总数的一半,就视为加锁成功。同理,进行解锁的时候,每个主节点都会进行一遍解锁。