在分布式系统中,为了解决单点问题,通常会把redis服务部署到多个服务器上,满足故障恢复和[负载均衡](https://cloud.tencent.com/product/clb?from_column=20065&from=20065)等请求。 所谓的单点问题,就是某个服务器程序,只有一个节点(只有一个[物理服务器](https://cloud.tencent.com/product/cbm?from_column=20065&from=20065),来部署这个服务器程序)。 存在的问题: - 可用性问题:如果这个机器挂了,那么部署的redis服务就挂了,服务就中断了。 - 性能/支持的并发量有限:毕竟一台主机的硬件资源(比如CPU,硬盘,网络带宽等等)是有限的,一个请求就要消耗一定量的硬件资源。一旦并发量太高,如果请求超过主机所能提供这些资源,那么可能就会出现异常,甚至主机直接宕机了。 在分布式系统中,往往希望有多个服务器来部署redis服务,从而构成一个redis集群,此时就可以让这个集群给分布式系统中的其他服务,提供更稳定/更高效的数据存储功能 。 主要存在以下几种部署方式: - 主从模式 - 主从+哨兵模式 - 集群模式 ### 一,主从复制 ##### 1,配置主从复制 **主从模式,即在若干个redis节点中,有的是主节点,有的是从节点 。** **redis主从模式中,从节点上的数据,不允许修改,只允许读取数据。要想修改数据,只能访问主节点。** 假设有三个物理服务器(三个节点),都部署了redis服务,此时就可以把其中一个看作是主节点,其余两个看作是从节点。从节点上的数据要跟随主节点的数据而变化,从节点的数据要和主节点保持一致。 本来,在主节点上保存了一堆数据,现在引入从节点,就是要把主节点上的数据复制出来,放到从节点中。后续,主节点这里对数据有任何修改,都会把这样的修改同步给从节点。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/23bb7b483aa4bd35387d69ff325bb064.png) 总结:主从模式,主要是针对读操作进行可用性和并发量的提高,而对于写操作来说,无论是可用性还是并发量,都很依赖与主节点,但是主节点又不能搞多个。 --- 如何在一个云服务器上部署多个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即可实现主从模式 ![](https://developer.qcloudimg.com/http-save/yehe-100000/7d0fdc53718347bf8c63695dd5f4375b.png) ![](https://developer.qcloudimg.com/http-save/yehe-100000/f1ff89af0fc6999c1ec6ca1c34dd57d4.png) ##### 2,断开主从复制 使用命令slaveof no one可以断开主从关系,这只是暂时的,当redis-server重启之后,主从关系就会恢复,所以要想一直断开主从关系,还是需要修改配置文件。 ##### 3,拓扑 我们的redis-server服务器可能有很多个节点,如何组织这些节点? ###### 一主一从结构 ![](https://developer.qcloudimg.com/http-save/yehe-100000/413ee0bc4aefee9b2cf0ee9db65d9abc.png) 如果其他客户端想要读取数据,从主节点A或 从节点B读取都可以,两个服务器上的数据是一致的。 如果是写数据请求,那么只能由主节点A来完成。 如果此时写请求太多,也会给主节点造成 一定的压力。我们可以通过关闭主节点的AOF,毕竟写内存比写磁盘快,只打开从节点的AOF。但是这种设定,有一个严重缺陷,主节点一旦挂了,不能立即重启,因为主节点这边没有使用AOF保存数据,如果重启了,那么数据就会丢失,进一步的主从同步,也就使从节点上数据也丢失了。改进办法是,当主节点挂了,先从从节点那里获取AOF文件,再启动,这样就可以保证数据没有问题了。 ###### 一主多从结构 ![](https://developer.qcloudimg.com/http-save/yehe-100000/44e367a0d68b1d47f05aeb3801c4ac0d.png) 同理,如果是读请求,可以访问主节点,也可以访问从节点。 如果是写请求 ,是能访问主节点,主节点上的数据发生变化,就会把改变的数据同步给其他从节点。 这个结构存在的问题:同步操作是需要消耗网络带宽的,如果子节点太多,就会大大消耗主节点主机的硬件资源。 ###### 树形结构 ![](https://developer.qcloudimg.com/http-save/yehe-100000/7bb71b3e886e429ea7f3d98caba3101a.png) 主节点将数据同步给从节点B和从节点C,再由从节点B将数据同步给从节点D和从节点E。 此时主节点A就不需要太高的网络带宽,但此时数据同步的延时会更长。 ##### 4,原理 ![](https://developer.qcloudimg.com/http-save/yehe-100000/6da6398227e8959c1bf54e8d65b1cd89.png) 同步数据的命令:**`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,表示全量获取,如果设置为具体的整数,则表示从当前整数位置来进行获取。 当然,从节点想要全量的获取数据,还是增量的获取数据,同时也取决于主节点,主节点会自行判定,看当前是否方便给部分数据,如果不方便,会直接给 全量数据。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/6fd30d0b139ae458ee2b1cff761ac2ce.png) ###### 全量复制的流程 在一个从节点与主节点第一次建立主从关系的时候,就会涉及到数据的同步,而此时一般就是进行全量复制。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/f2396822b24165e86addb30fed4aece7.png) 在进行全量复制的时候,主节点要进行生成rdb文件的操作,然后再把该文件通过网络传输发送给从节点,从节点也是先将该rdb文件保存,然后读取该文件来获取数据。 而redis也支持"无硬盘模式"(diskless):主节点生成的rdb的二进制数据,不直接保存到文件中,而是直接通过网络传输发送给从节点 ,省去了读写硬盘的操作,而从节点现在也可以省去这个操作了,直接将收到数据加载到内存中...... 但是,即使引入了"无硬盘模式",对全量复制的效率提升不是很大,因为全量复制整个操作是比较重量的,数据规模较大。相比于网络传输,读写硬盘操作算是快的了。所以减少了对硬盘的操作,但网络传输无法省去,意味着对整个操作的效率提升不大。 ###### 部分复制流程 在主节点与从节点连接的过程中,可能由于网络抖动等原因,主从节点的连接断开,此时重新建立连接并进行数据同步的时候,使用的就是部分复制。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/83577a7663c06a47a5b627bee9024a40.png) ###### 实时复制 全量复制:主要适用于从节点刚连上主节点进行数据初始化的工作。 部分复制:是全量复制的一种特殊情况,属于全量复制的一种优化。 实时复制:在从节点与主节点建立好连接,并且完成数据同步之后,此时主节点可能会受到源源不断的请求,其中就会包含修改数据的请求,主节点的数据就会发生变化,此时就要把数据也同步给从节点。从节点和主节点之间建立有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,哨兵自动恢复主节点故障 ![](https://developer.qcloudimg.com/http-save/yehe-100000/aa54d94afbba42a8bbcfa3a814b82010.png) 如果一个哨兵节点发现主节点挂了,为了防止预判,还需多个哨兵节点共同认为这件事情。 如果主节点确实挂了,这些哨兵节点会选出一个作为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的数据,如果某个机器挂了怎么办,所以我们还需要为每个机器 再分配几个从节点。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/bc649c778f118c0182738fe6ce9dd965.png) 这三组机器的数据都是不同的,每个 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这个数据空间,映射到一个圆环上,按照顺时针方向增长。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/642b9996e5a797d824c04e5fb62cae97.png) 第二步:假设分成3个分片,将分片放到对应的位置上 每个分片就会对应一个值,比如0号分片对应的值就是0 ![](https://developer.qcloudimg.com/http-save/yehe-100000/ef99ed9f6536cea7bc4d0c10e0cc483d.png) 第三步:假定有一个key,计算得到的hash值为H,如何计算这个key是在哪个分区?此时H会在圆环上的某个位置,从这个位置开始,顺时针向下找,找到的第一个分片,就是这个key所属 的分片。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/f6f7d13dc8b18e8555d3d3b8e7c94a7b.png) 这就相当于,N个分片,把整个圆环分成N个区域,key的hash值落在哪个区域,它就属于哪个分片。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/659bf9d0c355369184acba47b8673539.png) 因此在 一致性哈希这样的设定下,把数据交替出现,改进成了连续出现。 在这种情况下,如何进行扩容,假设新增一个分片。 如下图所示,在圆环上找一个位置,设为3号分片的位置。该部分本来是0号分片上的,这样一来,只需将0号分片上的这段数据搬运到3号分片上即可,其他分片上的数据不需要搬运。 这种搬运的成本是有的,但是比之前哈希求余的方式低了不少。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/4aaf8c07c37d689dc7fc0aa33ee929f9.png) 这种方式,虽然搬运的成本降低了,但是也导致了各个分片上的数据量不均匀,称作数据倾斜。 ##### 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能够正常工作。 ###### 故障判定 识别某个节点是否挂了。 1. 节点A给节点B发送ping包,B就会给A返回一个pong包。ping和pong除了携带message type属性之外,其他部分都是一样的。还会包含集群的配置信息(该节点的id,该节点属于哪个分片,该节点是主节点还是从节点,从属于哪个主节点,持有哪些slot槽位)。 2. 每个节点,每秒钟都会给一些 随机的节点发送ping包,而不是给所有节点都发送。这样设定设为了在节点很多的时候,心跳包也会非常多。 3. 当节点A向节点B发送ping包后,B不能如期回应的时候,此时A就会尝试重置和B的TCP连接,看能否连接成功,如果连接失败,就会认为B节点下线了,A就会把B设为PFAIL状态(相当于主观下线)。 4. A判定B为PFAIL后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态。(每个 节点都会维护一个自己的下线列表,由于视角不同,每个节点的下线列表也就不同)。 5. 此时A发现很多节点也认为B为PFAIL,并且数目超过集群节点个数的一半,那么A就会把B标记为PFAIL(相当于客观下线),并且把这个消息同步给其他节点(其他节点收到后,也会把B标记为PFAI)。 以下三种情况会出现集群宕机: - 某个分片,所有的主节点和从节点都挂了。 - 某个分片,主节点挂了,没有从节点。 - 整个集群超过一半的主节点挂了。 ###### 故障迁移 还是上述的例子。 如果B是从节点挂了,那么就不需要进行故障迁移,毕竟从节点挂了,还可以通过访问同一个 分片内的主节点或者其他从节点来获取数据。 如果B是主节点,就会由B的从节点(比如C和D)发生故障迁移。重新挑选一个主节点,代替之前主节点的位置。 具体过程如下: 1. 从节点需要判断自己是否具有参选资格,如果主节点和从节点太久没有进行通信(此时认为主节点和从节点的数据差异太大了),就失去竞选资格。 2. 具有资格的节点,比如C和D,就会先休眠一段时间,休眠时间=500ms基础时间+【0,500ms】随机事件+排名*1000ms。offset值越大,排名就越靠前(越小)。offset表示主节点和从节点数据同步的进度。 3. 比如C的休眠时间到了,C就会给集群中其他所有节点,进行拉票操作。但是只有主节点才有投票资格。 4. 主节点就会把自己的票投给C节点(每个主节点只有一票),当C收到的票数超过主节点数目的一半,C就会晋升为主节点(C会自己执行slaveof no one,并且让D执行slaveof D)。 5. 同时,C还会把自己称为主节点的消息,同步给集群中的其他节点,大家也都会更新自己保存的集群信息结构。 总之,哪个节点会成为主节点,就看哪个节点先被唤醒,哪个节点的休眠时间短,大概率就是新的主节点。 如果两个节点被唤醒的时间是差不多的,那么此时就各凭本事了,取决于网络延迟,线程调度等等因素。 **上述选举的过程,称为Raft算法。** ### 四,Redis典型应用——缓存 Redis最主要的三个用途: 1. 存储数据(内存数据库) 2. 缓存 3. 消息队列 ##### 1,使用redis作为数据库的缓存 在一个网站中,通常会使用[关系型数据库](https://cloud.tencent.com/product/tencentdb-catalog?from_column=20065&from=20065)(如MySQL)来存储数据,关系型数据库虽然强大,但是有一个很大的缺陷,就是性能不高。(换言之 ,进行一次 查询操作消耗的系统资源较多)。 > 为什么说关系型数据库的性能不高? 1. 数据库是把数据存储在硬盘上的,硬盘的IO速度并不快,尤其是随机访问。 2. 如果查询不能命中索引,就需要进行表的遍历,这就会大大增加 IO的次数。 3. 关系型数据库对SQL的操作会进行一系列的解析,校验,优化工作。 4. 如果是一些复杂查询,比如联合查询,需要进行笛卡尔积的操作,效率更是降低很多。 因为MySQL等数据库 ,效率比较低,所以承担的并发量就有限了,一旦请求量多了,数据库的压力就很大,甚至很容易就宕机了。对于服务器的每一个请求,都要消耗一定的硬件资源(CPU,内存,硬盘,网络带宽等等),任意一种资源的消耗超出了机器能提供的上限,机器就很容易出故障。 如何提高MySQL能承担的并发量? 1. 开源:引入更多的机器,构成数据集群。 2. 节流:引入缓存,将一些热点数据保存到缓存中。后续在查询数据的时候,如果数据库中已经存在了,就不再访问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,实时生成** 1. 如果使定期生成,就不涉及到预热。 2. 如果是实时生成,在redis服务首次接入之后,服务器里是没有数据的,此时客户端的所有请求就都会打给MySQL,如果请求量太多,可能就会导致MySQL服务挂了。随着时间的推移,reids中的数据越来越多,MySQl承担的压力也就越来越小了。 缓存预热,就是为了解决上述问题。把定期生成和实时生成相结合,先通过离线的方式,通过一些统计的途径,先找到一批热点数据,导入到redis中。此时导入的这批热点数据就能帮MySQL分担一些压力了。随着时间的推移,使用新的热点数据来淘汰旧的热点数据。 在刚开始架构演进的时候,没有缓存,此时要加入缓存,就要进行缓存预热。还有当服务器进行重启的时候,我们要保证重启之后缓存中是否有数据以及 这里的数据 是否是热点数据,这也涉及到缓存预热。 --- **缓存穿透(Cache penetration)** 在一次查询的过程中,如果要查询的某个key,在redis中没有,在MySQL中也没有。也就意味着此时这个key是不会被放到redis中,那么下次访问依然会访问数据库,这就会导致数据库承担的请求太多,压力很大。这种情况称为缓存穿透。 出现这种情况可能的原因: - 业务设计不合理:比如缺少必要的参数检验环节,导致非法到的key也被进行查询了。 - 开发/运维误操作:不小心把部分数据从数据库中删除。 - 黑客恶意攻击。 解决方案: 1. 如果发现这个key,在redis和MySQL中都不存在在,仍然写入redis,将value设成一个非法值(比如"")。再应用层程序可以检查出这是一个非法的key。 2. 还可以引入布隆过滤器,每次查询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**,突然过期了,导致大量的请求访问到数据库上,导致数据库宕机了。 解决方案: 1. 基于统计的方式发现热点key,并设置为永不过期。这种方案往往需要服务器做出较大的调整。比如把当前访问哪些key的日志记录下来,接到一个消息队列中,再通过一些计算,将结果再返回给我们的服务器。 2. 进行必要的降级服务。例如访问数据库的时候,使用分布式锁,限制同时请求数据库的并发数。 ### 五,分布式锁 在一个分布式系统中,会涉及到多个节点访问同一个公共资源的问题,此时就需要通过 锁 来做互斥控制,避免出现类似于 线程安全的问题。而C++中的std::mutex,这样的锁只能在当前进程中生效。 而在分布式系统中,是有很多进程的(每个服务器,都是独立的进程)。因此,之前的锁就难以对现在分布式系统中的多个进程之前产生制约。分布式系统中,多个进程之间的执行顺序也是不确定的。 此时就需要引入"分布式锁",来解决上述 问题。 **所谓的分布式锁,也是一个/一组单独的服务器程序,给其他的服务器提供"加锁"这样的服务。redis是一种典型的可以是实现分布式锁的方案,但不是唯一的一种。** ![](https://developer.qcloudimg.com/http-save/yehe-100000/a8e89bbb5a581a4cdb426262e46ed632.png) 买票服务器在进行买票的过程中,就需要先加锁,就是往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误执行了解锁。因此就可能给我们的系统带来严重的问题。(比如票数超卖) 为了解决这个问题,就引入了校验机制。 1. 给服务器编号,每个服务器都有自己的身份标识。 2. 进行加锁的时候,设置key-val。**key对应着服务器要访问的资源,val表示服务器的编号。** 3. 在解锁的时候,先查询这个锁对应的服务器编号,然后判定这个编号和执行解锁的服务器的编号是否一致,如果是,才能真正执行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都进行加锁操作。如果某个主节点挂了(加不上锁),没关系,继续给下一个主节点加锁。如果加锁成功的主节点个数超过总结点总数的一半,就视为加锁成功。同理,进行解锁的时候,每个主节点都会进行一遍解锁。 ![](https://developer.qcloudimg.com/http-save/yehe-100000/22a60af03db7b1c2cc410117f99171b4.png)