Redis

1. Redis 和 Memcached 有什么区别?

共同点

  • 都是基于内存的数据库,一般都用来当做缓存使用。

  • 都有过期策略。

  • 两者的性能都非常高。

不同点

  • Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;

  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;

  • Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;

  • Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;

2. 为什么用 Redis 作为 MySQL 的缓存?

  • Redis 具备高性能

  • Redis 具备高并发

3. Redis 是单线程吗?

Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;

  • Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大 key。

关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列

  • BIO_CLOSE_FILE

  • BIO_AOF_FSYNC

  • BIO_LAZY_FREE

4. Redis 采用单线程为什么还这么快?

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU。

  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

5. Redis 6.0 之前为什么使用单线程?

CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络 I/O 的限制

使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

6. Redis 6.0 之后为什么引入了多线程?

随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。

Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理

Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。

Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程)

  • Redis-server : Redis 的主线程,主要负责执行命令;

  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF 刷盘任务、释放内存任务;

  • io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。

7. Redis 如何实现数据不丢失?

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;

  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;

  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;

8. AOF 日志是如何实现的?

Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。

9. AOF 为什么先执行命令,再把数据写入日志呢?

  • 避免额外的检查开销:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。

  • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

当然,这样做也会带来风险:

  • 数据可能会丢失: 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。

  • 可能阻塞其他操作: 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。

10. AOF 写回策略有几种?

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;

  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;

  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

11. AOF 日志过大,会触发什么机制?

AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

在使用重写机制后,就会读取 key 最新的 value

12. 重写 AOF 日志的过程是怎样的?

重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的

  • 避免堵塞主进程

  • 发生写时复制,复用加锁保证数据安全

但是重写过程中,主进程依然可以正常处理命令,为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;

  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

13. RDB 快照是如何实现的呢?

RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据

在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以

14. RDB 做快照时会阻塞线程吗?

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;

  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

15. RDB 在执行快照的时候,数据能修改吗?

如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。

16. 为什么会有混合持久化?

混合持久化优点:

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

混合持久化缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;

  • 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

17. Redis 如何实现服务高可用?

  • 主从模式

  • 哨兵模式

  • 切片集群模式

18. 集群脑裂导致数据丢失怎么办?

由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方案

  • min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。

  • min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。

19. Redis 使用的过期删除策略是什么?

Redis 使用的过期删除策略是「惰性删除 + 定期删除」这两种策略配和使用。

20. 什么是惰性删除策略?

不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

惰性删除策略的优点:

  • 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。

惰性删除策略的缺点:

  • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。

21. 什么是定期删除策略?

每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key。

定期删除策略的优点:

  • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。

定期删除策略的缺点:

  • 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。

22. Redis 持久化时,对过期键会如何处理的?

RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。

  • RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。

  • RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:

    • 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;

    • 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。

  • AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。

  • AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。

23. Redis 主从模式中,对过期键会如何处理?

当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。

从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

24. Redis 内存满了,会发生什么?

在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。

  • 在 64 位操作系统中,maxmemory 的默认值是 0

  • 在 32 位操作系统中,maxmemory 的默认值是 3G,因为 32 位的机器最大只支持 4GB 的内存

25. 如何避免缓存雪崩?

大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃

  • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。

  • 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

  • 双 key 策略:我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。

26. 如何避免缓存击穿?

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮

  • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

27. 如何避免缓存穿透?

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

  • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

28. 如何设计一个缓存策略,可以动态缓存热点数据呢?

通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。

  • 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;

  • 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;

  • 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。

29. Cache Aside(旁路缓存)策略

写策略的步骤:

  • 先更新数据库中的数据,再删除缓存中的数据。

读策略的步骤:

  • 如果读取的数据命中了缓存,则直接返回数据;

  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

Cache Aside 策略适合读多写少的场景,不适合写多的场景

30. Redis 如何实现延迟队列?

使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

31. Redis 的大 key

一般而言,下面这两种情况被称为大 key:

  • String 类型的值大于 10 KB;

  • Hash、List、Set、ZSet 类型的元素的个数超过 5000 个;

大 key 会带来以下四种影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。

  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。

  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。

  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

32. 如何删除大 key?

  1. 分批次删除

  2. 异步删除

33. Redis 管道有什么用?

使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。

但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。

要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。

34. Redis 事务支持回滚吗?

Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果

35. 为什么 Redis 不支持事务回滚?

  • 他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;

  • 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。

36. 如何用 Redis 实现分布式锁的?

Redis 的 SET 命令有个 NX 参数可以实现「key 不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;

  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

37. 基于 Redis 实现分布式锁有什么优缺点?

基于 Redis 实现分布式锁的优点:

  • 性能高效(这是选择缓存实现分布式锁最核心的出发点)。

  • 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。

  • 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。

基于 Redis 实现分布式锁的缺点:

  • 超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。

    • 那么如何合理设置超时时间呢? 我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。

  • Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

38. Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。

39. Redis 内存淘汰策略

针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。

在设置了过期时间的数据中进行淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值;

  • volatile-ttl:优先淘汰更早过期的键值。

  • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;

  • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值;

  • allkeys-lru:淘汰整个键值中最久未使用的键值;

  • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

40. Redis 是如何实现 LRU 算法的?

Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

Redis 实现的 LRU 算法的优点:

  • 不用为所有的数据维护一个大链表,节省了空间占用;

  • 不用在每次数据访问时都移动链表项,提升了缓存的性能;

但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

41. Redis 是如何实现 LFU 算法的?

在 LFU 算法中,Redis 对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。

  • ldt 是用来记录 key 的访问时间戳;

  • logc 是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的 logc 初始值为 5。

注意,logc 并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的。

对 logc 做完衰减操作后,就开始对 logc 进行增加操作,增加操作并不是单纯的 + 1,而是根据概率增加,如果 logc 越大的 key,它的 logc 就越难再增加。

所以,Redis 在访问 key 时,对于 logc 是这样变化的:

  • 先按照上次访问距离当前的时长,来对 logc 进行衰减;

  • 然后,再按照一定概率增加 logc 的值

42. String

内部实现

String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。

  • SDS 获取字符串长度的时间复杂度是 O(1)。

  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。

应用场景

  • 缓存对象

  • 常规计数

  • 分布式锁(NX)

  • 共享 Session 信息

43. List

内部实现

List 类型的底层数据结构是由双向链表或压缩列表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;

  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

应用场景

消息队列

  1. 如何满足消息保序需求?

List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。

  1. 如何处理重复的消息?

消费者要实现重复消息的判断,需要 2 个方面的要求:

  • 每个消息都有一个全局的 ID。

  • 消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。

但是 List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一 ID,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。

  1. 如何保证消息可靠性?

当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。

为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。

  • 消息保序:使用 LPUSH + RPOP;

  • 阻塞读取:使用 BRPOP;

  • 重复消息处理:生产者自行实现全局唯一 ID;

  • 消息的可靠性:使用 BRPOPLPUSH

44. List 作为消息队列有什么缺陷?

List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。

45. Hash

内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;

  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

应用场景

  • 缓存对象

  • 购物车

46. Set

Set 类型和 List 类型的区别如下:

  • List 可以存储重复元素,Set 只能存储非重复元素;

  • List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。

内部实现

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries 配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;

  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

应用场景

  • 点赞

  • 共同关注

  • 抽奖活动

46. Zset

有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

内部实现

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;

  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

ZSet 使用跳表的主要原因是为了实现有序集合的快速访问和范围查询。跳表通过层级结构和索引节点,提供了快速查找和范围查询的能力,并且具有较小的空间占用。这使得 ZSet 能够高效地处理有序集合的各种操作。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

应用场景

  • 排行榜

  • 电话、姓名排序

47. BitMap

内部实现

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

应用场景

  • 签到统计

  • 判断用户登录态

  • 连续签到用户总数

47. HyperLogLog

HyperLogLog 提供不精确的去重计数,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数

应用场景

  • 百万级网页 UV 计数

48. GEO

内部实现

GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

应用场景

  • 滴滴打车

48. Stream

支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

  • 消息保序:XADD/XREAD

  • 阻塞读取:XREAD block

  • 重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;

  • 消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;

  • 支持消费组形式消费数据

49. Redis 基于 Stream 消息队列与专业的消息队列有哪些差距?

  • Redis 本身可能会丢数据;

  • 面对消息挤压,内存资源会紧张;

50. AOF 总结

AOF 方法,这个方法是每执行一条写操作命令,就将该命令以追加的方式写入到 AOF 文件,然后在恢复时,以逐一执行命令的方式来进行数据恢复。

Redis 提供了三种将 AOF 日志写回硬盘的策略,分别是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。

随着执行的命令越多,AOF 文件的体积自然也会越来越大,为了避免日志文件过大, Redis 提供了 AOF 重写机制,它会直接扫描数据中所有的键值对数据,然后为每一个键值对生成一条写操作命令,接着将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。

用 AOF 日志的方式来恢复数据其实是很慢的,因为 Redis 执行命令由单线程负责的,而 AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个「重放」的过程就会很慢了。

51. 大 Key 总结

当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。

AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;

  • 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。

大 key 除了会影响持久化之外,还会有以下的影响。

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。

  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。

  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。

  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

如何避免大 Key 呢?

最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。

52. Redis 主从复制

主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制。

主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。

第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。

如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。

如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。

53. 怎么判断 Redis 某个节点是否正常工作?

Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:

  • Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数 repl-ping-slave-period 控制发送频率。

  • Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:

    • 实时监测主从节点网络状态;

    • 上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。

54. 主从复制架构中,过期 key 如何处理?

主节点处理了一个 key 或者通过淘汰算法淘汰了一个 key,这个时间主节点模拟一条 del 命令发送给从节点,从节点收到该命令后,就进行删除 key 的操作。

55. Redis 是同步复制还是异步复制?

Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。

56. 主从复制中两个 Buffer(replication buffer 、repl backlog buffer) 有什么区别?

replication buffer 、repl backlog buffer 区别如下:

  • 出现的阶段不一样:

    • repl backlog buffer 是在增量复制阶段出现,一个主节点只分配一个 repl backlog buffer;

    • replication buffer 是在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点,分配一个 replication buffer;

  • 这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:

    • 当 repl backlog buffer 满了,因为是环形结构,会直接覆盖起始位置数据;

    • 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制。

57. 为什么会出现主从数据不一致?

之所以会出现主从数据不一致的现象,是因为主从节点间的命令复制是异步进行的,所以无法实现强一致性保证(主从数据时时刻刻保持一致)。

具体来说,在主从节点命令传播阶段,主节点收到新的写命令后,会发送给从节点。但是,主节点并不会等到从节点实际执行完命令后,再把结果返回给客户端,而是主节点自己在本地执行完命令后,就会向客户端返回结果了。如果从节点还没有执行主节点同步过来的命令,主从节点间的数据就不一致了。

58. 如何如何应对主从数据不一致?

第一种方法,尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房。

第二种方法,可以开发一个外部程序来监控主从节点间的复制进度。具体做法:

  • Redis 的 INFO replication 命令可以查看主节点接收写命令的进度信息(master_repl_offset)和从节点复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用 INFO replication 命令查到主、从节点的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从节点和主节点间的复制进度差值了。

  • 如果某个从节点的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从节点都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。

59. 主从切换如何减少数据丢失?

异步复制同步丢失

Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接收任何请求。

假设将 min-slaves-max-lag 配置为 10s 后,根据目前 master->slave 的复制速度,如果数据同步完成所需要时间超过 10s,就会认为 master 未来宕机后损失的数据会很多,master 就拒绝写入新请求。这样就能将 master 和 slave 数据差控制在 10s 内,即使 master 宕机也只是这未复制的 10s 数据。

那么对于客户端,当客户端发现 master 不可写后,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间(等 master 恢复正常)后重新写入 master 来保证数据不丢失,也可以将数据写入 kafka 消息队列,等 master 恢复正常,再隔一段时间去消费 kafka 中的数据,让将数据重新写入 master 。

集群产生脑裂数据丢失

当主节点发现「从节点下线的数量太多」,或者「网络延迟太大」的时候,那么主节点会禁止写操作,直接把错误返回给客户端。

在 Redis 的配置文件中有两个参数我们可以设置:

  • min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。

  • min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果主从同步的延迟超过 x 秒,主节点会禁止写数据。

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主节点连接的从节点中至少有 N 个从节点,「并且」主节点进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主节点就不会再接收客户端的写请求了。

60. 主从如何做到故障自动切换?

主节点挂了 ,从节点是无法自动升级为主节点的,这个过程需要人工处理,在此期间 Redis 无法对外提供写操作。

此时,Redis 哨兵机制就登场了,哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。

61. 为什么要有哨兵

Redis 在 2.8 版本以后提供的哨兵(Sentinel)机制,它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

哨兵一般是以集群的方式部署,至少需要 3 个哨兵节点,哨兵集群主要负责三件事情:监控、选主、通知。

哨兵节点通过 Redis 的发布者/订阅者机制,哨兵之间可以相互感知,相互连接,然后组成哨兵集群,同时哨兵又通过 INFO 命令,在主节点里获得了所有从节点连接信息,于是就能和从节点建立连接,并进行监控了。

  1. 第一轮投票:判断主节点下线

当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。

当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。

  1. 第二轮投票:选出哨兵 leader

某个哨兵判定主节点客观下线后,该哨兵就会发起投票,告诉其他哨兵,它想成为 leader,想成为 leader 的哨兵节点,要满足两个条件:

  • 第一,拿到半数以上的赞成票;

  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

  1. 由哨兵 leader 进行主从故障转移

选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤:

  • 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点,选择的规则:

    • 过滤掉已经离线的从节点;

    • 过滤掉历史网络连接状态不好的从节点;

    • 将剩下的从节点,进行三轮考察:优先级、复制进度、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。

  • 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;

  • 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;

  • 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;

62. 数据库和缓存如何保证一致性?

针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。

63. 如何保证两个操作都能执行成功?

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

64. Redis 使用的协议

Redis 底层使用的通信协议是 RESP(Redis Serialization Protocol 的缩写),RESP 协议可以序列化多种类型,比如 Simple Strings(简单字符串),Errors(错误类型),Integers(整形),Bulk Strings(批量串)和 Arrays(数组),但此协议只适用于 Redis 客户端 - 服务端 之间的通信, Redis 集群中节点间通信使用的是 Gossip 协议。

65. 大量服务向 Redis 拿缓存的解决办法

如果服务大量向 Redis 拿缓存,可能会遇到缓存击穿的问题。缓存击穿是指一个不存在于缓存中的数据,每次请求都会直接访问数据库,导致数据库压力过大,甚至宕机。解决这个问题的方法有很多,其中一种是使用互斥锁(mutex)。当一个请求发现缓存中没有数据时,它会尝试获取一个互斥锁。如果获取成功,就从数据库中获取数据并写入缓存;否则,就等待一段时间后重试。这样可以避免大量请求同时访问数据库。

另外,还可以使用 Redis 的一些高级特性来解决这个问题。例如,使用 Redis 的 SETNX 命令来实现分布式锁。

66. RDB 的 save 操作 fork 了一个 redis 子进程,那么这个时候内存会变为两倍吗?为什么?

在子进程执行 RDB 保存操作期间,它会复制父进程的内存数据。这意味着在保存操作期间,Redis 的内存会变为两倍。原因是在保存操作期间,子进程需要拷贝父进程的内存数据,这样确保了保存操作的原子性和一致性。因此,在保存操作完成之前,Redis 的内存会占用两倍的空间。

67. Redis 集群有多少个槽

Redis 集群默认情况下有 16384 个槽。这是因为 Redis 使用哈希槽(hash slots)来分片数据,并将数据分布在多个节点上。每个槽可以保存一个键值对,因此 Redis 集群最多可以保存 16384 个键值对。

68. Redis 如何解决 hash 结构的冲突

Redis 在哈希结构中使用了链地址法来解决冲突。具体实现中,Redis 使用了链表和跳表(Skip List)这两种数据结构来存储冲突的键。当链表过长时,Redis 会将链表转换为跳表,以提高查找效率。

69. Redis 保证 incr 命令原子性的原理是什么?

因为 Redis 是单线程的。

70. Redis PubSub

在 Redis 中,Pub/Sub 使用了两种主要的数据结构来实现消息队列的功能:

  1. 哈希表 (Hash) :Redis 使用哈希表来存储订阅关系。每个频道都被视为一个键 (key) ,对应的值 (value) 是一个链表,链表中存储了订阅该频道的所有客户端。

  2. 链表 (List) :当一个消息被发布到某个频道时,Redis 将通过哈希表找到对应的订阅者链表,并将消息添加到链表的尾部。

通过这两种数据结构的结合,Redis 实现了简单的消息队列功能。发布者将消息发布到指定的频道,而订阅者可以订阅感兴趣的频道并接收相应的消息。

71. 用 Redis ZSet 实现排行榜先用分数再用时间排序怎么实现?

如果是用 41bit 表示时间戳,22bit 表示积分的话,那么 score 的组成就是这样的: 0(最高位不用)|0000000 00000000 0000000(22bit 表示积分)|0 00000000 00000000 00000000 00000000 00000000(41bit 表示时间戳)

因为排序首先按积分排再按时间排,所以积分在高位,时间戳在低位,这样不管时间戳的值是多少,积分越大,64bit 表示的数值就越大。

但我们需要的是按时间升序排,也就是最先达到 xx 积分的用户排在最前面,所以我们不能单纯的使用 41bit 存储时间戳,而应该是存储一个随时间流逝而变小的数值。

由于排行榜都会有一个周期,如周榜是一周,月榜是一个月,所以我们使用 41bit 存储的是一个周期的结束时间 yyy-MM-dd 23:59:59 对应的时间戳与用户积分更新时间的时间戳的差值,这个值会随着时间的推移而变小,而且不会出现负数的情况,刚好能够达到目的。

72. 判断 Key 是否存在

要判断 Redis 中的 Key 是否存在,可以使用 EXISTS 命令。EXISTS 命令用于检查给定的 Key 是否存在于 Redis 中。它的基本语法如下:

EXISTS KEY_NAME

如果 Key 存在,命令返回整数 1;如果 Key 不存在,命令返回整数 0。

73. Redis 的扩容方式

  1. 水平扩容 (分区) :通过增加更多的 Redis 服务器来分摊数据和负载。数据户端实现将被分散 (分片) 存储到多个 Redis 实例中,每个实例只存储整个数据的一部分。这种方式需要在客分片逻辑,Redis Cluster 提供了自动分片和高可用性的解决方案。

  2. 垂直扩容:通过增加单个 Redis 服务器的硬件资源 (如 RAM、CPU 等) 来提升其性能和容量。这种方式的缺点是硬件资源是有上限的,无法无限扩容。

  3. Redis Cluster:Redis Cluster 是一种水平扩展的解决方案,可以将数据自动分片并分布到多个 Redis 节点上。Redis Cluster 提供了高可用性和故障转移机制,能够在节点故障时自动进行主从切换,保证数据的可用性。使用 Redis Cluster 可以实现数据的自动扩容和负载均衡。

74. Redis 如何实现乐观锁

Redis 并没有内置乐观锁的功能,但我们可以通过 WATCH 命令配合 MULTIEXEC 来模拟乐观锁的行为。核心的想法是:我们“监视”一个变量,然后在执行事务前检查监视的变量是否已经发生改变。如果变量发生了改变,我们就放弃执行事务。

75. Redis 渐进式 Rehash

重新哈希/扩容操作会占用大量的 CPU 和内存资源,如果一次性完成该操作,可能会导致 Redis 在一段时间内阻塞,无法对外提供服务。为了避免这种情况,Redis 引入了一种称为 " 渐进式 rehash" 的机制。

在渐进式 rehash 中,Redis 会同时维持新旧两个哈希表,新的哈希表是旧的哈希表大小的两倍。Redis 在对哈希表执行任何操作时,会顺带将旧哈希表中的一部分键值对移动到新哈希表中。具体移动的数量由服务器的配置决定。

假设服务器配置了每次把 100 个键值对从旧哈希表移动到新哈希表,Redis 每次执行一个哈希表操作,都会顺带完成这个移动操作;当旧哈希表的所有键值对都移动到新哈希表后,Redis 就释放旧哈希表的内存空间,渐进式 rehash 操作完成。

这种渐进方式的好处是将高负载的操作分摊到了一个时间段内,避免了 Redis 在进行哈希表扩容的时候服务暂停,提高了 Redis 的高可用性。

Last updated