Redis进阶篇(高并发与高性能) & 面试题

Redis进阶篇

  • 一. 前言
  • 二. 项目中是如何使用缓存的?为什么要使用缓存?缓存使用不当会有什么后果?
  • 三. Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?
  • 四. Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?
  • 五. Redis 的过期策略都有哪些?手写一下 LRU 代码实现?
  • 六. Redis高并发与高可用
    • 6.1 Redis与系统高并发之间的关系
    • 6.2. Redis单机的瓶颈
    • 6.3 Redis如何支撑超过10万QPS的并发量
    • 6.4 Redis replication核心机制
    • 6.5 master节点开启持久化功能对于主从架构安全保障的意义
    • 6.6 主从复制
      • 6.6.1 主从复制(同步数据)的过程
      • 6.6.2 主从复制的断点续传
      • 6.6.3 无磁盘化复制
      • 6.6.4 slave节点对过期key的处理
      • 6.6.5 full synchronization中的细节
      • 6.6.6 全量复制
      • 6.6.7 增量复制
      • 6.6.8 healthbeat
      • 6.6.9 redis主从架构下如何做到99.99%的高可用性?
      • 6.6.10 sentinal哨兵
      • 6.6.11 两种数据丢失的情况和缓解方案
      • 6.6.12 sdown和odown
      • 6.6.13 Redis的底层持久化原理
      • 6.6.14 Redis Cluster集群模式
      • 6.6.15 Redis缓存雪崩和穿透
      • 6.6.16 如何保证缓存与数据库双写时的数据一致性?
      • 6.6.17 你能说说redis的并发竞争问题该如何解决吗?
      • 6.6.18 你们公司生产环境的redis集群的部署架构是什么样的?

一. 前言

本文是我学习石杉老师所讲的Java面试突击课程时所做的笔记。

二. 项目中是如何使用缓存的?为什么要使用缓存?缓存使用不当会有什么后果?

项目中将监控、性能等指标数据通过复杂计算后,存入缓存中。
使用缓存是为了高性能和高并发。
高性能: 如果不存入缓存,每次收到请求后需要花费几分钟计算相关指标,如果能提前将数据计算好存入缓存,后续收到请求后可以直接从缓存内取出数据并返回,只需要花费几毫秒的时间。
高并发: 直接使用数据库(如Mysql)一般只能承受2000QPS,而内存天然支持高并发,承受几万QPS都不在话下。
缓存使用不当可能会出现: 缓存雪崩、缓存穿透、缓存与数据库的双写一致性问题以及缓存并发竞争问题等。

三. Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?

Reid与Memcached的区别:

  1. Redis支持复杂的数据结构和更丰富的数据操作。memcahced只支持简单的key-value存储,不支持枚举,不支持持久化。Redis支持list、set、sorted set、hash等众多数据结构,同时还支持持久化和复制的功能。redis提供了大量api来支持复杂的数据操作,而memcached则没有提供,必须将数据从缓存中取出,接着在客户端进行一系列的运算,再重新塞回缓存。
  2. Redis原生支持集群模式。在Redis的3.X版本开始便能提供Redis Cluster集群模式,而memcached没有原生的集群模式,必须依靠客户端来实现向集群内不同的分片中写数据。
  3. 性能上有区别。由于Redis只使用单核,而memcached可以使用多核,所以平均每个核上,Redis在存储小数据时比memcached性能更高,而在100K以上的数据中,memcached的性能要高于Redis。(待补充: Redis最新版本的特性)

四. Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?

Redis支持String、set、sorted set、hash、list等数据类型。使用场景: 传送门
(待补充: Redis其它数据类型)。

五. Redis 的过期策略都有哪些?手写一下 LRU 代码实现?

  • Redis的过期策略
    Redis的过期策略是定期删除+惰性删除。
    定期删除: Redis每隔一段时间(默认100ms)会随机抽取一部分设置了过期时间的key,检查其是否过期,如果过期则物理删除。请注意Redis绝不会每隔一段时间就会扫描所有设置了过期时间的key,因为这样做会给Redis带来极大的负担。由于部分扫描的特性,导致了Redis中会存在许多已经到期、且被标记成逻辑删除,但实际上并没有被物理删除的key占据了宝贵的内存空间,所以就需要用到惰性删除了。
    惰性删除: 在请求获取某个key时,Redis主动查询它是否设置了过期时间,如果到期了则将其物理删除。

  • 内存淘汰机制
    即便使用了定期删除+惰性删除,Redis内存中仍然会存在一些已过期,但没有被扫描到,也没有被主动请求获取的key,它们的数量可能会越来越多,甚至导致Redis内存耗尽的严重后果。此外,没有设置过期时间,却被人为删除的key也可能导致Redis内存耗尽。所以内存淘汰机制就派上用场了!
    当内存不足以容纳新的写入数据时,Redis支持以下内存淘汰机制:
    1)noeviction: 新写入操作会报错。(没人用)
    2)all-keys-lru: 在键空间中,移除最近最少使用的key。(最长使用)
    3)allkeys-random: 在键空间中,随机移除某个key。(较少使用,无法控制移除哪个key)
    4)volatile-lru: 在设置了过期时间的键空间中,移除最近最少使用的key。
    5)volatile-random: 在设置了过期时间的键空间中,随机移除某个key。
    6)volatile-ttl: 在设置了过期时间的键空间中,优先移除有更早过期时间的key。

  • 手写LRU算法

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int CACHE_SIZE;

    public LRUCache(int cacheSize) {
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
        CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > CACHE_SIZE;
    }
}

六. Redis高并发与高可用

6.1 Redis与系统高并发之间的关系

想要设计一套能承载数十万甚至上百万QPS的系统,光凭Redis是远远不够的,Redis只不过是支撑高并发的大型缓存架构中的一个非常重要的环节。比如系统底层的缓存中间件使用Redis,再加上良好的缓存架构设计(多级缓存架构,热点缓存等)的共同作用下,才能支撑数十万甚至上百万的高并发。

6.2. Redis单机的瓶颈

根据业务操作的复杂度,单机版本的Redis最大能够承载的QPS在数万左右。(如果我们只是做简单的set get操作,那么承载的QPS会较高,但如果使用Redis的复杂api,比如运行lua脚本,会导致Redis承载QPS变低)。此时,如果让上千万、甚至上亿的用户直接接入Redis,就会导致Redis出现处理不及时、卡死等情况。

单机部署的Redis几乎不可能支撑超过10万QPS,除非机器性能特别好,并且所做的操作也不复杂。

6.3 Redis如何支撑超过10万QPS的并发量

方法很简单,只有四个字: 读写分离

缓存承载的并发操作大都是"读操作",也就是支撑"读高并发"的,而反观"写操作"的频率相对会小很多。
Redis进阶篇(高并发与高性能) & 面试题_第1张图片
所有的写请求只会发往一台Redis,这台Redis被称作master。master只负责向其它Redis同步数据至slave节点和处理写请求,slave redis对外提供读操作服务。图中这种架构方案又被称作"主从架构"。

打个比方,假设一台slave redis的能承载的读QPS为5W,在使用上图中的主从架构后,整套缓存中间件能承载的读QPS为5W * 2 = 10W。此外,主从架构支持水平扩容,如果读请求QPS遇到瓶颈了,只需要额外增Redis(slave)即可。

6.4 Redis replication核心机制

  1. Redis采用异步的方式复制数据到slave节点,不过从redis2.8开始,slave node会周期性的向master node发送请求,确认自己每次复制的数据量,用于检测master同步给slave的数据与slave确认的数据的数据一致性。
  2. 一个master node可以配置多个slave node。比如选择水平扩容后,除了在整套主从架构中增加一台slave node外,还需要在master node中增加slave node的配置信息。
  3. slave node可以连接其他slave node。
  4. slave node在做复制操作时,不会阻塞master node的正常工作。
  5. slave node在处理、同步从master node发送而来的数据时,它会用旧数据对外提供查询操作,不会导致查询阻塞。但当复制完成后,删除旧数据集并切换、加载新数据集的过程中,会暂停对外提供查询操作。
  6. slave node主要就是用来进行横向扩容的,通过读写分离,我们不断地向主从架构中添加slave node,以达到整体架构对外提供读操作的吞吐量。

6.5 master节点开启持久化功能对于主从架构安全保障的意义

上一节提到,master节点通过异步的方式,已经把数据同步到了slave节点上了,那么为什么还要为master节点开启本地持久化功能呢?难道不能直接把slave节点看作是master节点的热备吗?

答: 不能。如果将master节点的RDB和AOF持久化方案全部关闭,那么master上的数据只会在内存中保留,一旦遇到服务器断电、宕机或者重启,master没有任何的可供恢复的本地数据,此时master会将空数据同步给所有的slave节点,导致整套主从架构中,所有slave节点中的数据也被清空。最终导致100%的数据丢失。

6.6 主从复制

6.6.1 主从复制(同步数据)的过程

Redis进阶篇(高并发与高性能) & 面试题_第2张图片

  • Step1: 发送请求ping,告知master节点,自己正处于活动状态。
  • Step2: 由于slave节点初次连接到master,因此会触发full synchronization全量复制。此时master会启动一个后台线程,生成RDB文件(持久化到磁盘上),存储了master节点上全量的数据,并将RDB文件发送给slave节点。slave节点接收到RDB文件后,首先会将其持久化到本地磁盘,接着将RDB文件中的内容一点点的读取到内存中。
  • Step3: 在发送RDB文件的过程中,master节点会把client客户端新发送的写命令缓存到内存中,等到RDB文件发送完毕后,再把这些写命令发送给slave节点,以此来实现master节点和slave节点数据的完全一致。Step2和Step3加在一起被称作"full synchronization全量复制"操作。
  • Step4: 在经历了一次full synchronization操作后,slave节点已经和master节点相识。后续再有新的数据写入master节点时,master节点只会将这一部分数据发送给slave节点,而不再触发full synchronization操作。

6.6.2 主从复制的断点续传

谁也不能保证master节点复制(同步数据)到slave节点时,传输100%不被中断。如果复制的过程中连接断掉了,那么等到网络稳定或者master节点和slave节点恢复正常后,master节点会从上一次断开的位置继续复制数据,而不是从头开始复制一份全量的数据。

那么这是如何做到的呢?实际上,master节点在内存中保存了一份backlog文件,这份文件内包含了若干个replica offset,相应地,slave节点也保存了backlog文件,包含一个master id和一个replica offset。如果数据同步时,网络传输不幸断开,网络恢复后,slave会主动向master发送它保存地replica offset,这样做一来是想让master节点从上次断掉地位置同步数据,二来方便master节点在backlog文件中寻找offset,如果找不到对应地offset,则会执行一次full synchronization。

6.6.3 无磁盘化复制

从第6.1节可以看到,full synchronization全量复制的操作中,master会以当前存储的全量数据,生成一份RDB文件并持久化到本地磁盘,最后发送给slave节点。如果选用"无磁盘化复制",master会在内存中直接创建RDB文件,不会持久化到磁盘,最后直接将文件流发送给slave节点。

无磁盘化复制涉及到redis.conf配置文件内的两个参数:

# 是否开启无磁盘化复制功能 默认no
repl-diskless-sync yes

# 等待5秒后,再开始复制  延迟以秒为单位,默认5秒。
# 我们知道,master与slave之间通过socket传输数据。一旦master与若干slave之间开始传递数据,
# 那么在此过程中,新产生的slave申请同步的请求就会被搁置,进入等待状态,直到下一次的RDB传输。
# 所以设置一个等待时间,即便已经在内存中生成好了RDB,也要延迟等待,以便让更多的slave请求至master。
repl-diskless-sync-delay 5

6.6.4 slave节点对过期key的处理

slave节点不会主动过期key,它只会等待master来过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一道del命令发送给slave。

6.6.5 full synchronization中的细节

  1. offset
    master和slave都会维护offset,每次新增、修改、删除操作后,offset的值会发生变化。
    slave每秒都会上报自己的offset给master,同时master也会保存每个slave中的offset。
    offset并不只是用于全量复制,主要是方便master和slave对比各自的offset,以此来检查双方是否存在数据不一致的问题。
  2. backlog
    backlog又叫复制积压缓冲区,它是保存在master node内存上的一串固定长度的队列,默认大小为1MB。若master node与其它slave node产生了关联,那么针对客户端发起的写命令,master node不但会把它们发送给slave node,还会推入backlog队列中。这种做法的目的是为了备份写命令,但值得注意的是,队列具有先进先出的特性,因此backlog只能保存master node最近接收到的写命令,而那些较早接收到的写命令则会被挤出队列。此外,backlog除了存储写命令外,还会给队列中的每一个字节绑定复制偏移量offset。
  3. master run_id
    直接根据host和ip来定位master node是不靠谱的。在master node重启或者数据发生变化(如数据恢复)后,Redis会改变自身的run_id。接着,master会与所有slave中保存的run_id进行比对,如果发现不同,则master会向该slave发起full synchronization。
    使用info server命令可以查看到当前master的run_id。
    Redis进阶篇(高并发与高性能) & 面试题_第3张图片
    如果希望重启redis后不改变run_id,则可以使用redis-cli debug reload命令
  4. psync
    slave节点使用psync命令向master节点发起数据复制请求 psync run_id offset
    master node根据自身情况返回响应信息,可能触发full synchronization全量复制,也可能是continue触发增量复制。

6.6.6 全量复制

全量复制用于初次复制或者无法进行增量复制的情况,它会将master node当前所有数据全部传递给slave node。由于数据量较大,因此耗费的时间较长。
Redis进阶篇(高并发与高性能) & 面试题_第4张图片

  1. slave node发送psync命令,请求进行全量复制。由于是第一次进行数据复制,slave node没有复制偏移量和主节点的运行id,所以发送psync ? -1。(第一个参数: master run_id 第二个参数: offset)
  2. master node根据接收到的"psync ? -1"明白了本次请求打算进行全量复制,因此向slave node回复"+FULLERSYNC"。
  3. slave node接收并保存master相关基本信息,包括master run_id和offset。
  4. master node接收到全量复制的请求后,会立刻执行bgsave(异步执行),在后端生成RDB文件(当前数据的快照)。
  5. master node发送RDB文件给slave node,slave node首先会把接收到的RDB文件保存在本地磁盘,接着从磁盘中读取到内存,最后正式作为当前节点的数据文件并对外提供读服务。(slave node接收完RDB文件后,会打印本次master node发送的数据量)。
    注意:
    ① 对于数据量较大的master node,在进行全量复制时,一次性同步超过6GB以上大小的RDB文件时,需要格外小心,因为传输文件非常耗时,速度取决于主从节点之间的网络带宽。
    ② 通过分析Full resync和MASTER <–>SLAVE这两行日志的时间差,可以算出RDB文件从创建到传输完毕所消耗的总时间。如果总时间超过repl-timeout所配置的值(默认是60秒),从节点将放弃接收RDB文件,并清理已经下载的临时文件,最终导致全量复制失败。建议根据具体情况,适当的调整repl-timeout参数,防止出现全量同步数据超时的情况。比如对于千兆网卡的机器来说,网卡带宽理论峰值大约每秒传输100MB,在不考虑其他进程消耗带宽的情况下,6GB的RDB文件至少需要60秒传输时间。默认配置下,极易出现主从数同步超时。
  6. master node会在内存中开辟一个缓冲区(被称作"复制缓冲区"),记录下从master node开始生成RDB文件,到slave node接收完毕的这个过程内master node新接收到的写命令请求。master node会在RDB文件传输完毕后,将复制缓冲区内积压的写命令发送给slave node,确保master和slave之间的数据一致性。
  7. slave node接收并将RDB文件持久化到本地磁盘后,会清空自身内存中的旧数据,为新数据腾出内存空间。
  8. slave node读取(加载)RDB文件,对于较大的RDB文件,这一步操作依然比较消耗时间,可以通过计算日志之间的实际差来判断加载RDB的总消耗时间。
  9. slave node成功加载完RDB文件后,如果当前节点开启了AOF持久化功能,它会立刻执行bgrewriteaof操作,目的是为了保证全量复制后,AOF文件可以立刻使用。

不难发现,全量复制是一个非常耗时的操作,它的实际开销主要包括:

  • master node的bgsave时间
  • RDB文件的网络传输时间
  • slave node清空旧数据的时间
  • slave node加载新数据的时间
  • 可能的AOF的重写时间

6.6.7 增量复制

增量复制(又叫部分复制),用于处理在主从复制的过程当中因网络抖动导致数据丢失的场景。当从节点再次连上master node,如果条件允许,master node会补发丢失数据给salve node。由于补发的数据远远小于全量数据,因此可以有效避免全量复制的过高开销(和重复开销)。但需要注意,如果网络中断时间过长,造成master node的复制缓冲区内没有能够完整地保存中断期间执行的写命令(复制缓冲区容量有限,新接收到的写命令过多,导致后接受到的写命令覆盖了新接收到的写命令,最终造成master node中的复制缓冲区内找不到slave node发送过来的offset 【丢失的数据过多】),则无法进行部分复制,仍使用全量复制 。
Redis进阶篇(高并发与高性能) & 面试题_第5张图片

6.6.8 healthbeat

  • 主从节点之间彼此都有心跳检测机制,各自模拟成对方的客户端进行通信。主节点的连接状态为flags=M,从节点的连接状态为flags=S。
  • 主节点默认每隔10秒钟向从节点发送一个ping命令,判断从节点的存活性和连接状态。可以通过repl-sync-slave-period控制发送频率。
  • 从节点在主线程中每隔1秒钟向主节点发送一个replconf ack {offset}命令,给主节点上报自身的offset。

6.6.9 redis主从架构下如何做到99.99%的高可用性?

什么是不可用?

  • 系统无法正常对外提供服务。造成这种情况的原因有很多,比如服务器的内存满了,报错OOM,或是磁盘满了,报错IO异常,甚至服务器宕机。

什么是高可用性?

  • 如果一个系统可以保证全年99.99%的时间内都处于可用状态,那么就可以称这个系统具有高可用性。

什么是Redis的不可用?

  • 比如我们对Redis集群做了读写分离,现在有1个master node和2个slave node,倘若master node宕机,则会导致整套系统无法对外提供写服务,这就是Redis的不可用。

怎样让Redis做到高可用呢?

  • 实现了读写分离的主从架构想要实现高可用性,需要借助"故障转移(又叫主备切换)"的思想。简单地说,主备切换就是自动检测出故障的master node,迅速在redis集群内选取一个slave node节点,将其切换/升级成新的master node。这个切换的过程可能会持续几秒或者几分钟,随后整套Redis集群又能继续对外提供写服务了。(PS: 将slave node切换成master node和检测master是否出现故障,这两步操作是由sentinal完成的)

6.6.10 sentinal哨兵

  • 哨兵的介绍
    哨兵sentinal是redis集群架构中的一个非常重要的组件,主要功能如下:
  1. 集群监控。负责监控redis master和slave进程是否正常工作。
  2. 消息通知。如果sentinal发现有redis进程出现异常,则会通过发送消息的方式将异常信息通知给管理员。
  3. 故障转移。如果master node挂掉了,则会自动选取一台slave node,将其提升为master node。
  4. 配置中心。如果故障转移发生了,则通过客户端使用新的master地址(便于发送写请求)。

哨兵本身支持分布式,它们会作为一个哨兵集群,互相协同工作。

  1. 故障转移,判断一个master node是否宕机,需要大部分的哨兵都同意才行,这里涉及到了分布式选举的问题。
  2. 之所以将哨兵配置成集群工作,是为了保证在部分哨兵节点挂掉的情况下,整个哨兵集群仍然能正常工作。毕竟设计哨兵的目的就是用来保证整套Redis主从架构实现高可用,如果连哨兵自己都无法保证高可用,那就没有意义了。
  • 哨兵的核心知识
  1. 一套哨兵集群至少需要3个哨兵实例,以此来保证自己的健壮性。
  2. 哨兵+redis主从的部署架构不能避免数据零丢失,只能保证redis集群的高可用性。
  3. 对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境甚至生产环境进行充足的测试和演练。
  • 为什么Redis集群只有两个节点时,每个节点上分别有一个哨兵时,无法正常工作?

哨兵集群中涉及到一个非常重要的参数:quorum,字面意思是"法定人数",它规定当集群内认为master node宕机的哨兵的数量达到法定个数时,则通过法案,同意进行故障转移。接着,哨兵集群内除master node所在的哨兵外,其余哨兵之间会通过投票的方式,选举出一个哨兵代表,来执行故障转移的操作。但一个棘手的问题来了,这个投票选举的操作需要大多数(majority)哨兵处于运行状态

majority的具体计算方法: 假设哨兵的个数为n,则majority为Math.floor((n+1)/2)
比如n=5,majority = Math.floor((5+1)/2) = 3
比如n=3,majority = Math.floor((3+1)/2) = 2
n=2非常特殊,虽然Math.floor((2+1)/2) = 1,但是majority=2。

回到问题,当Redis哨兵集群内只有两个节点,且每个节点上分别有一个哨兵时,一旦某个节点宕机(假设是master node),会导致整套哨兵集群内只有一个哨兵存活,虽然不影响quorum判断master node宕机,但却无法继续执行故障转移,因为剩下的一个哨兵无法达到majority的数量(2个)。

经典的3节点哨兵集群
Redis进阶篇(高并发与高性能) & 面试题_第6张图片
一般我们把quorum设置成2,majority也设置成2。那么即便是master所在的节点宕机了,哨兵集群内仍然有2个正常运行的哨兵可以故障转移。

6.6.11 两种数据丢失的情况和缓解方案

  • 异步复制导致的数据丢失问题
    master node接收到客户端发送而来的写请求后,首先会在本地执行写命令,接着会创建线程,通过异步的方式向slave node同步数据。如果此时master node不幸宕机,那么存储在内存的复制缓冲区内的写命令就永久性的会丢失了。
  • 集群脑裂导致的数据丢失问题
    master node因为网络问题,与sentinal cluster和slave node产生了网络隔离,sentinal误以为master node宕机,通过故障转移,将slave node提升成了master node,但事实上旧的master node并没有宕机,这就造成了整个Redis集群中出现了两个master node。假如在出现上述问题时,客户端恰好能与旧的master node通信,那么在网络恢复之前,客户端新发出的写入请求全部堆积在了旧的master node上,哨兵在监测到有两个master node后,会将旧的master node降级为slave node,并让新的master node同步数据至这个slave node上。显然,在恢复网络的过程中客户端发出的写请求会永久性的丢失。

只需要通过配置以下两个参数,就可以减少异步复制或集群脑裂导致的数据丢失问题:

  1. min-slaves-to-write 1
  2. min-slaves-max-lag 10
    要求至少有1个slave node,数据同步复制所耗费的时间不能超过10秒。如果发现所有slave node数据同步的时间都超过了10秒,那么master node将不再接收任何请求。
  • 减少异步复制的数据丢失
    min-slaves-max-lag这个配置可以保证,一旦检测到slave node复制数据、返回ack所耗费的时间太长,为了避免master宕机后丢失的数据过多,master node自身会拒绝客户端发出的写请求(不再将新的写请求写入到复制缓冲区),将损失降低到可控范围。还有一种可能性,比如因为网络传输或者IO读写速度不对等,导致master中的数据永远比slave多,并且差距越来越大。当检测到slave node落后了自己超过10秒的数据后,master node会停止接收写请求(停顿一段时间),等待slave同步master中的数据,接着再恢复工作。(master node停止接收请求后,我们一般会对客户端做处理,比如对客户端做降级,新产生的写请求暂时不发送给Redis,而是优先写入到本地磁盘中,接着针对外部的请求,客户端再做一些限流措施,减缓外部请求涌入系统内部的速度。或者先将数据写入Kafka等消息队列中,接着每隔10分钟从Kafak中取出数据,重新向Redis的master发送写请求)
  • 减少脑裂导致的数据丢失
    如果Redis集群出现了脑裂,旧master发现自己slave node返回ack的时间不满足上述两个参数的配置时,会拒绝接收客户端发出的写请求。

注意,这么做只能减缓而不能完全解决数据丢失的问题,比如min-slaves-max-lag=10,那么在最坏的场景下,还是会有10秒的写请求永久性的丢失。如果我们把min-slaves-max-lag配置的过于严苛,又会导致客户端经常性的服务降级,因此只能寻找一个折中的配置方案。

6.6.12 sdown和odown

  • sdown和odwn的转换机制
    sentinal判定master node宕机的两种失败状态。sdown(subjective down)是主观宕机,一个哨兵认为某个master node宕机了,这就是该master node的主观宕机。odown(objective down)是客观宕机,如果达到quorum数量的哨兵都觉得某个master node宕机了,则这就是该master node的客观宕机。
    sdown判定的条件非常简单,若某个哨兵ping一个master node,发现返回ack的时间超过了is-master-down-after-millseconds指定的毫秒数后,就认为是主观宕机了。(is-master-down-after-millseconds参数在哨兵的配置文件内指定)

  • 哨兵与slave集群的自动发现机制
    哨兵之间通过redis的pub/sub系统来互相发现,每个哨兵都会向"__sentinal__:hello"这个channel中发送一个消息,同时其它哨兵会消费到这条消息,并感知到其它哨兵的存在。每隔两秒钟,每个哨兵都会往自己监控的某个master+slave对应的"__sentinal__:hello"的channel内发送一条消息,内容为自己的host、ip、run_id以及对这个master的监控配置,同时也会监听对应的channel,感知其它在监听此master的哨兵。此外,哨兵们会与其他哨兵交换对master的监控配置,实现监控配置的同步。

  • 自动纠正slave的配置
    当通过故障转移,将一个slave切换成master后,被选举出的哨兵会负责更新剩下的slave内连接master的配置信息,以确保这些slave连接到正确的master上。

  • 选举出slave并切换成master的算法(重点)
    选择某个slave作为新的master时,需要考虑以下几个因素:
    1> slave与master断开连接的时长
    2> slave的优先级
    3> 当前slave内已经同步到的数据对应的offset
    4> run id
    如果某个slave与master断开连接的时间超过了down-after-milliseconds的10倍加上master自身宕机时间,则这个slave就会被认为不适合选举成新的master。
    若有多个slave满足上述条件,则继续按照下述方式进一步的筛选:
    1> 按照slave的优先级进行排序,slave-priority越低,优先级越高。
    2> 如果slave-priority相同,则比对replica的offset。offset越靠后,说明该slave上已同步的数据越新,从而被选举成master后整套系统丢失的数据就越少(数据更完整)。
    3>如果offset也相同,那么就选择run id比较小的那个slave

  • quorum和majority的区别
    做主备切换之前,必须满足超过quorum数量的哨兵认为某个master node sdown了,也即转化成某个master odown了。然后哨兵集群会选举出一个哨兵来做主备切换,这个选举的过程涉及到了majority,当且仅当超过majority数量的哨兵都授权某个哨兵有资格作为本次主备切换的执行人,此时,这个哨兵才真正有权利开始执行主备切换。
    如果quorum,比如有集群内有5个哨兵,majority肯定是3了(算法算出来的),quorum被设置成2(人为指定的),在此情况下,只要达到3个及以上的哨兵授予某个哨兵执行权限,那么这个哨兵就板上钉钉的成为本次主备切换的执行人了。
    但是如果quorum>majority,这个时候有可能部分认为master sdown的哨兵们没有向目标哨兵投票,为了充分尊重每一个认为master sdown了的哨兵,Redis规定必须所有认为master sdown了的哨兵都同意授权,这个目标哨兵才能作为主备切换的执行人。(每一个认为master sdown的哨兵都是有话语权的)

6.6.13 Redis的底层持久化原理

  • Redis持久化的意义
    Redis持久化的意义在于故障恢复。如果仅仅把Redis中的数据存放在内存中,遇到突发的灾难性故障时,Redis宕机,通过重启服务器并恢复进程后,内存中的数据会全部丢失。即便是做了文件持久化,还是会有风险,比如Redis所在的服务器坏了,或者磁盘烧了,因此业界的解决方案是文件持久化+定期将持久化文件同步至云存储中,即便Redis所在服务器出现问题,只要再搭建一套Redis环境,将持久化文件从云端下载到本地,重新导入,即可立刻恢复大部分的数据,重新对外提供服务。(云存储,国外常用亚马逊的S3,国内常用阿里云的ODPS)

  • RDB
    RDB就是周期性的对内存中的数据生成一份完整的快照
    Redis进阶篇(高并发与高性能) & 面试题_第7张图片

  • AOF
    AOF机制将每一条写入命令作为日志,以append-only的模式写入磁盘中的一份日志文件中。AOF文件内存放的是一条条的写命令(如set,del,add等),文本类型,按照Redis的命令请求协议选择合适的格式来保存文件。比如:
    在这里插入图片描述
    为了提高写入效率,在现代操作系统内执行写入操作时,先将数据暂时写入到内存中(OS Cache),等到缓冲区的空间被填满或间隔一定时间后,才真正地将缓冲区内的数据写入到本次磁盘上。但这种做法存在一个安全问题,如果遇到突发性的灾难故障,那么尚在缓冲区内的数据将永久性的丢失。为此操作系统提供了fsync和fdatasync两个函数,它们可以强制让操作系统立刻将缓冲区内的全部数据刷新(写入)到本地磁盘中。
    Redis进阶篇(高并发与高性能) & 面试题_第8张图片

  • AOF文件是否会无限膨胀呢?rewrite原理
    一个Redis节点中有且只会有一份AOF文件,随着Redis运行时间变长,AOF内记录的写请求信息会越来越多,当文件的大小接近阈值时,Redis会基于内存中现有的数据,对AOF文件执行rewrite操作,创建出一份新的体积较小的AOF文件,并删除旧文件。(注意: 由于内存大小有限,内存内的数据量膨胀到一定级别后,会自动执行缓存淘汰算法,比如LRU)

  • 同时使用AOF和RDB进行数据备份
    如果同时使用RDB和AOF进行持久化,在数据恢复阶段将优先使用AOF文件,这是因为AOF一般间隔几秒钟写入一次请求,而RDB一般间隔几分钟才会备份一次内存中数据的镜像,因此从数据恢复的角度来看,通过AOF恢复数据后,丢失的数据少于RDB。

  • RDB持久化机制的优点

  1. RDB机制会生成多份数据文件,分别代表着各个时刻中Redis的数据,这种多个数据的备份方式非常适合做冷备份。在实际应用中,我们可以在Linux服务器上部署shell脚本,添加定时任务,定时扫描并将RDB文件上传到远程安全的云存储上。
  2. 由于使用RDB机制做备份,对于Redis对外提供服务的影响非常小,因此可以让Redis保持高性能。Redis主进程只需要fork一个子进程,让子进程执行磁盘的IO操作,完成RDB的持久化。
  3. 相对于AOF持久化机制来说,使用RDB数据文件来重启和恢复Redis进程的速度更快。因为AOF中存放的是指令日志,数据恢复时需要回放和执行所有的指令日志,而RDB本身就是一份数据文件,直接加载到内存即可。
  • RDB持久化机制的缺点
  1. 由于执行RDB生成数据快照的时间间隔基本在分钟级别,甚至更长,一旦服务器宕机,那么就会永久性的丢失上一次RDB文件生成到宕机的这段时间内的数据。正因为如此,RDB不适合作为最优先选择的数据备份方案。
  2. 虽然RDB文件持久化的工作不需要主进程执行,但遇到内存中数据量非常大的场景下,主进程fork子进程花费的时间较多(可能达到数秒),此期间Redis会暂停对外提供服务。
  • AOF持久化机制的优点
  1. AOF机制可以做到每隔1秒备份一次数据(将数据写入OS Cache),接着通过后台进程执行fsync操作,最多只丢失1秒钟的数据,更好的保护的数据的完整性。
  2. AOF日志 文件以append-only模式写入,所以没有任何的磁盘寻址开销,写入的性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
  • AOF持久化机制的缺点
  1. 对于同一份数据来说,AOF日志文件通常比RDB的快照文件大。
  2. AOF开启后,支持的写QPS会比开启RDB支持的写QPS低,这是因为AOF在进行fsync时,会在内存和磁盘之间产品IO操作,如果fsync过于频繁,比如为了确保数据完全不丢失,每写入一条指令都执行一次fsync,那么就会极大的降低Redis对外提供的写性能。实际生产环境证明,保持在1秒左右的fsync执行频率不会对Redis的写性能造成太大影响。
  3. 做数据恢复的时候比较慢,做冷备(比如定期备份)不太方便,需要自己手写比较复杂的脚本。

6.6.14 Redis Cluster集群模式

可能的问题:

  1. Redis集群模式的工作原理是什么?
  2. 在集群模式下,redis的key是如何寻址的?寻址都有哪些算法?
  3. 什么是一致性hash算法?
  4. Redis集群模式与哨兵模式的区别是什么?
  • 为什么要使用Redis集群模式?
    原先我们使用的单master主从模式下,由于只有master能够接收写请求,并且只有一台master节点,因此master节点所在服务器的内存大小直接决定了整套系统能够存储的数据量。为了突破单机瓶颈,支撑海量数据的缓存,Redis Cluster集群模式支持一套系统中存在多个master节点,每个master节点分别存放一部分的数据,这样一来,我们只需要横向的扩展master节点,就能满足海量数据的缓存了。当然,master仍然可以挂载多个slave,因此读写分离的特性被延续了下来。注意,如果某个master宕机,那么redis cluster会自动从master对应的slave集群中选择一个合适的节点升级成新的master。
    简而言之,Redis Cluster的特点是支持多个master,并且支持读写分离和高可用。
    在redis cluster的架构下, redis需要放开两个端口,比如6379和16379(加上10000的端口号),redis内会架设一条集群总线(cluster bus),节点之间借助集群总线,通过16379端口进行通信。
  • 在集群模式下,Redis的key如何寻址?(数据存放到哪个master上)? 寻址都有哪些算法?
  1. hash算法(最古老、弊病最多的算法)
    Redis进阶篇(高并发与高性能) & 面试题_第9张图片
    问题非常明显,假设集群中某个master节点宕机,会导致集群中绝大部分的缓存数据失效。
    这是因为写入数据时是按照节点数量来取模,一旦某台master节点宕机后,便会影响节点数量,后续读取数据时,经过取模运算计算出的master节点编号就会不准确(错位了)。

  2. 一致性hash算法
    Redis进阶篇(高并发与高性能) & 面试题_第10张图片
    一致性hash算法下,任何一台master节点宕机,只会导致这一台master上缓存的数据失效,不会影响整套架构内其它数据缓存。请求数据会顺时针找到距离自己最近的下一台master节点上,虽然仍然会造成数据丢失,但起码比只使用hash算法取模运算时,一台master节点宕机导致几乎所有缓存失效的结果要好得多。
    一致性hash算法也有自己的缺点——热点问题。如果大量的数据聚集在环上的某个区间内,导致大部分数据都存储在了同一个master节点上,数据分布不均匀。

  3. hash slot算法
    hash slot算法是对hash算法的修复。文章

  • 节点之间的内部通信机制
  1. 基础通信原理
    节点之间的通信一般是为了维护集群的元数据,通常有两种做法: 集中式和gossip。
    Redis进阶篇(高并发与高性能) & 面试题_第11张图片
    Redis进阶篇(高并发与高性能) & 面试题_第12张图片
    redis cluster内节点之间采用gossip协议进行通信。与"集中式"通信方案不同,gossip会让每个节点分别维护一份元数据信息,通过节点之间不断的通信,以此来保证整个集群内所有节点的各自的元数据是完整的。

集中式存储的优点:读取和更新元数据的时效性非常好,由于整套系统中只有一个地方维护元数据,因此一旦某个节点发生变化,只需要将变更信息发送到集中式的存储中,其余的节点下次自动获取到最新的元数据。

集中式存储的缺点:由于整套系统中所有的节点都只和一个地方进行元数据交互,因此必然会对集中式存储带来压力。

gossip的优点: 是元数据的存储较为分散,读取和更新元数据的压力被分摊在了每个节点上。

gossip的缺点: 缺点是元数据变动后,需要通信的节点数量变多了,因此在达到所有节点元数据一致之前,可能会产生一些延迟。

  1. 10000端口
    每个节点都有一个专门用于与其它节点通信(传递元数据信息)的端口,就是自己提供服务的端口号+10000。

  2. 交换的信息
    每个节点每隔一段时间都会向其它节点发送ping信息,其它节点在接收到ping之后会返回pong。交换的信息包括如故障信息、节点的增加和移除信息、master-slave信息、hash slot与master节点之间的映射关系等等。

  3. gossip协议细节
    使用gossip协议的通信机制包含了多种消息,包括ping、pong、meet、fail等等。
    ping: 每个节点都会非常频繁的向其它节点发送ping,其中包含自己的状态还有集群元数据。
    pong: 当节点接收到ping后,就会返回pong。pong实际上就是ping和meet的组合,不仅可以用于信息广播,还能用于元数据更新。
    meet: 新增节点后,集群内部会向这个新节点发送meet消息,通知它加入集群
    fail: 当一个节点发现某个节点宕掉后,就会发送fail给其它节点,通知它们,集群内指定节点宕机的消息。

  4. ping消息深入
    节点之间ping消息的发送最为频繁,由于发送的过程中需要携带一定量的元数据,所以势必会加重网络负担。此外,由于发送ping的目的是保证集群内所有节点元数据的一致性,因此Redis在通过gossip实现节点消息通信时指定了一系列的策略,比如节点每秒会对外发送10次ping消息,每次会选择5个最久没有通信的其它节点(并非是向其它所有节点分别通信)。如果发现某个节点的通信延时达到了cluster_node_timeout / 2,那么将立刻向其发送ping消息,避免数据交换延时过长(太久没有和该节点交换数据了)。
    ping消息中,除了包含自己节点的信息,还会带上1/10其它节点的信息用于数据交换。至少包含3个其它节点的信息,最多包含总结点数-2个其他节点的信息

  • 面向集群的Jedis内部实现原理
  1. 基于重定向的客户端
    1)请求重定向
    客户端向任意一个Redis实例发送请求,每个Redis实例接收到请求后,会计算待请求的key对应的hash slot是否在当前Redis实例内,若在则直接在本地处理,否则moved给客户端,让客户端重定向请求。使用cluster keyslot mykey可以查看目标key对应的hash slot值。我们用redis-cli命令时,可以增加-c参数来支持自动的请求重定向,也就是说,当redis-cli接收到moved消息后,会自动为我们重定向至对应的Redis实例上执行命令。
    2)计算hash slot
    hash slot = CRC16(key) % 16383
    使用hash tag可以手动指令key的slot,同一个hash tag下的key会存放在同一个hash slot中,
    比如set key1:{120} value1和set key2:{120} value2
    3)hash slot查找
    节点之间通过gossip协议进行数据交换,进而通过元数据信息找到hash slot所在的Redis实例。
    不难发现,基于重定向的客户端浪费了大量的性能在网络IO上。
  2. smart jedis
  1. 什么是smart jedis
    本地维护一份hashslot->redis的缓存表,在大部分情况下直接通过本地缓存就能直到key对应的目标Redis,不需要走moved的重定向了。
  2. Jedis Cluster的工作原理
    Jedis Cluster在初始化阶段会随机选择一个node,初始化本地的hashslot->node的映射表,同时为每个node都创建一个JedisPool连接池。每次基于Jedis Cluster执行操作时,首先会计算出key对应的hash slot,然后再本地映射表中找到对应的节点,对其直接发送请求。如果hash slot正好在该Redis实例上,则请求OK,如果该Redis实例经历过rehash操作,那么对应的节点将返回moved消息和最新的hashslot-node映射表,Jedis Cluster可以利用映射表更新本地缓存。反复重复上述操作,直到找到对应的节点,若重复超过5次,则会报错:JedisClusterMasterRedirectionException。
    Jedis的老版本,可能会出现在集群中某个节点发生故障且还没有完成自动切换恢复时,频繁的更新hash slot,频繁ping节点检查活跃,导致大量的网络IO开销。
    Jedis的新版本,对于这些过度的hash slot更新和ping检查都进行了优化。
  1. hashslot迁移与ask重定向
    如果jedis请求的节点恰好在执行迁移操作,那么该节点将返回ask重定向。Jedis接收到该结果后,会重新定位并请求新的目标节点,但因为ask发生在节点迁移hash slot的过程中,因此不会更新hashslot的本地缓存表,当且仅当收到moved消息后 ,才会更新。
  • 高可用性与主备切换的原理
    Redis Cluster的高可用性原理与哨兵架构非常类似。
  1. 判断节点是否宕机
    如果一个节点认为另一个节点宕机,那么就可以认为是pfail,主观宕机。
    如果多个节点同时认为某个节点宕机,那么旧可以认为是fail,客观宕机。
    若在 cluster-node-timeout的时间内,某个节点一直没有返回pong,则会被认为是pfail。
  2. 从节点过滤
    既然master node宕机了,那么就需要在该master对应的slave集群中选择一个合适的node切换成master。这时需要检查每个slave node与master node断开连接的时间,若某个slave node超过cluster-node-timeout * cluster-slave-validity-factor,则没有资格被切换成master。
  3. 从节点选举
    每个slave自身都会维护一份offset,用于体现自身数据与master数据的同步情况,显然offset越大,自身持有的数据越多,选举时的优先级就会越高。当然,slave自身是没有投票权的,所有的master node都可以投票,它们会在宕机的master绑定的slave集群中,择优选择一个合适的slave作为新的master,如果大部分的master node Math.floor((n+1)/2)都投票给了某个从节点,那么选举便会通过,由slave自己完成主备切换。
  4. 与哨兵比较
    整个流程与哨兵非常类似,可以说Redis Cluster集成了replication和sentinel的功能。

6.6.15 Redis缓存雪崩和穿透

  • Redis缓存雪崩
    正常情况下,用户首次发送的请求会进入数据库,接着将数据缓存至Redis,后续部分请求可以直接走Redis,无需发送至数据库。
    缓存雪崩是指大量的缓存同时失效或Redis所在的服务器宕机了(所有缓存失效),导致大量的请求同时涌入了数据库,超过了数据库本身的请求承载能力,最终导致数据库卡死或进程死掉。
    这种现象的后果非常严重,数据库死掉意味着整套系统无法对外提供服务。如果不了解缓存雪崩背后的原理,一味的重启数据库所在的服务器,只会导致数据库不断地重复:重启->进程死掉。
  • Redis缓存雪崩的解决(缓解)方案
    严格来说,缓存雪崩无法解决,但我们可以通过事前、事中以及事后的一系列措施来缓解缓存雪崩带来的危害。
    事前: Redis整体架构必须设计成高可用的。也就是Redis 主从架构+哨兵,或者Redis Cluster,避免全盘崩溃。
    事中: 多级缓存+请求降级。整套系统内不要只使用Redis作为缓存,我们可以在服务本地再做一道ehcache小缓存,存储一部分的热点数据。系统接收到外部请求后,优先在ehcache中找数据,找不到去Redis中找,最后才去数据库中找。系统中做一个限流装置用来控制请求发送到数据库的数量,比如每秒接收到了5000个请求,在经历了ehcache和Redis后,仍然有4000个请求需要处理,此时限流装置会将其中的大部分请求转向限流处理逻辑(事先编写好,比如友情提示服务器正忙,请稍后重试),仅将一小部分、可以承受的请求转发至数据库中。只要数据库不死掉,那么最多会导致用户体验度降低,但是系统整体运行正常,用户只需要多次请求,还是能正常执行业务服务的。请求降级可以使用hystrix。
    Redis进阶篇(高并发与高性能) & 面试题_第13张图片
    事后: Redis必须做持久化,一旦集群出现全面宕机,可以通过立即重启,自动从磁盘(或者远程的云存储)上拉取最近同步的数据,并进行数据恢复。
  • 缓存穿透
    缓存穿透指的是大量的请求在缓存中找不到对应的数据,在数据库中也找不到。出现这种情况,可能是因为黑客攻击或者系统本身出bug了。老师的解决方案是将这种请求直接放在缓存中,比如<-1,UNKNOWN>,后续再次接收到这种请求时,直接在缓存中找到数据并返回。我不赞成这种做法,因为Redis被分配的内存是有限的,如果存放了大量的无效请求,会使得正确的热点数据被积压和淘汰掉,在正常的高并发请求和黑客发出的无效请求场景下,会导致正常的热点数据反复的从数据库中取出并存放到Redis中。网上使用布隆过滤器的做法较为妥当,将所有可能存在的数据存放到一个足够大的bitmap中,那么一定不存在的数据将会被这个bitmap给过滤掉。

6.6.16 如何保证缓存与数据库双写时的数据一致性?

  • Cache Aside Pattern缓存 + 数据库读写模式的分析
  1. Cache Aside Pattern
    Cache Aside Pattern是最经典的缓存+数据库读写模式,它的概念非常简单:
    读数据的时候,优先读缓存,如果缓存内没有,再去读数据库,最后取出数据并放入缓存,同时返回响应。
    写数据时,先删除缓存,再更新数据库。
  2. 写数据时,为什么要先删除缓存,而不是更新缓存?
    从线程安全的角度来看,若线程A和B同时写入数据,可能会出现写入数据时A在B之前,但更新缓存时B在A之前,导致线程A将旧数据更新到了缓存中的后果。从业务场景的角度来看,如果是写多读少的场景,系统在1分钟之内可能接受到了100次写请求,但只接受到了20次读请求,而写入到缓存中的数据不是简单的写入数据库的值,而是需要经过一系列复杂的计算,这就会导致系统将大量的性能消耗在更新缓存时的复杂计算上了。显然删除缓存更为合适。我个人认为写多读少的场景使用缓存这件事情本身就有问题,缓存就是为读多写少的场景设计的,如果存入缓存的数据真的需要经过复杂、较长时间的计算,那么在接收到写入请求时,也不能简单地删除缓存,而是在更新数据库后,等待一段时间,重新计算并写入缓存,绝对不能等到下一次用户请求到来后,再重新计算数据并写入缓存,因为这样就失去缓存的意义了。思考: 延时双删(场景: 缓存已经删除,但数据库更新操作尚未执行完毕,此时收到读请求,错误的读取了数据库中的脏数据并放入缓存,导致缓存与更新成功后的数据库数据不一致的问题。延时双删: 写请求在更新完数据库之后,等待一段时间,比如1秒钟,再次淘汰缓存。这里之所以要等待一段时间,就是为了等那些在写更新尚未执行完毕,却错误的拿到了脏数据的读请求将脏数据放到缓存中。我们可以根据结合具体业务场景,通过评估读请求将数据从数据库放入缓存的时间,来适当的调整等待时间)
  • 高并发场景下的缓存+数据库双写不一致问题分析与解决方案
  1. 为什么会出现缓存与数据库双写不一致的情况? (初级场景)
    接收到写入请求后,执行了错误的操作: 先更新数据库,再更新缓存。
    这种做法可能会导致更新数据库成功了,但更新缓存失败,导致缓存内存放的是旧数据,而数据库内存放的是新数据,造成双写不一致的后果。
    解决方案是改变执行操作: 先删除缓存,当且仅当缓存删除成功,再更新数据库。这样做的好处是哪怕数据库更新失败了,反正缓存中没有数据,系统下一次接收到读请求后,将会查询数据库,把旧数据存入缓存,虽然数据不是最新的(脏数据、错误数据),但起码缓存和数据库的数据是一致的。
  2. 为什么会出现缓存与数据库双写不一致的情况?(复杂场景)
    复杂场景下,严格遵循Cache Aside Pattern,并且接收到写请求后,先删除缓存,再更新数据库,即便这样做,在并发的场景下,仍然有可能出现双写不一致的情况。
    比如系统接收到了一条写请求后,立刻删除对应的缓存,紧接着准备去数据库中更新数据(还没更新),但此时此刻,系统又收到了一条读请求,根据Cache Aside Pattern,系统优先找缓存,由于缓存中不存在对应数据,因此系统将请求转发到数据库,取出(脏)数据存入缓存,并返回结果,最后,数据库终于完成了先前的写请求,将数据更新到了数据库中。这样一来,便出现了数据库(新数据)与缓存(旧、脏数据)双写不一致的情况。
  3. 为什么上亿流量的高并发场景下,缓存会出现这个问题呢?
    如果系统接收到的读请求非常少,每次处理更新请求时,几乎不会出现并发的读请求,更新请求的整套处理流程不会出现任何干扰,则不会出现上述问题。但在高并发的场景下进行读写操作,比如每秒并发读是几万,那么只要有数据更新的请求,就可能出现数据库+缓存双写不一致的情况。
  4. 数据库与缓存更新、读取操作的异步串行化(经典)
    综合前面的各种场景,出现问题的原因在于高并发下,针对同一条数据,存在写操作未执行完毕,却开始执行读操作的问题。如果能把同一条数据的各种操作串行化,那么就可以解决这个问题。
    如下图所示,系统在内存中创建一批请求队列,分别对应着一段取值范围,接着解析的请求,为涉及的key计算出hash(key),根据取值来匹配并放入合适的请求队列中(可以借鉴hash slot算法解决热点问题),底层为每个请求队列分配一个工作线程,以串行化的方式将请求依次取出,并严格遵守Cache Aside Pattern的法则执行后续操作。
    Redis进阶篇(高并发与高性能) & 面试题_第14张图片
  5. 高并发场景下,异步串行化方案需要注意的问题
    (1) 读请求长时间阻塞
    如果更新请求非常频繁,同一个请求队列中积压了大量的写请求,此时,由于队列先进先出的特性,后续接收到的读请求需要等待较长时间,才能得到执行。考虑到这种场景的存在,我们必须通过模拟真实场景进行大量的测试,计算出读请求等待的最大延时。业界一般要求系统对外提供读请求在TP99下,请求延时维持在200ms。换句话说,如果每个写请求的处理时间是10ms,那么一个请求队列只要排在读请求之前的写请求数量不超过20个,系统整体性能就没什么问题。如果经过计算确实无法满足需求,那么就可以考虑水平扩容增加机器数量,比如一台机器能提供20个请求队列,如果有10台机器,那么整套系统就有20*10=200个请求队列。
    (2) 读请求并发量过高
    同样需要经过详细的测算。比如遇到先执行写请求、再执行读取的数据冲突。经过上方的分析,我们都知道处理写请求时,缓存首当其冲会被清空,如果此时突然涌入大量的读请求,每一个读请求都知道缓存没数据,所有都希望从数据库读取数据并放入缓存,这显然非常的荒唐,因为只需要一次从数据库中取出更新后的数据并放入缓存就可以了,后续的读请求直接从缓存内取数据,所以此处可以对大量的读请求的更新缓存操作进行去重。
    根据经验来看,一般写操作与读操作的请求数量比例大概维持在1:1至1:3之间(仅仅只是经验,具体要看业务场景)。
    (3) 多服务示例部署时的请求路由设计方案
    务必保证相同数据的请求发送到同一个请求队列中。因为如果服务器1和服务器2都有某数据的请求队列,由于无法对并发请求串行化执行,仍然会出现写入与读取操作冲突的问题。
    这里有两种做法:
    方案1: 自己写算法控制请求落在哪台服务器的哪条请求队列中。比如服务器1中的队列1只接收id为1~100的数据相关请求,服务器2中的队列只接收id为500~600的请求。(当然也可以参考hash slot,创建若干个槽,每个请求队列挂载若干槽,系统通过id计算出请求应该被放在哪个槽中,从而找到对应的请求队列)
    Redis进阶篇(高并发与高性能) & 面试题_第15张图片
    方案2: 通过nginx路由强制固定hash的方式,将请求定向发送到目标服务实例中。
    (4) 热点数据的路由问题
    如果某个数据的读写请求非常频繁,导致大量的请求被发送到了同一个服务器实例的请求队列中,对该实例所在的服务器造成极大的压力。老师说这种场景下只要更新频率不太高,不会导致多次清空缓存->从数据库取数据->放入缓存的过程,那么影响就不会很大。我个人认为可以通过如下方式解决: 为这种压力较大的请求队列做集群(放在不同服务器上),每次更新时,通过更新共享参数的方式,在集群内实现请求的串行化执行,这里一定要注意事务(比如多个工作线程、多条请求队列、但只有一条工作流)。

一般来说,如果系统不是严格要求缓存+数据库必须一致的话,最好不要做这套方案。因为异步串行化为了实现双写一致性,将读、写请求放到了一个内存请求队列中进行串行化处理了,这不仅会降低系统的吞吐量,还会导致整套系统不得不使用比之前多几倍数量的服务器来搭建内存请求队列,以满足整套方案的串行处理能力。

6.6.17 你能说说redis的并发竞争问题该如何解决吗?

  • Redis的并发竞争问题是什么?
    在某个时刻,系统的多个实例同时去更新某个key,如下图所示:
    Redis进阶篇(高并发与高性能) & 面试题_第16张图片
  • Redis并发竞争问题的解决方案
    思路: 分布式锁(zookeeper) + 待缓存数据的时间戳
    Redis进阶篇(高并发与高性能) & 面试题_第17张图片
    通过分布式锁来控制同一时间段,只能由一个线程更新缓存中的同一个key。
    通过时间戳(或版本号)来控制更新数据的顺序,当且仅当打算更新的值比缓存内的值更新,才能执行更新操作。

6.6.18 你们公司生产环境的redis集群的部署架构是什么样的?

生产环境使用的是redis cluster,总共10台机器,其中5台部署了redis主示例,另外5台部署了redis的从实例,每个主示例外挂了一个从实例,总共有5个节点对外提供读写服务,压测后,每个节点的读写高峰qps可以达到每秒5万,5台机器最多是25万读写请求/s。

机器的配置: 64G内存+8核CPU,但是分配给redis进程的是10G内存,一般在线上生产环境,redis的内存尽量不要超过10G,否则可能会出现问题(比如rdb文件过大,传输速度太慢,跑满了master节点所在服务器的带宽)。现在有5台机器对外提供读写,一共有50G可以存储数据。

由于每个主实例都挂载了一个从实例,所以是高可用的。任何一个主实例宕机,都会自动进行故障转移,将从实例变成主实例,继续对外提供读写服务。

一般往内存中写的是什么数据?每条数据的大小是多少?比如我们公司一般往Redis内写一些经过计算后的性能、指标数据,每条数据的大小差不多是10kb,那么10万条数据就是1g,常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存(50G)的50%。

问题:

  1. 一个slave能够连接多个master吗?
  2. Redis cluster架构下有没有sentinal的概念呢?
  3. redis为什么是key,value的?
  4. redis为什么不支持SQL呢?
  5. redis是多线程还是单线程的?
  6. redis持久化开启了RDB和AOF后,重启服务是如何加载的?
  7. redis如果做集群该如何规划?AKF/CAP如何实现和设计?
  8. 10万用户一年365天的登陆情况如何用redis存储并快速检索任意时间窗内的活跃用户?
  9. redis的5种value类型你用过几种?能举例吗?
    10.100万并发4G数据,10万并发400G数据,如何设计Redis的存储方式?
  10. redis AOF是如何对内存中现有数据还原成写入操作的?是不是全部变成了写入操作。
  11. redis内核原理、源码
  12. 如果要缓存几百GB,甚至几TB的数据,该怎么做?
  13. 如果缓存中某个value特别大,可能有几十M,占满了带宽导致系统无法对外正常提供服务了,怎么办?
  14. 如果集群架构使用的是主从+哨兵,那么集群是怎么部署的呢?是用的Redis Quotes还是twemproxy这种缓存集群中间件?

你可能感兴趣的:(#,Redis,面试)