1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

Redis常见面试题总结(下)

1、词句勘误和调整;
2、标点符号勘误和调整。
This commit is contained in:
xuqi 2025-03-02 15:43:49 +08:00
parent fffb0f5100
commit 51b3caecec

View File

@ -28,7 +28,7 @@ Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将
### 如何使用 Redis 事务?
Redis 可以通过 **`MULTI``EXEC``DISCARD``WATCH`** 等命令来实现事务(Transaction)功能。
Redis 可以通过 **`MULTI``EXEC``DISCARD``WATCH`** 等命令来实现事务Transaction功能。
```bash
> MULTI
@ -47,8 +47,8 @@ QUEUED
这个过程是这样的:
1. 开始事务(`MULTI`
2. 命令入队(批量操作 Redis 的命令先进先出FIFO的顺序执行)
3. 执行事务(`EXEC`)
2. 命令入队(批量操作 Redis 的命令先进先出FIFO的顺序执行
3. 执行事务`EXEC`
你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。
@ -138,10 +138,10 @@ Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io
Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性:**1. 原子性****2. 隔离性****3. 持久性****4. 一致性**。
1. **原子性Atomicity** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2. **隔离性Isolation** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
3. **持久性Durability** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
4. **一致性Consistency** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
1. **原子性Atomicity**事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2. **隔离性Isolation**并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
3. **持久性Durability**一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
4. **一致性Consistency**:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
Redis 事务在运行错误的情况下除了执行过程中出现错误的命令外其他命令都能正常执行。并且Redis 事务是不支持回滚roll back操作的。因此Redis 事务其实是不满足原子性的。
@ -149,23 +149,23 @@ Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis
![Redis 为什么不支持回滚](https://oss.javaguide.cn/github/javaguide/database/redis/redis-rollback.png)
**相关 issue** :
**相关 issue**
- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452)。
- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)
- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)
### Redis 事务支持持久性吗?
Redis 不同于 Memcached 的很重要一点就是Redis 支持持久化,而且支持 3 种持久化方式:
Redis 不同于 Memcached 的很重要一点就是Redis 支持持久化,而且支持 3 种持久化方式
- 快照snapshottingRDB
- 只追加文件append-only file, AOF
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
- 快照snapshottingRDB
- 只追加文件append-only fileAOF
- RDB 和 AOF 的混合持久化Redis 4.0 新增)。
与 RDB 持久化相比AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式(`fsync` 策略),它们分别是:
```bash
appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件fsync完成后线程返回这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件
appendfsync no #让操作系统决定何时进行同步一般为30秒一次
```
@ -190,19 +190,19 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常
除了下面介绍的内容之外,再推荐两篇不错的文章:
- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ)
- [Redis 常见阻塞原因总结 - JavaGuide](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)
- [你的 Redis 真的变慢了吗?性能优化如何做 - 阿里开发者](https://mp.weixin.qq.com/s/nNEuYw0NlYGhuKKKKoWfcQ)
- [Redis 常见阻塞原因总结 - JavaGuide](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html)
### 使用批量操作减少网络传输
一个 Redis 命令的执行可以简化为以下 4 步:
1. 发送命令
2. 命令排队
3. 命令执行
4. 返回结果
1. 发送命令
2. 命令排队
3. 命令执行
4. 返回结果
其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip Time (RTT,往返时间)** ,也就是数据在网络上传输的时间。
其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip TimeRTT往返时间**,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
@ -212,12 +212,12 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常
Redis 中有一些原生支持批量操作的命令,比如:
- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)
- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)
- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)
- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)
- `SADD`(向指定集合添加一个或多个元素)
- ……
不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
@ -227,15 +227,15 @@ Redis 中有一些原生支持批量操作的命令,比如:
如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。
> Redis Cluster 并没有使用一致性哈希,采用的是 **哈希槽分区** ,每一个键值对都属于一个 **hash slot**(哈希槽) 。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公式找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
> Redis Cluster 并没有使用一致性哈希,采用的是 **哈希槽分区**,每一个键值对都属于一个 **hash slot(哈希槽)**。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公式找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
>
> 我在 [Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html) 这篇文章中详细介绍了 Redis Cluster 这部分的内容,感兴趣地可以看看。
#### pipeline
对于不支持批量操作的命令,我们可以利用 **pipeline流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
对于不支持批量操作的命令,我们可以利用 **pipeline流水线** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
`MGET``MSET`等原生批量操作命令一样pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
`MGET``MSET` 等原生批量操作命令一样pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:
@ -263,7 +263,7 @@ Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作
不过, Lua 脚本依然存在下面这些缺陷:
- 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。
- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。
### 大量 key 集中过期问题
@ -339,7 +339,7 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28
0 zsets with 0 members (00.00% of keys, avg size 0.00
```
从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
从这个命令的运行结果,我们可以看出:这个命令会扫描ScanRedis 中的所有 key,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
在线上执行该命令时,为了降低对 Redis 的影响,需要指定 `-i` 参数控制扫描的频率。`redis-cli -p 6379 --bigkeys -i 3` 表示扫描过程中每次扫描后休息的时间间隔为 3 秒。
@ -363,8 +363,8 @@ Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28
网上有现成的代码/工具可以直接拿来使用:
- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools)Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys) : Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
- [redis-rdb-tools](https://github.com/sripathikrishnan/redis-rdb-tools)Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
- [rdb_bigkeys](https://github.com/weiyanwei412/rdb_bigkeys)Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
**4、借助公有云的 Redis 分析服务。**
@ -497,10 +497,10 @@ hotkey 的常见处理以及优化办法如下(这些方法可以配合起来
我们知道一个 Redis 命令的执行可以简化为以下 4 步:
1. 发送命令
2. 命令排队
3. 命令执行
4. 返回结果
1. 发送命令
2. 命令排队
3. 命令执行
4. 返回结果
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
@ -527,13 +527,13 @@ Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n)
`redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。
当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than`阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后Redis 会把最早的执行命令依次舍弃。
当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than` 阈值的命令时,就会将该命令记录在慢查询日志slow log中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后Redis 会把最早的执行命令依次舍弃。
⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。
`slowlog-log-slower-than``slowlog-max-len`的默认配置如下(可以自行修改)
`slowlog-log-slower-than``slowlog-max-len` 的默认配置如下(可以自行修改)
```nginx
```properties
# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that a negative number disables the slow log, while
# a value of zero forces the logging of every command.
@ -555,7 +555,7 @@ CONFIG SET slowlog-max-len 128
获取慢查询日志的内容很简单,直接使用 `SLOWLOG GET` 命令即可。
```java
```bash
127.0.0.1:6379> SLOWLOG GET #慢日志查询
1) 1) (integer) 5
2) (integer) 1684326682
@ -593,7 +593,7 @@ OK
**相关问题**
1. 什么是内存碎片?为什么会有 Redis 内存碎片?
1. 什么是内存碎片?为什么会有 Redis 内存碎片?
2. 如何清理 Redis 内存碎片?
**参考答案**[Redis 内存碎片详解](https://javaguide.cn/database/redis/redis-memory-fragmentation.html)。
@ -616,7 +616,7 @@ OK
**1缓存无效 key**
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key会导致 Redis 中缓存大量无效的 key。很明显这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
另外,这里多说一嘴,一般情况下我们是这样设计 key 的:`表名:列名:主键名:主键值`
@ -655,7 +655,7 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器之后的缓存处理流程图如下
加入布隆过滤器之后的缓存处理流程图如下
![加入布隆过滤器之后的缓存处理流程图](https://oss.javaguide.cn/github/javaguide/database/redis/redis-cache-penetration-bloom-filter.png)
@ -707,15 +707,15 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
#### 有哪些解决办法?
**针对 Redis 服务不可用的情况**
**针对 Redis 服务不可用的情况**
1. **Redis 集群**:采用 Redis 集群避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,详细介绍可以参考:[Redis 集群详解(付费)](https://javaguide.cn/database/redis/redis-cluster.html)。
2. **多级缓存**:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
**针对大量缓存同时失效的情况**
**针对大量缓存同时失效的情况**
1. **设置随机失效时间**(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
2. **提前预热**(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
3. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
#### 缓存预热如何实现?
@ -739,8 +739,8 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说有两个解决方案:
1. **缓存失效时间变短(不推荐,治标不治本)**:我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
2. **增加缓存更新重试机制(常用)**:如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
1. **缓存失效时间变短**(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
2. **增加缓存更新重试机制**(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。虽然说多引入了一个消息队列,但其整体带来的收益还是要更高一些。
相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd)。
@ -753,10 +753,10 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
**Redis Sentinel**
1. 什么是 Sentinel 有什么用?
2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别
3. Sentinel 是如何实现故障转移的?
4. 为什么建议部署多个 sentinel 节点(哨兵集群)?
5. Sentinel 如何选择出新的 master选举机制?
5. Sentinel 如何选择出新的 master选举机制
6. 如何从 Sentinel 集群中选择出 Leader
7. Sentinel 可以防止脑裂吗?
@ -764,7 +764,7 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
1. 为什么需要 Redis Cluster解决了什么问题有什么优势
2. Redis Cluster 是如何分片的?
3. 为什么 Redis Cluster 的哈希槽是 16384 个?
3. 为什么 Redis Cluster 的哈希槽是 16384 个
4. 如何确定给定 key 的应该分布到哪个哈希槽中?
5. Redis Cluster 支持重新分配哈希槽吗?
6. Redis Cluster 扩容缩容期间可以提供服务吗?
@ -790,10 +790,10 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
- 《Redis 开发与运维》
- 《Redis 设计与实现》
- Redis Transactions : <https://redis.io/docs/manual/transactions/>
- Redis Transactions<https://redis.io/docs/manual/transactions/>
- What is Redis Pipeline<https://buildatscale.tech/what-is-redis-pipeline/>
- 一文详解 Redis 中 BigKey、HotKey 的发现与处理:<https://mp.weixin.qq.com/s/FPYE1B839_8Yk1-YSiW-1Q>
- Bigkey 问题的解决思路与方式探索:<https://mp.weixin.qq.com/s/Sej7D9TpdAobcCmdYdMIyA>
- Bigkey 问题的解决思路与方式探索<https://mp.weixin.qq.com/s/Sej7D9TpdAobcCmdYdMIyA>
- Redis 延迟问题全面排障指南:<https://mp.weixin.qq.com/s/mIc6a9mfEGdaNDD3MmfFsg>
<!-- @include: @article-footer.snippet.md -->