Redis(牛客Java面试宝典补充)

Redis(牛客Java面试宝典补充)

根据牛客面试指南Java工程师模块问题进行整理和细微补充,仅供参考,不合理地方请评论区指出共勉

1.Redis可以用来做什么?

参考文章

Redis 是一个基于 C 语言开发的开源数据库(BSD 许可),与传统数据库不同的是 Redis 的数据是存在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。并且,Redis 存储的是 KV 键值对数据。

为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。

Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis。

  1. Redis最常用来做缓存,是实现分布式缓存的首先中间件;
  2. Redis可以作为数据库,实现诸如点赞、关注、排行等对性能要求极高的互联网需求;
  3. Redis可以作为计算工具,能用很小的代价,统计诸如PV/UV、用户在线天数等数据;
  4. Redis还有很多其他的使用场景,例如:可以实现分布式锁,可以作为消息队列使用。

2.Redis和传统的关系型数据库有什么不同?

Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的。Redis的数据都存储于内存中,因此它的速度惊人,读写性能可达10万/秒,远超关系型数据库。

关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis。

3.Redis有哪些数据类型?

  • 5 种基础数据结构:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。扩展阅读

  • 3 种特殊数据结构:HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。扩展阅读

4.Redis是单线程的,为什么还能这么快?

参考文章

  1. 对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗;
  2. Redis的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因;
  3. Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。

why-redis-so-fast

关于Redis的单线程架构实现,如下图:

Redis单线程实现

5.Redis在持久化时fork出一个子进程,这时已经有两个进程了,怎么能说是单线程呢?

Redis是单线程的,主要是指Redis键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说Redis是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。

6.set和zset有什么区别?

set:

  • 集合中的元素是无序、不可重复的,一个集合最多能存储232 -1个元素;
  • 集合除了支持对元素的增删改查之外,还支持对多个集合取交集、并集、差集。

zset:

  • 有序集合保留了集合元素不能重复的特点;
  • 有序集合会给每个元素设置一个分数,并以此作为排序的依据;
  • 有序集合不能包含相同的元素,但是不同元素的分数可以相同。

7.说一下Redis中的watch命令

客户端通过watch命令,要求服务器对一个或多个key进行监视,如果在客户端执行事务之前,这些key发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。

8. 说说Redis中List结构的相关操作

列表是线性有序的数据结构,它内部的元素是可以重复的,并且一个列表最多能存储232-1个元素。列表包含如下的常用命令:

  • lpush/rpush:从列表的左侧/右侧添加数据;
  • lrange:指定索引范围,并返回这个范围内的数据;
  • lindex:返回指定索引处的数据;
  • lpop/rpop:从列表的左侧/右侧弹出一个数据;
  • blpop/brpop:从列表的左侧/右侧弹出一个数据,若列表为空则进入阻塞状态。

9.你要如何设计Redis的过期时间?

普通数据:在设置过期时间时,可以附加一个随机数,避免大量的key同时过期,导致缓存雪崩。
热点数据:. 热点数据不设置过期时间,使其达到“物理”上的永不过期,可以避免缓存击穿问题。

10. Redis中,sexnx命令的返回值是什么,如何使用该命令实现分布式锁?

分布式锁拓展

setnx命令返回整数值,当返回1时表示设置值成果,当返回0时表示设置值失败(key已存在)。

setnx实现加锁:

第一版本
这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败

setnx key value

第二版本
给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。

setnx key value
expire key seconds

第三版本
通过“set…nx…”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。

set key value nx ex seconds

第四版本
上面虽然已经解决了死锁的问题,但是还没有解决下面这个问题:

进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,它此时释放的可能是其他线程的锁。

在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。

这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。

# 加锁
set key random-value nx ex seconds
# 解锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

11.说一说Redis的持久化策略

持久化机制详解

Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。

  • RDB:

    RDB(Redis Database)是Redis默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾,内部存储了各个数据库的键值对数据等信息。RDB持久化的触发方式有两种:

    • 手动触发:通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;
    • 自动触发:通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令。

    其中,SAVE命令执行期间,Redis服务器将阻塞,直到“.rdb”文件创建完毕为止。而BGSAVE命令是异步版本的SAVE命令,它会使用Redis服务器进程的子进程,创建“.rdb”文件。BGSAVE命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。总之,BGSAVE命令是针对SAVE阻塞问题做的优化,Redis内部所有涉及RDB的操作都采用BGSAVE的方式,而SAVE命令已经废弃!

    BGSAVE命令的执行流程,如下图:

    BGSAVE命令的执行流程

    BGSAVE命令的原理,如下图:

    BGSAVE命令的原理

    RDB持久化的优缺点如下:

    • 优点:RDB生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快;
    • 缺点:BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行,所以RDB持久化没办法做到实时的持久化。
  • AOF:

    AOF(Append Only File),解决了数据持久化的实时性,是目前Redis持久化的主流方式。AOF以独立日志的方式,记录了每次写入命令,重启时再重新执行AOF文件中的命令来恢复数据 。AOF的工作流程包括:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load):

    命令追加(append):所有的写命令会追加到 AOF 缓冲区中。

    文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。

    文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。

    文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。

    重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。

    AOF的工作流程

    AOF默认不开启,需要修改配置项来启用它:

    appendonly yes         # 启用AOF appendfilename "appendonly.aof"  # 设置文件名
    

    AOF以文本协议格式写入命令,如:

    *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
    

    文本协议格式具有如下的优点:

    1. 文本协议具有很好的兼容性;
    2. 直接采用文本协议格式,可以避免二次处理的开销;
    3. 文本协议具有可读性,方便直接修改和处理。

    AOF持久化的文件同步机制:

    为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。

    1. 当程序调用write对文件写入时,系统不会直接把数据写入硬盘,而是先将数据写入内存的缓冲区中;
    2. 当达到特定的时间周期或缓冲区写满时,系统才会执行flush操作,将缓冲区中的数据冲洗至硬盘中;

    这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性。

    1. 对于AOF这样的持久化功能来说,冲洗机制将直接影响AOF持久化的安全性;
    2. 为了消除上述机制的不确定性,Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率;
    3. Linux的glibc提供了fsync函数,可以将指定文件强制从缓冲区刷到硬盘,上述选项正是基于此函数。

    appendfsync选项的取值和含义如下:

    appendfsync选项的取值

    AOF持久化的优缺点如下:

    • 优点:与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。
    • 缺点:AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。
  • RDB-AOF混合持久化:

    Redis从4.0开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化构建而来的。用户可以通过配置文件中的“aof-use-rdb-preamble yes”配置项开启AOF混合持久化。Redis服务器在执行AOF重写操作时,会按照如下原则处理数据:

    • 像执行BGSAVE命令一样,根据数据库当前的状态生成相应的RDB数据,并将其写入AOF文件中;
    • 对于重写之后执行的Redis命令,则以协议文本的方式追加到AOF文件的末尾,即RDB数据之后。

    通过使用RDB-AOF混合持久化,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。

  • 如何选用持久化方案

    • Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。

    • 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。

    • 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。

12.如何实现Redis的高可用?

原文链接

Redis 的高可用主要包括以下两个方面:数据的高可用和服务的高可用。

  • 数据高可用

    Redis 数据的高可用主要通过主从复制和哨兵机制来实现。

    • 主从复制

      主从复制是指将 Redis 数据库中的数据从一台主服务器复制到多台从服务器上,从而实现数据的备份和读写分离。在主从复制中,主服务器会将写入操作同步到从服务器上,从服务器则只能进行读取操作。当主服务器宕机时,可以通过手动或自动将其中一台从服务器提升为主服务器,从而实现数据的高可用。

    • 哨兵机制

      哨兵机制是一种自动监控 Redis 服务器状态的机制,能够在主服务器宕机时自动将其中一台从服务器提升为主服务器,并将其他从服务器切换到新的主服务器下。哨兵机制的实现需要至少三台 Redis 服务器,其中一台为主服务器,其他为从服务器。每个从服务器都会向主服务器发送心跳包,一旦主服务器宕机,哨兵会选举出新的主服务器,并通知其他从服务器切换到新的主服务器下。

  • 服务的高可用

    Redis 服务的高可用可以通过以下两种方式来实现:Redis Cluster 和 Redis Sentinel。

    • Redis Cluster

      Redis Cluster 是 Redis 官方提供的分布式解决方案,能够将数据分散到多个节点上,实现数据的高可用和水平扩展。在 Redis Cluster 中,将数据分片到多个节点上,每个节点都会存储部分数据。当其中一台节点宕机时,其他节点可以继续提供服务,从而实现服务的高可用。

    • Redis Sentinel

      Redis Sentinel 是一种基于哨兵机制的 Redis 高可用解决方案,能够自动监控 Redis 服务器状态,并在主服务器宕机时自动切换到从服务器,从而实现服务的高可用。Redis Sentinel 可以配置多个哨兵,当主服务器宕机时,哨兵会协作选择新的主服务器,并将其他从服务器切换到新的主服务器下。

Redis 的高可用主要包括数据的高可用和服务的高可用。数据的高可用可以通过主从复制和哨兵机制来实现,而服务的高可用可以通过 Redis Cluster 和 Redis Sentinel 来实现。在生产环境中,我们需要根据实际情况选择适合的高可用方案,以确保 Redis 的服务可用性。

13.Redis的主从同步是如何实现的?

链接文章

从2.8版本开始,Redis使用psync命令完成主从数据同步,同步过程分为全量复制和部分复制。全量复制一般用于初次复制的场景,部分复制则用于处理因网络中断等原因造成数据丢失的场景。psync命令需要以下参数的支持:

  1. 复制偏移量:主节点处理写命令后,会把命令长度做累加记录,从节点在接收到写命令后,也会做累加记录;从节点会每秒钟上报一次自身的复制偏移量给主节点,而主节点则会保存从节点的复制偏移量。
  2. 积压缓冲区:保存在主节点上的一个固定长度的队列,默认大小为1M,当主节点有连接的从节点时被创建;主节点处理写命令时,不但会把命令发送给从节点,还会写入积压缓冲区;缓冲区是先进先出的队列,可以保存最近已复制的数据,用于部分复制和命令丢失的数据补救。
  3. 主节点运行ID:每个Redis节点启动后,都会动态分配一个40位的十六进制字符串作为运行ID;如果使用IP和端口的方式标识主节点,那么主节点重启变更了数据集(RDB/AOF),从节点再基于复制偏移量复制数据将是不安全的,因此当主节点的运行ID变化后,从节点将做全量复制。

14.Redis为什么存的快,内存断电数据怎么恢复?

Redis存的快是因为它的数据都存放在内存里,并且为了保证数据的安全性,Redis还提供了三种数据的持久化机制,即RDB持久化、AOF持久化、RDB-AOF混合持久化。若服务器断电,那么我们可以利用持久化文件,对数据进行恢复。理论上来说,AOF/RDB-AOF持久化可以将丢失数据的窗口控制在1S之内。

15.说一说Redis的缓存淘汰策略

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
策略描述版本
noeviction直接返回错误;
volatile-ttl从设置了过期时间的键中,选择将要过期的键,进行淘汰;
volatile-random从设置了过期时间的键中,随机选择键,进行淘汰;
volatile-lru从设置了过期时间的键中,使用LRU算法选择键,进行淘汰;
volatile-lfu从设置了过期时间的键中,使用LFU算法选择键,进行淘汰;4.0
allleys-random从所有的键中,随机选择键,进行淘汰;
allkeys-lru从所有的键中,使用LRU算法选择键,进行淘汰;
allkeys-lfu从所有的键中,使用LFU算法选择键,进行淘汰;4.0

16.请介绍一下Redis的过期策略

惰性删除
访问key的时候,Redis会先检查它的过期时间,如果过期就立刻删除key。

定期删除
Redis将设置了过期时间的key放到字典中,当已过期key的比例超过25%,就随机扫描20个key并且删除已过期的key;

17.缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决?

参考文章

  • 缓存穿透:总是访问一些数据库都没有的数据,导致缓存也一直没有,所以一直都会直接访问数据库。

    • 解决

      • 缓存无效 key

        如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

      • 布隆过滤器

        把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

  • 缓存击穿:某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力

    • 解决

      • 设置热点数据永不过期或者过期时间比较长。

      • 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。

      • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

  • 缓存雪崩:在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库(一大片)

    • 解决

      • 针对 Redis 服务不可用的情况:
        1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
        2. 限流,避免同时处理大量的请求。
      • 针对热点缓存失效的情况:
        1. 设置不同的失效时间比如随机设置缓存的失效时间。
        2. 缓存永不失效(不太推荐,实用性太差)。
        3. 设置二级缓存。

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

参考文章

四种同步策略:

  • 先更新缓存数据,再更新数据库数据;
  • 先更新数据库数据,再更新缓存数据;
  • 先删除缓存数据,再更新数据库数据;
  • 先更新数据库数据,再删除缓存数据;

删除与更新缓存数据的选择
虽然更新缓存可以在查询时更容易命中,但是更新缓存数据消耗性能比较大,所以建议选择删除。

缓存和数据库谁先操作
先更新数据库、再删除缓存会更好。如果第二步出现失败的情况,则可以采用重试机制解决问题。(如果第二次删除依然失败,则可以增加重试的次数,但是这个次数要有限制,当超出一定的次数时,要采取报错、记日志、发邮件提醒等措施。)

19.请介绍Redis集群的实现方案

  • Redis集群的分区方案:

    Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:

    1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
    2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
    3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

    Redis集群中数据的分片逻辑如下图:

    Redis集群中数据的分片逻辑

  • Redis集群的功能限制:

    Redis集群方案在扩展了Redis处理能力的同时,也带来了一些使用上的限制:

    1. key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。
    2. key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
    3. key作为数据分区的最小粒度,因此不能将一个大的键值对象(如hash、list等)映射到不同的节点。
    4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即DB0。
    5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
  • Redis集群的通信方案:

    在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。

    Redis集群采用P2P的Gossip(流言)协议,Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。通信的大致过程如下:

    1. 集群中每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口号上加10000;
    2. 每个节点再固定周期内通过特定规则选择几个节点发送ping消息;
    3. 接收ping消息的节点用pong消息作为响应。

    其中,Gossip协议的主要职责就是信息交换,而信息交换的载体就是节点彼此发送的Gossip消息,Gossip消息分为:meet消息、ping消息、pong消息、fail消息等。

    • meet消息:用于通知新节点加入,消息发送者通知接受者加入到当前集群。meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
    • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息封装了自身节点和一部分其他节点的状态数据。
    • pong消息:当接收到meet、ping消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内封装了自身状态数据,节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
    • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

    虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。因为Redis集群内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。所以,Redis集群的Gossip协议需要兼顾信息交换的实时性和成本的开销。

    • 集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。
    • 如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的超时选项设置时长的一半(cluster-node-timeout/2),那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
    • 每个消息主要的数据占用:slots槽数组(2KB)和整个集群1/10的状态数据(10个节点状态数据约1KB)。

20.说一说Redis集群的分片机制

参考阅读

Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:

  1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
  2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
  3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

Redis集群中数据的分片逻辑如下图:

Redis集群中数据的分片逻辑

21.说一说Redis集群的应用和优劣势

  • 优势

    Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。

  • 劣势

    Redis集群方案在扩展了Redis处理能力的同时,也带来了一些使用上的限制:

    1. key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。
    2. key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
    3. key作为数据分区的最小粒度,因此不能将一个大的键值对象(如hash、list等)映射到不同的节点。
    4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即DB0。
    5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

22.说一说hash类型底层的数据结构

  • 标准回答:

    Redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable(字典)。当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,Redis会使用ziplist作为哈希的内部实现。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而 hashtable的读写时间复杂度为O(1)。

哈希对象有两种编码方案,当同时满足以下条件时,哈希对象采用ziplist编码,否则采用hashtable编码:

  • 哈希对象保存的键值对数量小于512个;
  • 哈希对象保存的所有键值对中的键和值,其字符串长度都小于64字节。

其中,ziplist编码采用压缩列表作为底层实现,而hashtable编码采用字典作为底层实现。

  • 压缩列表

    压缩列表是Redis为了节约内存而开发的,它是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。一个压缩列表的重要保存部分包括—zlbytes、zltail、zllen、entryX、zlend,压缩列表的结构如下图所示:

    压缩列表的结构

    其类型长度以及用途如下表所示:

    属性类型长度说明
    zlbytesuint32_t4字节压缩列表占用的内存字节数;
    zltailuint32_t4字节压缩列表表尾节点距离列表起始地址的偏移量(单位字节);
    zllenuint16_t2字节压缩列表包含的节点数量,等于UINT16_MAX时,需遍历列表计算真实数量;
    entryX列表节点不固定压缩列表包含的节点,节点的长度由节点所保存的内容决定;
    zlenduint8_t1字节压缩列表的结尾标识,是一个固定值0xFF;

    压缩列表的节点如下图所示:

    压缩列表的节点

    previous_entry_length(pel)属性以字节为单位,记录当前节点的前一节点的长度,其自身占据1字节或5字节:

    1. 如果前一节点的长度小于254字节,则“pel”属性的长度为1字节,前一节点的长度就保存在这一个字节内;
    2. 如果前一节点的长度达到254字节,则“pel”属性的长度为5字节,其中第一个字节被设置为0xFE,之后的四个字节用来保存前一节点的长度;

    基于“pel”属性,程序便可以通过指针运算,根据当前节点的起始地址计算出前一节点的起始地址,从而实现从表尾向表头的遍历操作。

    content属性负责保存节点的值(字节数组或整数),其类型和长度则由encoding属性决定。

  • 字典

    字典(dict)又称为散列表,是一种用来存储键值对的数据结构。C语言没有内置这种数据结构,所以Redis构建了自己的字典实现。Redis字典的实现主要涉及三个结构体:字典、哈希表、哈希表节点。其中,每个哈希表节点保存一个键值对,每个哈希表由多个哈希表节点构成,而字典则是对哈希表的进一步封装。

    字典:三个结构体:字典、哈希表、哈希表节点之间的关系图:

    字典、哈希表、哈希表节点之间的关系图

    其中,dict代表字典dictht代表哈希表dictEntry代表哈希表节点。可以看出,dictEntry是一个数组,这很好理解,因为一个哈希表里要包含多个哈希表节点。而dict里包含2个dictht,多出的哈希表用于REHASH。

    当哈希表保存的键值对数量过多或过少时,需要对哈希表的大小进行扩展或收缩操作,在Redis中,扩展和收缩哈希表是通过REHASH实现的,执行REHASH的大致步骤如下:

    1.为字典的ht[1]哈希表分配内存空间

    ​ 如果执行的是扩展操作,则ht[1]的大小为第1个大于等于ht[0].used*2的2n。如果执行的是收缩操作,则ht[1]的大小为第1个大于等于ht[0].used的2n。

    2.将存储在ht[0]中的数据迁移到ht[1]上

    ​ 重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。

    3.将字典的ht[1]哈希表晋升为默认哈希表

    ​ 迁移完成后,清空ht[0],再交换ht[0]和ht[1]的值,为下一次REHASH做准备。

    当满足以下任何一个条件时,程序会自动开始对哈希表执行扩展操作:

    1. 服务器目前没有执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于1;
    2. 服务器目前正在执行bgsave或bgrewriteof命令,并且哈希表的负载因子大于等于5。

    为了避免REHASH对服务器性能造成影响,REHASH操作不是一次性地完成的,而是分多次、渐进式地完成的。渐进式REHASH的详细过程如下:

    • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表;

    • 在字典中的索引计数器rehashidx设置为0,表示REHASH操作正式开始;

    • 在REHASH期间,每次对字典执行添加、删除、修改、查找操作时,程序除了执行指定的操作外,还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中,再将rehashidx的值加1;

    • 随着字典不断被访问,最终在某个时刻,ht[0]上的所有键值对都被迁移到ht[1]上,此时程序将rehashidx属性值设置为-1,标识REHASH操作完成。

    REHSH期间,字典同时持有两个哈希表,此时的访问将按照如下原则处理:

    • 新添加的键值对,一律被保存到ht[1]中;

    • 删除、修改、查找等其他操作,会在两个哈希表上进行,即程序先尝试去ht[0]中访问要操作的数据,若不存在则到ht[1]中访问,再对访问到的数据做相应的处理。

23.介绍一下zset类型底层的数据结构

有序集合对象有2种编码方案,当同时满足以下条件时,集合对象采用ziplist编码,否则采用skiplist编码:

  • 有序集合保存的元素数量不超过128个;
  • 有序集合保存的所有元素的成员长度都小于64字节。

其中,``ziplist编码的有序集合采用压缩列表作为底层实现,skiplist编码的有序集合采用zset结构作为底层实现。`

其中,zset是一个复合结构,它的内部采用字典和跳跃表来实现,其源码如下。其中,dict保存了从成员到分支的映射关系,zsl则按分值由小到大保存了所有的集合元素。这样,当按照成员来访问有序集合时可以直接从dict中取值,当按照分值的范围访问有序集合时可以直接从zsl中取值,采用了空间换时间的策略以提高访问效率。

typedef struct zset {     
    dict *dict;  // 字典,保存了从成员到分值的映射关系;     
    zskiplist *zsl; // 跳跃表,按分值由小到大保存所有集合元素; 
} zset;

综上,zset对象的底层数据结构包括:压缩列表、字典、跳跃表。

压缩列表、字典参考hash结构解析

  • 跳跃表:skipList扩展阅读

    跳跃表的查找复杂度为平均O(logN),最坏O(N),效率堪比红黑树,却远比红黑树实现简单。跳跃表是在链表的基础上,通过增加索引来提高查找效率的。

    有序链表插入、删除的复杂度为O(1),而查找的复杂度为O(N)。例:若要查找值为60的元素,需要从第1个元素依次向后比较,共需比较6次才行,如下图:

    链表查找例

    跳跃表是从有序链表中选取部分节点,组成一个新链表,并以此作为原始链表的一级索引。再从一级索引中选取部分节点,组成一个新链表,并以此作为原始链表的二级索引。以此类推,可以有多级索引,如下图:

    跳跃表

    跳跃表在查找时,优先从高层开始查找,若next节点值大于目标值,或next指针指向NULL,则从当前节点下降一层继续向后查找,这样便可以提高查找的效率了。

    跳跃表的实现主要涉及2个结构体:zskiplist、zskiplistNode,它们的关系如下图所示:

    zskiplist、zskiplistNode关系图

    其中,蓝色的表格代表zskiplist,红色的表格代表zskiplistNode。zskiplist有指向头尾节点的指针,以及列表的长度,列表中最高的层级。zskiplistNode的头节点是空的,它不存储任何真实的数据,它拥有最高的层级,但这个层级不记录在zskiplist之内。

24.如何利用Redis实现分布式Session?

参考

第一是创建令牌的程序,就是在用户初次访问服务器时,给它创建一个唯一的身份标识,并且使用cookie封装这个标识再发送给客户端。那么当客户端下次再访问服务器时,就会自动携带这个身份标识了,这和SESSIONID的道理是一样的,只是改由我们自己来实现了。另外,在返回令牌之前,我们需要将它存储起来,以便于后续的验证。而这个令牌是不能保存在服务器本地的,因为其他服务器无法访问它。因此,我们可以将其存储在服务器之外的一个地方,那么Redis便是一个理想的场所。

第二是验证令牌的程序,就是在用户再次访问服务器时,我们获取到了它之前的身份标识,那么我们就要验证一下这个标识是否存在了。验证的过程很简单,我们从Redis中尝试获取一下就可以知道结果。

分布式session

25.如何利用Redis实现一个分布式锁?

分布式锁拓展

  • 何时需要分布式锁?

    在分布式的环境下,当多个server并发修改同一个资源时,为了避免竞争就需要使用分布式锁。那为什么不能使用Java自带的锁呢?因为Java中的锁是面向多线程设计的,它只局限于当前的JRE环境。而多个server实际上是多进程,是不同的JRE环境,所以Java自带的锁机制在这个场景下是无效的。

  • 如何实现分布式锁

    • setnx命令

      setnx实现加锁:

      第一版本
      这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败

      setnx key value
      

      第二版本
      给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。

      setnx key value
      expire key seconds
      

      第三版本
      通过“set…nx…”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。

      set key value nx ex seconds
      

      第四版本
      上面虽然已经解决了死锁的问题,但是还没有解决下面这个问题:

      进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,它此时释放的可能是其他线程的锁。

      在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。

      这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。

      # 加锁
      set key random-value nx ex seconds
      # 解锁
      if redis.call("get",KEYS[1]) == ARGV[1] then
      return redis.call("del",KEYS[1])
      else
      return 0
      end
      
    • 基于RedLock算法的分布式锁

      上述分布式锁的实现方案,是建立在单个主节点之上的。它的潜在问题如下图所示,如果进程A在主节点上加锁成功,然后这个主节点宕机了,则从节点将会晋升为主节点。若此时进程B在新的主节点上加锁成果,之后原主节点重启,成为了从节点,系统中将同时出现两把锁,这是违背锁的唯一性原则的。

      RedLock适用场景

      总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的。若要保证分布式锁的高可用,则可以采用多个节点的实现方案。这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案。该算法基于多个Redis节点,它的基本逻辑如下:

      • 这些节点相互独立,不存在主从复制或者集群协调机制;
      • 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;
      • 解锁:向所有的实例发送DEL命令,进行解锁;

      RedLock算法的示意图如下,我们可以自己实现该算法,也可以直接使用Redisson框架。

      RedLock实现加锁示例

26.说一说你对布隆过滤器的理解

阅读补充

布隆过滤器可以用很低的代价,估算出数据是否真实存在。例如:给用户推荐新闻时,要去掉重复的新闻,就可以利用布隆过滤器,判断该新闻是否已经推荐过。

布隆过滤器的核心包括两部分:

  1. 一个大型的位数组;
  2. 若干个不一样的哈希函数,每个哈希函数都能将哈希值算的比较均匀。

布隆过滤器的工作原理:

  1. 添加key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置,并将位数组中这个位置的值设置为1。
  2. 询问key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置。然后对比这些哈希函数在位数组中对应位置的数值:
    • 如果这几个位置中,有一个位置的值是0,就说明这个布隆过滤器中,不存在这个key。
    • 如果这几个位置中,所有位置的值都是1,就说明这个布隆过滤器中,极有可能存在这个key。之所以不是百分之百确定,是因为也可能是其他的key运算导致该位置为1。

27.多台Redis抗高并发访问该怎么设计?

Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。

(可以参考P19Redis集群实现方案)

28.如果并发量超过30万,怎么设计Redis架构?

Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。

(可以参考P19Redis集群实现方案)

抗住双十一亿级高并发架构,是如何演进的?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是哪托

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值