mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
Redis常见面试题总结(下)
1、词句勘误和调整; 2、标点符号勘误和调整。
This commit is contained in:
parent
fffb0f5100
commit
51b3caecec
@ -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,28 +149,28 @@ Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis
|
||||
|
||||

|
||||
|
||||
**相关 issue** :
|
||||
**相关 issue**:
|
||||
|
||||
- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) 。
|
||||
- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)
|
||||
- [issue#452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452)。
|
||||
- [Issue#491:关于 Redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491)。
|
||||
|
||||
### Redis 事务支持持久性吗?
|
||||
|
||||
Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:
|
||||
Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:
|
||||
|
||||
- 快照(snapshotting,RDB)
|
||||
- 只追加文件(append-only file, AOF)
|
||||
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
|
||||
- 快照(snapshotting,RDB);
|
||||
- 只追加文件(append-only file,AOF);
|
||||
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)。
|
||||
|
||||
与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( `fsync`策略),它们分别是:
|
||||
与 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秒一次
|
||||
```
|
||||
|
||||
AOF 持久化的`fsync`策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。
|
||||
AOF 持久化的 `fsync` 策略为 no、everysec 时都会存在数据丢失的情况。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。
|
||||
|
||||
因此,Redis 事务的持久性也是没办法保证的。
|
||||
|
||||
@ -180,7 +180,7 @@ Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常
|
||||
|
||||
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
|
||||
|
||||
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。**
|
||||
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,**严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。**
|
||||
|
||||
如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
|
||||
|
||||
@ -190,34 +190,34 @@ 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 Time(RTT,往返时间)**,也就是数据在网络上传输的时间。
|
||||
|
||||
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
|
||||
|
||||
另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在`read()`和`write()`系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:<https://redis.io/docs/manual/pipelining/> 。
|
||||
另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在 `read()` 和 `write()` 系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:<https://redis.io/docs/manual/pipelining/>。
|
||||
|
||||
#### 原生批量操作命令
|
||||
|
||||
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 的是有区别的,使用的时候需要注意:
|
||||
|
||||
@ -252,18 +252,18 @@ Redis 中有一些原生支持批量操作的命令,比如:
|
||||
|
||||

|
||||
|
||||
另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本** 。
|
||||
另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本**。
|
||||
|
||||
#### Lua 脚本
|
||||
|
||||
Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作** 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
|
||||
Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作**。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
|
||||
|
||||
并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。
|
||||
|
||||
不过, Lua 脚本依然存在下面这些缺陷:
|
||||
|
||||
- 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
|
||||
- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。
|
||||
- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot(哈希槽)** 上。
|
||||
|
||||
### 大量 key 集中过期问题
|
||||
|
||||
@ -274,7 +274,7 @@ Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作
|
||||
**如何解决呢?** 下面是两种常见的方法:
|
||||
|
||||
1. 给 key 设置随机过期时间。
|
||||
2. 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
|
||||
2. 开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
|
||||
|
||||
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
|
||||
|
||||
@ -299,7 +299,7 @@ bigkey 通常是由于下面这些原因产生的:
|
||||
|
||||
bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。
|
||||
|
||||
在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md)这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
|
||||
在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
|
||||
|
||||
1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
|
||||
2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
|
||||
@ -339,13 +339,13 @@ 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 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
|
||||
从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan)Redis 中的所有 key,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
|
||||
|
||||
在线上执行该命令时,为了降低对 Redis 的影响,需要指定 `-i` 参数控制扫描的频率。`redis-cli -p 6379 --bigkeys -i 3` 表示扫描过程中每次扫描后休息的时间间隔为 3 秒。
|
||||
|
||||
**2、使用 Redis 自带的 SCAN 命令**
|
||||
|
||||
`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN`等命令返回其长度或成员数量。
|
||||
`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN` 等命令返回其长度或成员数量。
|
||||
|
||||
| 数据结构 | 命令 | 复杂度 | 结果(对应 key) |
|
||||
| ---------- | ------ | ------ | ------------------ |
|
||||
@ -363,14 +363,14 @@ 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 分析服务。**
|
||||
|
||||
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。
|
||||
|
||||
这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址:<https://www.alibabacloud.com/help/zh/apsaradb-for-redis/latest/use-the-real-time-key-statistics-feature> 。
|
||||
这里以阿里云 Redis 为例说明,它支持 bigkey 实时分析、发现,文档地址:<https://www.alibabacloud.com/help/zh/apsaradb-for-redis/latest/use-the-real-time-key-statistics-feature>。
|
||||
|
||||

|
||||
|
||||
@ -381,7 +381,7 @@ bigkey 的常见处理以及优化办法如下(这些方法可以配合起来
|
||||
- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
|
||||
- **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。
|
||||
- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
|
||||
- **开启 lazy-free(惰性删除/延迟释放)** :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
|
||||
- **开启 lazy-free(惰性删除/延迟释放)**:lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
|
||||
|
||||
### Redis hotkey(热 Key)
|
||||
|
||||
@ -432,7 +432,7 @@ maxmemory-policy allkeys-lfu
|
||||
|
||||
需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。
|
||||
|
||||
**2、使用`MONITOR` 命令。**
|
||||
**2、使用 `MONITOR` 命令。**
|
||||
|
||||
`MONITOR` 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。
|
||||
|
||||
@ -473,7 +473,7 @@ OK
|
||||
|
||||
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都提供了)。
|
||||
|
||||
这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:<https://www.alibabacloud.com/help/zh/apsaradb-for-redis/latest/use-the-real-time-key-statistics-feature> 。
|
||||
这里以阿里云 Redis 为例说明,它支持 hotkey 实时分析、发现,文档地址:<https://www.alibabacloud.com/help/zh/apsaradb-for-redis/latest/use-the-real-time-key-statistics-feature>。
|
||||
|
||||

|
||||
|
||||
@ -497,16 +497,16 @@ hotkey 的常见处理以及优化办法如下(这些方法可以配合起来
|
||||
|
||||
我们知道一个 Redis 命令的执行可以简化为以下 4 步:
|
||||
|
||||
1. 发送命令
|
||||
2. 命令排队
|
||||
3. 命令执行
|
||||
4. 返回结果
|
||||
1. 发送命令;
|
||||
2. 命令排队;
|
||||
3. 命令执行;
|
||||
4. 返回结果。
|
||||
|
||||
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
|
||||
|
||||
Redis 为什么会有慢查询命令呢?
|
||||
|
||||
Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:
|
||||
Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如:
|
||||
|
||||
- `KEYS *`:会返回所有符合规则的 key。
|
||||
- `HGETALL`:会返回一个 Hash 中所有的键值对。
|
||||
@ -517,23 +517,23 @@ Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n)
|
||||
|
||||
由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
|
||||
|
||||
除了这些 O(n)时间复杂度的命令可能会导致慢查询之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如:
|
||||
除了这些 O(n) 时间复杂度的命令可能会导致慢查询之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如:
|
||||
|
||||
- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
|
||||
- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
|
||||
- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
|
||||
- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,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.
|
||||
@ -553,9 +553,9 @@ CONFIG SET slowlog-log-slower-than 10000
|
||||
CONFIG SET slowlog-max-len 128
|
||||
```
|
||||
|
||||
获取慢查询日志的内容很简单,直接使用`SLOWLOG GET` 命令即可。
|
||||
获取慢查询日志的内容很简单,直接使用 `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)。
|
||||
@ -604,7 +604,7 @@ OK
|
||||
|
||||
#### 什么是缓存穿透?
|
||||
|
||||
缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中** 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
|
||||
缓存穿透说简单点就是大量请求的 key 是不合理的,**根本不存在于缓存中,也不存在于数据库中**。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
|
||||
|
||||

|
||||
|
||||
@ -616,9 +616,9 @@ 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 的:`表名:列名:主键名:主键值` 。
|
||||
另外,这里多说一嘴,一般情况下我们是这样设计 key 的:`表名:列名:主键名:主键值`。
|
||||
|
||||
如果用 Java 代码展示的话,差不多是下面这样的:
|
||||
|
||||
@ -655,11 +655,11 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
|
||||
|
||||
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
|
||||
|
||||
加入布隆过滤器之后的缓存处理流程图如下。
|
||||
加入布隆过滤器之后的缓存处理流程图如下:
|
||||
|
||||

|
||||
|
||||
更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html) ,强烈推荐。
|
||||
更多关于布隆过滤器的详细介绍可以看看我的这篇原创:[不了解布隆过滤器?一文给你整的明明白白!](https://javaguide.cn/cs-basics/data-structure/bloom-filter.html),强烈推荐。
|
||||
|
||||
**3)接口限流**
|
||||
|
||||
@ -673,7 +673,7 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
|
||||
|
||||
#### 什么是缓存击穿?
|
||||
|
||||
缓存击穿中,请求的 key 对应的是 **热点数据** ,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)** 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
|
||||
缓存击穿中,请求的 key 对应的是 **热点数据**,该数据 **存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)**。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
|
||||
|
||||

|
||||
|
||||
@ -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. **持久缓存策略**(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
|
||||
|
||||
#### 缓存预热如何实现?
|
||||
@ -735,12 +735,12 @@ Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数
|
||||
|
||||
下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。
|
||||
|
||||
Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直接删除缓存 。
|
||||
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,18 +753,18 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
|
||||
**Redis Sentinel**:
|
||||
|
||||
1. 什么是 Sentinel? 有什么用?
|
||||
2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
|
||||
2. Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
|
||||
3. Sentinel 是如何实现故障转移的?
|
||||
4. 为什么建议部署多个 sentinel 节点(哨兵集群)?
|
||||
5. Sentinel 如何选择出新的 master(选举机制)?
|
||||
6. 如何从 Sentinel 集群中选择出 Leader ?
|
||||
5. Sentinel 如何选择出新的 master(选举机制)?
|
||||
6. 如何从 Sentinel 集群中选择出 Leader?
|
||||
7. Sentinel 可以防止脑裂吗?
|
||||
|
||||
**Redis Cluster**:
|
||||
|
||||
1. 为什么需要 Redis Cluster?解决了什么问题?有什么优势?
|
||||
2. Redis Cluster 是如何分片的?
|
||||
3. 为什么 Redis Cluster 的哈希槽是 16384 个?
|
||||
3. 为什么 Redis Cluster 的哈希槽是 16384 个?
|
||||
4. 如何确定给定 key 的应该分布到哪个哈希槽中?
|
||||
5. Redis Cluster 支持重新分配哈希槽吗?
|
||||
6. Redis Cluster 扩容缩容期间可以提供服务吗?
|
||||
@ -777,23 +777,23 @@ Cache Aside Pattern 中遇到写请求是这样的:更新数据库,然后直
|
||||
实际使用 Redis 的过程中,我们尽量要准守一些常见的规范,比如:
|
||||
|
||||
1. 使用连接池:避免频繁创建关闭客户端连接。
|
||||
2. 尽量不使用 O(n)指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF`等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
|
||||
3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET`等等)、pipeline、Lua 脚本。
|
||||
2. 尽量不使用 O(n) 指令,使用 O(n) 命令时要关注 n 的数量:像 `KEYS *`、`HGETALL`、`LRANGE`、`SMEMBERS`、`SINTER`/`SUNION`/`SDIFF` 等 O(n) 命令并非不能使用,但是需要明确 n 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。
|
||||
3. 使用批量操作减少网络传输:原生批量操作命令(比如 `MGET`、`MSET` 等等)、pipeline、Lua 脚本。
|
||||
4. 尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
|
||||
5. 禁止长时间开启 monitor:对性能影响比较大。
|
||||
6. 控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。
|
||||
7. ……
|
||||
|
||||
相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067) 。
|
||||
相关文章推荐:[阿里云 Redis 开发规范](https://developer.aliyun.com/article/531067)。
|
||||
|
||||
## 参考
|
||||
|
||||
- 《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 -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user