From a77eb0474068e168ff9328adc789c49dae559f1b Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 20 Jul 2022 22:27:00 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20update]redis=20=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=8B=86=E5=88=86&=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=86=85=E5=AE=B9=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- docs/.vuepress/sidebar.ts | 1 + .../redis/redis-data-structures-02.md | 100 +++--- docs/database/redis/redis-questions-01.md | 316 ++---------------- docs/database/redis/redis-questions-02.md | 295 ++++++++++++++++ docs/zhuanlan/readme.md | 17 +- 6 files changed, 394 insertions(+), 343 deletions(-) create mode 100644 docs/database/redis/redis-questions-02.md diff --git a/README.md b/README.md index f6b7b809..de53bdc2 100755 --- a/README.md +++ b/README.md @@ -191,7 +191,13 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle ### Redis -- [Redis 常见问题总结](docs/database/redis/redis-questions-01.md) +**知识点/面试题总结** : (必看:+1: ): + +- [Redis 常见问题总结(上)](docs/database/redis/redis-questions-01.md) +- [Redis 常见问题总结(下)](docs/database/redis/redis-questions-02.md) + +**重要知识点:** + - [3种常用的缓存读写策略详解](docs/database/redis/3-commonly-used-cache-read-and-write-strategies.md) - [Redis 5 种基本数据结构详解](./docs/database/redis/redis-data-structures-01.md) - [Redis 内存碎片详解](./docs/database/redis/redis-memory-fragmentation.md) diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index f7a9fb91..e0a9de37 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -258,6 +258,7 @@ export const sidebarConfig = defineSidebarConfig({ children: [ "cache-basics", "redis-questions-01", + "redis-questions-02", { text: "重要知识点", icon: "star", diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md index b3510969..f7d437bf 100644 --- a/docs/database/redis/redis-data-structures-02.md +++ b/docs/database/redis/redis-data-structures-02.md @@ -5,92 +5,84 @@ tag: - Redis --- -### Bitmap +## Bitmap -#### 介绍 +### 介绍 Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 -#### 常用命令 +你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/redis/image-20220720194154133.png) + +### 常用命令 + +| 命令 | 介绍 | +| ------------------------------------- | ------------------------------------------------------------ | +| SETBIT key offset value | 设置指定 offset 位置的值 | +| GETBIT key offset | 获取指定 offset 位置的值 | +| BITCOUNT key start end | 获取 start 和 end 之前值为 1 的元素个数 | +| BITOP operation destkey key1 key2 ... | 对一个或多个 Bitmap 进行运算,可用运算符有AND, OR, XOR以及NOT | + -`setbit` 、`getbit` 、`bitcount`、`bitop` ```bash # SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位 -> setbit mykey 7 1 +> SETBIT mykey 7 1 (integer) 0 -> setbit mykey 7 0 +> SETBIT mykey 7 0 (integer) 1 -> getbit mykey 7 +> GETBIT mykey 7 (integer) 0 -> setbit mykey 6 1 +> SETBIT mykey 6 1 (integer) 0 -> setbit mykey 8 1 +> SETBIT mykey 8 1 (integer) 0 # 通过 bitcount 统计被被设置为 1 的位的数量。 -> bitcount mykey +> BITCOUNT mykey (integer) 2 ``` -#### 应用场景 +### 应用场景 -适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频) +**需要保存状态信息(0/1即可表示)的场景** -**用户行为分析** -很多网站为了分析你的喜好,需要研究你点赞过的内容。 +- 举例 :用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。 +- 相关命令 :`SETBIT`、`GETBIT`、`BITCOUNT`、`BITOP`。 -```bash -# 记录你喜欢过 001 号小姐姐 -> setbit beauty_girl_001 uid 1 -``` +## HyperLogLog -**统计活跃用户** +### 介绍 -使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1 +`HyperLogLog` 是一种有名的基数计数概率算法 ,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。 -那么我该如何计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只要有一天在线就称为活跃),有请下一个 redis 的命令 +Redis 提供的 `HyperLogLog` 占用空间非常非常小,只需要 12k 的空间就能存储接近`2^64`个不同元素。这是真的厉害,这就是数学的魅力么!并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数: -```bash -# 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 -# BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数 -BITOP operation destkey key [key ...] -``` +- **稀疏矩阵** :计数较少的时候,占用空间很小。 +- **稠密矩阵** :计数达到某个阈值的时候,占用 12k 的空间。 -初始化数据: +不过, `HyperLogLog` 的计数结果并不是一个精确值,存在一定的误差(标准误差为 `0.81%` 。),这是由于它本质上是用概率算法导致的。 -```bash -> setbit 20210308 1 1 -(integer) 0 -> setbit 20210308 2 1 -(integer) 0 -> setbit 20210309 1 1 -(integer) 0 -``` +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/redis/image-20220720194154133.png) -统计 20210308~20210309 总活跃用户数: 1 +`HyperLogLog` 的使用非常简单,但原理非常复杂。`HyperLogLog` 的原理以及在 Redis 中的实现可以看这篇文章:[HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的](https://juejin.cn/post/6844903785744056333) 。 -```bash -> bitop and desk1 20210308 20210309 -(integer) 1 -> bitcount desk1 -(integer) 1 -``` +再推荐一个可以帮助理解HyperLogLog原理的工具:[Sketch of the Day: HyperLogLog — Cornerstone of a Big Data Infrastructure](http://content.research.neustar.biz/blog/hll.html) 。 -统计 20210308~20210309 在线活跃用户数: 2 +### 常用命令 -```bash -> bitop or desk2 20210308 20210309 -(integer) 1 -> bitcount desk2 -(integer) 2 -``` -**用户在线状态** -对于获取或者统计用户在线状态,使用 Bitmap 是一个节约空间且效率又高的一种方法。 +### 应用场景 -只需要一个 key,然后用户 ID 为 offset,如果在线就设置为 1,不在线就设置为 0。 +## Geospatial index -### HyperLogLog +地理空间数据管理。 -### Stream \ No newline at end of file +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/redis/image-20220720194359494.png) + +## Stream + +## 参考 + +- \ No newline at end of file diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index 59af2c2f..1767b5b1 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -1,5 +1,5 @@ --- -title: Redis常见面试题总结 +title: Redis常见面试题总结(上) category: 数据库 tag: - Redis @@ -119,8 +119,8 @@ Redis 5.0 新增加的一个数据结构 `Stream` 可以用来做消息队列, ### Redis 常用的数据结构有哪些? -- **5 种基础数据类型** :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 -- **3 种特殊数据类型** :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 +- **5 种基础数据结构** :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 +- **3 种特殊数据结构** :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。 关于 5 种基础数据类型的详细介绍请看这篇文章:[Redis 5 种基本数据结构详解](./redis-data-structures-01.md)。 @@ -166,6 +166,39 @@ Redis 中有一个叫做 `sorted set` 的数据结构经常被用在各种排行 - `SPOP key count` : 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。 - `SRANDMEMBER key count` : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。 +### 使用 Bitmap 统计活跃用户怎么做? + +使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。 + +初始化数据: + +```bash +> SETBIT 20210308 1 1 +(integer) 0 +> SETBIT 20210308 2 1 +(integer) 0 +> SETBIT 20210309 1 1 +(integer) 0 +``` + +统计 20210308~20210309 总活跃用户数: + +```bash +> BITOP and desk1 20210308 20210309 +(integer) 1 +> BITCOUNT desk1 +(integer) 1 +``` + +统计 20210308~20210309 在线活跃用户数: + +```bash +> BITOP or desk2 20210308 20210309 +(integer) 1 +> BITCOUNT desk2 +(integer) 2 +``` + ## Redis 线程模型 ### Redis 单线程模型了解吗? @@ -387,283 +420,6 @@ Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通 ![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/booksimage-20210807145107290.png) -## Redis 事务 - -### 如何使用 Redis 事务? - -Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(transaction)功能。 - -```bash -> MULTI -OK -> SET USER "Guide哥" -QUEUED -> GET USER -QUEUED -> EXEC -1) OK -2) "Guide哥" -``` - -使用 [`MULTI`](https://redis.io/commands/multi) 命令后可以输入多个命令。Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [`EXEC`](https://redis.io/commands/exec) 命令将执行所有命令。 - -这个过程是这样的: - -1. 开始事务(`MULTI`)。 -2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。 -3. 执行事务(`EXEC`)。 - -你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 - -```bash -> MULTI -OK -> SET USER "Guide哥" -QUEUED -> GET USER -QUEUED -> DISCARD -OK -``` - -[`WATCH`](https://redis.io/commands/watch) 命令用于监听指定的键,当调用 `EXEC` 命令执行事务时,如果一个被 `WATCH` 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。 - -```bash -> WATCH USER -OK -> MULTI -> SET USER "Guide哥" -OK -> GET USER -Guide哥 -> EXEC -ERR EXEC without MULTI -``` - -Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io/topics/transactions) 如下: - -![redis事务](./images/redis-all/redis事务.png) - -### Redis 支持原子性吗? - -Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: **1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。 - -1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; -3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 -4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; - -Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。 - -Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。 - -![redis roll back](./images/redis-all/redis-rollBack.png) - -你可以将 Redis 中的事务就理解为 :**Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。** - -除了不满足原子性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。 - -因此,Redis 事务是不建议在日常开发中使用的。 - -**相关 issue** : - -- [issue452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) 。 -- [Issue491:关于 redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491) - -### 如何解决 Redis 事务的缺陷? - -Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 - -一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 - -如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的。因此,严格来说,通过 Lua 脚本来批量执行 Redis 命令也是不满足原子性的。 - -另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 - -## Redis 性能优化 - -### Redis bigkey - -#### 什么是 bigkey? - -简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 - -#### bigkey 有什么危害? - -除了会消耗更多的内存空间,bigkey 对性能也会有比较大的影响。 - -因此,我们应该尽量避免写入 bigkey! - -#### 如何发现 bigkey? - -**1、使用 Redis 自带的 `--bigkeys` 参数来查找。** - -```bash -# redis-cli -p 6379 --bigkeys - -# Scanning the entire keyspace to find biggest keys as well as -# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec -# per 100 SCAN commands (not usually needed). - -[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes -[00.00%] Biggest list found so far '"my-list"' with 17 items - --------- summary ------- - -Sampled 5 keys in the keyspace! -Total key length in bytes is 264 (avg len 52.80) - -Biggest list found '"my-list"' has 17 items -Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' has 4437 bytes - -1 lists with 17 items (20.00% of keys, avg size 17.00) -0 hashs with 0 fields (00.00% of keys, avg size 0.00) -4 strings with 4831 bytes (80.00% of keys, avg size 1207.75) -0 streams with 0 entries (00.00% of keys, avg size 0.00) -0 sets with 0 members (00.00% of keys, avg size 0.00) -0 zsets with 0 members (00.00% of keys, avg size 0.00 -``` - -从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 string 数据类型,包含元素最多的复合数据类型)。 - -**2、分析 RDB 文件** - -通过分析 RDB 文件来找出 big key。这种方案的前提是你的 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 快照文件用的工具,性能更好。 - -### 大量 key 集中过期问题 - -我在上面提到过:对于过期 key,Redis 采用的是 **定期删除+惰性/懒汉式删除** 策略。 - -定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。 - -如何解决呢?下面是两种常见的方法: - -1. 给 key 设置随机过期时间。 -2. 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 - -个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。 - -## Redis 生产问题 - -### 缓存穿透 - -#### 什么是缓存穿透? - -缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 - -#### 缓存穿透情况的处理流程是怎样的? - -如下图所示,用户的请求最终都要跑到数据库中查询一遍。 - -![缓存穿透情况](https://img-blog.csdnimg.cn/6358650a9bf742838441d636430c90b9.png) - -#### 有哪些解决办法? - -最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 - -**1)缓存无效 key** - -如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: `SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 - -另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值` 。 - -如果用 Java 代码展示的话,差不多是下面这样的: - -```java -public Object getObjectInclNullById(Integer id) { - // 从缓存中获取数据 - Object cacheValue = cache.get(id); - // 缓存为空 - if (cacheValue == null) { - // 从数据库中获取 - Object storageValue = storage.get(key); - // 缓存空对象 - cache.set(key, storageValue); - // 如果存储数据为空,需要设置一个过期时间(300秒) - if (storageValue == null) { - // 必须设置过期时间,否则有被攻击的风险 - cache.expire(key, 60 * 5); - } - return storageValue; - } - return cacheValue; -} -``` - -**2)布隆过滤器** - -布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 - -具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 - -加入布隆过滤器之后的缓存处理流程图如下。 - -![](./images/redis-all/加入布隆过滤器后的缓存处理流程.png) - -但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: **布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** - -_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_ - -我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:** - -1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 - -我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:** - -1. 对给定元素再次进行相同的哈希计算; -2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 - -然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率) - -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://javaguide.cn/cs-basics/data-structure/bloom-filter/) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 - -### 缓存雪崩 - -#### 什么是缓存雪崩? - -我发现缓存雪崩这名字起的有点意思,哈哈。 - -实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 - -举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。 - -还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法: - -举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。 - -#### 有哪些解决办法? - -**针对 Redis 服务不可用的情况:** - -1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 -2. 限流,避免同时处理大量的请求。 - -**针对热点缓存失效的情况:** - -1. 设置不同的失效时间比如随机设置缓存的失效时间。 -2. 缓存永不失效。 - -### 如何保证缓存和数据库数据的一致性? - -细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 - -下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 - -Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。 - -如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: - -1. **缓存失效时间变短(不推荐,治标不治本)** :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 -2. **增加 cache 更新重试机制(常用)**: 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。 - -相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd) - ## 参考 - 《Redis 开发与运维》 diff --git a/docs/database/redis/redis-questions-02.md b/docs/database/redis/redis-questions-02.md new file mode 100644 index 00000000..3c493449 --- /dev/null +++ b/docs/database/redis/redis-questions-02.md @@ -0,0 +1,295 @@ +--- +title: Redis常见面试题总结(下) +category: 数据库 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis基础,Redis常见数据结构,Redis线程模型,Redis内存管理,Redis事务,Redis性能优化 + - name: description + content: 一篇文章总结Redis常见的知识点和面试题,涵盖Redis基础、Redis常见数据结构、Redis线程模型、Redis内存管理、Redis事务、Redis性能优化等内容。 +--- + +## Redis 事务 + +### 如何使用 Redis 事务? + +Redis 可以通过 **`MULTI`,`EXEC`,`DISCARD` 和 `WATCH`** 等命令来实现事务(transaction)功能。 + +```bash +> MULTI +OK +> SET USER "Guide哥" +QUEUED +> GET USER +QUEUED +> EXEC +1) OK +2) "Guide哥" +``` + +使用 [`MULTI`](https://redis.io/commands/multi) 命令后可以输入多个命令。Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 [`EXEC`](https://redis.io/commands/exec) 命令将执行所有命令。 + +这个过程是这样的: + +1. 开始事务(`MULTI`)。 +2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。 +3. 执行事务(`EXEC`)。 + +你也可以通过 [`DISCARD`](https://redis.io/commands/discard) 命令取消一个事务,它会清空事务队列中保存的所有命令。 + +```bash +> MULTI +OK +> SET USER "Guide哥" +QUEUED +> GET USER +QUEUED +> DISCARD +OK +``` + +[`WATCH`](https://redis.io/commands/watch) 命令用于监听指定的键,当调用 `EXEC` 命令执行事务时,如果一个被 `WATCH` 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。 + +```bash +> WATCH USER +OK +> MULTI +> SET USER "Guide哥" +OK +> GET USER +Guide哥 +> EXEC +ERR EXEC without MULTI +``` + +Redis 官网相关介绍 [https://redis.io/topics/transactions](https://redis.io/topics/transactions) 如下: + +![redis事务](./images/redis-all/redis事务.png) + +### Redis 支持原子性吗? + +Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: **1. 原子性**,**2. 隔离性**,**3. 持久性**,**4. 一致性**。 + +1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; +2. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; +3. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 +4. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; + +Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。 + +Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。 + +![redis roll back](./images/redis-all/redis-rollBack.png) + +你可以将 Redis 中的事务就理解为 :**Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。** + +除了不满足原子性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。 + +因此,Redis 事务是不建议在日常开发中使用的。 + +**相关 issue** : + +- [issue452: 关于 Redis 事务不满足原子性的问题](https://github.com/Snailclimb/JavaGuide/issues/452) 。 +- [Issue491:关于 redis 没有事务回滚?](https://github.com/Snailclimb/JavaGuide/issues/491) + +### 如何解决 Redis 事务的缺陷? + +Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 + +一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 + +如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的。因此,严格来说,通过 Lua 脚本来批量执行 Redis 命令也是不满足原子性的。 + +另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 + +## Redis 性能优化 + +### Redis bigkey + +#### 什么是 bigkey? + +简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。 + +#### bigkey 有什么危害? + +除了会消耗更多的内存空间,bigkey 对性能也会有比较大的影响。 + +因此,我们应该尽量避免写入 bigkey! + +#### 如何发现 bigkey? + +**1、使用 Redis 自带的 `--bigkeys` 参数来查找。** + +```bash +# redis-cli -p 6379 --bigkeys + +# Scanning the entire keyspace to find biggest keys as well as +# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec +# per 100 SCAN commands (not usually needed). + +[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes +[00.00%] Biggest list found so far '"my-list"' with 17 items + +-------- summary ------- + +Sampled 5 keys in the keyspace! +Total key length in bytes is 264 (avg len 52.80) + +Biggest list found '"my-list"' has 17 items +Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' has 4437 bytes + +1 lists with 17 items (20.00% of keys, avg size 17.00) +0 hashs with 0 fields (00.00% of keys, avg size 0.00) +4 strings with 4831 bytes (80.00% of keys, avg size 1207.75) +0 streams with 0 entries (00.00% of keys, avg size 0.00) +0 sets with 0 members (00.00% of keys, avg size 0.00) +0 zsets with 0 members (00.00% of keys, avg size 0.00 +``` + +从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 string 数据类型,包含元素最多的复合数据类型)。 + +**2、分析 RDB 文件** + +通过分析 RDB 文件来找出 big key。这种方案的前提是你的 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 快照文件用的工具,性能更好。 + +### 大量 key 集中过期问题 + +我在上面提到过:对于过期 key,Redis 采用的是 **定期删除+惰性/懒汉式删除** 策略。 + +定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。 + +如何解决呢?下面是两种常见的方法: + +1. 给 key 设置随机过期时间。 +2. 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 + +个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。 + +## Redis 生产问题 + +### 缓存穿透 + +#### 什么是缓存穿透? + +缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 + +#### 缓存穿透情况的处理流程是怎样的? + +如下图所示,用户的请求最终都要跑到数据库中查询一遍。 + +![缓存穿透情况](https://img-blog.csdnimg.cn/6358650a9bf742838441d636430c90b9.png) + +#### 有哪些解决办法? + +最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 + +**1)缓存无效 key** + +如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: `SET key value EX 10086` 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 + +另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值` 。 + +如果用 Java 代码展示的话,差不多是下面这样的: + +```java +public Object getObjectInclNullById(Integer id) { + // 从缓存中获取数据 + Object cacheValue = cache.get(id); + // 缓存为空 + if (cacheValue == null) { + // 从数据库中获取 + Object storageValue = storage.get(key); + // 缓存空对象 + cache.set(key, storageValue); + // 如果存储数据为空,需要设置一个过期时间(300秒) + if (storageValue == null) { + // 必须设置过期时间,否则有被攻击的风险 + cache.expire(key, 60 * 5); + } + return storageValue; + } + return cacheValue; +} +``` + +**2)布隆过滤器** + +布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 + +具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 + +加入布隆过滤器之后的缓存处理流程图如下。 + +![](./images/redis-all/加入布隆过滤器后的缓存处理流程.png) + +但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: **布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** + +_为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!_ + +我们先来看一下,**当一个元素加入布隆过滤器中的时候,会进行哪些操作:** + +1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 +2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 + +我们再来看一下,**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:** + +1. 对给定元素再次进行相同的哈希计算; +2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 + +然后,一定会出现这样一种情况:**不同的字符串可能哈希出来的位置相同。** (可以适当增加位数组大小或者调整我们的哈希函数来降低概率) + +更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://javaguide.cn/cs-basics/data-structure/bloom-filter/) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 + +### 缓存雪崩 + +#### 什么是缓存雪崩? + +我发现缓存雪崩这名字起的有点意思,哈哈。 + +实际上,缓存雪崩描述的就是这样一个简单的场景:**缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。** 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。 + +举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。 + +还有一种缓存雪崩的场景是:**有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。** 这样的情况,有下面几种解决办法: + +举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。 + +#### 有哪些解决办法? + +**针对 Redis 服务不可用的情况:** + +1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 +2. 限流,避免同时处理大量的请求。 + +**针对热点缓存失效的情况:** + +1. 设置不同的失效时间比如随机设置缓存的失效时间。 +2. 缓存永不失效。 + +### 如何保证缓存和数据库数据的一致性? + +细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。 + +下面单独对 **Cache Aside Pattern(旁路缓存模式)** 来聊聊。 + +Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。 + +如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案: + +1. **缓存失效时间变短(不推荐,治标不治本)** :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。 +2. **增加 cache 更新重试机制(常用)**: 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。 + +相关文章推荐:[缓存和数据库一致性问题,看这篇就够了 - 水滴与银弹](https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd) + +## 参考 + +- 《Redis 开发与运维》 +- 《Redis 设计与实现》 +- Redis Transactions : https://redis.io/docs/manual/transactions/ 。 \ No newline at end of file diff --git a/docs/zhuanlan/readme.md b/docs/zhuanlan/readme.md index 33d50793..a82840c7 100644 --- a/docs/zhuanlan/readme.md +++ b/docs/zhuanlan/readme.md @@ -26,17 +26,19 @@ category: 知识星球 ## 星球限时优惠 -两年前,星球的定价是 **50/年** ,这是星球的最低定价,我还附送了 33 元优惠券。扣除了星球手续费,发了各种福利之后,几乎就是纯粹做公益。 +欢迎准备 Java 面试以及学习 Java 的同学加入我的[知识星球](../about-the-author/zhishixingqiu-two-years.md),干货非常多,学习非常好!收费虽然是白菜价,但星球里的内容或许比你参加上万的培训班质量还要高。 -感兴趣的小伙伴可以看看我在 2020-01-03 发的头条:[做了一个很久没敢做的事情](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486049&idx=1&sn=e0161b409e8f164251bdaa0c83a476bc&chksm=cea245aaf9d5ccbcafdb95a546d959508814085620aabdbb4385c4b8cea6e50bf157c3697041&token=1614894361&lang=zh_CN#rd),去考古一下。 +两年前,星球的定价是 **50/年** ,这是星球的最低定价,我还附送了 33 元优惠券。扣除了星球手续费,发了各种福利之后,几乎就是纯粹做公益。感兴趣的小伙伴可以看看我在 2020-01-03 发的头条:[做了一个很久没敢做的事情](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486049&idx=1&sn=e0161b409e8f164251bdaa0c83a476bc&chksm=cea245aaf9d5ccbcafdb95a546d959508814085620aabdbb4385c4b8cea6e50bf157c3697041&token=1614894361&lang=zh_CN#rd),去考古一下。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/image-20220311203414600.png) +随着时间推移,星球积累的干货资源越来越多,我花在星球上的时间也越来越多。于是,星球的定价慢慢被调整为了 **159/年**! -随着时间推移,星球积累的干货资源越来越多,我花在星球上的时间也越来越多。于是,我将星球的定价慢慢调整为了 **159/年**!后续会将星球的价格调整为 **199/年**,想要加入的小伙伴一定要尽早。 +目前已经有 **1.3w+** 人加入星球,随着加入的人数增加,这个定价还会继续调整。 -你可以添加我的微信(没有手机号再申请微信,故使用企业微信。不过,请放心,这个号的消息也是我本人处理,平时最常看这个微信)领取星球专属优惠券,限时 **100 元/年** 加入(**续费半价**)。 +这里再送出一张新人优惠券,使用优惠券 **139/年** 加入(即将调整为 **199/年**)。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/IMG_3007.jpg) +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/xingqiuyouhuijuan-20.png) + +如果你在犹豫要不要加入的话,我建议你进来体验一下,三天之内知识星球 APP 右上角可以申请全额退款,秒到账!绝对不会坑大家! 进入星球之后,你可以为自己制定一个目标,比如自己想要进入某某还不错的公司或者达成什么成就(一定要是还算有点挑战的目标)。 @@ -44,5 +46,4 @@ category: 知识星球 **真诚欢迎准备面试的小伙伴加入星球一起交流!真心希望能够帮助到更多小伙伴!** -**加入星球之后记得看置顶主题,你会发现物超所值!** - +**加入星球之后记得看[置顶主题](https://t.zsxq.com/04iiiAqf6),你会发现物超所值!**