Redis 基础:为什么要用分布式缓存?
缓存的基本思想
很多同学只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么
缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的 确对系统的性能提升的性价比非常高
其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到比如 CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数 据用于解决硬盘访问速度过慢的问题。 再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地 址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache)。
我们知道,缓存中的数据通常存储于内存中,因此访问速度非常快。为了避免内存中的数据在重启或者 宕机之后丢失,很多缓存中间件会利用磁盘做持久化。
也就是说,缓存相比较于我们常用的关系型数据库(比如 MySQL )来说访问速度要快非常多。为了避免 用户请求数据库中的数据速度过于缓慢,我们可以在数据库之上增加一层缓存
除了能够提高访问速度之外,缓存支持的并发量也要更大,有了缓存之后,数据库的压力也会随变
小
缓存的分类
本地缓存
什么是本地缓存 ?
这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。
本地缓存位于应用内部,其最大的优点是应用存在于同一个进程内部,请求本地缓存的速度非常快,不 存在额外的网络开销。
同一个数据库,并且使用的是本地缓存。 本地缓存的方案有哪些?
1 、 JDK 自带的 HashMap 和 ConcurrentHashMap 了。
ConcurrentHashMap 可以看作是线程安全版本的 HashMap ,两者都是存放 key/value 形式的键值
对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如 过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这 三点。
2 、 Ehcache 、 Guava Cache 、 Spring Cache 这三者是使用的比较多的本地缓存框架。
Ehcache 的话相比于其他两者更加重量。不过,相比于 Guava Cache 、 Spring Cache 来说,
Ehcache 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到
本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。
Guava Cache 和 Spring Cache 两者的话比较像。 Guava 相比于 Spring Cache 的话使用的更多
一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也 比较干净,很多地方都和 ConcurrentHashMap 的思想有异曲同工之妙。
使用 Spring Cache 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓
存穿透、内存溢出。
3 、后起之秀 Caffeine 。
相比于 Guava 来说 Caffeine 在各个方面比如性能要更加优秀,一般建议使用其来替代 Guava 。并且, Guava 和 Caffeine 的使用方式很像! 本地缓存有什么痛点?
本地的缓存的优势非常明显:低依赖、轻量、简单、成本低。
但是,本地缓存存在下面这些缺陷:
本地缓存应用耦合,对分布式架构支持不友好 ,比如同一个相同的服务部署在多台机器上的时候,
各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
本地缓存容量受服务部署所在的机器限制明显 。 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
分布式缓存
什么是分布式缓存?
我们可以把分布式缓存( Distributed Cache ) 看作是一种内存数据库的服务,它的最终作用就是提供缓 存数据的服务。
分布式缓存脱离于应用独立存在,多个应用可直接的共同使用同一个分布式缓存服务。
如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同 的应用到服务器,两个服务使用同一个数据库和缓存。使用分布式缓存之后,缓存服务可以部署在一台单独的服务器上,即使同一个相同的服务部署在多台机
器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。
软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。 你使用的方式得当,就能为系统带来 很大的收益。否则,只是费了精力不讨好
简单来说,为系统引入分布式缓存之后往往会带来下面这些问题:
系统复杂性增加 : 引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存、保证缓存服务的高可用等等。
系统开发成本往往会增加 : 引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成 本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。 分布式缓存的方案有哪些?
分布式缓存的话,比较老牌同时也是使用的比较多的还是 Memcached 和 Redis 。不过,现在基本没有 看过还有项目使用 Memcached 来做缓存,都是直接用 Redis
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转 而使用更加强大的 Redis 了
另外,腾讯也开源了一款类似于 Redis 的分布式高性能 KV 存储数据库,基于知名的开源项目 RocksDB 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型,名为 Tendis
关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章: Redis vs Tendis :冷热混合存储版架构揭秘 ,可以简单参考一下。
从这个项目的 Github 提交记录可以看出, Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存
多级缓存
我们这里只来简单聊聊 本地缓存 + 分布式缓存 的多级缓存方案
这个时候估计有很多小伙伴就会问了: 既然用了分布式缓存,为什么还要用本地缓存呢? 。
的确,一般情况下,我们也是不建议使用多级缓存的,这会增加维护负担(比如你需要保证一级缓存和二级缓存的数据一致性),并且,实际带来的提升效果对于绝大部分项目来说其实并不是很大。
多级缓存方案中,第一级缓存( L1 )使用本地内存(比如 Caffeine) ),第二级缓存( L2 )使用分布式缓 存(比如 Redis )。读取缓存数据的时候,我们先从 L1 中读取,读取不到的时候再去 L2 读取。这样可 以降低 L2 的压力,减少 L2 的读次数。并且,本地内存的访问速度是最快的,不存在什么网络开销。 J2Cache 就是一个基于本地内存和分布式缓存的两级 Java 缓存框架,感兴趣的同学可以研究一下
Redis Sentinel:如何实现自动化地故障转移?
普通的主从复制方案下,一旦 master 宕机,我们需要从 slave 中手动选择一个新的 master ,同时需要 修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。人工干 预大大增加了问题的处理时间以及出错的可能性。
我们可以借助 Redis 官方的 Sentinel (哨兵)方案来帮助我们解决这个痛点,实现自动化地故障切换
1. 什么是 Sentinel ? 有什么用?
2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别 ?
3. Sentinel 是如何实现故障转移的?
4. 为什么建议部署多个 sentinel 节点(哨兵集群)?
5. Sentinel 如何选择出新的 master (选举机制) ?
6. 如何从 Sentinel 集群中选择出 Leader ?
7. Sentinel 可以防止脑裂吗?
什么是 Sentinel?
Sentinel (哨兵) 只是 Redis 的一种运行模式 ,不提供读写服务,默认运行在 26379 端口上,依赖于 Redis 工作。 Redis Sentinel 的稳定版本是在 Redis 2.8 之后发布的。
Redis 在 Sentinel 这种特殊的运行模式下,使用专门的命令表,也就是说普通模式运行下的 Redis 命令 将无法使用。
通过下面的命令就可以让 Redis 以 Sentinel 的方式运行 :
Redis 源码中的 sentinel.conf 是用来配置 Sentinel 的,一个常见的最小配置如下所示:
Redis Sentinel 实现 Redis 集群高可用,只是在主从复制实现集群的基础下,多了一个 Sentinel 角色来 帮助我们监控 Redis 节点的运行状态并自动实现故障转移。
当 master 节点出现故障的时候, Sentinel 会帮助我们实现故障转移,自动根据一定的规则选出一个 slave 升级为 master ,确保整个 Redis 系统的可用性。整个过程完全自动,不需要人工介入。
redis-sentinel /path/to/sentinel.conf
或者
redis-server /path/to/sentinel.conf --sentinel
Sentinel 有什么作用?
根据 Redis Sentinel 官方文档 的介绍, sentinel 节点主要可以提供 4 个功能:
监控: 监控所有 redis 节点(包括 sentinel 节点自身)的状态是否正常。
故障转移: 如果一个 master 出现故障, Sentinel 会帮助我们实现故障转移,自动将某一台 slave
升级为 master ,确保整个 Redis 系统的可用性。
通知 : 通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave 。
配置提供 : 客户端连接 sentinel 请求 master 的地址,如果发生故障转移, sentinel 会通知新的
master 链接信息给客户端。
Redis Sentinel 本身设计的就是一个分布式系统,建议多个 sentinel 节点协作运行。这样做的好处是:
多个 sentinel 节点通过投票的方式来确定 sentinel 节点是否真的不可用,避免误判(比如网络问
题可能会导致误判)
Sentinel 自身就是高可用
如果想要实现高可用,建议将哨兵 Sentinel 配置成单数且大于等于 3 台
一个最简易的 Redis Sentinel 集群如下所示(官方文档中的一个例子),其中:
M1 表示 master , R2 、 R3 表示 slave ; S1、 S2 、 S3 都是 sentinel ; quorum 表示判定 master 失效最少需要的仲裁节点数。这里的值为 2 ,也就是说当有 2 个 sentinel 认为 master 失效时, master 才算真正失效。
如果 M1 出现问题,只要 S1 、 S2 、 S3 其中的两个投票赞同的话,就会开始故障转移工作,从 R2 或者 R3 中重新选出一个作为 master 。
Sentinel 如何检测节点是否下线?
相关的问题:
主观下线与客观下线的区别 ?
Sentinel 是如何实现故障转移的?
为什么建议部署多个 sentinel 节点(哨兵集群)?
Redis Sentinel 中有两个下线( Down )的概念:
主观下线 (SDOWN) : sentinel 节点认为某个 Redis 节点已经下线了(主观下线),但还不是很确
定,需要其他 sentinel 节点的投票。
客观下线 (ODOWN) : 法定数量(通常为过半)的 sentinel 节点认定某个 Redis 节点已经下线(客
观下线),那它就算是真的下线了。
也就是说, 主观下线 当前的 sentinel 自己认为节点宕机,客观下线是 sentinel 整体达成一致认为节点 如果对应的节点超过规定的时间( down-after-millisenconds )没有进行有效回复的话,就会被其认定 为是 主观下线 (SDOWN) 。注意!这里的有效回复不一定是 PONG ,可以是 -LOADING 或者 - MASTERDOWN
如果被认定为主观下线的是 slave 的话, sentinel 不会做什么事情,因为 slave 下线对 Redis 集群的影 响不大,Redis 集群对外正常提供服务。但如果是 master 被认定为主观下线就不一样了, sentinel 整体 还要对其进行进一步核实,确保 master 是真的下线了。
所有 sentinel 节点要以每秒一次的频率确认 master 的确下线了,当法定数量(通常为过半)的
sentinel 节点认定 master 已经下线, master 才被判定为 客观下线 (ODOWN) 。这样做的目的是为了防止误判,毕竟故障转移的开销还是比较大的,这也是为什么 Redis 官方推荐部署多个 sentinel 节点 (哨兵集群)
随后, sentinel 中会有一个 Leader 的角色来负责故障转移,也就是自动地从 slave 中选出一个新的 master 并执行完相关的一些工作 ( 比如通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave) 。 如果没有足够数量的 sentinel 节点认定 master 已经下线的话,当 master 能对 sentinel 的 PING 命令
进行有效回复之后, master 也就不再被认定为主观下线,回归正常
Sentinel 如何选择出新的 master?
slave 必须是在线状态才能参加新的 master 的选举,筛选出所有在线的 slave 之后,通过下面 3 个维度 进行最后的筛选(优先级依次降低):
1. slave 优先级 : 可以通过 slave-priority 手动设置 slave 的优先级,优先级越高得分越高,优先级 最高的直接成为新的 master 。如果没有优先级最高的,再判断复制进度
2. 复制进度 : Sentinel 总是希望选择出数据最完整(与旧 master 数据最接近)也就是复制进度最快 的 slave 被提升为新的 master ,复制进度越快得分也就越高
3. runid( 运行 id) : 通常经过前面两轮筛选已经成果选出来了新的 master ,万一真有多个 slave 的优 先级和复制进度一样的话,那就 runid 小的成为新的 master ,每个 redis 节点启动时都有一个 40 字节随机字符串作为运行 id
如何从 Sentinel 集群中选择出 Leader ?
我们前面说了,当 sentinel 集群确认有 master 客观下线了,就会开始故障转移流程,故障转移流程的 第一步就是在 sentinel 集群选择一个 leader ,让 leader 来负责完成故障转移。
如何选择出 Leader 角色呢?
这就需要用到分布式领域的 共识算法 了。简单来说,共识算法就是让分布式系统中的节点就一个问题达 成共识。在 sentinel 选举 leader 这个场景下,这些 sentinel 要达成的共识就是谁才是 leader 。
大部分共识算法都是基于 Paxos 算法改进而来,在 sentinel 选举 leader 这个场景下使用的是 Raft 算法 。这是一个比 Paxos 算法更易理解和实现的共识算法 —Raft 算法。更具体点来说, Raft 是 MultiPaxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。
对于学有余力并且想要深入了解 Raft 算法实践以及 sentinel 选举 leader 的详细过程的同学,推荐
Sentinel 可以防止脑裂吗?
还是上面的例子,如果 M1 和 R2 、 R3 之间的网络被隔离,也就是发生了脑裂, M1 和 R2 、 R3 隔离在 了两个不同的网络分区中。这意味着,R2 或者 R3 其中一个会被选为 master ,这里假设为 R2 。
但是!这样会出现问题了!!
如果客户端 C1 是和 M1 在一个网络分区的话,从网络被隔离到网络分区恢复这段时间, C1 写入 M1 的 数据都会丢失,并且,C1 读取的可能也是过时的数据。这是因为当网络分区恢复之后, M1 将会成为 slave 节点。 想要解决这个问题的话也不难,对 Redis 主从复制进行配置即可。
下面对这两个配置进行解释:
min-replicas-to-write 1 : 用于配置写 master 至少写入的 slave 数量,设置为 0 表示关闭该功
能。 3 个节点的情况下,可以配置为 1 ,表示 master 必须写入至少 1 个 slave ,否则就停止接受
新的写入命令请求
min-replicas-max-lag 10 : 用于配置 master 多长时间(秒)无法得到从节点的响应,就认为这
个节点失联。我们这里配置的是 10 秒,也就是说 master 10 秒都得不到一个从节点的响应,就会
认为这个从节点失联,停止接受新的写入命令请求。
不过,这样配置会降低 Redis 服务的整体可用性,如果 2 个 slave 都挂掉, master 将会停止接受新的写 入命令请求