Redis面试题4

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis面试题4

为什么用redis用来保存验证码

  • redis读写性能高,如果一个验证码读和写要半天那还得了
  • 可以设置过期时间,可以通过expire来设定过期策略


redis过期数据的处理(过期策略)

在 redis 中,对于已经过期的数据,Redis 采用两种策略来处理这些数据,分别是惰性删除和定期删除

1、惰性删除

惰性删除不会去主动删除数据,而是在访问数据的时候,再检查当前键值是否过期,如果过期则执行删除并返回 null 给客户端,如果没有过期则返回正常信息给客户端。


它的优点是简单,不需要对过期的数据做额外的处理,只有在每次访问的时候才会检查键值是否过期,缺点是删除过期键不及时,造成了一定的空间浪费。


2、定期删除

定期删除:Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。


3、Redis内存淘汰策略

定期没有删除掉,又很久没有访问这个 key,因此不会引起惰性删除,当这样的数据越来越多的时候,会占用很大的内存。当redis的内存超过最大允许的内存之后,Redis会触发内存淘汰策略,删除一些不常用的数据,以保证redis服务器的正常运行


在redis 4.0以前,redis的内存淘汰策略有以下6种


1.noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键

2.allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键

3.volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键

4.allkeys-random:加入键的时候如果过限,从所有key随机删除

5volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐

6.volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键 在redis 4.0以后,又增加了以下两种

7.volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键

8.allkeys-lfu:从所有键中驱逐使用频率最少的键

内存淘汰策略可以通过配置文件来修改,redis.conf对应的配置项是maxmemory-policy 修改对应的值就行,默认是noeviction


Redis事务

什么是Redis事务?

官方给出的定义是这样子的:


Redis事务可以一次执行多个命令, 并且带有以下两个重要的保证:


事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行(注意Redis的事务是不会因为其中的一条异常而回滚或者是全部就都不执行了,而是一直执行完事务中的所有命令)

官方腔换成方言就是:


Redis事务提供了一种 “将多个命令打包, 然后一次性、按顺序地执行” 的机制, 并且事务在执行的期间不会中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。


Redis事务的三个阶段

Redis事务的三个阶段

  • 事务开始 MULTI
  • 命令入队
  • 事务执行 EXEC


事务开始 MULTI

MULTI命令的执行标志着事务的开始

MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的 flags属性中打开REDTS MAULTI标识来完成的,MULTI命令的实现可以用以下伪代码来表示


命令入队

当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行


与此不同的是,当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作


如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令。

与此相反,如果客户端发送的命令是EXEC、DISCARD、WATCH、MULTI四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。


事务执行 EXEC

当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。


Redis事务相关命令

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。


MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。

EXEC:执行事务中的所有操作命令。

DISCARD:取消事务,放弃执行事务块中的所有命令。

WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。

UNWATCH:取消WATCH对所有key的监视。


127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 33
QUEUED
127.0.0.1:6379> set k2 34
QUEUED
127.0.0.1:6379> DISCARD
OK
redis> WATCH lock lock_times
OK
redis 127.0.0.1:6379> WATCH lock lock_times
OK
redis 127.0.0.1:6379> UNWATCH
OK

事务管理(ACID)概述

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)

事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后顺序都是合法数据状态(事务前后数据的完整性必须保持一致),数据库的完整性约束包括但不限于:


实体完整性(如行的主键存在且唯一);

列完整性(如字段的类型、大小、长度要符合要求)

外键约束;

用户自定义完整性(如转账前后,两个账户余额的和应该不变)。


隔离性(Isolation)

事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰,多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性(Durability)

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

Redis的事务注意其原子性只支持单条命令,事务是不支持原子性的,并且也不会回滚

Redis事务支持隔离性吗

Redis事务和关系型数据库的事务不太一样,它不保证原子性,也没有隔离级别的概念。

没有隔离级别

如上图所示,当事务开启时,事务期间的命令并没有执行,而是加入队列,只有执行EXEC命令时,事务中的命令才会按照顺序一一执行,从而事务间就不会导致数据脏读、不可重复读、幻读的问题,因此就没有隔离级别。


Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

不保证原子性

如上图所示,在通过EXEC执行事务时,其中命令执行失败不会影响到其他命令的执行,并没有保证同时成功和同时失败的原子操作,尽管这样,Redis事务中也没有提供回滚的支持,官方提供了两个理由:


使用Redis命令语法错误,或是将命令运用在错误的数据类型键上(如对字符串进行加减乘除等),从而导致业务数据有问题,这种情况认为是编程导致的错误,应该在开发过程中解决,避免在生产环境中发生;

由于不用支持回滚功能,Redis内部简单化,而且还比较快;

在事务命令入队过程中,发现相关命令逻辑使用错误,可以进行放弃该事务;如果使用错误的Redis命令,且没有放弃事务,最终也会导致事务整体执行失败,这也算是为原子性扳回一局


Redis事务其他实现方式?

基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完

基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐


Redis集群

什么是Redis集群?

如果部署到多台电脑,就跟普通的集群一样;因为Redis是单线程处理的,多核CPU也只能使用一个核,所以部署在同一台电脑上,通过运行多个Redis实例组成集群,然后能提高CPU的利用率。


Redis3.0加入了Redis的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。


Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。


Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。


如上图所示,Redis集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点(其中只有master节点具有处理请求的能力,slave节点主要是用于节点的高可用),由上面图可以看出主从部署和集群化部署是两码事,Redis集群是指的单个服务器开多个Redis实例,每个节点可以采用主从复制的架构,所以看得出集群是大于主从复制的


Reids的三种集群模式

Redis 支持三种集群方案

  • 主从复制模式
  • Sentinel(哨兵)模式
  • Cluster 模式

主从复制模式

主从复制的作用


通过持久化功能,Redis保证了即使在服务器重启的情况下也不会丢失(或少量丢失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。 但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。


为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。


为此, Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。


在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。


引入主从复制机制的目的有两个


一个是读写分离,分担 "master" 的读写压力

一个是方便做容灾恢复

主从复制原理


从数据库启动成功后,连接主数据库,发送 SYNC 命令;

主数据库接收到 SYNC 命令后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;

主数据库 BGSAVE 执行完后,向所有从数据库发送快照文件,并在发送期间继续记录被执行的写命令;

从数据库收到快照文件后丢弃所有旧数据,载入收到的快照;

主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令;

从数据库完成对快照的载入,开始接收命令请求,并执行来自主数据库缓冲区的写命令;(从数据库初始化完成)

主数据库每执行一个写命令就会向从数据库发送相同的写命令,从数据库接收并执行收到的写命令(从数据库初始化完成后的操作)

出现断开重连后,2.8之后的版本会将断线期间的命令传给重数据库,增量复制。

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主从复制优缺点


主从复制优点


支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;

为了分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成;

Slave 同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力;

Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在 Master-Slave 同步期间,客户端仍然可以提交查询或修改请求;

Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据;

主从复制缺点


Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复(也就是要人工介入);

主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;

如果多个 Slave 断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要 Slave 启动,就会发送sync 请求和主机全量同步,当多个 Slave 重启的时候,可能会导致 Master IO 剧增从而宕机。

Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;


Sentinel(哨兵)模式

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。


哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个 Redis 实例。


哨兵模式的作用


通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;

当哨兵监测到 master 宕机,会自动将 slave 切换成 master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;

然而一个哨兵进程对Redis服务器进行监控,也可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。


故障切换的过程


假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。


哨兵模式的工作方式:


每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的 Master 主服务器,Slave 从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。

如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)

如果一个 Master 主服务器被标记为主观下线(SDOWN),则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态

当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态(SDOWN), 则 Master 主服务器会被标记为客观下线(ODOWN)

在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有 Master 主服务器、Slave 从服务器发送 INFO 命令。

当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

哨兵模式的优缺点


优点:


哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。

主从可以自动切换,系统更健壮,可用性更高(可以看作自动版的主从复制)。

缺点:


Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。


Cluster 集群模式(Redis官方)

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式(Redis Cluster),实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容。一组Redis Cluster是由多个Redis实例组成,官方推荐我们使用6实例,其中3个为主节点,3个为从结点。一旦有主节点发生故障的时候,Redis Cluster可以选举出对应的从结点成为新的主节点,继续对外服务,从而保证服务的高可用性。那么对于客户端来说,知道对应的key是要路由到哪一个节点呢?原来,Redis Cluster 把所有的数据划分为16384个不同的槽位,可以根据机器的性能把不同的槽位分配给不同的Redis实例,对于Redis实例来说,他们只会存储部门的Redis数据,当然,槽的数据是可以迁移的,不同的实例之间,可以通过一定的协议,进行数据迁移。


集群的数据分片


Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念。


Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:


节点 A 包含 0 到 5460 号哈希槽

节点 B 包含 5461 到 10922 号哈希槽

节点 C 包含 10923 到 16383 号哈希槽

这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。


在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。


Redis 集群的主从复制模型


为了保证高可用,redis-cluster集群引入了主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了或半数以上的主节点都挂了,那么该集群就无法再提供服务了。


集群的特点


所有的 redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。

节点的 fail 是通过集群中超过半数的节点检测失效时才生效。

客户端与 Redis 节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。


基于代理服务器分片

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

透明接入,业务程序不用关心后端Redis实例,切换成本低Proxy 的逻辑和存储的逻辑是隔离的代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis


生产环境中的 redis 是怎么部署的?

面试官心理分析


看看你了解不了解你们公司的 redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 redis 给几个 G 的内存?设置了哪些参数?压测后你们 redis 集群承载多少 QPS?


redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务, 每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。


机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。


5 台机器对外提供读写,一共有 50g 内存。


因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。


你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。


其实大型的公司,会有基础架构的 team 负责缓存集群的运维。


说说Redis哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念,Redis 集群中内置了 16384 个哈希槽(注意是Cluster 集群模式才有哈希槽这个概念),当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数(即CRC16(key) % 16384),这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,集群的每个节点负责一部分hash槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。


举个例子,比如当前Redis集群有3个节点,那么:

  • 节点 A 包含 0 到 5460 号哈希槽
  • 节点 B 包含 5461 到 10922 号哈希槽
  • 节点 C 包含 10923 到 16383 号哈希槽


这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D , 我只需要从节点 A, B, C 中得部分槽到 D 上。如果我想移除节点 A ,只需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。


在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。


slot返回有关哪个集群插槽映射到哪个redis实例的详细信息。该命令适用于redis集群客户端库实现,以便检索(或在收到重定向时更新)将集群散列槽与实际节点网络坐标(由ip地址和tcp端口组成)关联的映射,以便在接收到命令时,可以将其发送到命令中指定的键的正确实例。


"用了哈希槽的概念,而没有用一致性哈希算法,不都是哈希么?这样做的原因是为什么呢?"

Redis Cluster(Cluster 集群模式)是作者自己做的crc16的简单hash算法,没有用一致性hash。Redis的作者认为它的crc16(key) mod 16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。

“集群总共有16384个哈希槽(2的14次方),那么每一个哈希槽中存的key 和 value是怎样的?”

当你往Redis Cluster中加入一个Key时,会根据crc16(key) mod 16384计算这个key应该分布到哪个hash slot中,一个hash slot中会有很多key和value。你可以理解成表的分区,使用单节点时的redis时只有一个表,所有的key都放在这个表里;改用Redis Cluster以后会自动为你生成16384个分区表,你insert数据时会根据上面的简单算法来决定你的key应该存在哪个分区,每个分区里有很多key。


如同时有大于16384个请求,我们是否等待16383个槽处理完之后再处理16384之后的请求呢?

答案是不需要,槽值一旦被计算出来,就只作为一个值来用,这个值决定了访问哪个节点的Redis,而计算这个值的速度,或者并发量(同一时刻允许多少次计算),并不取决与槽有多少,而是取决与这台服务器的计算能力


hash算法

哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。

普通的hash算法在分布式应用中的不足:

比如,在分布式的存储系统中,要将数据存储到具体的节点上,如果我们采用普通的hash算法进行路由,将数据映射到具体的节点上,如key%N,key是数据的key,N是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了。


举个例子


假如线程N为7,然后有个key为28,那么key%N得到的是0,就把key为28的存到0号节点,假如现在6号节点挂了,那N就成6了,那所有的节点映射的数据都会失效,例如原本key为28的经过key%N后应该是映射到0号节点,但是由于N变成了6,那就会映射到4号节点,同理所有的节点映射的位置都会变化,则所有的数据映射都无效了


所以为了解决这个问题引用了一致性hash算法


一致性Hash算法

一致性Hash算法也是使用取模的方法,不过,上述的取模方法是对机器节点数量进行取模,而一致性的Hash算法是对2的32方取模。即一致性Hash算法将整个Hash空间组织成一个虚拟的圆环,Hash函数的值空间为0 ~ 2^32 - 1(一个32位无符号整型),整个哈希环如下:

整个圆环以顺时针方向组织,圆环正上方的点代表0,0点右侧的第一个点代表1,以此类推。

第二步,我们将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台服务器就确定在了哈希环的一个位置上,比如我们有三台机器,使用IP地址哈希后在环空间的位置如图1-4所示:


现在,我们使用以下算法定位数据访问到相应的服务器:


将数据Key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针查找,遇到的服务器就是其应该定位到的服务器。


例如,现在有ObjectA,ObjectB,ObjectC三个数据对象,经过哈希计算后,在环空间上的位置如下:


那么A对象就是存在A服务器,B对象存在B服务器,C对象存在C服务器

一致性hash深入了解:一致性Hash算法详解 - 知乎

一致性hash深入了解:一致性Hash原理与实现 - 简书

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。


以下情况可能导致写操作丢失:


1、过期 key 被清理。


2、最大内存不足,导致 Redis 自动清理部分 key 以节省空间。


3、主库故障后自动重启,数据还没来得及持久化


4、网络不稳定触发哨兵的自动切换主从节点,切换期间会有数据丢失。


Redis集群之间是如何复制的?

主从复制机制的演变

2.8 版本之前 Redis 复制采用 sync 命令,无论是第一次主从复制还是断线重连后再进行复制都采用全量同步,成本高

2.8 ~ 4.0 之间复制采用 psync 命令,这一特性主要添加了 Redis 在断线重连时候可通过 offset 信息使用部分同步

4.0 版本之后也采用 psync,相比于 2.8 版本的 psync 优化了增量复制,这里我们称为 psync2,2.8 版本的 psync 可以称为 psync1


2.8 版以前( psync1)

Redis 通过同步(sync)和指令传播(command propagate)两个操作完成同步


同步(sync):将从节点的数据库状态更新至与主节点的数据库状态一致


指令传播(command propagate):主节点数据被修改,会主动向从节点发送执行的写指令,从节点执行之后,两个节点数据状态又保持一致


2.8 版~4.0版( psync2)

为了解决旧版 SYNC 在处理断线重连复制场景下的低效问题,Redis 2.8 采用 PSYNC 代替 SYNC 命令。PSYNC 命令具有全量同步和部分同步两种模式。


全量重同步


前者和 SYNC 大致相同,都是让 master 生成并发送 RDB 文件,然后再将保存在缓冲区中的写命令传播给 slave 来进行同步,相当于只有同步和命令传播两个阶段。


部分重同步


部分同步适用于断线重连之后的同步,slave 只需要接收断线期间丢失的写命令就可以,不需要进行全量同步。为了实现部分同步,引入了复制偏移量(offset)、复制积压缓冲区(replication backlog buffer)和运行 ID (run_id)三个概念。


复制偏移量


执行主从复制的双方都会分别维护一个复制偏移量,master 每次向 slave 传播 N 个字节,自己的复制偏移量就增加 N;同理 slave 接收 N 个字节,自身的复制偏移量也增加 N。通过对比主从之间的复制偏移量就可以知道主从间的同步状态。


复制积压缓冲区


复制积压缓冲区是 master 维护的一个固定长度的 FIFO 队列,默认大小为 1MB。当 master 进行命令传播时,不仅将写命令发给 slave 还会同时写进复制积压缓冲区,因此 master 的复制积压缓冲区会保存一部分最近传播的写命令。当 slave 重连上 master 时会将自己的复制偏移量通过 PSYNC 命令发给 master,master 检查自己的复制积压缓冲区,如果发现这部分未同步的命令还在自己的复制积压缓冲区中的话就可以利用这些保存的命令进行部分同步,反之如果断线太久这部分命令已经不在复制缓冲区中了,那没办法只能进行全量同步。


运行 ID


令人疑惑的是上述逻辑看似已经很圆满了,这个 run_id 是做什么用呢?其实这是因为 master 可能会在 slave 断线期间发生变更,例如可能超时失去联系或者宕机导致断线重连的是一个崭新的 master,不再是断线前复制的那个了。自然崭新的 master 没有之前维护的复制积压缓冲区,只能进行全量同步。因此每个 Redis server 都会有自己的运行 ID,由 40 个随机的十六进制字符组成。当 slave 初次复制 master 时,master 会将自己的运行 ID 发给 slave 进行保存,这样 slave


重连时再将这个运行 ID 发送给重连上的 master ,master 会接受这个 ID 并于自身的运行 ID 比较进而判断是否是同一个 master。


psync1 流程


如果 slave 以前没有复制过任何 master,或者之前执行过 SLAVEOF NO ONE 命令,那么 slave 在开始一次新的复制时将向主服务器发送 PSYNC ? -1 命令,主动请求 master 进行完整重同步(因为这时不可能执行部分重同步)。相反地,如果 slave 已经复制过某个 master,那么 slave 在开始一次新的复制时将向 master 发送 PSYNC runid offset 命令:其中 runid 是上一次复制的 master 的运行 ID,而 offset 则是 slave 当前的复制偏移量,接收到这个命令的 master 会通过这两个参数来判断应该对 slave 执行哪种同步操作。


根据情况,接收到 PSYNC 命令的 master 会向 slave 返回以下三种回复的其中一种:


如果 master 返回 +FULLRESYNC runid offset 回复,那么表示 master 将与 slave 执行完整重同步操作:其中 runid 是这个 master 的运行 ID,slave 会将这个 ID 保存起来,在下一次发送 PSYNC 命令时使用;而 offset 则是 master 当前的复制偏移量,slave 会将这个值作为自己的初始化偏移量


如果 master 返回 +CONTINUE 回复,那么表示 master 将与 slave 执行部分同步操作,slave 只要等着 master 将自己缺少的那部分数据发送过来就可以了


如果 master 返回 -ERR 回复,那么表示 master 的版本低于 Redis 2.8,它识别不了 psync 命令,slave 将向 master 发送 SYNC 命令,并与 master 执行完整同步操作


由此可见 psync 也有不足之处,当 slave 重启以后 master runid 发生变化,也就意味者 slave 还是会进行全量复制,而在实际的生产中进行 slave 的维护很多时候会进行重启,而正是有由于全量同步需要 master 执行快照,以及数据传输会带不小的影响。因此在 4.0 版本,psync 命令做了改进,我们称之为 psync2。


4.0版以后(psync2)

Redis 4.0 版本新增 混合持久化,还优化了 psync(以下称 psync2),psync2 最大的变化支持两种场景下的部分重同步,一个场景是 slave 提升为 master 后,其他 slave 可以从新提升的 master 进行部分重同步,这里需要 slave 默认开启复制积压缓冲区;另外一个场景就是 slave 重启后,可以进行部分重同步。这里要注意和 psync1 的运行 ID 相比,这里的复制 ID 有不一样的意义。


优化细节


Redis 4.0 引入另外一个变量 master_replid 2 来存放同步过的 master 的复制 ID,同时复制 ID 在 slave 上的意义不同于之前的运行 ID,复制 ID 在 master 的意义和之前运行 ID 仍然是一样的,但对于 slave 来说,它保存的复制 ID(即 replid) 表示当前正在同步的 master 的复制 ID 。master_replid 2 则表示前一个 master 的复制 ID(如果它之前没复制过其他的 master,那这个字段没用),这个在主从角色发生改变的时候会用到。


struct redisServer {
    ...
    /* Replication (`master`) */                                        
    char replid[CONFIG_RUN_ID_SIZE+1];  /* My current replication ID. */
    char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from `master`*/ 
}

slave 在意外关闭前会调用 rdbSaveInfoAuxFields 函数把当前的复制 ID(即关闭前正在复制的 master 的 replid,因为 slave 中的 replid 字段保存的是 master 的复制 ID) 和复制偏移量一起保存到 RDB 文件中,后面该 slave 重启的时候,就可以从 RDB 文件中读取复制 ID 和复制偏移量,然后重连上 master 后 slave 将这两个值发送给 master,master 会如下判断是否允许 psync


// 如果 `slave` 发送过来的复制 ID 是当前 `master` 的复制 ID, 说明 `master` 没变过
    if (strcasecmp(master_replid, server.replid) &&   
        // 或者和现在的新 `master` 曾经属于同一 `master`
        (strcasecmp(master_replid, server.replid2) ||      
         // 但同步进度不能比当前 `master` 还快
         psync_offset > server.second_replid_offset)) {
        ... ...
    }
    // 判断同步进度是否已经超过范围
    if (!server.repl_backlog ||                                                        
        psync_offset < server.repl_backlog_off ||                                      
        psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) {                                                                                  
        ... ...
    }  

Redis集群最大节点个数是多少?

16384个

为什么是16384(2^14)个?

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。


虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。


Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

注意:Redis集群没办法选择数据库,但是的单个Redis是可以选择数据库的


单个Redis选择数据库

redis-cli命令下选择数据库分区可以有2种方式:


1、 使用命令select选择数据库


127.0.0.1:6379> select 2

OK

2、登录时指定要连接的数据库


那么我们登录的时候,如何指定登录哪个库呢?


./redis-cli -n 3 指定登录下标为3的数据库


./redis-cli -n 0 等价于 ./redis-cli 默认登录下标为0的数据库


[root@db redis-3.2.5]# ./redis-cli -h 192.168.1.8  -p  6379 -n 1
192.168.1.8:6379[1]> dbsize
(integer) 0
************实例****************
[root@db redis-3.2.5]# ./redis-cli -h 192.168.1.8  -p  6379 -n 2
192.168.1.8:6379[2]> dbsize
(integer) 24
192.168.1.8:6379[2]> dbsize
(integer) 24
192.168.1.8:6379[2]> dbsize
(integer) 24
192.168.1.8:6379[2]> dbsize
(integer) 24
192.168.1.8:6379[2]> select 1
OK
192.168.1.8:6379[1]> dbsize
(integer) 0
192.168.1.8:6379[1]> dbsize
(integer) 0
192.168.1.8:6379[1]> select 2
OK
192.168.1.8:6379[2]> dbsiz
(error) ERR unknown command 'dbsiz'
192.168.1.8:6379[2]> dbsize
(integer) 24
192.168.1.8:6379[2]> dbsize
(integer) 24

即使开始使用./redis-cli –n指定了数据库,也能在使用连接后切换

Springboot配置选择数据库分区:

在application.yml 中配置,通过spring.redis.database key来设置

例如下例选择了数据库分区2:

spring:
  application:
    name: demo-web
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+08:00
  redis:
    host: 192.168.1.8
    port: 6379
    database: 2
  session:
    storeType: REDIS


为什么要做Redis分区(分片)?

什么是分区?

Redis Partitioning即Redis分区,简单的说就是将数据分布到不同的redis实例中,因此对于每个redis实例所存储的内容仅仅是所有内容的一个子集。 如果只使用一个redis实例时,其中保存了服务器中全部的缓存数据,这样会有很大风险,如果单台redis服务宕机了将会影响到整个服务。解决的方法就是我们可以采用分片/分区的技术,将原来一个redis实例的数据分布到不同的redis实例中,因此对于每个redis实例所存储的内容仅仅是所有内容的一个子集(注意一个服务器(一台电脑)是可以开多个Redis实例的)


为什么要分区?

增加可用性:如果数据库的某个节点出现了故障,在其他节点的数据仍旧可用

维护方便:如果数据库的某个节点出现故障,需要修复数据,只需要修复该节点

均衡IO:可以将不同的请求映射到各节点以平衡IO,改善整个系统的性能

分片可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。

性能的提升,分片使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

存储的横向扩展,即使Redis的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储容量,将数据分散到多台机器上存储使得Redis服务可以横向扩展。

改善查询性能:对分区对象的查询可以仅搜索自己关心的节点,提高检索速度分布式存储首先要解决把整个数据集按分区规则映射到多个节点的问题,即把数据集划分到多个节点,每个节点负责整体数据的一个子集:


你知道有哪些Redis分区(分片)实现方案?

Redis分区基础

实际应用中有很多分区的具体策略,举个例子,假设我们已经有了一组四个Redis实例分别为R0、R1、R2、R3,另外我们有一批代表用户的键,如:user:1,user:2,……等等,其中“user:”后面的数字代表的是用户的ID,我们要做的事情是把这些键分散存储在这四个不同的Redis实例上。最简单的一种方式是范围分区(range partitioning)


范围分区

范围分区,也叫做顺序分区,最简单的分区方式。


通过映射对象的范围到指定的 Redis 实例来完成分片(将一个范围内的key都映射到同一个Redis实例中)。


具体做法如下:我们可以将用户ID从0到10000的用户数据映射到R0实例,而将用户ID从10001到20000的对象映射到R1实例,依次类推。


这种方法虽然简单,但是在实际应用中是很有效的,不过还是有问题:


我们需要一张表,这张表用来存储用户ID范围到Redis实例的映射关系,比如用户ID0-10000的是映射到R0实例……。

我们不仅需要对这张表进行维护,而且对于每种对象类型我们都需要一个这样的表,比如我们当前存储的是用户信息,如果存储的是订单信息,我们就需要再建一张映射关系表。

如果我们想要存储的数据的key并不能按照范围划分怎么办,比如我们的key是一组uuid,这个时候就不好用范围分区了。


哈希分区

哈希分区跟范围分区相比一个明显的优点是哈希分区适合任何形式的key,而不像范围分区一样需要key的形式为object_name:< id>,而且分区方法也很简单,一个公式就可以表达:


id=hash(key)%N

其中id代表Redis实例的编号,公式描述的是


首先根据key和一个hash函数(如crc32函数)计算出一个数值型的值。接着上面的例子,我们的第一个要处理的key是user:1,hash(user:1)的结果是93024922。

然后对哈希结果进行取模,取模的目的是计算出一个介于0到n之间的值,因此这个值才可以被映射到我们的一台Redis实例上面。比如93024922%4结果是2,我们就会知道foobar将要被存储在R2上面。


上面的两种是分区的具体实现思想,下面的三种是分区实现方法

客户端分区

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取(下图是用的上面的哈希分区)

代理分区

客户端将请求发往代理服务器,代理服务器实现了Redis协议,因此代理服务器可以代理客户端和Redis服务器通信。

代理服务器通过配置的分区模式来将客户端的请求转发到正确的Redis实例中,同时将反馈消息返回给客户端。

查询路由

查询路由是redis cluster实现的一种redis分区方式,是指你可以把一个请求发送给一个随机的实例,这时实例会把该请求转发给正确的节点。通过客户端重定向(客户端的请求不用直接从一个实例转发到另一个实例,而是被重定向到正确的节点),Redis集群实现了一种混合查询路由。


Redis分区(分片)有什么缺点?

多键操作是不被支持的,比如我们将要批量操作的键映射到了不同的Redis实例中。

多键的Redis事务是不被支持的。

Redis是以键来分区,因此不能使用单个大键进行分区,例如一个非常大的有序集作为键(key),那就不能通过这个大键进行分区。

数据备份不方便,比如我们需要处理多个rdb/aof文件,将分布在不同实例的文件聚集到一起备份。

添加和删除节点也会变得复杂。例如通过在运行时添加和删除节点,Redis集群通常支持透明地再均衡数据,但是其他系统像客户端分区或者代理分区的特性就不支持该特性。不过Pre-sharding(预分片)可以在这方面提供帮助。


分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),最好是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。


一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。


这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
NoSQL Redis Sentinel
【怒怼大厂面试官】听说你精通Redis?说说Redis哨兵
面试官:Redis哨兵知道吧?知道的,Sentinel哨兵本质是一个运行在特殊模式下的Redis服务器。面试官:嗯然后呢?它的主要作用是通过检测Redis主从服务器的下线状态,选举出新Redis主服务器,也就是故障转移,来保证Redis的高可用性。
79 4
【怒怼大厂面试官】听说你精通Redis?说说Redis哨兵
|
21天前
|
NoSQL MongoDB Redis
Python与NoSQL数据库(MongoDB、Redis等)面试问答
【4月更文挑战第16天】本文探讨了Python与NoSQL数据库(如MongoDB、Redis)在面试中的常见问题,包括连接与操作数据库、错误处理、高级特性和缓存策略。重点介绍了使用`pymongo`和`redis`库进行CRUD操作、异常捕获以及数据一致性管理。通过理解这些问题、易错点及避免策略,并结合代码示例,开发者能在面试中展现其技术实力和实践经验。
311 8
Python与NoSQL数据库(MongoDB、Redis等)面试问答
|
1月前
|
缓存 NoSQL Java
面试官:Redis如何实现延迟任务?
延迟任务是计划任务,用于在未来特定时间执行。常见应用场景包括定时通知、异步处理、缓存管理、计划任务、订单处理、重试机制、提醒和数据采集。Redis虽无内置延迟任务功能,但可通过过期键通知、ZSet或Redisson实现。然而,这种方法精度有限,稳定性较差,适合轻量级需求。Redisson的RDelayedQueue提供更简单的延迟队列实现。
363 9
|
1月前
|
缓存 NoSQL 定位技术
深入探索Redis:面试中必须掌握的关键知识点
深入探索Redis:面试中必须掌握的关键知识点
|
1月前
|
NoSQL Java 测试技术
面试官:如何搭建Redis集群?
**Redis Cluster** 是从 Redis 3.0 开始引入的集群解决方案,它分散数据以减少对单个主节点的依赖,提升读写性能。16384 个槽位分配给节点,客户端通过槽位信息直接路由请求。集群是无代理、去中心化的,多数命令直接由节点处理,保持高性能。通过 `create-cluster` 工具快速搭建集群,但适用于测试环境。在生产环境,需手动配置文件,启动节点,然后使用 `redis-cli --cluster create` 分配槽位和从节点。集群动态添加删除节点、数据重新分片及故障转移涉及复杂操作,包括主从切换和槽位迁移。
35 0
面试官:如何搭建Redis集群?
|
2月前
|
运维 负载均衡 NoSQL
【大厂面试官】知道Redis集群和Redis主从有什么区别吗
集群节点之间的故障检测和Redis主从中的哨兵检测很类似,都是通过PING消息来检测的。。。面试官抓抓脑袋,继续看你的简历…得想想考点你不懂的😰。
69 1
|
2月前
|
NoSQL Redis
【怒怼大厂面试官】听说你精通Redis?Redis数据同步懂吗
面试官:不用慌尽管说,错了也没关系。。。来说说Redis数据同步。是这样的,Redis有一个叫命令传播的概念,如果像面试官说的这种场景,再使用上面我提到的AOF缓冲区就有点浪费内存空间了。所以Redis会将主服务器的这条Del删除命令
63 2
【怒怼大厂面试官】听说你精通Redis?Redis数据同步懂吗
|
2月前
|
NoSQL Redis 数据库
【怒怼大厂面试官】听说你精通Redis?说说Redis持久化
咳咳咳,看你简历写了精通Redis,那我就随便问问。主要有RDB持久化、AOF持久化。是这样,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。
56 1
【怒怼大厂面试官】听说你精通Redis?说说Redis持久化
|
3月前
|
缓存 NoSQL Linux
面试必备:一线大厂Redis设计规范与性能优化
本文梳理了在使用Redis过程需要遵循的一些最佳实践,包括针对架构维度的一些深入性能优化的知识,如果面试官问你:"说下在使用Redis的过程中,需要注意哪些规范?",如果你按照本文的思路回答,肯定能让面试官眼前一亮,offer自然就到手了。
57 0
面试必备:一线大厂Redis设计规范与性能优化
|
3月前
|
存储 NoSQL Java
面试官:Redis如何保证高可用?
面试官:Redis如何保证高可用?
82 2
面试官:Redis如何保证高可用?