十三、Redis

86 阅读23分钟

Redis 是一个开源的使用 C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

Redis 面试题通常会涉及以下几个方面:

  1. Redis 的数据类型

Redis 支持的数据类型包括字符串、列表、集合、有序集合、哈希表。

  1. Redis 的事务控制

Redis 的事务可以一起执行多个命令,在事务执行过程中,不会中断并保证命令的原子性。

  1. Redis 的持久化机制

Redis 提供了 RDB 和 AOF 两种持久化机制,RDB 是定时将内存中的数据快照保存到磁盘,AOF 是保存 Redis 服务器所执行的所有写操作命令到文件。

  1. Redis 的主从复制和分布式

Redis 的主从复制可以实现数据的多点备份,分布式则可以将数据分散到不同的节点上以提高系统的扩展性。

  1. Redis 的性能测试和调优

可以使用 Redis 自带的性能测试工具进行基准性能测试,并根据测试结果进行调优。

  1. Redis 的内存管理和内存溢出处理

Redis 使用内存管理机制来优化内存的使用,并提供了内存溢出时的警告机制。

  1. Redis 的分区和分片

分区可以将数据分散到不同的 Redis 实例上,分片可以提高数据的读写性能。

  1. Redis 的高可用和故障转移 Redis Sentinel 和 Cluster 可以提供高可用性和故障转移机制。

1、Redis 为什么这么快?

Redis 内部做了非常多的性能优化,比较重要的主要有下面 3 点:

(1)Redis 基于内存,内存的访问速度是磁盘的上千倍;

(2)Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);

(3)Redis 内置了多种优化过后的数据结构实现,性能非常高。

2、Redis 除了做缓存,还能做什么?

(1)分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。

(2)限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。

(3)消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。

(4) 复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。

Redis是一个高性能的内存数据库,支持多种数据结构,包括字符串(string),哈希(hash),列表(list),集合(set),有序集合(sorted set),位图(bitmap)和计数器(hyperloglog)。这些数据结构在不同的应用场景中有不同的适用性。

字符串是最简单的数据结构,支持存储单个字符串值,可以用于简单的键值对存储,比如对于用户的 session 记录、对于限流的计数等场景。

哈希是一种将键值对组织在一起的数据结构,适用于存储复杂的数据结构,比如用户的基本信息,订单的详细信息等。

列表是一种链表数据结构,支持高效的元素插入和删除,可以用于存储消息队列,消息发布/订阅等场景。

集合是一种无序的数据结构,支持集合的交并差运算,可以用于存储关注的用户,关注的话题等。

有序集合是一种按照分数排序的集合,支持对于元素的排序,可以用于存储热门商品,热门

3、Redis 可以做消息队列么?

Redis 5.0 新增加的一个数据结构 Stream 可以用来做消息队列,Stream 支持:

(1)发布 / 订阅模式

(2)按照消费者组进行消费

(3)消息持久化( RDB 和 AOF)

不过,和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议是不使用 Redis 来做消息队列的,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。

4、如何基于 Redis 实现分布式锁?

一个最基本的分布式锁需要满足:

(1)互斥 :任意一个时刻,锁只能被一个线程持有;

(2)高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。

(3)可重入:一个节点获取了锁之后,还可以再次获取锁。

通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点。

5、如何基于 Redis 实现一个最简易的分布式锁?

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。 ​ 释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

为了误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
​
如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。
​
如何实现锁的优雅续期?
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
​
进程意外死亡,持有锁不释放,定时任务,检查锁,防止不释放
​
Redis 支持通过使用 Redlock 算法来实现锁的分布式实现。Redlock 算法是用于分布式锁的一种常用方法,它通过在多个 Redis 节点上同时执行获取锁和释放锁操作,从而保证锁的可靠性。
​
实现分布式锁的步骤如下:
​
为每个 Redis 节点分配一个独立的 UUID。
在所有的 Redis 节点上执行 SETNX 操作,该操作将 UUID 设置为锁的值。
如果至少有一个 Redis 节点成功地设置了 UUID,则说明锁已经被获取。
在释放锁时,通过使用 Lua 脚本来保证锁的安全释放,即只有在当前锁的值与之前设置的 UUID 相同时才可以释放锁。
通过使用 Redis 的分布式锁功能,可以保证在 Redis 集群环境下锁的可靠性。

6、如何实现可重入锁?

可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。 实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

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

为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

针对这个问题,Redis 之父 antirez 设计了 Redlock 算法 来解决。 Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。 Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。 实际项目中不建议使用 Redlock 算法,成本和收益不成正比。如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 Zookeeper 来做,只是性能会差一些。

8、缓存雪崩!!!

Redis挂掉了,请求全部走数据库。 对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。 缓存雪崩如果发生了,很可能就把我们的数据库搞垮,导致整个服务瘫痪!

如何解决缓存雪崩?

1、在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。

2、对于“Redis挂掉了,请求全部走数据库”这种情况,我们可以有以下的思路:

事发前:实现Redis的高可用(主从架构+Sentinel(哨兵) 或者Redis Cluster(集群)),尽量避免Redis挂掉这种情况发生。

事发中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)

事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

1.在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。

2.可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存

3.不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀

4.做二级缓存,或者双缓存策略。A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问A2,A1 缓存失效时间设置为短期,A2 设置为长期。

9、缓存穿透!!!

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

如何解决缓存穿透?

(1)对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃。

(2)将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

(3) 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

10、redis 的安全机制(你们公司 redis 的安全这方面怎么考虑的?)

漏洞介绍:redis 默认情况下,会绑定在bind0.0.0.0:6379,这样就会将redis的服务暴露到公网上,如果在没有开启认证的情况下,密码简单,可以导致任意用户在访问目标服务器的情况下,未授权就可访问 redis 以及读取 redis 的数据,攻击者就可以在未授权访问 redis 的情况下可以利用 redis 的相关方法,成功在 redis 服务器上写入公钥,进而可以直接使用私钥进行直接登录目标主机;

(1)禁止一些高危命令。修改 redis.conf 文件,用来禁止远程修改 DB 文件地址,比如 rename-command FLUSHALL "" 、rename-command CONFIG"" 、rename-command EVAL “” 等;

(2)以低权限运行 redis 服务。为 redis 服务创建单独的用户和根目录,并且配置禁止登录;

(3)为 redis 添加密码验证。修改 redis.conf 文件,添加 requirepass mypassword;

(4)禁止外网访问 redis。修改 redis.conf 文件,添加或修改 bind 127.0.0.1,使得 redis 服务只在当前主机使用;

(5)做 log 监控,及时发现攻击;

11、redis 的哨兵机制(redis2.6 以后出现的)哨兵机制:

监控:监控主数据库和从数据库是否正常运行;

提醒:当被监控的某个 redis 出现问题的时候,哨兵可以通过 API 向管理员或者其他应用程序发送通知;

自动故障迁移:主数据库出现故障时,可以自动将从数据库转化为主数据库,实现自动切换;

具体的配置步骤参考的网上的文档。要注意的是,如果 master 主服务器设置了密码,记得在哨兵的配置文件(sentinel.conf)里面配置访问密码

12、Redis 提供 6 种数据淘汰策略

volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)

allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

13、redis 有事务吗?

Redis 是有事务的,redis 中的事务是一组命令的集合,这组命令要么都执行,要不都不执行,保证一个事务中的命令依次执行而不被其他命令插入。redis 的事务是不支持回滚操作的。 redis 事务的实现,需要用到 MULTI(事务的开始)和 EXEC(事务的结束)命令

Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。

Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

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

常见的解决方案如下:

(1)双写:这是最常见的解决方案,它涉及在向缓存写入数据之前先向数据库写入相同的数据,并在缓存读取数据之前先从数据库读取相同的数据。这种方法可以保证数据库和缓存之间的一致性,但也增加了系统的复杂度。

(2)乐观锁:这种方法通过在缓存和数据库中存储版本号,并在写入操作时使用版本号来保证一致性。如果缓存中的版本号与数据库中的版本号不同,则说明数据已被更改,需要重新读取数据。

(3)悲观锁:这种方法通过在写入操作时对数据加锁来保证一致性。只有当写入操作完成并释放锁之后,缓存才能读取数据。这种方法可以保证数据的一致性,但也会降低系统的性能。

(4)异步更新:这种方法通过在写入数据库后在后台异步更新缓存来保证一致性。这种方法可以减少写入操作的延迟,但同时也带来了一定的不一致性

Cache Aside Pattern 是一种常用的缓存架构模式。这种模式的主要思想是将缓存与数据存储分离,并在缓存中存储数据的副本。 下面是 Cache Aside Pattern 的基本流程: 首先,应用程序试图从缓存中读取数据。 如果缓存命中(即缓存中存在请求的数据),应用程序直接从缓存读取数据并使用。 如果缓存未命中,应用程序从数据存储读取数据,并将数据存储在缓存中。 应用程序使用从数据存储读取的数据。 Cache Aside Pattern 的优点在于缓存和数据存储之间的分离,可以提高系统的性能和可扩展性,并使应用程序更加简单。但是,由于数据不一定是实时的,因此在使用缓存时需要考虑数据一致性问题。

15、RDB、AOF(Append-Only File)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

1、RDB是 Redis 默认采用的持久化方式

在 redis.conf 配置文件中默认有此下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。
Redis 提供了两个命令来生成 RDB 快照文件:
save : 主线程执行,会阻塞主线程;
bgsave : 子线程执行,不会阻塞主线程,默认选项。

2、AOF 文件的保存位置和 RDB 文件的位置相同

与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启: appendonly yes

3、AOF 持久化方式

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

4、AOF 日志是如何实现的?

关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。

5、为什么是在执行完命令之后记录日志呢?

避免额外的检查开销,AOF 记录日志不会对命令进行语法检查; 在命令执行完之后再记录,不会阻塞当前的命令执行。

这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过): 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失; 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。

16、如何选择 RDB 和 AOF?

RDB 比 AOF 优秀的地方 :

RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。

AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会必 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。

使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

AOF 比 RDB 优秀的地方 : RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。

AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。

RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。

AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

17、redis 的并发竞争问题是什么?如何解决这个问题?

Redis 是一个单线程模型的内存数据库,它不支持内置的并发控制,因此在多个客户端同时对 Redis 进行操作时,很容易出现并发竞争问题。 ​

常见的 Redis 并发竞争问题包括:
(1)资源争夺:多个客户端同时对相同的键进行写操作,从而造成数据不一致。
(2)超时问题:当客户端执行的操作耗时过长时,其他客户端的操作可能被阻塞,从而造成性能问题。
​
解决 Redis 并发竞争问题的常用方法包括:
(1)使用 Redis 事务:Redis 提供了事务机制,可以将多个命令封装在一个事务中,并保证事务中的命令要么全部执行,要么全部不执行。
(2)使用 Redis 锁:可以使用 Redis 提供的锁机制,对关键数据进行加锁,避免多个客户端同时对该数据进行修改。
(3)使用 Redis 管道:可以通过使用 Redis 管道技术,将多个命令合并成一个管道,从而提高 Redis 性能。
以上三种方法都可以有效地解决 Redis 并发竞争问题,但每种方法都有其优缺点,需要根据实际情况

18、了解 redis 事务的 CAS 方案吗?

CAS 方案是一种解决 Redis 事务竞争的技术,它的主要思想是通过对关键数据进行加锁,从而避免多个客户端同时对该数据进行修改。

在 Redis 中,可以使用 WATCH 命令实现 CAS 方案。具体操作流程如下:

(1)客户端向 Redis 发送 WATCH 命令,监视关键数据的变化。

(2)客户端向 Redis 发送事务命令,对关键数据进行修改。

(3)Redis 检查关键数据是否已经被修改,如果关键数据已经被修改,则 Redis 返回一个错误。

(4)如果关键数据没有被修改,则 Redis 将事务命令提交到数据库中。

通过使用 Redis 的 CAS 方案,可以有效地解决 Redis 事务竞争问题,从而提高 Redis 的数据一致性。

19、生产环境中的 redis 是怎么部署的?或者说redis的高可用?

一般来说,生产环境中的 Redis 部署方案包括以下几个方面:

(1)集群部署:通过将 Redis 节点分布在多个服务器上,从而实现 Redis 集群,提高 Redis 的读写性能和可用性。

(2)备份与恢复:通过定期对 Redis 数据进行备份,从而在数据丢失或损坏时进行恢复。

(3)数据持久化:通过使用 Redis 的 RDB 持久化或 AOF 持久化,从而保证 Redis 的数据不丢失。

(4)数据迁移:当 Redis 集群的规模扩大时,可以通过将 Redis 数据迁移到更多的服务器上,从而提高 Redis 的读写性能。

(5)监控与告警:通过实时监控 Redis 的性能和状态,从而在 Redis 出现异常时进行告警。

生产环境中的 Redis 部署需要结合业务场景进行设计,从而保证 Redis 的可用性和稳定性。