redis
<h1>Redis缓存</h1>
<h2>1. 为什么要使用缓存</h2>
<p>使用缓存,主要为了高并发、高性能。</p>
<ul>
<li>高性能:在写少读多的业务中,从缓存里面读取数据比从数据库里面读取数据要快两个数量级</li>
<li>高并发:mysql单机并发最多支持到2000QPS,redis单机轻松可支持几万到十几万的并发,并发能力提高几十倍
<h3>1.1 redis特点</h3></li>
<li>数据存储在内存中,高速的读写</li>
<li>提供丰富多样的数据类型</li>
<li>提供了 AOF 和 RDB 两种数据的持久化保存方式,保证了 Redis 重启后数据不丢失</li>
<li>redis中的所有操作都是原子性的
<h2>2. redis的线程模型</h2>
<p>redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。
它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。</p></li>
</ul>
<p>文件事件处理器的结构包含 4 个部分:</p>
<ul>
<li>多个socket</li>
<li>IO 多路复用程序</li>
<li>文件事件分派器</li>
<li>事件处理器</li>
</ul>
<p>多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,
会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。</p>
<p>来看客户端与 redis 的一次通信过程:
<img src="https://s2.ax1x.com/2019/12/17/QIZ4G6.png" alt="通信过程" /></p>
<ol>
<li>首先,redis服务端进程初始化的时候,会将server socket的AE_READABLE事件与连接应答处理器相关联</li>
<li>客户端socket01向redis进程的server socket请求建立连接,此时server socket会产生AE_READABLE事件,IO多路复用程序监听到server socket产生的AE_READABLE事件后,将给socket压入queue中。文件事件分派器从队列中获取socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的socket01,并将该socket01的AE_READABLE事件与命令请求处理器相关联。</li>
<li>假设此时客户端发送了一个set key value的请求,redis服务端的socket01会产生一个AE_READABLE事件,IO多路复用程序将socket01压入队列,事件分派器从队列中获取socket01产生的AE_READABLE事件,由于之前AE_READABLE事件与命令请求处理器相关联了,因此事件分派器会将事件交给命令请求处理器进行处理,命令请求处理器读取了socket01中的key value并进行相应处理后,会将socket01产生的AE_WRITABLE事件与命令回复处理器相关联。</li>
<li>若此时客户端做好接收响应的准备后,redis中的socket01会产生AE_WRITABLE事件,IO多路复用程序将该socket压入队列,事件分派处理器从队列中取出socket交给命令回复处理器,命令回复处理器输出本次操作的一个结果,比如:OK。之后接触AE_WRITABLE事件与事件处理器的关联。
<h3>2.1 为什么redis是单线程模型的效率还这么高?</h3>
<ul>
<li>redis是用C语言写的,更接近底层,速度更快;</li>
<li>redis是内存级操作,内存里的操作都是很快速的;</li>
<li>正因为redis是单线程操作的,避免了线程切换的时间消耗,预防了多线程竞争产生的消耗;</li>
<li>redis是基于非阻塞的IO多路复用机制实现的;
<h2>3. redis的数据类型</h2>
<h3>3.1 redis有哪些数据类型</h3></li>
<li>string,最基本的数据类型,是二进制安全的,这意味着string值关心二进制的字符串,不关心具体格式。可以存储json格式或图片格式的字符串等。</li>
<li>hash,hash类型很像一个关系型数据库的表,hash 的 Key 是一个唯一值,Value 部分是一个 hashMap 的结构。</li>
<li>list,顺序列表,可从头部和尾部插入数据(双向链表实现)</li>
<li>set,无序不重复的集合,提供了多个 set 之间的聚合运算(交、并、补等运算)</li>
<li>sorted set,在set的基础上实现的有序集合
<h3>3.2 应用场景</h3>
<h4>3.2.1 string</h4></li>
<li>存储MySQL中的某个字段值,将key设计为 表名:主键名:主键值:字段名</li>
<li>存储json、图片等(图片可序列化存储或转换成BASE64串存储)</li>
<li>生成自增id(所有操作都是原子性的)
<h4>3.2.2 hash</h4></li>
<li>十分适合存储对象数据类型,比用string存储json串更高效灵活(内存开销占优,可任意添加或删除字段)
<h4>3.2.3 list</h4></li>
<li>消息队列, lpush命令在list头部插入元素,用rpop(brpop)命令在list尾取出数据</li>
<li>获取最新内容,list结构在获取两端附近的数据性能非常好
<h4>3.2.4 set</h4></li>
<li>共同好友(爱好),通过多set集合的交集运算</li>
<li>全局去重,分布式、集群中的去重
<h4>3.2.5 sorted set</h4></li>
<li>需要有序去重的场景,如网站排行等
<h1>4. 过期策略</h1>
<h2>4.1 定期删除</h2>
<p>所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。</p>
<h2>4.2 惰性删除</h2>
<p>定期删除是随机抽取key来删除的,可能会导致很多已过期的key并没有被抽取到用来删除,那就要使用惰性删除了。</p></li>
</ul></li>
</ol>
<p>所谓惰性删除,指的是获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。</p>
<h2>4.3 内存淘汰机制</h2>
<p>对于很多冷数据来说,惰性删除是没有用的。当大量的冷数据过期后,并不能及时删除,会导致redis内存耗尽,这时候就需要走内存淘汰机制了。</p>
<ul>
<li>noeviction:当内存不足以容纳新写入数据时,新写入操作会报错</li>
<li>allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key</li>
<li>allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key</li>
<li>volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key</li>
<li>volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key</li>
<li>volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除
<h1>5. redis如何实现高可用、高并发</h1>
<p>redis 实现高并发主要依靠主从架构,一主多从。redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。</p>
<h2>5.1 主从架构</h2>
<p>单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,
一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
<img src="https://s2.ax1x.com/2019/12/17/QIZyMF.png" alt="主从架构" /></p>
<blockquote>
<p>redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发</p>
<h3>5.1.2 redis replication的核心机制</h3>
</blockquote></li>
<li>redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;</li>
<li>一个 master node 是可以配置多个 slave node 的;</li>
<li>slave node 也可以连接其他的 slave node;</li>
<li>slave node 做复制的时候,不会 block master node 的正常工作;</li>
<li>slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;</li>
<li>slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。</li>
</ul>
<p>注意,如果采用了主从架构,那么建议必须开启 master node 的持久化(另外,可以对master节点数据进行备份,避免本地文件丢失无法恢复数据)。避免 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。
即使采用了高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致所有的 slave node 数据被清空。</p>
<h3>5.1.3 主从复制的核心原理</h3>
<ol>
<li>当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。</li>
<li>如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。</li>
<li>RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中。</li>
<li>slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
<img src="https://s2.ax1x.com/2019/12/17/QIZD2T.png" alt="主从复制原理" />
<h4>5.1.3.1 断点续传</h4>
<p>主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。</p></li>
</ol>
<p>master node 会在内存中维护一个 backlog,master 和 slave 都会保存一个 replica offset 还有一个 master run id,offset 就是保存在 backlog 中的。如果 master 和 slave 网络连接断掉了,
slave 会让 master 从上次 replica offset 开始继续复制,如果没有找到对应的 offset,那么就会执行一次 resynchronization。</p>
<h4>5.1.3.2 无磁盘化复制</h4>
<p>master 在内存中直接创建 RDB,然后发送给 slave,不会在自己本地落地磁盘了。只需要在配置文件中开启 repl-diskless-sync yes 即可。</p>
<h4>5.1.3.3 过期key处理</h4>
<p>slave 不会过期 key,只会等待 master 过期 key。如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave。</p>
<h3>5.1.4 复制的完整流程</h3>
<ol>
<li>slave node 启动时,会在自己本地保存 master node 的信息,包括 master node 的host和ip,但是复制流程没开始。</li>
<li>slave node 内部有个定时任务,每秒检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立 socket 网络连接。</li>
<li>然后 slave node 发送 ping 命令给 master node。如果 master 设置了 requirepass,那么 slave node 必须发送 masterauth 的口令过去进行认证。</li>
<li>master node 第一次执行全量复制,将所有数据发给 slave node。</li>
<li>而在后续,master node 持续将写命令,异步复制给 slave node。
<img src="https://s2.ax1x.com/2019/12/17/QIZhPx.png" alt="复制流程" />
<h4>5.1.4.1 全量复制</h4>
<ul>
<li>master 执行 bgsave ,在本地生成一份 rdb 快照文件。</li>
<li>master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s)</li>
<li>master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将新的写命令复制给 slave node。</li>
<li>如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。
<code>client-output-buffer-limit slave 256MB 64MB 60</code></li>
<li>slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时基于旧的数据版本对外提供服务。</li>
<li>如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。
<h4>5.1.4.2 增量复制</h4></li>
<li>如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。</li>
<li>master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。</li>
<li>master 就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。
<blockquote>
<p>heartbeat</p>
</blockquote></li>
</ul></li>
</ol>
<p>主从节点互相都会发送 heartbeat 信息。master 默认每隔 10秒 发送一次 heartbeat,slave node 每隔 1秒 发送一个 heartbeat。</p>
<h4>5.1.4.3 异步复制</h4>
<p>master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 slave node。</p>
<h2>5.2 哨兵机制</h2>
<h3>5.2.1 哨兵的介绍</h3>
<p>sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:</p>
<ul>
<li>集群监控:负责监控 redis master 和 slave 进程是否正常工作。</li>
<li>消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。</li>
<li>故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。</li>
<li>配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。</li>
</ul>
<p>哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。</p>
<ul>
<li>故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。</li>
<li>即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
<h3>5.2.2 哨兵的核心知识</h3></li>
<li>哨兵至少需要 3 个实例,来保证自己的健壮性。</li>
<li>哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。</li>
<li>对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。</li>
</ul>
<p>倘若master node宕机了,需要大部分哨兵都同意master node宕机了,然后选择其中一个哨兵来执行故障转移。
同时,需要 majority,也就是大多数哨兵都是运行的,才允许执行故障转移。</p>
<h3>5.2.3 数据丢失</h3>
<ul>
<li>异步复制导致数据丢失</li>
</ul>
<p>因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
<img src="https://s2.ax1x.com/2019/12/17/QIZRaR.png" alt="数据丢失1" /></p>
<ul>
<li>脑裂导致数据丢失</li>
</ul>
<p>脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。
此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。</p>
<p>此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。
因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,
重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
<img src="https://s2.ax1x.com/2020/02/22/3Q4hFg.png" alt="数据丢失2" /></p>
<ul>
<li>数据丢失问题的解决方案</li>
</ul>
<p>进行如下配置:</p>
<pre><code class="language-angular2html">min-slaves-to-write 1
min-slaves-max-lag 10</code></pre>
<p>表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。</p>
<h3>5.2.4 sdown和odown</h3>
<ul>
<li>sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机</li>
<li>odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机
<h3>5.2.5 哨兵集群的自动发现机制</h3>
<p>哨兵互相之间的发现,是通过 redis 的 pub/sub 系统实现的,每个哨兵都会往 __sentinel__:hello 这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。</p></li>
</ul>
<p>每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的 __sentinel__:hello channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。</p>
<p>每个哨兵也会去监听自己监控的每个 master+slaves 对应的 __sentinel__:hello channel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。</p>
<p>每个哨兵还会跟其他哨兵交换对 master 的监控配置,互相进行监控配置的同步。</p>
<h3>5.2.6 slave配置的自动纠正</h3>
<p>哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;
如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。</p>
<h3>5.2.7 slave->master 选举算法</h3>
<p>如果一个 master 被认为 odown 了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:</p>
<ul>
<li>跟 master 断开连接的时长</li>
<li>slave 优先级</li>
<li>复制 offset</li>
<li>run id</li>
</ul>
<p>如果一个 slave 跟 master 断开连接的时间已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。</p>
<p><code>(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state</code></p>
<p>接下来会对 slave 进行排序:</p>
<ul>
<li>按照 slave 优先级进行排序,slave priority 越低,优先级就越高。</li>
<li>如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。</li>
<li>如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
<h3>5.2.8 quorum和majority</h3>
<p>每次一个哨兵要做主备切换,首先需要 quorum 数量的哨兵认为 odown,然后选举出一个哨兵来做切换,这个哨兵还需要得到 majority 哨兵的授权,才能正式执行切换。</p></li>
</ul>
<p>如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为 2,那么就 3 个哨兵授权就可以执行切换。</p>
<p>但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。</p>
<h3>5.2.9 configuration epoch</h3>
<p>哨兵会对一套 redis master+slaves 进行监控,有相应的监控的配置。</p>
<p>执行切换的那个哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。</p>
<p>如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch,作为新的 version 号。</p>
<h3>5.2.10 configuration 传播</h3>
<p>哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub 消息机制。</p>
<p>这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,
新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。</p>
<h1>6. redis持久化</h1>
<p>持久化主要用来做灾难恢复、数据恢复,比如redis挂了。可以通过持久化数据快速恢复数据,对外提供服务。</p>
<h2>6.1redis持久化的两种方式</h2>
<ul>
<li>RDB:RDB 持久化机制,是对 redis 中的数据执行周期性的持久化。</li>
<li>AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。</li>
</ul>
<p>如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。</p>
<h2>6.2 RDB和AOF如何选择</h2>
<ul>
<li>RDB通过生成文件快照来备份数据,恢复数据的时候会比较快,但是每5分钟执行一次,如果宕机,会丢失最近5分钟的数据。</li>
<li>AOF是对每条写入命令以append-only模式每秒写入日志文件,最多只会丢失1秒的数据,数据更完整。</li>
<li>我们可以同时开启两种持久化方式,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
<h1>7. redis集群</h1>
<h2>7.1 redis集群介绍</h2></li>
<li>redis集群可以做到多个master node组成一个集群,每个master节点存储一部分数据,而且每个master节点下可以挂多个slave节点做主从架构。</li>
<li>自动将数据进行分片,每个 master 上放一部分数据。</li>
<li>提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的。</li>
</ul>
<p>在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。
cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。</p>
<h2>7.2 节点间的内部通信</h2>
<p>集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。</p>
<ul>
<li>集中式:将集群元数据(节点信息、故障等等)几种存储在某个节点上。
<ul>
<li>优点:元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;</li>
<li>缺点:所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。</li>
</ul></li>
<li>Gossip协议:所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
<ul>
<li>优点:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;</li>
<li>缺点:元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。</li>
</ul></li>
</ul>
<p>gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。</p>
<ul>
<li>meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。</li>
<li>ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。</li>
<li>pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。</li>
<li>fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
<h2>7.3 分布式寻址算法</h2></li>
<li>hash 算法(大量缓存重建)</li>
<li>一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)</li>
<li>redis cluster 的 hash slot 算法(最常用)
<h3>7.3.1 hash算法</h3>
<p>来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,
所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,
导致大量的流量涌入数据库。</p>
<h3>7.3.2 一致性hash算法</h3>
<p>一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。</p></li>
</ul>
<p>来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。</p>
<p>在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。</p>
<p>燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存不平衡的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,
每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。</p>
<h3>7.3.3 hash slot算法</h3>
<p>redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。</p>
<p>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 来实现。</p>
<p>任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。</p>
<h2>7.4 redis集群的高可用和主备切换</h2>
<p>redis cluster 的高可用的原理,几乎跟哨兵是类似的。</p>
<h3>7.4.1 判断节点宕机</h3>
<p>如果一个节点认为另外一个节点宕机,那么就是 pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown。</p>
<p>在 cluster-node-timeout 内,某个节点一直没有返回 pong,那么就被认为 pfail。</p>
<p>如果一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其他节点,如果超过半数的节点都认为 pfail 了,那么就会变成 fail。</p>
<h3>7.4.2 从节点过滤</h3>
<p>对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。</p>
<p>检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成 master。</p>
<h3>7.4.3 从节点选举</h3>
<p>每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。</p>
<p>所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。</p>
<p>从节点执行主备切换,从节点切换为主节点。</p>
<h3>7.4.4 与哨兵比较</h3>
<p>整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。</p>
<h2>7 缓存的常见问题及解决方案</h2>
<h3>7.1 缓存雪崩</h3>
<p>描述:
缓存中大量key在同一时间过期,此时大量的请求不命中,全都落到数据库,数据库压力过大而宕机。</p>
<p>解决方案:</p>
<ol>
<li>缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。</li>
<li>如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。</li>
<li>设置热点数据永远不过期。
<h3>7.2 缓存穿透</h3>
<p>描述:
用户(黑客)不断发起请求,请求的数据在缓存和数据库中都没有,导致所有的请求都落在数据库上,造成数据库压力过大而宕机。</p></li>
</ol>
<p>解决方案:</p>
<ol>
<li>接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;</li>
<li>从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击</li>
</ol>
<h3>7.3 缓存击穿</h3>
<p>描述:
热点key在失效的瞬间,大量的请求访问这个key不命中,所有的请求都落到数据库导致数据库宕机。</p>
<p>解决方案:</p>
<ol>
<li>设置热点数据永远不过期。</li>
<li>加互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
<h3>7.4 双写一致性问题</h3>
<p>缓存+数据库的更新模式</p></li>
<li>先更新数据库,再删除缓存(lazy(惰性、懒)加载思想)</li>
<li>先删除缓存,再更新数据库</li>
</ol>
<p>双写不一致问题分析:</p>
<ul>
<li>第一种更新模式导致
<ul>
<li>描述:先更新数据库,再删除缓存。如果删除缓存失败了,
那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。</li>
<li>解决方案:先删除缓存,再更新数据库。如果数据库更新失败了,
那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。</li>
</ul></li>
<li>第二种更新模式导致
<ul>
<li>描述:写请求先删除了缓存,在还没修改之前。读请求来读缓存,发现缓存空了,
去查询数据库,查到了写请求修改前的旧数据,放到了缓存中。随后写请求将新数据写入数据库,导致数据不一致。</li>
<li>解决方案:在操作数据库之前将操作数据库的请求放入队列中串行执行。这里可以做个优化,如果发现队列中最后
一个请求为更新缓存的请求(读请求),那么就不用再放个更新缓存操作进去了,直接等待前面的更新操作完成即可。</li>
</ul></li>
</ul>
<p>如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,
即:读请求和写请求串行化,串到一个内存队列里去。这会导致缓存的性能下降。</p>
<h3>7.5 并发竞争问题</h3>
<p>描述:
就是多用户(线程)同时并发写一个 key,可能本来应该先到的数据后到了,导致数据错乱。</p>
<p>解决方案:
每个用户(线程)通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系用户在操作某个 key,别人都不允许读和写。</p>
<p>具体操作:
写入缓存的数据,都是从 mysql 里查出来的,都得先写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
每次要写缓存之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。</p>
<h3>7.6 redis宕机问题</h3>
<p>描述:
缓存机器意外宕机,导致所有的请求都落在数据库上,数据库承受不住压力宕机。</p>
<p>解决方案:</p>
<ul>
<li>事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。</li>
<li>事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。</li>
<li>事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
<img src="https://s2.ax1x.com/2019/12/17/QIZWI1.png" alt="高并发部署" /></li>
</ul>
<p>好处:</p>
<ul>
<li>数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。</li>
<li>只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
<h5>* 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。</h5></li>
</ul>