mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
Compare commits
11 Commits
1df6301eb9
...
97ae1aeb1a
Author | SHA1 | Date | |
---|---|---|---|
|
97ae1aeb1a | ||
|
86af2d5626 | ||
|
51b3caecec | ||
|
fffb0f5100 | ||
|
83f39a33b8 | ||
|
f9eb32f623 | ||
|
24d7fb1092 | ||
|
23fa4b81bd | ||
|
374027a9ba | ||
|
8f6d7e9d6d | ||
|
a2b71a0ef2 |
@ -11,17 +11,17 @@ tag:
|
||||
|
||||
## 数据库命名规范
|
||||
|
||||
- 所有数据库对象名称必须使用小写字母并用下划线分割
|
||||
- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
|
||||
- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
|
||||
- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀
|
||||
- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
|
||||
- 所有数据库对象名称必须使用小写字母并用下划线分割。
|
||||
- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)。
|
||||
- 数据库对象的命名要能做到见名识义,并且最好不要超过 32 个字符。
|
||||
- 临时库表必须以 `tmp_` 为前缀并以日期为后缀,备份表必须以 `bak_` 为前缀并以日期 (时间戳) 为后缀。
|
||||
- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
|
||||
|
||||
## 数据库基本设计规范
|
||||
|
||||
### 所有表必须使用 InnoDB 存储引擎
|
||||
|
||||
没有特殊要求(即 InnoDB 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 InnoDB)。
|
||||
没有特殊要求(即 InnoDB 无法满足的功能如:列存储、存储空间数据等)的情况下,所有表必须使用 InnoDB 存储引擎(MySQL5.5 之前默认使用 MyISAM,5.6 以后默认的为 InnoDB)。
|
||||
|
||||
InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
|
||||
|
||||
@ -33,19 +33,19 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
|
||||
|
||||
### 所有表和字段都需要添加注释
|
||||
|
||||
使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
|
||||
使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护。
|
||||
|
||||
### 尽量控制单表数据量的大小,建议控制在 500 万以内
|
||||
|
||||
500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。
|
||||
|
||||
可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小
|
||||
可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
|
||||
|
||||
### 谨慎使用 MySQL 分区表
|
||||
|
||||
分区表在物理上表现为多个文件,在逻辑上表现为一个表;
|
||||
分区表在物理上表现为多个文件,在逻辑上表现为一个表。
|
||||
|
||||
谨慎选择分区键,跨分区查询效率可能更低;
|
||||
谨慎选择分区键,跨分区查询效率可能更低。
|
||||
|
||||
建议采用物理分表的方式管理大数据。
|
||||
|
||||
@ -71,7 +71,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
|
||||
|
||||
### 禁止在线上做数据库压力测试
|
||||
|
||||
### 禁止从开发环境,测试环境直接连接生产环境数据库
|
||||
### 禁止从开发环境、测试环境直接连接生产环境数据库
|
||||
|
||||
安全隐患极大,要对生产环境抱有敬畏之心!
|
||||
|
||||
@ -79,22 +79,22 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
|
||||
|
||||
### 优先选择符合存储需要的最小的数据类型
|
||||
|
||||
存储字节越小,占用也就空间越小,性能也越好。
|
||||
存储字节越小,占用空间也就越小,性能也越好。
|
||||
|
||||
**a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整型数据。**
|
||||
**a.某些字符串可以转换成数字类型存储,比如可以将 IP 地址转换成整型数据。**
|
||||
|
||||
数字是连续的,性能更好,占用空间也更小。
|
||||
|
||||
MySQL 提供了两个方法来处理 ip 地址
|
||||
MySQL 提供了两个方法来处理 ip 地址:
|
||||
|
||||
- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位)
|
||||
- `INET_NTOA()` :把整型的 ip 转为地址
|
||||
- `INET_ATON()`:把 ip 转为无符号整型 (4-8 位);
|
||||
- `INET_NTOA()`:把整型的 ip 转为地址。
|
||||
|
||||
插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型,显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
|
||||
插入数据前,先用 `INET_ATON()` 把 ip 地址转为整型;显示数据时,使用 `INET_NTOA()` 把整型的 ip 地址转为地址显示即可。
|
||||
|
||||
**b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。**
|
||||
**b.对于非负型的数据 (如自增 ID、整型 IP、年龄) 来说,要优先使用无符号整型来存储。**
|
||||
|
||||
无符号相对于有符号可以多出一倍的存储空间
|
||||
无符号相对于有符号可以多出一倍的存储空间:
|
||||
|
||||
```sql
|
||||
SIGNED INT -2147483648~2147483647
|
||||
@ -103,7 +103,7 @@ UNSIGNED INT 0~4294967295
|
||||
|
||||
**c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。**
|
||||
|
||||
### 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
|
||||
### 避免使用 TEXT、BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
|
||||
|
||||
**a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中。**
|
||||
|
||||
@ -113,30 +113,30 @@ MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查
|
||||
|
||||
**2、TEXT 或 BLOB 类型只能使用前缀索引**
|
||||
|
||||
因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的
|
||||
因为 MySQL 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的。
|
||||
|
||||
### 避免使用 ENUM 类型
|
||||
|
||||
- 修改 ENUM 值需要使用 ALTER 语句;
|
||||
- ENUM 类型的 ORDER BY 操作效率低,需要额外操作;
|
||||
- ENUM 数据类型存在一些限制比如建议不要使用数值作为 ENUM 的枚举值。
|
||||
- 修改 ENUM 值需要使用 ALTER 语句。
|
||||
- ENUM 类型的 ORDER BY 操作效率低,需要额外操作。
|
||||
- ENUM 数据类型存在一些限制,比如建议不要使用数值作为 ENUM 的枚举值。
|
||||
|
||||
相关阅读:[是否推荐使用 MySQL 的 enum 类型? - 架构文摘 - 知乎](https://www.zhihu.com/question/404422255/answer/1661698499) 。
|
||||
|
||||
### 尽可能把所有列定义为 NOT NULL
|
||||
|
||||
除非有特别的原因使用 NULL 值,应该总是让字段保持 NOT NULL。
|
||||
除非有特别的原因使用 NULL 值,否则应该总是让字段保持 NOT NULL。
|
||||
|
||||
- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间;
|
||||
- 索引 NULL 列需要额外的空间来保存,所以要占用更多的空间。
|
||||
- 进行比较和计算时要对 NULL 值做特别的处理。
|
||||
|
||||
相关阅读:[技术分享 | MySQL 默认值选型(是空,还是 NULL)](https://opensource.actionsky.com/20190710-mysql/) 。
|
||||
|
||||
### 一定不要用字符串存储日期
|
||||
|
||||
对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳。
|
||||
对于日期类型来说,一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和数值型时间戳。
|
||||
|
||||
这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
|
||||
这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家在实际开发中选择正确的存放时间的数据类型:
|
||||
|
||||
| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
|
||||
| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- |
|
||||
@ -148,10 +148,10 @@ MySQL 时间类型选择的详细介绍请看这篇:[MySQL 时间类型数据
|
||||
|
||||
### 同财务相关的金额类数据必须使用 decimal 类型
|
||||
|
||||
- **非精准浮点**:float,double
|
||||
- **非精准浮点**:float、double
|
||||
- **精准浮点**:decimal
|
||||
|
||||
decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据
|
||||
decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节。并且,decimal 可用于存储比 bigint 更大的整型数据。
|
||||
|
||||
不过, 由于 decimal 需要额外的空间和计算开销,应该尽量只在需要对数据进行精确计算时才使用 decimal 。
|
||||
|
||||
@ -161,13 +161,13 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
|
||||
|
||||
## 索引设计规范
|
||||
|
||||
### 限制每张表上的索引数量,建议单张表索引不超过 5 个
|
||||
### 限制每张表上的索引数量,建议单张表索引不超过 5 个
|
||||
|
||||
索引并不是越多越好!索引可以提高效率同样可以降低效率。
|
||||
索引并不是越多越好!索引可以提高效率,同样可以降低效率。
|
||||
|
||||
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
|
||||
|
||||
因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
|
||||
因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划。如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
|
||||
|
||||
### 禁止使用全文索引
|
||||
|
||||
@ -175,46 +175,46 @@ decimal 类型为精准浮点数,在计算时不会丢失精度。占用空间
|
||||
|
||||
### 禁止给表中的每一列都建立单独的索引
|
||||
|
||||
5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
|
||||
5.6 版本之前,一个 sql 只能使用到一个表中的一个索引;5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
|
||||
|
||||
### 每个 InnoDB 表必须有个主键
|
||||
|
||||
InnoDB 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
|
||||
|
||||
InnoDB 是按照主键索引的顺序来组织表的
|
||||
InnoDB 是按照主键索引的顺序来组织表的。
|
||||
|
||||
- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)
|
||||
- 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
|
||||
- 主键建议使用自增 ID 值
|
||||
- 不要使用更新频繁的列作为主键,不使用多列主键(相当于联合索引)。
|
||||
- 不要使用 UUID、MD5、HASH、字符串列作为主键(无法保证数据的顺序增长)。
|
||||
- 主键建议使用自增 ID 值。
|
||||
|
||||
### 常见索引列建议
|
||||
|
||||
- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
|
||||
- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
|
||||
- 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
|
||||
- 多表 join 的关联列
|
||||
- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。
|
||||
- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。
|
||||
- 不要将符合 1 和 2 中的字段的列都建立一个索引,通常将 1、2 中的字段建立联合索引效果更好。
|
||||
- 多表 join 的关联列。
|
||||
|
||||
### 如何选择索引列的顺序
|
||||
|
||||
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
|
||||
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
|
||||
|
||||
- **区分度最高的列放在联合索引的最左侧:** 这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。
|
||||
- **最频繁使用的列放在联合索引的左侧:** 这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。
|
||||
- **字段长度:** 字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。 对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。
|
||||
- **区分度最高的列放在联合索引的最左侧**:这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。
|
||||
- **最频繁使用的列放在联合索引的左侧**:这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。
|
||||
- **字段长度**:字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。
|
||||
|
||||
### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
|
||||
|
||||
- 重复索引示例:primary key(id)、index(id)、unique index(id)
|
||||
- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
|
||||
- 重复索引示例:primary key(id)、index(id)、unique index(id)。
|
||||
- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)。
|
||||
|
||||
### 对于频繁的查询优先考虑使用覆盖索引
|
||||
### 对于频繁的查询,优先考虑使用覆盖索引
|
||||
|
||||
> 覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引
|
||||
> 覆盖索引:就是包含了所有查询字段 (where、select、order by、group by 包含的字段) 的索引
|
||||
|
||||
**覆盖索引的好处:**
|
||||
**覆盖索引的好处**:
|
||||
|
||||
- **避免 InnoDB 表进行索引的二次查询,也就是回表操作:** InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
|
||||
- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
|
||||
- **避免 InnoDB 表进行索引的二次查询,也就是回表操作**:InnoDB 是以聚集索引的顺序来存储的,对于 InnoDB 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询(回表),减少了 IO 操作,提升了查询效率。
|
||||
- **可以把随机 IO 变成顺序 IO 加快查询效率**:由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
|
||||
|
||||
---
|
||||
|
||||
@ -222,9 +222,9 @@ InnoDB 是按照主键索引的顺序来组织表的
|
||||
|
||||
**尽量避免使用外键约束**
|
||||
|
||||
- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
|
||||
- 外键可用于保证数据的参照完整性,但建议在业务端实现
|
||||
- 外键会影响父表和子表的写操作从而降低性能
|
||||
- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。
|
||||
- 外键可用于保证数据的参照完整性,但建议在业务端实现。
|
||||
- 外键会影响父表和子表的写操作从而降低性能。
|
||||
|
||||
## 数据库 SQL 开发规范
|
||||
|
||||
@ -238,7 +238,7 @@ InnoDB 是按照主键索引的顺序来组织表的
|
||||
|
||||
### 充分利用表上已经存在的索引
|
||||
|
||||
避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的)
|
||||
避免使用双%号的查询条件。如:`a like '%123%'`(如果无前置%,只有后置%,是可以用到列上的索引的)。
|
||||
|
||||
一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
|
||||
|
||||
@ -248,18 +248,18 @@ InnoDB 是按照主键索引的顺序来组织表的
|
||||
|
||||
- `SELECT *` 会消耗更多的 CPU。
|
||||
- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。
|
||||
- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
|
||||
- `SELECT <字段列表>` 可减少表结构变更带来的影响、
|
||||
- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快、效率极高、业界极为推荐的查询优化方式)。
|
||||
- `SELECT <字段列表>` 可减少表结构变更带来的影响。
|
||||
|
||||
### 禁止使用不含字段列表的 INSERT 语句
|
||||
|
||||
如:
|
||||
**不推荐**:
|
||||
|
||||
```sql
|
||||
insert into t values ('a','b','c');
|
||||
```
|
||||
|
||||
应使用:
|
||||
**推荐**:
|
||||
|
||||
```sql
|
||||
insert into t(c1,c2,c3) values ('a','b','c');
|
||||
@ -273,7 +273,7 @@ insert into t(c1,c2,c3) values ('a','b','c');
|
||||
|
||||
### 避免数据类型的隐式转换
|
||||
|
||||
隐式转换会导致索引失效如:
|
||||
隐式转换会导致索引失效,如:
|
||||
|
||||
```sql
|
||||
select name,phone from customer where id = '111';
|
||||
@ -283,9 +283,9 @@ select name,phone from customer where id = '111';
|
||||
|
||||
### 避免使用子查询,可以把子查询优化为 join 操作
|
||||
|
||||
通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
|
||||
通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
|
||||
|
||||
**子查询性能差的原因:** 子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
|
||||
**子查询性能差的原因**:子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
|
||||
|
||||
### 避免使用 JOIN 关联太多的表
|
||||
|
||||
@ -293,7 +293,7 @@ select name,phone from customer where id = '111';
|
||||
|
||||
在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
|
||||
|
||||
如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
|
||||
如果程序中大量地使用了多表关联的操作,同时 join_buffer_size 设置得也不合理,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
|
||||
|
||||
同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。
|
||||
|
||||
@ -303,25 +303,25 @@ select name,phone from customer where id = '111';
|
||||
|
||||
### 对应同一列进行 or 判断时,使用 in 代替 or
|
||||
|
||||
in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
|
||||
in 的值不要超过 500 个。in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
|
||||
|
||||
### 禁止使用 order by rand() 进行随机排序
|
||||
|
||||
order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
|
||||
order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值。如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
|
||||
|
||||
推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
|
||||
|
||||
### WHERE 从句中禁止对列进行函数转换和计算
|
||||
|
||||
对列进行函数转换或计算时会导致无法使用索引
|
||||
对列进行函数转换或计算时会导致无法使用索引。
|
||||
|
||||
**不推荐:**
|
||||
**不推荐**:
|
||||
|
||||
```sql
|
||||
where date(create_time)='20190101'
|
||||
```
|
||||
|
||||
**推荐:**
|
||||
**推荐**:
|
||||
|
||||
```sql
|
||||
where create_time >= '20190101' and create_time < '20190102'
|
||||
@ -329,43 +329,43 @@ where create_time >= '20190101' and create_time < '20190102'
|
||||
|
||||
### 在明显不会有重复值时使用 UNION ALL 而不是 UNION
|
||||
|
||||
- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
|
||||
- UNION ALL 不会再对结果集进行去重操作
|
||||
- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作。
|
||||
- UNION ALL 不会再对结果集进行去重操作。
|
||||
|
||||
### 拆分复杂的大 SQL 为多个小 SQL
|
||||
|
||||
- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
|
||||
- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
|
||||
- SQL 拆分后可以通过并行执行来提高处理效率
|
||||
- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL。
|
||||
- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算。
|
||||
- SQL 拆分后可以通过并行执行来提高处理效率。
|
||||
|
||||
### 程序连接不同的数据库使用不同的账号,禁止跨库查询
|
||||
|
||||
- 为数据库迁移和分库分表留出余地
|
||||
- 降低业务耦合度
|
||||
- 避免权限过大而产生的安全风险
|
||||
- 为数据库迁移和分库分表留出余地。
|
||||
- 降低业务耦合度。
|
||||
- 避免权限过大而产生的安全风险。
|
||||
|
||||
## 数据库操作行为规范
|
||||
|
||||
### 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
|
||||
### 超 100 万行的批量写 (UPDATE、DELETE、INSERT) 操作,要分批多次进行操作
|
||||
|
||||
**大批量操作可能会造成严重的主从延迟**
|
||||
|
||||
主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
|
||||
主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况。
|
||||
|
||||
**binlog 日志为 row 格式时会产生大量的日志**
|
||||
|
||||
大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因
|
||||
大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。
|
||||
|
||||
**避免产生大事务操作**
|
||||
|
||||
大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
|
||||
|
||||
特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批
|
||||
特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。
|
||||
|
||||
### 对于大表使用 pt-online-schema-change 修改表结构
|
||||
|
||||
- 避免大表修改产生的主从延迟
|
||||
- 避免在对表字段进行修改时进行锁表
|
||||
- 避免大表修改产生的主从延迟。
|
||||
- 避免在对表字段进行修改时进行锁表。
|
||||
|
||||
对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
|
||||
|
||||
@ -373,13 +373,13 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表
|
||||
|
||||
### 禁止为程序使用的账号赋予 super 权限
|
||||
|
||||
- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
|
||||
- super 权限只能留给 DBA 处理问题的账号使用
|
||||
- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接。
|
||||
- super 权限只能留给 DBA 处理问题的账号使用。
|
||||
|
||||
### 对于程序连接数据库账号,遵循权限最小原则
|
||||
### 对于程序连接数据库账号,遵循权限最小原则
|
||||
|
||||
- 程序使用数据库账号只能在一个 DB 下使用,不准跨库
|
||||
- 程序使用的账号原则上不准有 drop 权限
|
||||
- 程序使用数据库账号只能在一个 DB 下使用,不准跨库。
|
||||
- 程序使用的账号原则上不准有 drop 权限。
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
|
@ -15,20 +15,20 @@ tag:
|
||||
|
||||
**索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。**
|
||||
|
||||
索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
|
||||
索引的作用就相当于书的目录。打个比方:我们在查字典的时候,如果没有目录,那我们就只能一页一页地去找我们需要查的那个字,速度很慢;如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
|
||||
|
||||
索引底层数据结构存在很多种类型,常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
|
||||
索引底层数据结构存在很多种类型,常见的索引结构有:B 树、 B+ 树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyISAM,都使用了 B+ 树作为索引结构。
|
||||
|
||||
## 索引的优缺点
|
||||
|
||||
**优点**:
|
||||
|
||||
- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量), 减少 IO 次数,这也是创建索引的最主要的原因。
|
||||
- 使用索引可以大大加快数据的检索速度(大大减少检索的数据量),减少 IO 次数,这也是创建索引的最主要的原因。
|
||||
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
||||
|
||||
**缺点**:
|
||||
|
||||
- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
|
||||
- 创建和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态地修改,这会降低 SQL 执行效率。
|
||||
- 索引需要使用物理文件存储,也会耗费一定空间。
|
||||
|
||||
但是,**使用索引一定能提高查询性能吗?**
|
||||
@ -39,7 +39,7 @@ tag:
|
||||
|
||||
### Hash 表
|
||||
|
||||
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
|
||||
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
|
||||
|
||||
**为何能够通过 key 快速取出 value 呢?** 原因在于 **哈希算法**(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
|
||||
|
||||
@ -50,7 +50,7 @@ index = hash % array_size
|
||||
|
||||

|
||||
|
||||
但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了减少链表过长的时候搜索时间过长引入了红黑树。
|
||||
但是!哈希算法有个 **Hash 冲突** 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 **链地址法**。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 `HashMap` 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后`HashMap`为了提高链表过长时的搜索效率,引入了红黑树。
|
||||
|
||||

|
||||
|
||||
@ -60,15 +60,15 @@ MySQL 的 InnoDB 存储引擎不直接支持常规的哈希索引,但是,Inn
|
||||
|
||||
既然哈希表这么快,**为什么 MySQL 没有使用其作为索引的数据结构呢?** 主要是因为 Hash 索引不支持顺序和范围查询。假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。并且,每次 IO 只能取一个。
|
||||
|
||||
试想一种情况:
|
||||
试想一种情况:
|
||||
|
||||
```java
|
||||
SELECT * FROM tb1 WHERE id < 500;
|
||||
```
|
||||
|
||||
在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
|
||||
在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
|
||||
|
||||
### 二叉查找树(BST)
|
||||
### 二叉查找树(BST)
|
||||
|
||||
二叉查找树(Binary Search Tree)是一种基于二叉树的数据结构,它具有以下特点:
|
||||
|
||||
@ -76,7 +76,7 @@ SELECT * FROM tb1 WHERE id < 500;
|
||||
2. 右子树所有节点的值均大于根节点的值。
|
||||
3. 左右子树也分别为二叉查找树。
|
||||
|
||||
当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。
|
||||
当二叉查找树是平衡的时候,也就是树的每个节点的左右子树深度相差不超过 1 的时候,查询的时间复杂度为 O(log2(N)),具有比较高的效率。然而,当二叉查找树不平衡时,例如在最坏情况下(有序插入节点),树会退化成线性链表(也被称为斜树),导致查询效率急剧下降,时间复杂退化为 O(N)。
|
||||
|
||||

|
||||
|
||||
@ -92,7 +92,7 @@ AVL 树是计算机科学中最早被发明的自平衡二叉查找树,它的
|
||||
|
||||
AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL 旋转、RR 旋转、LR 旋转和 RL 旋转。其中 LL 旋转和 RR 旋转分别用于处理左左和右右失衡,而 LR 旋转和 RL 旋转则用于处理左右和右左失衡。
|
||||
|
||||
由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。 **磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
|
||||
由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了数据库写操作的性能。并且, 在使用 AVL 树时,每个树节点仅存储一个数据,而每次进行磁盘 IO 时只能读取一个节点的数据,如果需要查询的数据分布在多个节点上,那么就需要进行多次磁盘 IO。**磁盘 IO 是一项耗时的操作,在设计数据库索引时,我们需要优先考虑如何最大限度地减少磁盘 IO 操作的次数。**
|
||||
|
||||
实际应用中,AVL 树使用的并不多。
|
||||
|
||||
@ -112,26 +112,26 @@ AVL 树采用了旋转操作来保持平衡。主要有四种旋转操作:LL
|
||||
|
||||
**红黑树的应用还是比较广泛的,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底层都用到了红黑树。对于数据在内存中的这种情况来说,红黑树的表现是非常优异的。**
|
||||
|
||||
### B 树& B+树
|
||||
### B 树& B+ 树
|
||||
|
||||
B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 `Balanced` (平衡)的意思。
|
||||
B 树也称 B- 树,全称为 **多路平衡查找树**,B+ 树是 B 树的一种变体。B 树和 B+ 树中的 B 是 `Balanced`(平衡)的意思。
|
||||
|
||||
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
|
||||
|
||||
**B 树& B+树两者有何异同呢?**
|
||||
**B 树& B+ 树两者有何异同呢?**
|
||||
|
||||
- B 树的所有节点既存放键(key) 也存放数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
|
||||
- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
|
||||
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
|
||||
- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
|
||||
- B 树的所有节点既存放键(key)也存放数据(data),而 B+ 树只有叶子节点存放 key 和 data,其他内节点只存放 key。
|
||||
- B 树的叶子节点都是独立的;B+ 树的叶子节点有一条引用链指向与它相邻的叶子节点。
|
||||
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+ 树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
|
||||
- 在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+ 树的范围查询,只需要对链表进行遍历即可。
|
||||
|
||||
综上,B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
|
||||
综上,B+ 树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
|
||||
|
||||
在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。(下面的内容整理自《Java 工程师修炼之道》)
|
||||
|
||||
> MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“**非聚簇索引(非聚集索引)**”。
|
||||
>
|
||||
> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引** ,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
|
||||
> InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“**聚簇索引(聚集索引)**”,而其余的索引都作为 **辅助索引**,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
|
||||
|
||||
## 索引类型总结
|
||||
|
||||
@ -140,12 +140,12 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
|
||||
- BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree,但二者实现方式不一样(前面已经介绍了)。
|
||||
- 哈希索引:类似键值对的形式,一次即可定位。
|
||||
- RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
|
||||
- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
|
||||
- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
|
||||
|
||||
按照底层存储方式角度划分:
|
||||
|
||||
- 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
|
||||
- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
|
||||
- 非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
|
||||
|
||||
按照应用维度划分:
|
||||
|
||||
@ -154,7 +154,7 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一
|
||||
- 唯一索引:加速查询 + 列值唯一(可以有 NULL)。
|
||||
- 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
|
||||
- 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
|
||||
- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
|
||||
- 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR`、`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
|
||||
- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
|
||||
|
||||
MySQL 8.x 中实现的索引新特性:
|
||||
@ -163,7 +163,7 @@ MySQL 8.x 中实现的索引新特性:
|
||||
- 降序索引:之前的版本就支持通过 desc 来指定索引为降序,但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。
|
||||
- 函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
|
||||
|
||||
## 主键索引(Primary Key)
|
||||
## 主键索引(Primary Key)
|
||||
|
||||
数据表的主键列使用的就是主键索引。
|
||||
|
||||
@ -177,16 +177,16 @@ MySQL 8.x 中实现的索引新特性:
|
||||
|
||||
二级索引(Secondary Index)的叶子节点存储的数据是主键的值,也就是说,通过二级索引可以定位主键的位置,二级索引又称为辅助索引/非主键索引。
|
||||
|
||||
唯一索引,普通索引,前缀索引等索引都属于二级索引。
|
||||
唯一索引、普通索引、前缀索引等索引都属于二级索引。
|
||||
|
||||
PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
|
||||
PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。
|
||||
|
||||
1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
|
||||
2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
|
||||
3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
|
||||
4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
|
||||
1. **唯一索引(Unique Key)**:唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
|
||||
2. **普通索引(Index)**:普通索引的唯一作用就是为了快速查询数据。一张表允许创建多个普通索引,并允许数据重复和 NULL。
|
||||
3. **前缀索引(Prefix)**:前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。
|
||||
4. **全文索引(Full Text)**:全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MyISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
|
||||
|
||||
二级索引:
|
||||
二级索引:
|
||||
|
||||

|
||||
|
||||
@ -198,25 +198,25 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
|
||||
|
||||
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
|
||||
|
||||
在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
||||
在 MySQL 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+ 树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
||||
|
||||
#### 聚簇索引的优缺点
|
||||
|
||||
**优点**:
|
||||
|
||||
- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
|
||||
- **查询速度非常快**:聚簇索引的查询速度非常的快,因为整个 B+ 树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
|
||||
- **对排序查找和范围查找优化**:聚簇索引对于主键的排序查找和范围查找速度非常快。
|
||||
|
||||
**缺点**:
|
||||
|
||||
- **依赖于有序的数据**:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
|
||||
- **依赖于有序的数据**:因为 B+ 树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
|
||||
- **更新代价大**:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
|
||||
|
||||
### 非聚簇索引(非聚集索引)
|
||||
|
||||
#### 非聚簇索引介绍
|
||||
|
||||
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
|
||||
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
|
||||
|
||||
非聚簇索引的叶子节点并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。
|
||||
|
||||
@ -224,22 +224,22 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
|
||||
|
||||
**优点**:
|
||||
|
||||
更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
|
||||
更新代价比聚簇索引要小。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的。
|
||||
|
||||
**缺点**:
|
||||
|
||||
- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
|
||||
- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
|
||||
- **依赖于有序的数据**:跟聚簇索引一样,非聚簇索引也依赖于有序的数据。
|
||||
- **可能会二次查询(回表)**:这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
|
||||
|
||||
这是 MySQL 的表的文件截图:
|
||||
这是 MySQL 的表的文件截图:
|
||||
|
||||

|
||||
|
||||
聚簇索引和非聚簇索引:
|
||||
聚簇索引和非聚簇索引:
|
||||
|
||||

|
||||
|
||||
#### 非聚簇索引一定回表查询吗(覆盖索引)?
|
||||
#### 非聚簇索引一定回表查询吗(覆盖索引)?
|
||||
|
||||
**非聚簇索引不一定回表查询。**
|
||||
|
||||
@ -251,7 +251,7 @@ PS: 不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,
|
||||
|
||||
那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
|
||||
|
||||
即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
|
||||
即使是 MyISAM 也是这样,虽然 MyISAM 的主键索引确实需要回表,因为它的主键索引的叶子节点存放的是指针。但是!**如果 SQL 查的就是主键呢?**
|
||||
|
||||
```sql
|
||||
SELECT id FROM table WHERE id=1;
|
||||
@ -263,7 +263,7 @@ SELECT id FROM table WHERE id=1;
|
||||
|
||||
### 覆盖索引
|
||||
|
||||
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)** 。
|
||||
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 **覆盖索引(Covering Index)**。
|
||||
|
||||
在 InnoDB 存储引擎中,非主键索引的叶子节点包含的是主键的值。这意味着,当使用非主键索引进行查询时,数据库会先找到对应的主键值,然后再通过主键索引来定位和检索完整的行数据。这个过程被称为“回表”。
|
||||
|
||||
@ -276,7 +276,7 @@ SELECT id FROM table WHERE id=1;
|
||||
|
||||
我们这里简单演示一下覆盖索引的效果。
|
||||
|
||||
1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便, `cus_order` 这张表只有 `id`、`score`、`name`这 3 个字段。
|
||||
1、创建一个名为 `cus_order` 的表,来实际测试一下这种排序方式。为了测试方便,`cus_order` 这张表只有 `id`、`score`、`name` 这 3 个字段。
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cus_order` (
|
||||
@ -320,7 +320,7 @@ CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据
|
||||
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
|
||||
```
|
||||
|
||||
使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort` ,我们发现是没有用到覆盖索引的。
|
||||
使用 `EXPLAIN` 命令分析这条 SQL 语句,通过 `Extra` 这一列的 `Using filesort`,我们发现是没有用到覆盖索引的。
|
||||
|
||||

|
||||
|
||||
@ -336,7 +336,7 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
|
||||
|
||||

|
||||
|
||||
通过 `Extra` 这一列的 `Using index` ,说明这条 SQL 语句成功使用了覆盖索引。
|
||||
通过 `Extra` 这一列的 `Using index`,说明这条 SQL 语句成功使用了覆盖索引。
|
||||
|
||||
关于 `EXPLAIN` 命令的详细介绍请看:[MySQL 执行计划分析](./mysql-query-execution-plan.md)这篇文章。
|
||||
|
||||
@ -356,13 +356,13 @@ ALTER TABLE `cus_order` ADD INDEX id_score_name(score, name);
|
||||
|
||||
最左匹配原则会一直向右匹配,直到遇到范围查询(如 >、<)为止。对于 >=、<=、BETWEEN 以及前缀匹配 LIKE 的范围查询,不会停止匹配(相关阅读:[联合索引的最左匹配原则全网都在说的一个错误结论](https://mp.weixin.qq.com/s/8qemhRg5MgXs1So5YCv0fQ))。
|
||||
|
||||
假设有一个联合索引`(column1, column2, column3)`,其从左到右的所有前缀为`(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
|
||||
假设有一个联合索引 `(column1, column2, column3)`,其从左到右的所有前缀为 `(column1)`、`(column1, column2)`、`(column1, column2, column3)`(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。
|
||||
|
||||
我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
|
||||
|
||||
我们这里简单演示一下最左前缀匹配的效果。
|
||||
|
||||
1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class`这 3 个字段。
|
||||
1、创建一个名为 `student` 的表,这张表只有 `id`、`name`、`class` 这 3 个字段。
|
||||
|
||||
```sql
|
||||
CREATE TABLE `student` (
|
||||
@ -386,21 +386,21 @@ EXPLAIN SELECT * FROM student WHERE name = 'Anne Henry' AND class = 'lIrm08RYVk'
|
||||
SELECT * FROM student WHERE class = 'lIrm08RYVk';
|
||||
```
|
||||
|
||||
再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1`会走索引么?`c=1` 呢?`b=1 AND c=1`呢?
|
||||
再来看一个常见的面试题:如果有索引 `联合索引(a,b,c)`,查询 `a=1 AND c=1` 会走索引么?`c=1` 呢?`b=1 AND c=1` 呢?
|
||||
|
||||
先不要往下看答案,给自己 3 分钟时间想一想。
|
||||
|
||||
1. 查询 `a=1 AND c=1`:根据最左前缀匹配原则,查询可以使用索引的前缀部分。因此,该查询仅在 `a=1` 上使用索引,然后对结果进行 `c=1` 的过滤。
|
||||
2. 查询 `c=1` :由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
|
||||
3. 查询`b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
|
||||
2. 查询 `c=1`:由于查询中不包含最左列 `a`,根据最左前缀匹配原则,整个索引都无法被使用。
|
||||
3. 查询 `b=1 AND c=1`:和第二种一样的情况,整个索引都不会使用。
|
||||
|
||||
MySQL 8.0.13 版本引入了索引跳跃扫描(Index Skip Scan,简称 ISS),它可以在某些索引查询场景下提高查询效率。在没有 ISS 之前,不满足最左前缀匹配原则的联合索引查询中会执行全表扫描。而 ISS 允许 MySQL 在某些情况下避免全表扫描,即使查询条件不符合最左前缀。不过,这个功能比较鸡肋, 和 Oracle 中的没法比,MySQL 8.0.31 还报告了一个 bug:[Bug #109145 Using index for skip scan cause incorrect result](https://bugs.mysql.com/bug.php?id=109145)(后续版本已经修复)。个人建议知道有这个东西就好,不需要深究,实际项目也不一定能用上。
|
||||
|
||||
## 索引下推
|
||||
|
||||
**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE`字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。
|
||||
**索引下推(Index Condition Pushdown,简称 ICP)** 是 **MySQL 5.6** 版本中提供的一项索引优化功能,它允许存储引擎在索引遍历过程中,执行部分 `WHERE` 字句的判断条件,直接过滤掉不满足条件的记录,从而减少回表次数,提高查询效率。
|
||||
|
||||
假设我们有一个名为 `user` 的表,其中包含 `id`, `username`, `zipcode`和 `birthdate` 4 个字段,创建了联合索引`(zipcode, birthdate)`。
|
||||
假设我们有一个名为 `user` 的表,其中包含 `id`、`username`、`zipcode` 和 `birthdate` 4 个字段,创建了联合索引 `(zipcode, birthdate)`。
|
||||
|
||||
```sql
|
||||
CREATE TABLE `user` (
|
||||
@ -417,7 +417,7 @@ SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3;
|
||||
```
|
||||
|
||||
- 没有索引下推之前,即使 `zipcode` 字段利用索引可以帮助我们快速定位到 `zipcode = '431200'` 的用户,但我们仍然需要对每一个找到的用户进行回表操作,获取完整的用户数据,再去判断 `MONTH(birthdate) = 3`。
|
||||
- 有了索引下推之后,存储引擎会在使用`zipcode` 字段索引查找`zipcode = '431200'` 的用户时,同时判断`MONTH(birthdate) = 3`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。
|
||||
- 有了索引下推之后,存储引擎会在使用 `zipcode` 字段索引查找 `zipcode = '431200'` 的用户时,同时判断 `MONTH(birthdate) = 3`。这样,只有同时满足条件的记录才会被返回,减少了回表次数。
|
||||
|
||||

|
||||
|
||||
@ -429,14 +429,14 @@ SELECT * FROM user WHERE zipcode = '431200' AND MONTH(birthdate) = 3;
|
||||
|
||||
MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处理查询解析、分析、优化、缓存以及与客户端的交互等操作,而存储引擎层负责数据的存储和读取,MySQL 支持 InnoDB、MyISAM、Memory 等多种存储引擎。
|
||||
|
||||
索引下推的**下推**其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。
|
||||
索引下推的 **下推** 其实就是指将部分上层(Server 层)负责的事情,交给了下层(存储引擎层)去处理。
|
||||
|
||||
我们这里结合索引下推原理再对上面提到的例子进行解释。
|
||||
|
||||
没有索引下推之前:
|
||||
|
||||
- 存储引擎层先根据 `zipcode` 索引字段找到所有 `zipcode = '431200'` 的用户的主键 ID,然后二次回表查询,获取完整的用户数据;
|
||||
- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据`MONTH(birthdate) = 3`这一条件再进一步做筛选。
|
||||
- 存储引擎层把所有 `zipcode = '431200'` 的用户数据全部交给 Server 层,Server 层根据 `MONTH(birthdate) = 3` 这一条件再进一步做筛选。
|
||||
|
||||
有了索引下推之后:
|
||||
|
||||
@ -449,8 +449,8 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
最后,总结一下索引下推应用范围:
|
||||
|
||||
1. 适用于 InnoDB 引擎和 MyISAM 引擎的查询。
|
||||
2. 适用于执行计划是 range, ref, eq_ref, ref_or_null 的范围查询。
|
||||
3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推 不会减少 I/O。
|
||||
2. 适用于执行计划是 range、ref、eq_ref、ref_or_null 的范围查询。
|
||||
3. 对于 InnoDB 表,仅用于非聚簇索引。索引下推的目标是减少全行读取次数,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经读入 InnoDB 缓冲区。在这种情况下使用索引下推不会减少 I/O。
|
||||
4. 子查询不能使用索引下推,因为子查询通常会创建临时表来处理结果,而这些临时表是没有索引的。
|
||||
5. 存储过程不能使用索引下推,因为存储引擎无法调用存储函数。
|
||||
|
||||
@ -458,7 +458,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
|
||||
### 选择合适的字段创建索引
|
||||
|
||||
- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
|
||||
- **不为 NULL 的字段**:索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0、1、true、false 这样语义较为清晰的短值或短字符作为替代。
|
||||
- **被频繁查询的字段**:我们创建索引的字段应该是查询操作非常频繁的字段。
|
||||
- **被作为条件查询的字段**:被作为 WHERE 条件查询的字段,应该被考虑建立索引。
|
||||
- **频繁需要排序的字段**:索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
|
||||
@ -470,7 +470,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
|
||||
### 限制每张表上的索引数量
|
||||
|
||||
索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率同样可以降低效率。
|
||||
索引并不是越多越好,建议单张表索引不超过 5 个!索引可以提高效率,同样可以降低效率。
|
||||
|
||||
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
|
||||
|
||||
@ -478,11 +478,11 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
|
||||
### 尽可能的考虑建立联合索引而不是单列索引
|
||||
|
||||
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
|
||||
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+ 树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
|
||||
|
||||
### 注意避免冗余索引
|
||||
|
||||
冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
|
||||
冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city)和(name)这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的。在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
|
||||
|
||||
### 字符串类型的字段使用前缀索引代替普通索引
|
||||
|
||||
@ -492,13 +492,13 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
|
||||
索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些:
|
||||
|
||||
- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
|
||||
- 创建了组合索引,但查询条件未遵守最左匹配原则;
|
||||
- 在索引列上进行计算、函数、类型转换等操作;
|
||||
- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
|
||||
- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
|
||||
- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
|
||||
- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
|
||||
- ~~使用 `SELECT *` 进行查询;~~ `SELECT *` 不会直接导致索引失效(如果不走索引大概率是因为 where 查询范围过大导致的),但它可能会带来一些其他的性能问题比如造成网络传输和数据处理的浪费、无法使用索引覆盖;
|
||||
- 创建了组合索引,但查询条件未遵守最左匹配原则;
|
||||
- 在索引列上进行计算、函数、类型转换等操作;
|
||||
- 以 % 开头的 LIKE 查询比如 `LIKE '%abc';`;
|
||||
- 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
|
||||
- IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同);
|
||||
- 发生[隐式转换](https://javaguide.cn/database/mysql/index-invalidation-caused-by-implicit-conversion.html);
|
||||
- ……
|
||||
|
||||
推荐阅读这篇文章:[美团暑期实习一面:MySQl 索引失效的场景有哪些?](https://mp.weixin.qq.com/s/mwME3qukHBFul57WQLkOYg)。
|
||||
|
@ -30,22 +30,22 @@ Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两
|
||||
|
||||

|
||||
|
||||
全世界有非常多的网站使用到了 Redis ,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis) ,感兴趣的话可以看看。
|
||||
全世界有非常多的网站使用到了 Redis,[techstacks.io](https://techstacks.io/) 专门维护了一个[使用 Redis 的热门站点列表](https://techstacks.io/tech/redis),感兴趣的话可以看看。
|
||||
|
||||
### Redis 为什么这么快?
|
||||
|
||||
Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
|
||||
Redis 内部做了非常多的性能优化,比较重要的有下面 4 点:
|
||||
|
||||
1. Redis 基于内存,内存的访问速度比磁盘快很多;
|
||||
2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
|
||||
3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
|
||||
3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高;
|
||||
4. Redis 通信协议实现简单且解析高效。
|
||||
|
||||
> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770) 。
|
||||
> 下面这张图片总结的挺不错的,分享一下,出自 [Why is Redis so fast?](https://twitter.com/alexxubyte/status/1498703822528544770)。
|
||||
|
||||

|
||||
|
||||
那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高且 Redis 提供的数据持久化仍然有数据丢失的风险。
|
||||
那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。
|
||||
|
||||
### 除了 Redis,你还知道其他分布式缓存方案吗?
|
||||
|
||||
@ -62,16 +62,16 @@ Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
|
||||
|
||||
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
|
||||
|
||||
有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [**Tendis**](https://github.com/Tencent/Tendis) 。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ) ,可以简单参考一下。
|
||||
有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 [**Tendis**](https://github.com/Tencent/Tendis)。Tendis 基于知名开源项目 [RocksDB](https://github.com/facebook/rocksdb) 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:[Redis vs Tendis:冷热混合存储版架构揭秘](https://mp.weixin.qq.com/s/MeYkfOIdnU6LYlsGb24KjQ),可以简单参考一下。
|
||||
|
||||
不过,从 Tendis 这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。
|
||||
|
||||
目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的):
|
||||
|
||||
- [Dragonfly](https://github.com/dragonflydb/dragonfly):一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
|
||||
- [KeyDB](https://github.com/Snapchat/KeyDB): Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
|
||||
- [KeyDB](https://github.com/Snapchat/KeyDB):Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
|
||||
|
||||
不过,个人还是建议分布式缓存首选 Redis ,毕竟经过这么多年的生考验,生态也这么优秀,资料也很全面!
|
||||
不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面!
|
||||
|
||||
PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。
|
||||
|
||||
@ -87,10 +87,10 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详
|
||||
|
||||
**区别**:
|
||||
|
||||
1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
|
||||
2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制而 Memcached 没有。
|
||||
3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 自 3.0 版本起是原生支持集群模式的。
|
||||
4. **线程模型**:Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
|
||||
1. **数据类型**:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而Memcached 只支持最简单的 k/v 数据类型。
|
||||
2. **数据持久化**:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制,而 Memcached 没有。
|
||||
3. **集群模式支持**:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;而 Redis 自 3.0 版本起是原生支持集群模式的。
|
||||
4. **线程模型**:Memcached 是多线程、非阻塞 IO 复用的网络模型;而 Redis 使用单线程的多路 IO 复用模型(Redis 6.0 针对网络数据的读写引入了多线程)。
|
||||
5. **特性支持**:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
|
||||
6. **过期数据删除**:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
|
||||
|
||||
@ -104,7 +104,7 @@ PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详
|
||||
|
||||
**2、高并发**
|
||||
|
||||
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
|
||||
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
|
||||
|
||||
> QPS(Query Per Second):服务器每秒可以执行的查询次数;
|
||||
|
||||
@ -126,7 +126,7 @@ Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、
|
||||
|
||||
### 常见的缓存读写策略有哪些?
|
||||
|
||||
关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html) 。
|
||||
关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:[3 种常用的缓存读写策略详解](https://javaguide.cn/database/redis/3-commonly-used-cache-read-and-write-strategies.html)。
|
||||
|
||||
### 什么是 Redis Module?有什么用?
|
||||
|
||||
@ -151,17 +151,17 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
|
||||
|
||||
### Redis 除了做缓存,还能做什么?
|
||||
|
||||
- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。
|
||||
- **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html)。
|
||||
- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
|
||||
- **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
|
||||
- **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
|
||||
- **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
|
||||
- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。
|
||||
- **分布式 Session**:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
|
||||
- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。
|
||||
- ……
|
||||
|
||||
### 如何基于 Redis 实现分布式锁?
|
||||
|
||||
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) 。
|
||||
关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)。
|
||||
|
||||
### Redis 可以做消息队列么?
|
||||
|
||||
@ -171,7 +171,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
|
||||
|
||||
**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。**
|
||||
|
||||
通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列:
|
||||
通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 即可实现简易版消息队列:
|
||||
|
||||
```bash
|
||||
# 生产者生产消息
|
||||
@ -184,7 +184,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特
|
||||
"msg1"
|
||||
```
|
||||
|
||||
不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。
|
||||
不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP` 这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。
|
||||
|
||||
因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息
|
||||
|
||||
@ -216,11 +216,11 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
|
||||
|
||||
为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持:
|
||||
|
||||
- 发布 / 订阅模式
|
||||
- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念)
|
||||
- 消息持久化( RDB 和 AOF)
|
||||
- ACK 机制(通过确认机制来告知已经成功处理了消息)
|
||||
- 阻塞式获取消息
|
||||
- 发布 / 订阅模式;
|
||||
- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念);
|
||||
- 消息持久化( RDB 和 AOF);
|
||||
- ACK 机制(通过确认机制来告知已经成功处理了消息);
|
||||
- 阻塞式获取消息。
|
||||
|
||||
`Stream` 的结构如下:
|
||||
|
||||
@ -230,7 +230,7 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
|
||||
|
||||
这里再对图中涉及到的一些概念,进行简单解释:
|
||||
|
||||
- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费
|
||||
- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。
|
||||
- `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。
|
||||
- `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。
|
||||
|
||||
@ -245,25 +245,25 @@ pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不
|
||||
- `XTRIM`:修剪流的长度,可以指定修建策略(`MAXLEN`/`MINID`)。
|
||||
- `XLEN`:获取流的长度。
|
||||
- `XGROUP CREATE`:创建消费者组。
|
||||
- `XGROUP DESTROY` : 删除消费者组
|
||||
- `XGROUP DESTROY`:删除消费者组。
|
||||
- `XGROUP DELCONSUMER`:从消费者组中删除一个消费者。
|
||||
- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID
|
||||
- `XGROUP SETID`:为消费者组设置新的最后递送消息 ID。
|
||||
- `XACK`:确认消费组中的消息已被处理。
|
||||
- `XPENDING`:查询消费组中挂起(未确认)的消息。
|
||||
- `XCLAIM`:将挂起的消息从一个消费者转移到另一个消费者。
|
||||
- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。
|
||||
- `XINFO`:获取流(`XINFO STREAM`)、消费组(`XINFO GROUPS`)或消费者(`XINFO CONSUMERS`)的详细信息。
|
||||
|
||||
`Stream` 使用起来相对要麻烦一些,这里就不演示了。
|
||||
|
||||
总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。
|
||||
总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决,比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。
|
||||
|
||||
综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。
|
||||
综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方,比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列,比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。
|
||||
|
||||
相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw)。
|
||||
|
||||
### Redis 可以做搜索引擎么?
|
||||
|
||||
Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch** ,这是一个基于 Redis 的搜索引擎模块。
|
||||
Redis 是可以实现全文搜索引擎功能的,需要借助 **RediSearch**,这是一个基于 Redis 的搜索引擎模块。
|
||||
|
||||
RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。
|
||||
|
||||
@ -274,7 +274,7 @@ RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检
|
||||
|
||||
对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。
|
||||
|
||||
对于比较复杂或者数据规模较大的搜索场景还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
|
||||
对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
|
||||
|
||||
1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。
|
||||
2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。
|
||||
@ -292,10 +292,10 @@ Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合
|
||||
|
||||
基于 Redis 实现延时任务的功能无非就下面两种方案:
|
||||
|
||||
1. Redis 过期事件监听
|
||||
2. Redisson 内置的延时队列
|
||||
1. Redis 过期事件监听。
|
||||
2. Redisson 内置的延时队列。
|
||||
|
||||
Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
|
||||
Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
|
||||
|
||||
Redisson 内置的延时队列具备下面这些优势:
|
||||
|
||||
@ -306,7 +306,7 @@ Redisson 内置的延时队列具备下面这些优势:
|
||||
|
||||
## Redis 数据类型
|
||||
|
||||
关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :
|
||||
关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/):
|
||||
|
||||
- [Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)
|
||||
- [Redis 3 种特殊数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-02.html)
|
||||
@ -328,7 +328,7 @@ String 的常见应用场景如下:
|
||||
|
||||
- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存;
|
||||
- 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数;
|
||||
- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
|
||||
- 分布式锁(利用 `SETNX key value` 命令可以实现一个最简易的分布式锁);
|
||||
- ……
|
||||
|
||||
关于 String 的详细介绍请看这篇文章:[Redis 5 种基本数据类型详解](https://javaguide.cn/database/redis/redis-data-structures-01.html)。
|
||||
@ -349,11 +349,11 @@ String 的常见应用场景如下:
|
||||
|
||||
### String 的底层实现是什么?
|
||||
|
||||
Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串) 来作为底层实现。
|
||||
Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 `\0` 结尾的字符数组),而是自己编写了 [SDS](https://github.com/antirez/sds)(Simple Dynamic String,简单动态字符串)来作为底层实现。
|
||||
|
||||
SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。
|
||||
|
||||
Redis7.0 的 SDS 的部分源码如下(<https://github.com/redis/redis/blob/7.0/src/sds.h>):
|
||||
Redis7.0 的 SDS 的部分源码如下(<https://github.com/redis/redis/blob/7.0/src/sds.h>):
|
||||
|
||||
```c
|
||||
/* Note: sdshdr5 is never used, we just access the flags byte directly.
|
||||
@ -388,7 +388,7 @@ struct __attribute__ ((__packed__)) sdshdr64 {
|
||||
};
|
||||
```
|
||||
|
||||
通过源码可以看出,SDS 共有五种实现方式 SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
|
||||
通过源码可以看出,SDS 共有五种实现方式:SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
|
||||
|
||||
| 类型 | 字节 | 位 |
|
||||
| -------- | ---- | --- |
|
||||
@ -400,10 +400,10 @@ struct __attribute__ ((__packed__)) sdshdr64 {
|
||||
|
||||
对于后四种实现都包含了下面这 4 个属性:
|
||||
|
||||
- `len`:字符串的长度也就是已经使用的字节数
|
||||
- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小
|
||||
- `buf[]`:实际存储字符串的数组
|
||||
- `flags`:低三位保存类型标志
|
||||
- `len`:字符串的长度也就是已经使用的字节数。
|
||||
- `alloc`:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小。
|
||||
- `buf[]`:实际存储字符串的数组。
|
||||
- `flags`:低三位保存类型标志。
|
||||
|
||||
SDS 相比于 C 语言中的字符串有如下提升:
|
||||
|
||||
@ -445,9 +445,9 @@ struct sdshdr {
|
||||
|
||||
### 使用 Redis 实现一个排行榜怎么做?
|
||||
|
||||
Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
|
||||
Redis 中有一个叫做 `Sorted Set`(有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
|
||||
|
||||
相关的一些 Redis 命令: `ZRANGE` (从小到大排序)、 `ZREVRANGE` (从大到小排序)、`ZREVRANK` (指定元素排名)。
|
||||
相关的一些 Redis 命令:`ZRANGE`(从小到大排序)、`ZREVRANGE`(从大到小排序)、`ZREVRANK`(指定元素排名)。
|
||||
|
||||

|
||||
|
||||
@ -455,15 +455,15 @@ Redis 中有一个叫做 `Sorted Set` (有序集合)的数据类型经常被
|
||||
|
||||

|
||||
|
||||
### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?
|
||||
### Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树?
|
||||
|
||||
这道面试题很多大厂比较喜欢问,难度还是有点大的。
|
||||
|
||||
- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 **O(log n)**。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。
|
||||
- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
|
||||
- B+树 vs 跳表:B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。
|
||||
- B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。
|
||||
|
||||
另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握 :[Redis 为什么用跳表实现有序集合](./redis-skiplist.md)。
|
||||
另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握:[Redis 为什么用跳表实现有序集合](./redis-skiplist.md)。
|
||||
|
||||
### Set 的应用场景是什么?
|
||||
|
||||
@ -471,8 +471,8 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但
|
||||
|
||||
`Set` 的常见应用场景如下:
|
||||
|
||||
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog`更适合一些)、文章点赞、动态点赞等等。
|
||||
- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
|
||||
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 `HyperLogLog` 更适合一些)、文章点赞、动态点赞等等。
|
||||
- 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。
|
||||
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
|
||||
|
||||
### 使用 Set 实现抽奖系统怎么做?
|
||||
@ -481,11 +481,11 @@ Redis 中 `Set` 是一种无序集合,集合中的元素没有先后顺序但
|
||||
|
||||
- `SADD key member1 member2 ...`:向指定集合添加一个或多个元素。
|
||||
- `SPOP key count`:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
|
||||
- `SRANDMEMBER key count` : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
|
||||
- `SRANDMEMBER key count`:随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
|
||||
|
||||
### 使用 Bitmap 统计活跃用户怎么做?
|
||||
|
||||
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
|
||||
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
|
||||
|
||||
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
|
||||
|
||||
@ -504,7 +504,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
|
||||
(integer) 0
|
||||
```
|
||||
|
||||
统计 20210308~20210309 总活跃用户数:
|
||||
统计 20210308~20210309 总活跃用户数:
|
||||
|
||||
```bash
|
||||
> BITOP and desk1 20210308 20210309
|
||||
@ -513,7 +513,7 @@ Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需
|
||||
(integer) 1
|
||||
```
|
||||
|
||||
统计 20210308~20210309 在线活跃用户数:
|
||||
统计 20210308~20210309 在线活跃用户数:
|
||||
|
||||
```bash
|
||||
> BITOP or desk2 20210308 20210309
|
||||
@ -543,15 +543,15 @@ PFCOUNT PAGE_1:UV
|
||||
|
||||
## Redis 持久化机制(重要)
|
||||
|
||||
Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html) 。
|
||||
Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:[Redis 持久化机制详解](https://javaguide.cn/database/redis/redis-persistence.html)。
|
||||
|
||||
## Redis 线程模型(重要)
|
||||
|
||||
对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
|
||||
对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
|
||||
|
||||
### Redis 单线程模型了解吗?
|
||||
|
||||
**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型** (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
|
||||
**Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型**(Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
|
||||
|
||||
《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。
|
||||
|
||||
@ -577,7 +577,7 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接
|
||||
|
||||

|
||||
|
||||
相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/) 。
|
||||
相关阅读:[Redis 事件机制详解](http://remcarpediem.net/article/1aa2da89/)。
|
||||
|
||||
### Redis6.0 之前为什么不使用多线程?
|
||||
|
||||
@ -598,10 +598,10 @@ Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接
|
||||
**那 Redis6.0 之前为什么不使用多线程?** 我觉得主要原因有 3 点:
|
||||
|
||||
- 单线程编程容易并且更容易维护;
|
||||
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
|
||||
- Redis 的性能瓶颈不在 CPU,主要在内存和网络;
|
||||
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
|
||||
|
||||
相关阅读:[为什么 Redis 选择单线程模型?](https://draveness.me/whys-the-design-redis-single-thread/) 。
|
||||
相关阅读:[为什么 Redis 选择单线程模型?](https://draveness.me/whys-the-design-redis-single-thread/)。
|
||||
|
||||
### Redis6.0 之后为何引入了多线程?
|
||||
|
||||
@ -620,13 +620,13 @@ io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建
|
||||
- io-threads 的个数一旦设置,不能通过 config 动态设置。
|
||||
- 当设置 ssl 后,io-threads 将不工作。
|
||||
|
||||
开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf` :
|
||||
开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf`:
|
||||
|
||||
```bash
|
||||
io-threads-do-reads yes
|
||||
```
|
||||
|
||||
但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启
|
||||
但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。
|
||||
|
||||
相关阅读:
|
||||
|
||||
@ -638,8 +638,8 @@ io-threads-do-reads yes
|
||||
我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:
|
||||
|
||||
- 通过 `bio_close_file` 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
|
||||
- 通过 `bio_aof_fsync` 后台线程调用 `fsync` 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
|
||||
- 通过 `bio_lazy_free`后台线程释放大对象(已删除)占用的内存空间.
|
||||
- 通过 `bio_aof_fsync` 后台线程调用 `fsync` 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘(AOF 文件)。
|
||||
- 通过 `bio_lazy_free` 后台线程释放大对象(已删除)占用的内存空间.
|
||||
|
||||
在`bio.h` 文件中有定义(Redis 6.0 版本,源码地址:<https://github.com/redis/redis/blob/6.0/src/bio.h>):
|
||||
|
||||
@ -685,7 +685,7 @@ OK
|
||||
(integer) 56
|
||||
```
|
||||
|
||||
注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外, `persist` 命令可以移除一个键的过期时间。
|
||||
注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 `setex` 外,其他方法都需要依靠 `expire` 命令来设置过期时间 。另外,`persist` 命令可以移除一个键的过期时间。
|
||||
|
||||
**过期时间除了有助于缓解内存的消耗,还有什么其他用么?**
|
||||
|
||||
@ -695,7 +695,7 @@ OK
|
||||
|
||||
### Redis 是如何判断数据是否过期的呢?
|
||||
|
||||
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
|
||||
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
|
||||
|
||||

|
||||
|
||||
@ -724,7 +724,7 @@ typedef struct redisDb {
|
||||
3. **延迟队列**:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
|
||||
4. **定时删除**:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
|
||||
|
||||
**Redis 采用的那种删除策略呢?**
|
||||
**Redis 采用的是那种删除策略呢?**
|
||||
|
||||
Redis 采用的是 **定期删除+惰性/懒汉式删除** 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。
|
||||
|
||||
@ -748,7 +748,7 @@ Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值
|
||||
|
||||
**每次随机抽查数量是多少?**
|
||||
|
||||
`expire.c`中定义了每次随机抽查的数量,Redis 7.2 版本为 20 ,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
|
||||
`expire.c` 中定义了每次随机抽查的数量,Redis 7.2 版本为 20,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
|
||||
|
||||
```c
|
||||
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
|
||||
@ -758,7 +758,7 @@ Redis 7.2 版本的执行时间阈值是 **25ms**,过期 key 比例设定值
|
||||
|
||||
在 Redis 中,定期删除的频率是由 **hz** 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。
|
||||
|
||||
hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
|
||||
hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会增加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
|
||||
|
||||
下面是 hz 参数的官方注释,我翻译了其中的重要信息(Redis 7.2 版本)。
|
||||
|
||||
@ -766,7 +766,7 @@ hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频
|
||||
|
||||
类似的参数还有一个 **dynamic-hz**,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,
|
||||
|
||||
这两个参数都在 Redis 配置文件 `redis.conf`中:
|
||||
这两个参数都在 Redis 配置文件 `redis.conf` 中:
|
||||
|
||||
```properties
|
||||
# 默认为 10
|
||||
@ -786,27 +786,27 @@ dynamic-hz yes
|
||||
因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:
|
||||
|
||||
1. 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
|
||||
2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
|
||||
2. 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且还需要引入并发控制。
|
||||
|
||||
### 大量 key 集中过期怎么办?
|
||||
|
||||
当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
|
||||
|
||||
- **请求延迟增加:** Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
|
||||
- **内存占用过高:** 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
|
||||
- **请求延迟增加**:Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
|
||||
- **内存占用过高**:过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
|
||||
|
||||
为了避免这些问题,可以采取以下方案:
|
||||
|
||||
1. **尽量避免 key 集中过期**: 在设置键的过期时间时尽量随机一点。
|
||||
2. **开启 lazy free 机制**: 修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
|
||||
1. **尽量避免 key 集中过期**:在设置键的过期时间时尽量随机一点。
|
||||
2. **开启 lazy free 机制**:修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
|
||||
|
||||
### Redis 内存淘汰策略了解么?
|
||||
|
||||
> 相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
|
||||
|
||||
Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过`redis.conf`的`maxmemory`参数来定义的。64 位操作系统下,`maxmemory` 默认为 0 ,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
|
||||
Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过 `redis.conf` 的 `maxmemory` 参数来定义的。64 位操作系统下,`maxmemory` 默认为 0,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
|
||||
|
||||
你可以使用命令 `config get maxmemory` 来查看 `maxmemory`的值。
|
||||
你可以使用命令 `config get maxmemory` 来查看 `maxmemory` 的值。
|
||||
|
||||
```bash
|
||||
> config get maxmemory
|
||||
@ -830,7 +830,7 @@ Redis 提供了 6 种内存淘汰策略:
|
||||
|
||||
`allkeys-xxx` 表示从所有的键值中淘汰数据,而 `volatile-xxx` 表示从设置了过期时间的键值中淘汰数据。
|
||||
|
||||
`config.c`中定义了内存淘汰策略的枚举数组:
|
||||
`config.c` 中定义了内存淘汰策略的枚举数组:
|
||||
|
||||
```c
|
||||
configEnum maxmemory_policy_enum[] = {
|
||||
@ -854,7 +854,7 @@ maxmemory-policy
|
||||
noeviction
|
||||
```
|
||||
|
||||
可以通过`config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。
|
||||
可以通过 `config set maxmemory-policy 内存淘汰策略` 命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 `redis.conf` 中的 `maxmemory-policy` 参数不会因为重启而失效,不过,需要重启之后修改才能生效。
|
||||
|
||||
```properties
|
||||
maxmemory-policy noeviction
|
||||
|
@ -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 -->
|
||||
|
@ -6,7 +6,8 @@ tag:
|
||||
---
|
||||
|
||||
> 本文由 JavaGuide 翻译自 [https://www.baeldung.com/jvm-parameters](https://www.baeldung.com/jvm-parameters),并对文章进行了大量的完善补充。
|
||||
>
|
||||
> 文档参数 [https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html)
|
||||
>
|
||||
> JDK 版本:1.8
|
||||
|
||||
## 1.概述
|
||||
@ -71,10 +72,10 @@ GC 调优策略中很重要的一条经验总结是这样说的:
|
||||
|
||||
另外,你还可以通过 **`-XX:NewRatio=<int>`** 来设置老年代与新生代内存的比值。
|
||||
|
||||
比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1:1,新生代占整个堆栈的 1/2。
|
||||
比如下面的参数就是设置新生代与老年代内存的比值为 2(默认值)。也就是说 young/old 所占比值为 2,新生代占整个堆栈的 2/3。
|
||||
|
||||
```plain
|
||||
-XX:NewRatio=1
|
||||
-XX:NewRatio=2
|
||||
```
|
||||
|
||||
### 2.3.显式指定永久代/元空间的大小
|
||||
|
Loading…
x
Reference in New Issue
Block a user