diff --git a/DB.md b/DB.md index 4a24762..7fd57da 100644 --- a/DB.md +++ b/DB.md @@ -33,7 +33,7 @@ 参考视频:https://www.bilibili.com/video/BV1zJ411M7TB -参考文章:https://time.geekbang.org/column/intro/139 +参考专栏:https://time.geekbang.org/column/intro/139 参考书籍:https://book.douban.com/subject/35231266/ @@ -127,205 +127,6 @@ MySQL 配置: -*** - - - -### 常用工具 - -#### mysql - -mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 - -```sh -mysql [options] [database] -``` - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器IP或域名 -* -P --port=#:指定连接端口 -* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 - -示例: - -```sh -mysql -h 127.0.0.1 -P 3306 -u root -p -mysql -uroot -p2143 db01 -e "select * from tb_book"; -``` - - - -*** - - - -#### admin - -mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 - -通过 `mysqladmin --help` 指令查看帮助文档 - -```sh -mysqladmin -uroot -p2143 create 'test01'; -``` - - - -*** - - - -#### binlog - -服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 - -```sh -mysqlbinlog [options] log-files1 log-files2 ... -``` - -* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 - -* -o --offset=#:忽略掉日志中的前 n 行命令。 - -* -r --result-file=name:将输出的文本格式日志输出到指定文件。 - -* -s --short-form:显示简单格式,省略掉一些信息。 - -* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 - -* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 - - - -*** - - - -#### dump - -##### 命令介绍 - -mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 - -```sh -mysqldump [options] db_name [tables] -mysqldump [options] --database/-B db1 [db2 db3...] -mysqldump [options] --all-databases/-A -``` - -连接选项: - -* -u --user=name:指定用户名 -* -p --password[=name]:指定密码 -* -h --host=name:指定服务器 IP 或域名 -* -P --port=#:指定连接端口 - -输出内容选项: - -* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 -* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) -* -n --no-create-db:不包含数据库的创建语句 -* -t --no-create-info:不包含数据表的创建语句 -* -d --no-data:不包含数据 -* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile - -示例: - -```sh -mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a -mysqldump -uroot -p2143 -T /tmp test city -``` - - - -*** - - - -##### 数据备份 - -命令行方式: - -* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 -* 恢复 - 1. 登录MySQL数据库:`mysql -u root p` - 2. 删除已经备份的数据库 - 3. 重新创建与备份数据库名称相同的数据库 - 4. 使用该数据库 - 5. 导入文件执行:`source 备份文件全路径` - -更多方式参考:https://time.geekbang.org/column/article/81925 - -图形化界面: - -* 备份 - - ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) - -* 恢复 - - ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) - - - - - -*** - - - -#### import - -mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 - -```sh -mysqlimport [options] db_name textfile1 [textfile2...] -``` - -示例: - -```sh -mysqlimport -uroot -p2143 test /tmp/city.txt -``` - -导入 sql 文件,可以使用 MySQL 中的 source 指令 : - -```mysql -source 文件全路径 -``` - - - -*** - - - -#### show - -mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 - -```sh -mysqlshow [options] [db_name [table_name [col_name]]] -``` - -* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) - -* -i:显示指定数据库或者指定表的状态信息 - -示例: - -```sh -#查询每个数据库的表的数量及表中记录的数量 -mysqlshow -uroot -p1234 --count -#查询test库中每个表中的字段书,及行数 -mysqlshow -uroot -p1234 test --count -#查询test库中book表的详细情况 -mysqlshow -uroot -p1234 test book --count -``` - - - *** @@ -334,7 +135,7 @@ mysqlshow -uroot -p1234 test book --count -## 体系结构 +## 体系架构 ### 整体架构 @@ -440,13 +241,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态 -*** +*** @@ -463,7 +264,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 1. 客户端发送一条查询给服务器 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段 3. 分析器进行 SQL 分析,再由优化器生成对应的执行计划 -4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 +4. 执行器根据优化器生成的执行计划,调用存储引擎的 API 来执行查询 5. 将结果返回给客户端 大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利 @@ -479,7 +280,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 ##### 缓存配置 -1. 查看当前的 MySQL 数据库是否支持查询缓存: +1. 查看当前 MySQL 数据库是否支持查询缓存: ```mysql SHOW VARIABLES LIKE 'have_query_cache'; -- YES @@ -598,7 +399,7 @@ SELECT * FROM t WHERE id = 1; 解析器:处理语法和解析查询,生成一课对应的解析树 * 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id -* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 +* 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 @@ -633,7 +434,7 @@ MySQL 中保存着两种统计数据: * innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 * innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 -MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 @@ -760,49 +561,50 @@ KILL CONNECTION id -**** +*** -## 单表操作 +### 常用工具 -### SQL +#### mysql -- SQL +mysql 不是指 mysql 服务,而是指 mysql 的客户端工具 - - Structured Query Language:结构化查询语言 - - 定义了操作所有关系型数据库的规则,每种数据库操作的方式可能会存在不一样的地方,称为“方言” +```sh +mysql [options] [database] +``` -- SQL 通用语法 +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器IP或域名 +* -P --port=#:指定连接端口 +* -e --execute=name:执行SQL语句并退出,在控制台执行SQL语句,而不用连接到数据库执行 - - SQL 语句可以单行或多行书写,以**分号结尾**。 - - 可使用空格和缩进来增强语句的可读性。 - - MySQL 数据库的 SQL 语句不区分大小写,**关键字建议使用大写**。 - - 数据库的注释: - - 单行注释:-- 注释内容 #注释内容(MySQL 特有) - - 多行注释:/* 注释内容 */ +示例: -- SQL 分类 +```sh +mysql -h 127.0.0.1 -P 3306 -u root -p +mysql -uroot -p2143 db01 -e "select * from tb_book"; +``` - - DDL(Data Definition Language)数据定义语言 - - 用来定义数据库对象:数据库,表,列等。关键字:create、drop,、alter 等 - - DML(Data Manipulation Language)数据操作语言 +*** - - 用来对数据库中表的数据进行增删改。关键字:insert、delete、update 等 - - DQL(Data Query Language)数据查询语言 - - 用来查询数据库中表的记录(数据)。关键字:select、where 等 +#### admin - - DCL(Data Control Language)数据控制语言 +mysqladmin 是一个执行管理操作的客户端程序,用来检查服务器的配置和当前状态、创建并删除数据库等 - - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 +通过 `mysqladmin --help` 指令查看帮助文档 - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL分类.png) +```sh +mysqladmin -uroot -p2143 create 'test01'; +``` @@ -810,11 +612,215 @@ KILL CONNECTION id -### DDL +#### binlog -#### 数据库 +服务器生成的日志文件以二进制格式保存,如果需要检查这些文本,就要使用 mysqlbinlog 日志管理工具 -* R(Retrieve):查询 +```sh +mysqlbinlog [options] log-files1 log-files2 ... +``` + +* -d --database=name:指定数据库名称,只列出指定的数据库相关操作 + +* -o --offset=#:忽略掉日志中的前 n 行命令。 + +* -r --result-file=name:将输出的文本格式日志输出到指定文件。 + +* -s --short-form:显示简单格式,省略掉一些信息。 + +* --start-datatime=date1 --stop-datetime=date2:指定日期间隔内的所有日志 + +* --start-position=pos1 --stop-position=pos2:指定位置间隔内的所有日志 + + + +*** + + + +#### dump + +##### 命令介绍 + +mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移,备份内容包含创建表,及插入表的 SQL 语句 + +```sh +mysqldump [options] db_name [tables] +mysqldump [options] --database/-B db1 [db2 db3...] +mysqldump [options] --all-databases/-A +``` + +连接选项: + +* -u --user=name:指定用户名 +* -p --password[=name]:指定密码 +* -h --host=name:指定服务器 IP 或域名 +* -P --port=#:指定连接端口 + +输出内容选项: + +* --add-drop-database:在每个数据库创建语句前加上 Drop database 语句 +* --add-drop-table:在每个表创建语句前加上 Drop table 语句 , 默认开启,不开启 (--skip-add-drop-table) +* -n --no-create-db:不包含数据库的创建语句 +* -t --no-create-info:不包含数据表的创建语句 +* -d --no-data:不包含数据 +* -T, --tab=name:自动生成两个文件:一个 .sql 文件,创建表结构的语句;一个 .txt 文件,数据文件,相当于 select into outfile + +示例: + +```sh +mysqldump -uroot -p2143 db01 tb_book --add-drop-database --add-drop-table > a +mysqldump -uroot -p2143 -T /tmp test city +``` + + + +*** + + + +##### 数据备份 + +命令行方式: + +* 备份命令:mysqldump -u root -p 数据库名称 > 文件保存路径 +* 恢复 + 1. 登录MySQL数据库:`mysql -u root p` + 2. 删除已经备份的数据库 + 3. 重新创建与备份数据库名称相同的数据库 + 4. 使用该数据库 + 5. 导入文件执行:`source 备份文件全路径` + +更多方式参考:https://time.geekbang.org/column/article/81925 + +图形化界面: + +* 备份 + + ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) + +* 恢复 + + ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) + + + + + +*** + + + +#### import + +mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件 + +```sh +mysqlimport [options] db_name textfile1 [textfile2...] +``` + +示例: + +```sh +mysqlimport -uroot -p2143 test /tmp/city.txt +``` + +导入 sql 文件,可以使用 MySQL 中的 source 指令 : + +```mysql +source 文件全路径 +``` + + + +*** + + + +#### show + +mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引 + +```sh +mysqlshow [options] [db_name [table_name [col_name]]] +``` + +* --count:显示数据库及表的统计信息(数据库,表 均可以不指定) + +* -i:显示指定数据库或者指定表的状态信息 + +示例: + +```sh +#查询每个数据库的表的数量及表中记录的数量 +mysqlshow -uroot -p1234 --count +#查询test库中每个表中的字段书,及行数 +mysqlshow -uroot -p1234 test --count +#查询test库中book表的详细情况 +mysqlshow -uroot -p1234 test book --count +``` + + + + + + + +**** + + + + + +## 单表操作 + +### SQL + +- SQL + + - Structured Query Language:结构化查询语言 + - 定义了操作所有关系型数据库的规则,每种数据库操作的方式可能会存在不一样的地方,称为“方言” + +- SQL 通用语法 + + - SQL 语句可以单行或多行书写,以**分号结尾**。 + - 可使用空格和缩进来增强语句的可读性。 + - MySQL 数据库的 SQL 语句不区分大小写,**关键字建议使用大写**。 + - 数据库的注释: + - 单行注释:-- 注释内容 #注释内容(MySQL 特有) + - 多行注释:/* 注释内容 */ + +- SQL 分类 + + - DDL(Data Definition Language)数据定义语言 + + - 用来定义数据库对象:数据库,表,列等。关键字:create、drop,、alter 等 + + - DML(Data Manipulation Language)数据操作语言 + + - 用来对数据库中表的数据进行增删改。关键字:insert、delete、update 等 + + - DQL(Data Query Language)数据查询语言 + + - 用来查询数据库中表的记录(数据)。关键字:select、where 等 + + - DCL(Data Control Language)数据控制语言 + + - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL分类.png) + + + +*** + + + +### DDL + +#### 数据库 + +* R(Retrieve):查询 * 查询所有数据库: @@ -1630,6 +1636,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 + + *** @@ -1642,7 +1650,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 约束介绍 -约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! +约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性 约束的分类: @@ -1716,7 +1724,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 主键自增 -主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 +主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增 * 建表时添加主键自增约束 @@ -2114,19 +2122,11 @@ STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只 salary DOUBLE -- 员工工资 ); -- 添加数据 - INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00), - (1002,'猪八戒',1005,8000.00), - (1003,'沙和尚',1005,8500.00), - (1004,'小白龙',1005,7900.00), - (1005,'唐僧',NULL,15000.00), - (1006,'武松',1009,7600.00), - (1007,'李逵',1009,7400.00), - (1008,'林冲',1009,8100.00), - (1009,'宋江',NULL,16000.00); + INSERT INTO employee VALUES (1001,'孙悟空',1005,9000.00),..,(1009,'宋江',NULL,16000.00); ``` - + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) - + * 数据查询 ```mysql @@ -2326,7 +2326,7 @@ BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会 子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 -* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 s2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 t2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 * 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 @@ -3577,7 +3577,7 @@ MySQL 支持的存储引擎: MyISAM 存储引擎: * 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 应用场景:**适用于读多写少的场景**,对事务的完整性要求不高,比如一些数仓、离线数据、支付宝的年度总结之类的场景,业务进行只读操作,查询起来会更快 * 存储方式: * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 @@ -3593,7 +3593,7 @@ InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) MEMORY 存储引擎: - 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,所以数据默认就是无序的,但是在需要快速定位记录可以提供更快的访问,**服务一旦关闭,表中的数据就会丢失**,存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 应用场景:**缓存型存储引擎**,通常用于更新不太频繁的小表,用以快速得到访问结果 - 存储方式:表结构保存在 .frm 中 MERGE 存储引擎: @@ -3642,14 +3642,10 @@ MERGE 存储引擎: | 批量插入速度 | 高 | 低 | 高 | | **外键** | **不支持** | **支持** | **不支持** | -MyISAM 和 InnoDB 的区别? +只读场景 MyISAM 比 InnoDB 更快: -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 - -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +* 底层存储结构有差别,MyISAM 是非聚簇索引,叶子节点保存的是数据的具体地址,不用回表查询 +* InnoDB 每次查询需要维护 MVCC 版本状态,保证并发状态下的读写冲突问题 @@ -3712,7 +3708,7 @@ MyISAM 和 InnoDB 的区别? #### 基本介绍 -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 **索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 @@ -3764,7 +3760,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 | R-tree | 不支持 | 支持 | 不支持 | | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -联合索引图示:根据身高年龄建立的组合索引(height,age) +联合索引图示:根据身高年龄建立的组合索引(height、age) ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) @@ -3991,6 +3987,8 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB * 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 +超过 16KB 的一条记录,主键索引页只会存储部分数据和指向**溢出页**的指针,剩余数据都会分散存储在溢出页中 + 数据页物理结构,从上到下: * File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 @@ -4139,10 +4137,12 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 -自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂** +自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,**避免了页分裂**,页分裂的目的就是保证后一个数据页中的所有行主键值比前一个数据页中主键值大 +参考文章:https://developer.aliyun.com/article/919861 + *** @@ -4248,7 +4248,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 * 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少回表的 IO 次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了 工作过程:用户表 user,(name, age) 是联合索引 @@ -4426,8 +4426,8 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的特点: -* 一个是第一次访问的时候需要访问所有分区 -* 在 Server 层认为这是同一张表,因此所有分区共用同一个 MDL 锁 +* 第一次访问的时候需要访问所有分区 +* 在 Server 层认为这是同一张表,因此**所有分区共用同一个 MDL 锁** * 在引擎层认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问需要的分区 @@ -4440,14 +4440,14 @@ INSERT INTO t VALUES('2017-4-1',1),('2018-4-1',1);-- 这两行记录分别落在 分区表的优点: -* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。 +* 对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁 * 分区表可以很方便的清理历史数据。按照时间分区的分区表,就可以直接通过 `alter table t drop partition` 这个语法直接删除分区文件,从而删掉过期的历史数据,与使用 drop 语句删除数据相比,优势是速度快、对系统影响小 使用分区表,不建议创建太多的分区,注意事项: * 分区并不是越细越好,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表 -* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉。 +* 分区不要提前预留太多,在使用之前预先创建即可。比如是按月分区,每年年底时再把下一年度的 12 个新分区创建上即可,并且对于没有数据的历史分区,要及时的 drop 掉 @@ -4579,8 +4579,6 @@ select v from ht where k >= M order by t_modified desc limit 100; #### 执行频率 -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 - MySQL 客户端连接成功后,查询服务器状态信息: ```mysql @@ -4712,12 +4710,10 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; MySQL **执行计划的局限**: * 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况, 不考虑各种 Cache * EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行**查询之前生成** -* EXPALIN 部分统计信息是估算的,并非精确值 * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同** +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句**实际的执行计划不同**,部分统计信息是估算的,并非精确值 SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 @@ -4857,7 +4853,7 @@ key_len: * Using where:搜索的数据需要在 Server 层判断,无法使用索引下推 * Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 * Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于**排序、去重(UNION)、分组**等场景 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5041,7 +5037,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 * **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** - 在查询时,没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 + 没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; @@ -5134,7 +5130,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); ``` -* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种场景,获取到数据以后 Server 层还会做判断 +* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种慢查询场景,获取到数据以后 Server 层还会做判断 @@ -5202,7 +5198,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; ##### 自增机制 -自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑 +自增主键可以让主键索引尽量地保持在数据页中递增顺序插入,不自增需要寻找其他页插入,导致随机 IO 和页分裂的情况 表的结构定义存放在后缀名为.frm 的文件中,但是并不会保存自增值,不同的引擎对于自增值的保存策略不同: @@ -5244,7 +5240,7 @@ MySQL 不同的自增 id 在达到上限后的表现不同: ```c++ do { - new_id = thread_id_counter++; + new_id = thread_id_counter++; } while (!thread_ids.insert_unique(new_id).second); ``` @@ -5388,7 +5384,7 @@ CREATE TABLE `emp` ( PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... -CREATE INDEX idx_emp_age_salary ON emp(age,salary); +CREATE INDEX idx_emp_age_salary ON emp(age, salary); ``` * 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 @@ -5425,7 +5421,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); 内存临时表,MySQL 有两种 Filesort 排序算法: -* rowid 排序:首先根据条件(回表)取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* rowid 排序:首先根据条件取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 @@ -5478,7 +5474,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 * 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); + CREATE INDEX idx_emp_age_salary ON emp(age, salary); ``` ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png) @@ -5554,7 +5550,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 + 连接查询之所以效率更高 ,是因为**不需要在内存中创建临时表**来完成逻辑上需要两个步骤的查询工作 @@ -5589,7 +5585,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 * 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 ``` @@ -5753,7 +5749,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 * 从 Flush 链表中刷新一部分页面到磁盘: * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST - * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找缓冲页直接释放,如果该页面是已经修改过的脏页就**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE * 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 @@ -5770,9 +5766,9 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 ##### LRU 链表 -当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: +Buffer Pool 需要保证缓存的命中率,所以 MySQL 创建了一个 LRU 链表,当访问某个页时: -* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部**,保证热点数据在链表头 * 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 MySQL 基于局部性原理提供了预读功能: @@ -5780,7 +5776,7 @@ MySQL 基于局部性原理提供了预读功能: * 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 * 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 -预读会造成加载太多用不到的数据页,造成那些使用**频率很高的数据页被挤到 LRU 链表尾部**,所以 InnoDB 将 LRU 链表分成两段: +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段,**冷热数据隔离**: * 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区,靠近链表头部的区域 * 一部分存储使用频率不高的冷数据,old 区,靠近链表尾部,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 @@ -5847,7 +5843,7 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 #### Change -InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,可以通过参数来动态设置,设置为 50 时表示 Change Buffer 的大小最多占用 Buffer Pool 的 50% * 唯一索引的更新不能使用 Change Buffer,需要将数据页读入内存,判断没有冲突在写入 * 普通索引可以使用 Change Buffer,**直接写入 Buffer 就结束**,不用校验唯一性 @@ -5904,7 +5900,7 @@ SHOW PROCESSLIST 获取线程信息后,处于 Sending to client 状态代表 read_rnd_buffer 是 MySQL 的随机读缓冲区,当按任意顺序读取记录行时将分配一个随机读取缓冲区,进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,大小是由 read_rnd_buffer_size 参数控制的 -**Multi-Range Read 优化**,将随机 IO 转化为顺序 IO 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 +Multi-Range Read 优化,**将随机 IO 转化为顺序 IO** 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 二级索引为 a,聚簇索引为 id,优化回表流程: @@ -6083,8 +6079,6 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - - *** @@ -6318,7 +6312,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR + 在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6348,7 +6342,7 @@ InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 u * redo log 用于保证事务持久性 * undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 +undo log 属于**逻辑日志**,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: @@ -6432,9 +6426,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 -在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到当前 undo log 对应的属性中**,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** -UPDATE、DELETE 操作产生的 undo 日志可能会用于其他事务的 MVCC 操作,所以不能立即删除 +UPDATE、DELETE 操作产生的 undo 日志会用于其他事务的 MVCC 操作,所以不能立即删除,INSERT 可以删除的原因是 MVCC 是对现有数据的快照 @@ -6555,7 +6549,7 @@ undo log 是逻辑日志,记录的是每个事务对数据执行的操作, undo log 的作用: * 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据 undo log 主要分为两种: @@ -6601,7 +6595,7 @@ Read View 几个属性: creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 -* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该先判断是否在活跃事务列表) +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该判断是否在活跃事务列表) * db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 * min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 @@ -6717,7 +6711,7 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 * redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能**恢复到最后一次提交**的位置 * redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -* 简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志 +* 简单的 redo log 是纯粹的物理日志,复杂的 redo log 会存在物理日志和逻辑日志 工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 @@ -6743,15 +6737,15 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是**顺序写入**的(先写入前面的 block,写满后继续写下一个) -* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(**碰撞指针**) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域 MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR * 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 -* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** +* 不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入** -InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, +InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样 * `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 * `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` @@ -6768,10 +6762,10 @@ redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的 ##### 日志刷盘 -redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: +redo log 需要在事务提交时将日志写入磁盘,但是比 Buffer Pool 修改的数据写入磁盘的速度快,原因: * 刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是**顺序写**,磁盘的顺序 IO 比随机 IO 速度要快 -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,减少无效 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,好几页的数据修改可能只记录在一个 redo log 页中,减少无效 IO * **组提交机制**,可以大幅度降低磁盘的 IO 消耗 InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的**刷盘策略**: @@ -6782,7 +6776,6 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fs * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 * 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** * 服务器关闭时 -* checkpoint 时(下小节详解) * 并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,**因为多个事务共用一个 redo log buffer**,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量 服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 @@ -6801,7 +6794,7 @@ lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: -* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性 * newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 @@ -6834,10 +6827,8 @@ SHOW ENGINE INNODB STATUS\G 恢复的过程:按照 redo log 依次执行恢复数据,优化方式 -* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** -* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn - -总结:先写 redo buffer,在写 change buffer,先刷 redo log,再刷脏,在删除完成刷脏 redo log +* 使用哈希表:根据 redo log 的 space id 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn @@ -6860,7 +6851,7 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 -binlog 为什么不支持奔溃恢复? +binlog 为什么不支持崩溃恢复? * binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 * binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 @@ -6875,14 +6866,14 @@ binlog 为什么不支持奔溃恢复? 更新一条记录的过程:写之前一定先读 -* 在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 +* 在 B+ 树中定位到该记录,如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 * 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 - 注意:修改 undo页面也是在**修改页面**,事务凡是修改页面就需要先记录相应的 redo 日志 + 注意:修改 undo 页面也是在**修改页面**,事务只要修改页面就需要先记录相应的 redo 日志 - * 然后**先记录对应的的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** + * 然后**记录对应的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** * 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 @@ -6923,7 +6914,7 @@ update T set c=c+1 where ID=2; * Prepare 阶段:存储引擎将该事务的 **redo 日志刷盘**,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务 * Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态 -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 +存储引擎层的 redo log 和 server 层的 binlog 可以认为是一个分布式事务, 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 @@ -6935,12 +6926,12 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? -工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: +工作流程:获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,**事务状态是活跃(未提交)的就全部回滚**,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: * 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 * 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共**同的数据字段叫 XID**,崩溃恢复的时候,会按顺序扫描 redo log: * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据,提交事务 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据 判断一个事务的 binlog 是否完整的方法: @@ -6999,8 +6990,6 @@ InnoDB 刷脏页的控制策略: - - **** @@ -7281,7 +7270,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,获取排他锁的事务是可以对数据读取和修改 RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 @@ -7412,7 +7401,7 @@ InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下 InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行 -* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的是间隙锁左开右开 +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的间隙锁是左开右开 * 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) 几种索引的加锁情况: @@ -7422,7 +7411,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 * 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 +间隙锁优点:RR 级别下间隙锁可以**解决事务的一部分的幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙 间隙锁危害: @@ -7501,7 +7490,7 @@ InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支 * 0:全部采用 AUTO_INC 锁 * 1:全部采用轻量级锁 -* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 +* 2:混合使用,在插入记录的数量确定时采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -7758,7 +7747,7 @@ MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程 主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- binlog thread:在主库事务提交时,把数据变更记录在日志文件 binlog 中,并通知 slave 有数据更新 - I/O thread:负责从主服务器上**拉取二进制日志**,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 - SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行 @@ -7842,7 +7831,7 @@ coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数 * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: @@ -7852,7 +7841,7 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 优缺点: -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多项的情况 * 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) * 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -7983,7 +7972,7 @@ SELECT master_pos_wait(file, pos[, timeout]); * 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 * 如果出现其他情况,需要到主库执行查询语句 -注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 @@ -8285,7 +8274,7 @@ GTID=source_id:transaction_id * source_id:是一个实例第一次启动时自动生成的,是一个全局唯一的值 * transaction_id:初始值是 1,每次提交事务的时候分配给这个事务,并加 1,是连续的(区分事务 ID,事务 ID 是在执行时生成) -启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 **GTID 集合**,用来对应当前实例执行过的所有事务 +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 GTID 集合,用来存储当前实例**执行过的所有事务** GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: @@ -8353,7 +8342,7 @@ GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_nex ### 日志分类 -在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。 +在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件 MySQL日志主要包括六种: @@ -8554,7 +8543,7 @@ mysqlbinlog log-file; #### 数据恢复 -误删库或者表时,需要根据 binlog 进行数据恢复, +误删库或者表时,需要根据 binlog 进行数据恢复 一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: @@ -8607,7 +8596,7 @@ SELECT * FROM tb_book WHERE id < 8 ### 慢日志 -慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒 +慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志long_query_time 默认为 10 秒,最小为 0, 精度到微秒 慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 `/etc/mysql/my.cnf`: @@ -8741,11 +8730,11 @@ long_query_time=10 ## NoSQL -### 基本介绍 +### 概述 -NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充。 +NoSQL(Not-Only SQL):泛指非关系型的数据库,作为关系型数据库的补充 -MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力。 +MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高,因此需要缓存的来减缓数据库的访问压力 作用:应对基于海量用户和海量数据前提下的数据处理问题 @@ -8764,8 +8753,6 @@ MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高, 参考视频:https://www.bilibili.com/video/BV1CJ411m7Gc -参考视频:https://www.bilibili.com/video/BV1Rv41177Af - *** @@ -8790,17 +8777,6 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 * 有序集合类型:zset/sorted_set(TreeSet) * 支持持久化,可以进行数据灾难恢复 -应用: - -* 为热点数据加速查询(主要场景),如热点商品、热点新闻、热点资讯、推广类等高访问量信息等 - -* 即时信息查询,如排行榜、网站访问统计、公交到站信息、在线人数(聊天室、网站)、设备信号等 - -* 时效性信息控制,如验证码控制、投票控制等 - -* 分布式数据共享,如分布式集群架构中的 session 分离 -* 消息队列 - *** @@ -8809,88 +8785,38 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 ### 安装启动 -#### CentOS +安装: -1. 下载 Redis +* Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 - 下载安装包: + ```sh + sudo apt update + sudo apt install redis-server + ``` - ```sh - wget http://download.redis.io/releases/redis-5.0.0.tar.gz - ``` +* 检查 Redis 状态 - 解压安装包: + ```sh + sudo systemctl status redis-server + ``` - ```sh - tar –xvf redis-5.0.0.tar.gz - ``` +启动: - 编译(在解压的目录中执行): +* 启动服务器——参数启动 - ```sh - make - ``` + ```sh + redis-server [--port port] + #redis-server --port 6379 + ``` - 安装(在解压的目录中执行): +* 启动服务器——配置文件启动 - ```sh - make install - ``` + ```sh + redis-server config_file_name + #redis-server /etc/redis/conf/redis-6397.conf + ``` - - -2. 安装 Redis - - redis-server,服务器启动命令 客户端启动命令 - - redis-cli,redis核心配置文件 - - redis.conf,RDB文件检查工具(快照持久化文件) - - redis-check-dump,AOF文件修复工具 - - redis-check-aof - - - -*** - - - -#### Ubuntu - -安装: - -* Redis 5.0 被包含在默认的 Ubuntu 20.04 软件源中 - - ```sh - sudo apt update - sudo apt install redis-server - ``` - -* 检查Redis状态 - - ```sh - sudo systemctl status redis-server - ``` - -启动: - -* 启动服务器——参数启动 - - ```sh - redis-server [--port port] - #redis-server --port 6379 - ``` - -* 启动服务器——配置文件启动 - - ```sh - redis-server config_file_name - #redis-server /etc/redis/conf/redis-6397.conf - ``` - -* 启动客户端: +* 启动客户端: ```sh redis-cli [-h host] [-p port] @@ -8923,7 +8849,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 mkdir data ``` -2. 创建配置文件副本放入 conf 目录,Ubuntu系统配置文件 redis.conf 在目录 `/etc/redis` 中 +2. 创建配置文件副本放入 conf 目录,Ubuntu 系统配置文件 redis.conf 在目录 `/etc/redis` 中 ```sh cat redis.conf | grep -v "#" | grep -v "^$" -> /conf/redis-6379.conf @@ -8985,7 +8911,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 #### 客户端 -* 服务器允许客户端连接最大数量,默认0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: +* 服务器允许客户端连接最大数量,默认 0,表示无限制,当客户端连接到达上限后,Redis 会拒绝新的连接: ```sh maxclients count @@ -9019,7 +8945,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 logfile filename ``` -注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志IO的频度 +注意:日志级别开发期设置为 verbose 即可,生产环境中配置为 notice,简化日志输出量,降低写日志 IO 的频度 @@ -9037,175 +8963,182 @@ dbfilename "dump-6379.rdb" - - *** +#### 基本指令 +帮助信息: -## 体系结构 - -### 线程模型 - -#### 单线程 +* 获取命令帮助文档 -Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 叫做单线程的模型 + ```sh + help [command] + #help set + ``` -文件事件处理器以单线程方式运行,但是使用 I/O 多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 单线程设计的简单性 +* 获取组中所有命令信息名称 -工作原理: + ```sh + help [@group-name] + #help @string + ``` -* 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 +退出服务 -* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会将处理请求放入**单线程的执行队列**中,等待调用套接字关联好的事件处理器来处理事件 +* 退出客户端: - 操作都会被放入一个执行队列顺序执行,所以不存在并发安全问题 + ```sh + quit + exit + ``` -**Redis 单线程也能高效的原因**: +* 退出客户端服务器快捷键: -* 纯内存操作 -* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 -* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 -* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + ```sh + Ctrl+C + ``` -**** -#### 多线程 -Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 -Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : +*** -```sh -io-threads-do-reads yesCopy to clipboardErrorCopied -``` -开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : -```sh -io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 -``` - +## 数据库 +### 服务器 -参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA +Redis 服务器将所有数据库保存在**服务器状态 redisServer 结构**的 db 数组中,数组的每一项都是 redisDb 结构,代表一个数据库,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小。在初始化服务器时,根据 dbnum 属性决定创建数据库的数量,该属性由服务器配置的 database 选项决定,默认 16 +```c +struct redisServer { + // 保存服务器所有的数据库 + redisDB *db; + + // 服务器数据库的数量 + int dbnum; +}; +``` + +**在服务器内部**,客户端状态 redisClient 结构的 db 属性记录了目标数据库,是一个指向 redisDb 结构的指针 +```c +struct redisClient { + // 记录客户端正在使用的数据库,指向 redisServer.db 数组中的某一个 db + redisDB *db; +}; +``` +每个 Redis 客户端都有目标数据库,执行数据库读写命令时目标数据库就会成为这些命令的操作对象,默认情况下 Redis 客户端的目标数据库为 0 号数据库,客户端可以执行 SELECT 命令切换目标数据库,原理是通过修改 redisClient.db 指针指向服务器中不同数据库 +命令操作: -*** +```sh +select index #切换数据库,index从0-15取值 +move key db #数据移动到指定数据库,db是数据库编号 +ping #测试数据库是否连接正常,返回PONG +echo message #控制台输出信息 +``` +Redis 没有可以返回客户端目标数据库的命令,但是 redis-cli 客户端旁边会提示当前所使用的目标数据库 +```sh +redis> SELECT 1 +OK +redis[1]> +``` -## 基本指令 -### 操作指令 -读写数据: +*** -* 设置 key,value 数据: - ```sh - set key value - #set name seazean - ``` -* 根据 key 查询对应的 value,如果**不存在,返回空(nil)**: +### 键空间 - ```sh - get key - #get name - ``` +#### key space -帮助信息: +Redis 是一个键值对(key-value pair)数据库服务器,每个数据库都由一个 redisDb 结构表示,redisDb.dict **字典中保存了数据库的所有键值对**,将这个字典称为键空间(key space) -* 获取命令帮助文档 +```c +typedef struct redisDB { + // 数据库键空间,保存所有键值对 + dict *dict +} redisDB; +``` - ```sh - help [command] - #help set - ``` +键空间和用户所见的数据库是直接对应的: -* 获取组中所有命令信息名称 +* 键空间的键就是数据库的键,每个键都是一个字符串对象 +* 键空间的值就是数据库的值,每个值可以是任意一种 Redis 对象 - ```sh - help [@group-name] - #help @string - ``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-数据库键空间.png) -退出服务 +当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会**进行一些维护操作**: -* 退出客户端: +* 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中 hit 次数或键空间不命中 miss 次数,这两个值可以在 `INFO stats` 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看 +* 更新键的 LRU(最后使用)时间,该值可以用于计算键的闲置时间,使用 `OBJECT idletime key` 查看键 key 的闲置时间 +* 如果在读取一个键时发现该键已经过期,服务器会**先删除过期键**,再执行其他操作 +* 如果客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务注意到这个键已经被修改过 +* 服务器每次修改一个键之后,都会对 dirty 键计数器的值增1,该计数器会触发服务器的持久化以及复制操作 +* 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知 - ```sh - quit - exit - ``` -* 退出客户端服务器快捷键: - ```sh - Ctrl+C - ``` +*** - -*** +#### 读写指令 +常见键操作指令: -### key 指令 +* 增加指令 -key 是一个字符串,通过 key 获取 redis 中保存的数据 + ```sh + set key value #添加一个字符串类型的键值对 -* 基本操作 +* 删除指令 ```sh del key #删除指定key unlink key #非阻塞删除key,真正的删除会在后续异步操作 - exists key #获取key是否存在 - type key #获取key的类型 - sort key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 - sort key alpha #对key中字母排序 - rename key newkey #改名 - renamenx key newkey #改名 ``` -* 时效性控制 +* 更新指令 ```sh - expire key seconds #为指定key设置有效期,单位为秒 - pexpire key milliseconds #为指定key设置有效期,单位为毫秒 - expireat key timestamp #为指定key设置有效期,单位为时间戳 - pexpireat key mil-timestamp #为指定key设置有效期,单位为毫秒时间戳 - - ttl key #获取key的有效时间,每次获取会自动变化(减小),类似于倒计时, - #-1代表永久性,-2代表不存在/失效 - pttl key #获取key的有效时间,单位是毫秒,每次获取会自动变化(减小) - persist key #切换key从时效性转换为永久性 + rename key newkey #改名 + renamenx key newkey #改名 ``` -* 查询模式 + 值得更新需要参看具体得 Redis 对象得操作方式,比如字符串对象执行 `SET key value` 就可以完成修改 + +* 查询指令 ```sh + exists key #获取key是否存在 + randomkey #随机返回一个键 keys pattern #查询key ``` - 查询模式规则:*匹配任意数量的任意符号;?配合一个任意符号;[]匹配一个指定符号 + KEYS 命令需要**遍历存储的键值对**,操作延时高,一般不被建议用于生产环境中 + 查询模式规则:* 匹配任意数量的任意符号、? 配合一个任意符号、[] 匹配一个指定符号 + ```sh keys * #查询所有key keys aa* #查询所有以aa开头 @@ -9215,7 +9148,19 @@ key 是一个字符串,通过 key 获取 redis 中保存的数据 keys u[st]er:1 #查询所有以u开头,以er:1结尾,中间包含一个字母,s或t ``` + +* 其他指令 + + ```sh + type key #获取key的类型 + dbsize #获取当前数据库的数据总量,即key的个数 + flushdb #清除当前数据库的所有数据(慎用) + flushall #清除所有数据(慎用) + ``` + 在执行 FLUSHDB 这样的危险命令之前,最好先执行一个 SELECT 命令,保证当前所操作的数据库是目标数据库 + + @@ -9223,27 +9168,46 @@ key 是一个字符串,通过 key 获取 redis 中保存的数据 -### DB 指令 +#### 时效设置 + +客户端可以以秒或毫秒的精度为数据库中的某个键设置生存时间(TimeTo Live, TTL),在经过指定时间之后,服务器就会自动删除生存时间为 0 的键;也可以以 UNIX 时间戳的方式设置过期时间(expire time),当键的过期时间到达,服务器会自动删除这个键 -Redis 在使用过程中,随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突,所以 Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 +```sh +expire key seconds #为指定key设置生存时间,单位为秒 +pexpire key milliseconds #为指定key设置生存时间,单位为毫秒 +expireat key timestamp #为指定key设置过期时间,单位为时间戳 +pexpireat key mil-timestamp #为指定key设置过期时间,单位为毫秒时间戳 +``` -* 基本操作 +* 实际上 EXPIRE、EXPIRE、EXPIREAT 三个命令**底层都是转换为 PEXPIREAT 命令**来实现的 +* SETEX 命令可以在设置一个字符串键的同时为键设置过期时间,但是该命令是一个类型限定命令 - ```sh - select index #切换数据库,index从0-15取值 - ping #测试数据库是否连接正常,返回PONG - echo message #控制台输出信息 - ``` +redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,字典称为过期字典: -* 扩展操作 +* 键是一个指针,指向键空间中的某个键对象(复用键空间的对象,不会产生内存浪费) +* 值是一个 long long 类型的整数,保存了键的过期时间,是一个毫秒精度的 UNIX 时间戳 - ```sh - move key db #数据移动到指定数据库,db是数据库编号 - dbsize #获取当前数据库的数据总量,即key的个数 - flushdb #清除当前数据库的所有数据 - flushall #清除所有数据 - ``` +```c +typedef struct redisDB { + // 过期字典,保存所有键的过期时间 + dict *expires +} redisDB; +``` +客户端执行 PEXPIREAT 命令,服务器会在数据库的过期字典中关联给定的数据库键和过期时间: + +```python +def PEXPIREAT(key, expire_time_in_ms): + # 如果给定的键不存在于键空间,那么不能设置过期时间 + if key not in redisDb.dict: + return 0 + + # 在过期字典中关联键和过期时间 + redisDB.expires[key] = expire_time_in_ms + + # 过期时间设置成功 + return 1 +``` @@ -9251,139 +9215,149 @@ Redis 在使用过程中,随着操作数据量的增加,会出现大量的 -### 通信指令 +#### 时效状态 -Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 +TTL 和 PTTL 命令通过计算键的过期时间和当前时间之间的差,返回这个键的剩余生存时间 -Redis 客户端可以订阅任意数量的频道 +* 返回正数代表该数据在内存中还能存活的时间 +* 返回 -1 代表永久性,返回 -2 代表键不存在 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) +```sh +ttl key #获取key的剩余时间,每次获取会自动变化(减小),类似于倒计时 +pttl key #获取key的剩余时间,单位是毫秒,每次获取会自动变化(减小) +``` -操作命令: +PERSIST 是 PEXPIREAT 命令的反操作,在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联 -1. 打开一个客户端订阅 channel1:`SUBSCRIBE channel1` -2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` -3. 第一个客户端可以看到发送的消息 +```sh +persist key #切换key从时效性转换为永久性 +``` - +Redis 通过过期字典可以检查一个给定键是否过期: -注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 +* 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间 +* 检查当前 UNIX 时间戳是否大于键的过期时间:如果是那么键已经过期,否则键未过期 +补充:AOF、RDB 和复制功能对过期键的处理 +* RDB : + * 生成 RDB 文件,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中 + * 载入 RDB 文件,如果服务器以主服务器模式运行,那么在载入时会对键进行检查,过期键会被忽略;如果服务器以从服务器模式运行,会载入所有键,包括过期键,但是主从服务器进行数据同步时就会删除这些键 +* AOF: + * 写入 AOF 文件,如果数据库中的某个键已经过期,但还没有被删除,那么 AOF 文件不会因为这个过期键而产生任何影响;当该过期键被删除,程序会向 AOF 文件追加一条 DEL 命令,显式的删除该键 + * AOF 重写,会对数据库中的键进行检查,忽略已经过期的键 +* 复制:当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制 + * 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键 + * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,会当作未过期键处理,只有在接到主服务器发来的 DEL 命令之后,才会删除过期键(数据不一致) -**** -### ACL 指令 -Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 +**** -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) -* acl cat:查看添加权限指令类别 -* acl whoami:查看当前用户 -* acl setuser username on >password ~cached:* +get:设置有用户名、密码、ACL 权限(只能 get) +### 过期删除 +#### 删除策略 +删除策略就是**针对已过期数据的处理策略**,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露 +针对过期数据有三种删除策略: +- 定时删除 +- 惰性删除(被动删除) +- 定期删除 -*** +Redis 采用惰性删除和定期删除策略的结合使用 +*** -## 数据结构 -### 字符串 +#### 定时删除 -#### SDS +在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作 -Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 +- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用 +- 缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量 +- 总结:用处理器性能换取存储空间(拿时间换空间) -```c -struct sdshdr { - // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 - int len; - - // 记录buf数组中未使用字节的数量 - int free; - - // 【字节】数组,用于保存字符串(不是字符数组) - char buf[]; -}; -``` +创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实 -SDS 遵循 C 字符串**以空字符结尾**的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) +*** -*** +#### 惰性删除 +数据到达过期时间不做处理,等下次访问到该数据时执行 **expireIfNeeded()** 判断: -#### 对比 +* 如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空 +* 如果输入键未过期,那么 expireIfNeeded 函数不做动作 -SDS 与 C 字符串的主要区别: +所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键 -常数复杂度获取字符串长度: +惰性删除的特点: -* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) -* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 +* 优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间 +* 缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏 +* 总结:用存储空间换取处理器性能(拿空间换时间) -杜绝缓冲区溢出: -* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) - s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): +*** - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) -* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 -二进制安全: +#### 定期删除 -* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 -* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据 +定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响 -兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 +* 如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上 +* 如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况 +定期删除是**周期性轮询 Redis 库中的时效性**数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度 +- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 `serverCron() → activeExpireCycle()` -*** +- activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式: + * 轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒 + * 全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理 + * 随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查 -#### 内存 +定期删除特点: -C 字符串每次增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 +- CPU 性能占用设置有峰值,检测频度可自定义设置 +- 内存压力不是很大,长期占用内存的**冷数据会被持续清理** +- 周期性抽查存储空间(随机抽查,重点抽查) -SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 -内存重分配涉及复杂的算法,需要执行系统调用,是一个比较耗时的操作,SDS 的两种优化策略: -* 空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 - * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 - s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) +*** - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) - * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 - 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** +### 数据淘汰 -* 惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用 +#### 逐出算法 - SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 +数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为**逐出算法** +逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,**出现 Redis 内存打满异常**: +```sh +(error) OOM command not allowed when used memory >'maxmemory' +``` @@ -9391,144 +9365,3304 @@ SDS 通过未使用空间解除了字符串长度和底层数组长度之间的 -### 链表 +#### 策略配置 -链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 +Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三 -链表节点: +内存配置方式: -```c -typedef struct listNode { - // 前置节点 - struct listNode *prev; - - // 后置节点 - struct listNode *next; - +* 通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节 + +* 通过命令修改(重启失效): + + * `config set maxmemory 104857600`:设置 Redis 最大占用内存为 100MB + * `config get maxmemory`:获取 Redis 最大占用内存 + + * `info` :可以查看 Redis 内存使用情况,`used_memory_human` 字段表示实际已经占用的内存,`maxmemory` 表示最大占用内存 + +影响数据淘汰的相关配置如下,配置 conf 文件: + +* 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能 + + ```sh + maxmemory-samples count + ``` + +* 达到最大内存后的,对被挑选出来的数据进行删除的策略 + + ```sh + maxmemory-policy policy + ``` + + 数据删除的策略 policy:3 类 8 种 + + 第一类:检测易失数据(可能会过期的数据集 server.db[i].expires): + + ```sh + volatile-lru # 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰 + volatile-lfu # 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰 + volatile-ttl # 对设置了过期时间的 key 选择将要过期的数据淘汰 + volatile-random # 对设置了过期时间的 key 选择任意数据淘汰 + ``` + + 第二类:检测全库数据(所有数据集 server.db[i].dict ): + + ```sh + allkeys-lru # 对所有 key 选择最近最少使用的数据淘汰 + allkeLyRs-lfu # 对所有 key 选择最近使用次数最少的数据淘汰 + allkeys-random # 对所有 key 选择任意数据淘汰,相当于随机 + ``` + + 第三类:放弃数据驱逐 + + ```sh + no-enviction #禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory) + ``` + +数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置 + + + + + +*** + + + +### 排序机制 + +#### 基本介绍 + +Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序,并不更改集合中的数据位置,只是查询 + +```sh +SORT key [ASC/DESC] #对key中数据排序,默认对数字排序,并不更改集合中的数据位置,只是查询 +SORT key ALPHA #对key中字母排序,按照字典序 +``` + + + + + +*** + + + +#### SORT + +`SORT ` 命令可以对一个包含数字值的键 key 进行排序 + +假设 `RPUSH numbers 3 1 2`,执行 `SORT numbers` 的详细步骤: + +* 创建一个和 key 列表长度相同的数组,数组每项都是 redisSortObject 结构 + + ```c + typedef struct redisSortObject { + // 被排序键的值 + robj *obj; + + // 权重 + union { + // 排序数字值时使用 + double score; + // 排序带有 BY 选项的字符串 + robj *cmpobj; + } u; + } + ``` + +* 遍历数组,将各个数组项的 obj 指针分别指向 numbers 列表的各个项 + +* 遍历数组,将 obj 指针所指向的列表项转换成一个 double 类型的浮点数,并将浮点数保存在对应数组项的 u.score 属性里 + +* 根据数组项 u.score 属性的值,对数组进行数字值排序,排序后的数组项按 u.score 属性的值**从小到大排列** + +* 遍历数组,将各个数组项的 obj 指针所指向的值作为排序结果返回给客户端,程序首先访问数组的索引 0,依次向后访问 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-sort排序.png) + +对于 `SORT key [ASC/DESC]` 函数: + +* 在执行升序排序时,排序算法使用的对比函数产生升序对比结果 +* 在执行降序排序时,排序算法使用的对比函数产生降序对比结果 + + + +**** + + + +#### BY + +SORT 命令默认使用被排序键中包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序 + +```sh +SORT BY # 数值 +SORT BY ALPHA # 字符 +``` + +```sh +redis> SADD fruits "apple" "banana" "cherry" +(integer) 3 +redis> SORT fruits ALPHA +1) "apple" +2) "banana" +3) "cherry" +``` + +```sh +redis> MSET apple-price 8 banana-price 5.5 cherry-price 7 +OK +# 使用水果的价钱进行排序 +redis> SORT fruits BY *-price +1) "banana" +2) "cherry" +3) "apple" +``` + +实现原理:排序时的 u.score 属性就会被设置为对应的权重 + + + + + +*** + + + +#### LIMIT + +SORT 命令默认会将排序后的所有元素都返回给客户端,通过 LIMIT 选项可以让 SORT 命令只返回其中一部分已排序的元素 + +```sh +LIMIT +``` + +* offset 参数表示要跳过的已排序元素数量 +* count 参数表示跳过给定数量的元素后,要返回的已排序元素数量 + +```sh +# 对应 a b c d e f g +redis> SORT alphabet ALPHA LIMIT 2 3 +1) "c" +2) "d" +3) "e" +``` + +实现原理:在排序后的 redisSortObject 结构数组中,将指针移动到数组的索引 2 上,依次访问 array[2]、array[3]、array[4] 这 3 个数组项,并将数组项的 obj 指针所指向的元素返回给客户端 + + + + + +*** + + + +#### GET + +SORT 命令默认在对键进行排序后,返回被排序键本身所包含的元素,通过使用 GET 选项, 可以在对键进行排序后,根据被排序的元素以及 GET 选项所指定的模式,查找并返回某些键的值 + +```sh +SORT GET +``` + +```sh +redis> SADD students "tom" "jack" "sea" +#设置全名 +redis> SET tom-name "Tom Li" +OK +redis> SET jack-name "Jack Wang" +OK +redis> SET sea-name "Sea Zhang" +OK +``` + +```sh +redis> SORT students ALPHA GET *-name +1) "Jack Wang" +2) "Sea Zhang" +3) "Tom Li" +``` + +实现原理:对 students 进行排序后,对于 jack 元素和 *-name 模式,查找程序返回键 jack-name,然后获取 jack-name 键对应的值 + + + + + +*** + + + +#### STORE + +SORT 命令默认只向客户端返回排序结果,而不保存排序结果,通过使用 STORE 选项可以将排序结果保存在指定的键里面 + +```sh +SORT STORE +``` + +```sh +redis> SADD students "tom" "jack" "sea" +(integer) 3 +redis> SORT students ALPHA STORE sorted_students +(integer) 3 +``` + +实现原理:排序后,检查 sorted_students 键是否存在,如果存在就删除该键,设置 sorted_students 为空白的列表键,遍历排序数组将元素依次放入 + + + + + +*** + + + +#### 执行顺序 + +调用 SORT 命令,除了 GET 选项之外,改变其他选项的摆放顺序并不会影响命令执行选项的顺序 + +```sh +SORT ALPHA [ASC/DESC] BY LIMIT GET STORE +``` + +执行顺序: + +* 排序:命令会使用 ALPHA 、ASC 或 DESC、BY 这几个选项,对输入键进行排序,并得到一个排序结果集 +* 限制排序结果集的长度:使用 LIMIT 选项,对排序结果集的长度进行限制 +* 获取外部键:根据排序结果集中的元素以及 GET 选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集 +* 保存排序结果集:使用 STORE 选项,将排序结果集保存到指定的键上面去 +* 向客户端返回排序结果集:最后一步命令遍历排序结果集,并依次向客户端返回排序结果集中的元素 + + + + + +*** + + + +### 通知机制 + +数据库通知是可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况 + +* 关注某个键执行了什么命令的通知称为键空间通知(key-space notification) +* 关注某个命令被什么键执行的通知称为键事件通知(key-event notification) + +图示订阅 0 号数据库 message 键: + + + +服务器配置的 notify-keyspace-events 选项决定了服务器所发送通知的类型 + +* AKE 代表服务器发送所有类型的键空间通知和键事件通知 +* AK 代表服务器发送所有类型的键空间通知 +* AE 代表服务器发送所有类型的键事件通知 +* K$ 代表服务器只发送和字符串键有关的键空间通知 +* EL 代表服务器只发送和列表键有关的键事件通知 +* ..... + +发送数据库通知的功能是由 notifyKeyspaceEvent 函数实现的: + +* 如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回 +* 如果给定的通知是服务器允许发送的通知 + * 检测服务器是否允许发送键空间通知,允许就会构建并发送事件通知 + * 检测服务器是否允许发送键事件通知,允许就会构建并发送事件通知 + + + + + +*** + + + + + +## 体系架构 + +### 事件驱动 + +#### 基本介绍 + +Redis 服务器是一个事件驱动程序,服务器需要处理两类事件 + +* 文件事件 (file event):服务器通过套接字与客户端(或其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信操作 +* 时间事件 (time event):Redis 服务器中的一些操作(比如 serverCron 函数)需要在指定时间执行,而时间事件就是服务器对这类定时操作的抽象 + + + + + +*** + + + +#### 文件事件 + +##### 基本组成 + +Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器 (file event handler) + +* 使用 I/O 多路复用 (multiplexing) 程序来同时监听多个套接字,并根据套接字执行的任务来为套接字关联不同的事件处理器 + +* 当被监听的套接字准备好执行连接应答 (accept)、 读取 (read)、 写入 (write)、 关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件分派器会调用套接字关联好的事件处理器来处理事件 + +文件事件处理器**以单线程方式运行**,但通过使用 I/O 多路复用程序来监听多个套接字, 既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,保持了 Redis 内部单线程设计的简单性 + +文件事件处理器的组成结构: + + + +I/O 多路复用程序将所有产生事件的套接字处理请求放入一个**单线程的执行队列**中,通过队列有序、同步的向文件事件分派器传送套接字,上一个套接字产生的事件处理完后,才会继续向分派器传送下一个 + + + +Redis 单线程也能高效的原因: + +* 纯内存操作 +* 核心是基于非阻塞的 IO 多路复用机制,单线程可以高效处理多个请求 +* 底层使用 C 语言实现,C 语言实现的程序距离操作系统更近,执行速度相对会更快 +* 单线程同时也**避免了多线程的上下文频繁切换问题**,预防了多线程可能产生的竞争问题 + + + +**** + + + +##### 多路复用 + +Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、epoll、 evport 和 kqueue 这些函数库来实现的,Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则,编译时自动选择系统中**性能最高的多路复用函数**来作为底层实现 + +I/O 多路复用程序监听多个套接字的 AE_READABLE 事件和 AE_WRITABLE 事件,这两类事件和套接字操作之间的对应关系如下: + +* 当套接字变得**可读**时(客户端对套接字执行 write 操作或者 close 操作),或者有新的**可应答**(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 连接操作),套接字产生 AE_READABLE 事件 +* 当套接字变得可写时(客户端对套接字执行 read 操作,对于服务器来说就是可以写了),套接字产生 AE_WRITABLE 事件 + +I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理 AE_READABLE 事件, 等 AE_READABLE 事件处理完之后才处理 AE_WRITABLE 事件 + + + +*** + + + +##### 处理器 + +Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求: + +* 连接应答处理器,用于对连接服务器的各个客户端进行应答,Redis 服务器初始化时将该处理器与 AE_READABLE 事件关联 +* 命令请求处理器,用于接收客户端传来的命令请求,执行套接字的读入操作,与 AE_READABLE 事件关联 +* 命令回复处理器,用于向客户端返回命令的执行结果,执行套接字的写入操作,与 AE_WRITABLE 事件关联 +* 复制处理器,当主服务器和从服务器进行复制操作时,主从服务器都需要关联该处理器 + +Redis 客户端与服务器进行连接并发送命令的整个过程: + +* Redis 服务器正在运作监听套接字的 AE_READABLE 事件,关联连接应答处理器 +* 当 Redis 客户端向服务器发起连接,监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行,对客户端的连接请求进行应答,创建客户端套接字以及客户端状态,并将客户端套接字的 **AE_READABLE 事件与命令请求处理器**进行关联 +* 客户端向服务器发送命令请求,客户端套接字产生 AE_READABLE 事件,引发命令请求处理器执行,读取客户端的命令内容传给相关程序去执行 +* 执行命令会产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的 **AE_WRITABLE 事件与命令回复处理器**进行关联 +* 当客户端尝试读取命令回复时,客户端套接字产生 AE_WRITABLE 事件,触发命令回复处理器执行,在命令回复全部写入套接字后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联 + + + + + +*** + + + +#### 时间事件 + +Redis 的时间事件分为以下两类: + +* 定时事件:在指定的时间之后执行一次(Redis 中暂时未使用) +* 周期事件:每隔指定时间就执行一次 + +一个时间事件主要由以下三个属性组成: + +* id:服务器为时间事件创建的全局唯一 ID(标识号),从小到大顺序递增,新事件的 ID 比旧事件的 ID 号要大 +* when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间 +* timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理事件 + +时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值: + +* 定时事件:事件处理器返回 AE_NOMORE,该事件在到达一次后就会被删除 +* 周期事件:事件处理器返回非 AE_NOMORE 的整数值,服务器根据该值对事件的 when 属性更新,让该事件在一段时间后再次交付 + +服务器将所有时间事件都放在一个**无序链表**中,新的时间事件插入到链表的表头: + + + +无序链表指是链表不按 when 属性的大小排序,每当时间事件执行器运行时就必须遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器处理 + +无序链表并不影响时间事件处理器的性能,因为正常模式下的 Redis 服务器**只使用 serverCron 一个时间事件**,在 benchmark 模式下服务器也只使用两个时间事件,所以无序链表不会影响服务器的性能,几乎可以按照一个指针处理 + + + +*** + + + +#### 事件调度 + +服务器中同时存在文件事件和时间事件两种事件类型,调度伪代码: + +```python +# 事件调度伪代码 +def aeProcessEvents(): + # 获取到达时间离当前时间最接近的时间事件 + time_event = aeSearchNearestTime() + + # 计算最接近的时间事件距离到达还有多少亳秒 + remaind_ms = time_event.when - unix_ts_now() + # 如果事件已到达,那么 remaind_ms 的值可能为负数,设置为 0 + if remaind_ms < 0: + remaind_ms = 0 + + # 根据 remaind_ms 的值,创建 timeval 结构 + timeval = create_timeval_with_ms(remaind_ms) + # 【阻塞并等待文件事件】产生,最大阻塞时间由传入的timeval结构决定,remaind_ms的值为0时调用后马上返回,不阻塞 + aeApiPoll(timeval) + + # 处理所有已产生的文件事件 + processFileEvents() + # 处理所有已到达的时间事件 + processTimeEvents() +``` + +事件的调度和执行规则: + +* aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间 +* 对文件事件和时间事件的处理都是**同步、有序、原子地执行**,服务器不会中途中断事件处理,也不会对事件进行抢占,所以两种处理器都要尽可地减少程序的阻塞时间,并在有需要时**主动让出执行权**,从而降低事件饥饿的可能性 + * 命令回复处理器在写入字节数超过了某个预设常量,就会主动用 break 跳出写入循环,将余下的数据留到下次再写 + * 时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行 +* 时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间稍晚 + + + + + +**** + + + +#### 多线程 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这是 Redis 的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络),多线程只是用来**处理网络数据的读写和协议解析**, 执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis 配置文件 `redis.conf` : + +```sh +io-threads-do-reads yesCopy to clipboardErrorCopied +``` + +开启多线程后,还需要设置线程数,否则是不生效的,同样需要修改 redis 配置文件 : + +```sh +io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + + + + + +参考文章:https://mp.weixin.qq.com/s/dqmiR0ECf4lB6Y2OyK-dyA + + + + + +**** + + + +### 客户端 + +#### 基本介绍 + +Redis 服务器是典型的一对多程序,一个服务器可以与多个客户端建立网络连接,服务器对每个连接的客户端建立了相应的 redisClient 结构(客户端状态,**在服务器端的存储结构**),保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构 + +Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构: + +```c +struct redisServer { + // 一个链表,保存了所有客户端状态 + list *clients; + + //... +}; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + + + + + +*** + + + +#### 数据结构 + +##### redisClient + +客户端的数据结构: + +```c +typedef struct redisClient { + //... + + // 套接字 + int fd; + // 名字 + robj *name; + // 标志 + int flags; + + // 输入缓冲区 + sds querybuf; + // 输出缓冲区 buf 数组 + char buf[REDIS_REPLY_CHUNK_BYTES]; + // 记录了 buf 数组目前已使用的字节数量 + int bufpos; + // 可变大小的输出缓冲区,链表 + 字符串对象 + list *reply; + + // 命令数组 + rboj **argv; + // 命令数组的长度 + int argc; + // 命令的信息 + struct redisCommand *cmd; + + // 是否通过身份验证 + int authenticated; + + // 创建客户端的时间 + time_t ctime; + // 客户端与服务器最后一次进行交互的时间 + time_t lastinteraction; + // 输出缓冲区第一次到达软性限制 (soft limit) 的时间 + time_t obuf_soft_limit_reached_time; +} +``` + +客户端状态包括两类属性 + +* 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工作,都要用到这些属性 +* 另一类是和特定功能相关的属性,比如操作数据库时用到的 db 属性和 dict id 属性,执行事务时用到的 mstate 属性,以及执行 WATCH 命令时用到的 watched_keys 属性等,代码中没有列出 + + + +*** + + + +##### 套接字 + +客户端状态的 fd 属性记录了客户端正在使用的套接字描述符,根据客户端类型的不同,fd 属性的值可以是 -1 或者大于 -1 的整数: + +* 伪客户端 (fake client) 的 fd 属性的值为 -1,命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络,所以不需要套接字连接 +* 普通客户端的 fd 属性的值为大于 -1 的整数,因为合法的套接字描述符不能是 -1 + +执行 `CLIENT list` 命令可以列出目前所有连接到服务器的普通客户端,不包括伪客户端 + + + +*** + + + +##### 名字 + +在默认情况下,一个连接到服务器的客户端是没有名字的,使用 `CLIENT setname` 命令可以为客户端设置一个名字 + + + +*** + + + +##### 标志 + +客户端的标志属性 flags 记录了客户端的角色以及客户端目前所处的状态,每个标志使用一个常量表示 + +* flags 的值可以是单个标志:`flags = ` +* flags 的值可以是多个标志的二进制:`flags = | | ... ` + +一部分标志记录**客户端的角色**: + +* REDIS_MASTER 表示客户端是一个从服务器,REDIS_SLAVE 表示客户端是一个从服务器,在主从复制时使用 +* REDIS_PRE_PSYNC 表示客户端是一个版本低于 Redis2.8 的从服务器,主服务器不能使用 PSYNC 命令与该从服务器进行同步,这个标志只能在 REDIS_ SLAVE 标志处于打开状态时使用 +* REDIS_LUA_CLIENT 表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端 + +一部分标志记录目前**客户端所处的状态**: + +* REDIS_UNIX_SOCKET 表示服务器使用 UNIX 套接字来连接客户端 +* REDIS_BLOCKED 表示客户端正在被 BRPOP、BLPOP 等命令阻塞 +* REDIS_UNBLOCKED 表示客户端已经从 REDIS_BLOCKED 所表示的阻塞状态脱离,在 REDIS_BLOCKED 标志打开的情况下使用 +* REDIS_MULTI 标志表示客户端正在执行事务 +* REDIS_DIRTY_CAS 表示事务使用 WATCH 命令监视的数据库键已经被修改 +* ..... + + + + + +**** + + + +##### 缓冲区 + +客户端状态的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但最大大小不能超过 1GB,否则服务器将关闭这个客户端,比如执行 `SET key value `,那么缓冲区 querybuf 的内容: + +```sh +*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n # +``` + +输出缓冲区是服务器用于保存执行客户端命令所得的命令回复,每个客户端都有两个输出缓冲区可用: + +* 一个是固定大小的缓冲区,保存长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等 +* 一个是可变大小的缓冲区,保存那些长度比较大的回复, 比如一个非常长的字符串值或者一个包含了很多元素的集合等 + +buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES (常量默认 16*1024 = 16KB) 字节的字节数组,bufpos 属性记录了 buf 数组目前已使用的字节数量,当 buf 数组的空间已经用完或者回复数据太大无法放进 buf 数组里,服务器就会开始使用可变大小的缓冲区 + +通过使用 reply 链表连接多个字符串对象,可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-可变输出缓冲区.png) + + + + + +*** + + + +##### 命令 + +服务器对 querybuf 中的命令请求的内容进行分析,得出的命令参数以及参数的数量分别保存到客户端状态的 argv 和 argc 属性 + +* argv 属性是一个数组,数组中的每项都是字符串对象,其中 argv[0] 是要执行的命令,而之后的其他项则是命令的参数 +* argc 属性负责记录 argv 数组的长度 + + + +服务器将根据项 argv[0] 的值,在命令表中查找命令所对应的命令的 redisCommand,将客户端状态的 cmd 指向该结构 + +命令表是一个字典结构,键是 SDS 结构保存命令的名字;值是命令所对应的 redisCommand 结构,保存了命令的实现函数、命令标志、 命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息 + + + + + +**** + + + +##### 验证 + +客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证 + +* authenticated 值为 0,表示客户端未通过身份验证 +* authenticated 值为 1,表示客户端已通过身份验证 + +当客户端 authenticated = 0 时,除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行 + +```sh +redis> PING +(error) NOAUTH Authentication required. +redis> AUTH 123321 +OK +redis> PING +PONG +``` + + + +*** + + + +##### 时间 + +ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,`CLIENT list` 命令的 age 域记录了这个秒数 + +lastinteraction 属性记录了客户端与服务器最后一次进行互动 (interaction) 的时间,互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。该属性可以用来计算客户端的空转 (idle) 时长, 就是距离客户端与服务器最后一次进行互动已经过去了多少秒,`CLIENT list` 命令的 idle 域记录了这个秒数 + +obuf_soft_limit_reached_time 属性记录了**输出缓冲区第一次到达软性限制** (soft limit) 的时间 + + + + + +*** + + + + + +#### 生命周期 + +##### 创建 + +服务器使用不同的方式来创建和关闭不同类型的客户端 + +如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用 connect 函数连接到服务器时,服务器就会调用连接应答处理器为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-服务器clients链表.png) + +服务器会在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将伪客户端关联在服务器状态的 lua_client 属性 + +```c +struct redisServer { + // 保存伪客户端 + redisClient *lua_client; + + //... +}; +``` + +lua_client 伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭 + +载入 AOF 文件时, 服务器会创建用于执行 AOF 文件包含的 Redis 命令的伪客户端,并在载入完成之后,关闭这个伪客户端 + + + +**** + + + +##### 关闭 + +一个普通客户端可以因为多种原因而被关闭: + +* 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭 +* 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端会**被服务器关闭** +* 客户端是 `CLIENT KILL` 命令的目标 +* 如果用户为服务器设置了 timeout 配置选项,那么当客户端的空转时间超过该值时将被关闭,特殊情况不会被关闭: + * 客户端是主服务器(REDIS_MASTER )或者从服务器(打开了 REDIS_SLAVE 标志) + * 正在被 BLPOP 等命令阻塞(REDIS_BLOCKED) + * 正在执行 SUBSCRIBE、PSUBSCRIBE 等订阅命令 +* 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为 1GB) +* 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小 + +理论上来说,可变缓冲区可以保存任意长的命令回复,但是为了回复过大占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作: + +* 硬性限制 (hard limit):输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器会关闭客户端(serverCron 函数中执行),积存在输出缓冲区中的所有内容会被**直接释放**,不会返回给客户端 +* 软性限制 (soft limit):输出缓冲区的大小超过了软性限制所设置的大小,小于硬性限制的大小,服务器的操作: + * 用属性 obuf_soft_limit_reached_time 记录下客户端到达软性限制的起始时间,继续监视客户端 + * 如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端 + * 如果在指定时间内不再超出软性限制,那么客户端就不会被关闭,并且 o_s_l_r_t 属性清零 + +使用 client-output-buffer-limit 选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式: + +```sh +client-output-buffer-limit + +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 +``` + +* 第一行:将普通客户端的硬性限制和软性限制都设置为 0,表示不限制客户端的输出缓冲区大小 +* 第二行:将从服务器客户端的硬性限制设置为 256MB,软性限制设置为 64MB,软性限制的时长为 60 秒 +* 第三行:将执行发布与订阅功能的客户端的硬性限制设置为 32MB,软性限制设置为 8MB,软性限制的时长为 60 秒 + + + + + +**** + + + +### 服务器 + +#### 执行流程 + +Redis 服务器与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转,所以一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作 + + + +##### 命令请求 + +Redis 服务器的命令请求来自 Redis 客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,通过连接到服务器的套接字,将协议格式的命令请求发送给服务器 + +```sh +SET KEY VALUE -> # 命令 +*3\r\nS3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n # 协议格式 +``` + +当客户端与服务器之间的连接套接字因为客户端的写入而变得可读,服务器调用**命令请求处理器**来执行以下操作: + +* 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面 +* 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里 +* 调用命令执行器,执行客户端指定的命令 + +最后客户端接收到协议格式的命令回复之后,会将这些回复转换成用户可读的格式打印给用户观看,至此整体流程结束 + + + +**** + + + +##### 命令执行 + +命令执行器开始对命令操作: + +* 查找命令:首先根据客户端状态的 argv[0] 参数,在**命令表 (command table)** 中查找参数所指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面,是一个 redisCommand 结构 + + 命令查找算法与字母的大小写无关,所以命令名字的大小写不影响命令表的查找结果 + +* 执行预备操作: + + * 检查客户端状态的 cmd 指针是否指向 NULL,根据 redisCommand 检查请求参数的数量是否正确 + * 检查客户端是否通过身份验证 + * 如果服务器打开了 maxmemory 功能,执行命令之前要先检查服务器的内存占用,在有需要时进行内存回收(**逐出算法**) + * 如果服务器上一次执行 BGSAVE 命令出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,那么如果本次执行的是写命令,服务会拒绝执行,并返回错误 + * 如果客户端当前正在用 SUBSCRIBE 或 PSUBSCRIBE 命令订阅频道,那么服务器会拒绝除了 SUBSCRIBE、SUBSCRIBE、 UNSUBSCRIBE、PUNSUBSCRIBE 之外的其他命令 + * 如果服务器正在进行载入数据,只有 sflags 带有 1 标识(比如 INFO、SHUTDOWN、PUBLISH等)的命令才会被执行 + * 如果服务器执行 Lua 脚本而超时并进入阻塞状态,那么只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令 + * 如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 四个命令,其他命令都会被**放进事务队列**中 + * 如果服务器打开了监视器功能,那么会将要执行的命令和参数等信息发送给监视器 + +* 调用命令的实现函数:被调用的函数会执行指定的操作并产生相应的命令回复,回复会被保存在客户端状态的输出缓冲区里面(buf 和 reply 属性),然后实现函数还会**为客户端的套接字关联命令回复处理器**,这个处理器负责将命令回复返回给客户端 + +* 执行后续工作: + + * 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志 + * 根据执行命令所耗费的时长,更新命令的 redisCommand 结构的 milliseconds 属性,并将命令 calls 计数器的值增一 + * 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 缓冲区里面 + * 如果有其他从服务器正在复制当前这个服务器,那么服务器会将执行的命令传播给所有从服务器 + +* 将命令回复发送给客户端:客户端**套接字变为可写状态**时,服务器就会执行命令回复处理器,将客户端输出缓冲区中的命令回复发送给客户端,发送完毕之后回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备 + + + +**** + + + +##### Command + +每个 redisCommand 结构记录了一个Redis 命令的实现信息,主要属性 + +```c +struct redisCommand { + // 命令的名字,比如"set" + char *name; + + // 函数指针,指向命令的实现函数,比如setCommand + // redisCommandProc 类型的定义为 typedef void redisCommandProc(redisClient *c) + redisCommandProc *proc; + + // 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N, 那么表示参数的数量大于等于N。 + // 注意命令的名字本身也是一个参数,比如 SET msg "hello",命令的参数是"SET"、"msg"、"hello" 三个 + int arity; + + // 字符串形式的标识值,这个值记录了命令的属性,, + // 比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,是否允许在Lua脚本中使用等等 + char *sflags; + + // 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是 flags 属性 + // 而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成 + int flags; + + // 服务器总共执行了多少次这个命令 + long long calls; + + // 服务器执行这个命令所耗费的总时长 + long long milliseconds; +}; +``` + + + + + +**** + + + +#### serverCron + +##### 基本介绍 + +Redis 服务器以周期性事件的方式来运行 serverCron 函数,服务器初始化时读取配置 server.hz 的值,默认为 10,代表每秒钟执行 10 次,即**每隔 100 毫秒执行一次**,执行指令 info server 可以查看 + +serverCron 函数负责定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行 + +* 更新服务器的各类统计信息,比如时间、内存占用、 数据库占用情况等 +* 清理数据库中的过期键值对 +* 关闭和清理连接失效的客户端 +* 进行 AOF 或 RDB 持久化操作 +* 如果服务器是主服务器,那么对从服务器进行定期同步 +* 如果处于集群模式,对集群进行定期同步和连接测试 + + + +**** + + + +##### 时间缓存 + +Redis 服务器中有很多功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的 unixtime 属性和 mstime 属性被用作当前时间的缓存 + +```c +struct redisServer { + // 保存了秒级精度的系统当前UNIX时间戳 + time_t unixtime; + // 保存了毫秒级精度的系统当前UNIX时间戳 + long long mstime; + +}; +``` + +serverCron 函数默认以每 100 毫秒一次的频率更新两个属性,所以属性记录的时间的精确度并不高 + +* 服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上 +* 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间 + + + +*** + + + +##### LRU 时钟 + +服务器状态中的 lruclock 属性保存了服务器的 LRU 时钟 + +```c +struct redisServer { + // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。 + unsigned lruclock:22; +}; +``` + +每个 Redis 对象都会有一个 lru 属性, 这个 lru 属性保存了对象最后一次被命令访问的时间 + +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` + +当服务器要计算一个数据库键的空转时间(即数据库键对应的值对象的空转时间),程序会用服务器的 lruclock 属性记录的时间减去对象的 lru 属性记录的时间 + +serverCron 函数默认以每 100 毫秒一次的频率更新这个属性,所以得出的空转时间也是模糊的 + + + +*** + + + +##### 命令次数 + +serverCron 中的 trackOperationsPerSecond 函数以每 100 毫秒一次的频率执行,函数功能是以**抽样计算**的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过 INFO status 命令的 instantaneous_ops_per_sec 域查看: + +```sh +redis> INFO stats +# Stats +instantaneous_ops_per_sec:6 +``` + +根据上一次抽样时间 ops_sec_last_sample_time 和当前系统时间,以及上一次已执行的命令数 ops_sec_last_sample_ops 和服务器当前已经执行的命令数,计算出两次函数调用期间,服务器平均每毫秒处理了多少个命令请求,该值乘以 1000 得到每秒内的执行命令的估计值,放入 ops_sec_samples 环形数组里 + +```c +struct redisServer { + // 上一次进行抽样的时间 + long long ops_sec_last_sample_time; + // 上一次抽样时,服务器已执行命令的数量 + long long ops_sec_last_sample_ops; + // REDIS_OPS_SEC_SAMPLES 大小(默认值为16)的环形数组,数组的每一项记录一次的抽样结果 + long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; + // ops_sec_samples数组的索引值,每次抽样后将值自增一,值为16时重置为0,让数组成为一个环形数组 + int ops_sec_idx; +}; +``` + + + + + +*** + + + +##### 内存峰值 + +服务器状态里的 stat_peak_memory 属性记录了服务器内存峰值大小,循环函数每次执行时都会查看服务器当前使用的内存数量,并与 stat_peak_memory 保存的数值进行比较,设置为较大的值 + +```c +struct redisServer { + // 已使用内存峰值 + size_t stat_peak_memory; +}; +``` + +INFO memory 命令的 used_memory_peak 和 used_memory_peak_human 两个域分别以两种格式记录了服务器的内存峰值: + +```sh +redis> INFO memory +# Memory +... +used_memory_peak:501824 +used_memory_peak_human:490.06K +``` + + + +*** + + + +##### SIGTERM + +服务器启动时,Redis 会为服务器进程的 SIGTERM 信号关联处理器 sigtermHandler 函数,该信号处理器负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识 + +```c +struct redisServer { + // 关闭服务器的标识:值为1时关闭服务器,值为0时不做操作 + int shutdown_asap; +}; +``` + +每次 serverCron 函数运行时,程序都会对服务器状态的 shutdown_asap 属性进行检查,并根据属性的值决定是否关闭服务器 + +服务器在接到 SIGTERM 信号之后,关闭服务器并打印相关日志的过程: + +```sh +[6794 | signal handler] (1384435690) Received SIGTERM, scheduling shutdown ... +[6794] 14 Nov 21:28:10.108 # User requested shutdown ... +[6794] 14 Nov 21:28:10.108 * Saving the final RDB snapshot before exiting. +[6794) 14 Nov 21:28:10.161 * DB saved on disk +[6794) 14 Nov 21:28:10.161 # Redisis now ready to exit, bye bye ... +``` + + + +*** + + + +##### 管理资源 + +serverCron 函数每次执行都会调用 clientsCron 和 databasesCron 函数,进行管理客户端资源和数据库资源 + +clientsCron 函数对一定数量的客户端进行以下两个检查: + +* 如果客户端与服务器之间的连接巳经超时(很长一段时间客户端和服务器都没有互动),那么程序释放这个客户端 +* 如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区耗费了过多的内存 + +databasesCron 函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时对字典进行收缩操作 + + + +*** + + + +##### 持久状态 + +服务器状态中记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的 ID + +```c +struct redisServer { + // 记录执行BGSAVE命令的子进程的ID,如果服务器没有在执行BGSAVE,那么这个属性的值为-1 + pid_t rdb_child_pid; + // 记录执行BGREWRITEAOF命令的子进程的ID,如果服务器没有在执行那么这个属性的值为-1 + pid_t aof_child_pid +}; +``` + +serverCron 函数执行时,会检查两个属性的值,只要其中一个属性的值不为 -1,程序就会执行一次 wait3 函数,检查子进程是否有信号发来服务器进程: + +* 如果有信号到达,那么表示新的 RDB 文件已经生成或者 AOF 重写完毕,服务器需要进行相应命令的后续操作,比如用新的 RDB 文件替换现有的 RDB 文件,用重写后的 AOF 文件替换现有的 AOF 文件 +* 如果没有信号到达,那么表示持久化操作未完成,程序不做动作 + +如果两个属性的值都为 -1,表示服务器没有进行持久化操作 + +* 查看是否有 BGREWRITEAOF 被延迟,然后执行 AOF 后台重写 + +* 查看服务器的自动保存条件是否已经被满足,并且服务器没有在进行持久化,就开始一次新的 BGSAVE 操作 + + 因为条件 1 可能会引发一次 AOF,所以在这个检查中会再次确认服务器是否已经在执行持久化操作 + +* 检查服务器设置的 AOF 重写条件是否满足,条件满足并且服务器没有进行持久化,就进行一次 AOF 重写 + +如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里还有待写入的数据, 那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里 + + + +*** + + + +##### 延迟执行 + +在服务器执行 BGSAVE 命令的期间,如果客户端发送 BGREWRITEAOF 命令,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到 BGSAVE 命令执行完毕之后,用服务器状态的 aof_rewrite_scheduled 属性标识延迟与否 + +```c +struct redisServer { + // 如果值为1,那么表示有 BGREWRITEAOF命令被延迟了 + int aof_rewrite_scheduled; +}; +``` + +serverCron 函数会检查 BGSAVE 或者 BGREWRITEAOF 命令是否正在执行,如果这两个命令都没在执行,并且 aof_rewrite_scheduled 属性的值为 1,那么服务器就会执行之前被推延的 BGREWRITEAOF 命令 + + + +**** + + + +##### 执行次数 + +服务器状态的 cronloops 属性记录了 serverCron 函数执行的次数 + +```c +struct redisServer { + // serverCron 函数每执行一次,这个属性的值就增 1 + int cronloops; +}; +``` + + + +**** + + + +##### 缓冲限制 + +服务器会关闭那些输入或者输出**缓冲区大小超出限制**的客户端 + + + + + +**** + + + +#### 初始化 + +##### 初始结构 + +一个 Redis 服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程 + +第一步:创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值,由 initServerConfig 函数进行初始化一般属性: + +* 设置服务器的运行 ID、默认运行频率、默认配置文件路径、默认端口号、默认 RDB 持久化条件和 AOF 持久化条件 +* 初始化服务器的 LRU 时钟,创建命令表 + +第二步:载入配置选项,用户可以通过给定配置参数或者指定配置文件,对 server 变量相关属性的默认值进行修改 + +第三步:初始化服务器数据结构(除了命令表之外),因为服务器**必须先载入用户指定的配置选项才能正确地对数据结构进行初始化**,所以载入配置完成后才进性数据结构的初始化,服务器将调用 initServer 函数: + +* server.clients 链表,记录了的客户端的状态结构;server.db 数组,包含了服务器的所有数据库 +* 用于保存频道订阅信息的 server.pubsub_channels 字典, 以及保存模式订阅信息的 server.pubsub_patterns 链表 +* 用于执行 Lua 脚本的 Lua 环境 server.lua +* 保存慢查询日志的 server.slowlog 属性 + +initServer 还进行了非常重要的设置操作: + +* 为服务器设置进程信号处理器 +* 创建共享对象,包含 OK、ERR、**整数 1 到 10000 的字符串对象**等 +* **打开服务器的监听端口** +* **为 serverCron 函数创建时间事件**, 等待服务器正式运行时执行 serverCron 函数 +* 如果 AOF 持久化功能已经打开,那么打开现有的 AOF 文件,如果 AOF 文件不存在,那么创建并打开一个新的 AOF 文件 ,为 AOF 写入做好准备 +* **初始化服务器的后台 I/O 模块**(BIO), 为将来的 I/O 操作做好准备 + +当 initServer 函数执行完毕之后, 服务器将用 ASCII 字符在日志中打印出 Redis 的图标, 以及 Redis 的版本号信息 + + + +*** + + + +##### 还原状态 + +在完成了对服务器状态的初始化之后,服务器需要载入RDB文件或者AOF 文件, 并根据文件记录的内容来还原服务器的数据库状态: + +* 如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态 +* 如果服务器没有启用 AOF 持久化功能,那么服务器使用 RDB 文件来还原数据库状态 + +当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长 + +```sh +[7171] 22 Nov 22:43:49.084 * DB loaded from disk: 0.071 seconds +``` + + + +*** + + + +##### 驱动循环 + +在初始化的最后一步,服务器将打印出以下日志,并开始**执行服务器的事件循环**(loop) + +```c +[7171] 22 Nov 22:43:49.084 * The server is now ready to accept connections on pert 6379 +``` + +服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了 + + + + + +***** + + + +### 慢日志 + +#### 基本介绍 + +Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,通过产生的日志来监视和优化查询速度 + +服务器配置有两个和慢查询日志相关的选项: + +* slowlog-log-slower-than 选项指定执行时间超过多少微秒的命令请求会被记录到日志上 +* slowlog-max-len 选项指定服务器最多保存多少条慢查询日志 + +服务器使用先进先出 FIFO 的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除 + +配置选项可以通过 CONFIG SET option value 命令进行设置 + +常用命令: + +```sh +SLOWLOG GET [n] # 查看 n 条服务器保存的慢日志 +SLOWLOG LEN # 查看日志数量 +SLOWLOG RESET # 清除所有慢查询日志 +``` + + + +*** + + + +#### 日志保存 + +服务器状态中包含了慢查询日志功能有关的属性: + +```c +struct redisServer { + // 下一条慢查询日志的ID + long long slowlog_entry_id; + + // 保存了所有慢查询日志的链表 + list *slowlog; + + // 服务器配置选项的值 + long long slowlog-log-slower-than; + // 服务器配置选项的值 + unsigned long slowlog_max_len; +} +``` + +slowlog_entry_id 属性的初始值为 0,每当创建一条新的慢查询日志时,这个属性就会用作新日志的 id 值,之后该属性增一 + +slowlog 链表保存了服务器中的所有慢查询日志,链表中的每个节点是一个 slowlogEntry 结构, 代表一条慢查询日志: + +```c +typedef struct slowlogEntry { + // 唯一标识符 + long long id; + // 命令执行时的时间,格式为UNIX时间戳 + time_t time; + // 执行命令消耗的时间,以微秒为单位 + long long duration; + // 命令与命令参数 + robj **argv; + // 命令与命令参数的数量 + int argc; +} +``` + + + + + +*** + + + +#### 添加日志 + +在每次执行命令的前后,程序都会记录微秒格式的当前 UNIX 时间戳,两个时间之差就是执行命令所耗费的时长,函数会检查命令的执行时长是否超过 slowlog-log-slower-than 选项所设置: + +* 如果是的话,就为命令创建一个新的日志,并将新日志添加到 slowlog 链表的表头 +* 检查慢查询日志的长度是否超过 slowlog-max-len 选项所设置的长度,如果是将多出来的日志从 slowlog 链表中删除掉 + +* 将 redisServer. slowlog_entry_id 的值增 1 + + + + + +*** + + + + + +## 数据结构 + +### 字符串 + +#### SDS + +Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现 + +```c +struct sdshdr { + // 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度 + int len; + + // 记录buf数组中未使用字节的数量 + int free; + + // 【字节】数组,用于保存字符串(不是字符数组) + char buf[]; +}; +``` + +SDS 遵循 C 字符串**以空字符结尾**的惯例,保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS底层结构.png) + + + +*** + + + +#### 对比 + +常数复杂度获取字符串长度: + +* C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N) +* SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成 + +杜绝缓冲区溢出: + +* C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow) + + s1 和 s2 是内存中相邻的字符串,执行 `strcat(s1, " Cluster")`(有空格): + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-内存溢出问题.png) + +* SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题 + +二进制安全: + +* C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据 +* SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,**使用 len 属性来判断数据的结尾**,所以可以保存图片、视频、压缩文件等二进制数据 + +兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数 + + + +*** + + + +#### 内存 + +C 字符串**每次**增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露 + +SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录 + +内存重分配涉及复杂的算法,需要执行**系统调用**,是一个比较耗时的操作,SDS 的两种优化策略: + +* 空间预分配:当 SDS 需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间 + + * 对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等 + + s 为 Redis,执行 `sdscat(s, " Cluster")` 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27) + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-SDS内存预分配.png) + + * 对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间 + + 在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从**必定 N 次降低为最多 N 次** + +* 惰性空间释放:当 SDS 缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来复用 + + SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题 + + + + + +**** + + + +### 链表 + +链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型 + +链表节点: + +```c +typedef struct listNode { + // 前置节点 + struct listNode *prev; + + // 后置节点 + struct listNode *next; + // 节点的值 void *value } listNode; ``` -多个 listNode 通过 prev 和 next 指针组成**双端链表**: +多个 listNode 通过 prev 和 next 指针组成**双端链表**: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) + +list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len + +```c +typedef struct list { + // 表头节点 + listNode *head; + // 表尾节点 + listNode *tail; + + // 链表所包含的节点数量 + unsigned long len; + + // 节点值复制函数,用于复制链表节点所保存的值 + void *(*dup) (void *ptr); + // 节点值释放函数,用于释放链表节点所保存的值 + void (*free) (void *ptr); + // 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等 + int (*match) (void *ptr, void *key); +} list; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表底层结构.png) + +Redis 链表的特性: + +* 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是 O(1) +* 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点 +* 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为 O(1) +* 带链表长度计数器:使用 len 属性来对 list 持有的链表节点进行计数,获取链表中节点数量的时间复杂度为 O(1) +* 多态:链表节点使用 void * 指针来保存节点值, 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数,所以链表可以保存各种**不同类型的值** + + + + + +**** + + + +### 字典 + +#### 哈希表 + +Redis 字典使用的哈希表结构: + +```c +typedef struct dictht { + // 哈希表数组,数组中每个元素指向 dictEntry 结构 + dictEntry **table; + + // 哈希表大小,数组的长度 + unsigned long size; + + // 哈希表大小掩码,用于计算索引值,总是等于 【size-1】 + unsigned long sizemask; + + // 该哈希表已有节点的数量 + unsigned long used; +} dictht; +``` + +哈希表节点结构: + +```c +typedef struct dictEntry { + // 键 + void *key; + + // 值,可以是一个指针,或者整数 + union { + void *val; // 指针 + uint64_t u64; + int64_t s64; + } + + // 指向下个哈希表节点,形成链表,用来解决冲突问题 + struct dictEntry *next; +} dictEntry; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希表底层结构.png) + + + +*** + + + +#### 字典结构 + +字典,又称为符号表、关联数组、映射(Map),用于保存键值对的数据结构,字典中的每个键都是独一无二的。底层采用哈希表实现,一个哈希表包含多个哈希表节点,每个节点保存一个键值对 + +```c +typedef struct dict { + // 类型特定函数 + dictType *type; + + // 私有数据 + void *privdata; + + // 哈希表,数组中的每个项都是一个dictht哈希表, + // 一般情况下字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 + dictht ht[2]; + + // rehash 索引,当 rehash 不在进行时,值为 -1 + int rehashidx; +} dict; +``` + +type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的: + +* type 属性是指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数 +* privdata 属性保存了需要传给那些类型特定函数的可选参数 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典底层结构.png) + + + +**** + + + +#### 哈希冲突 + +Redis 使用 MurmurHash 算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快 + +将一个新的键值对添加到字典里,需要先根据键 key 计算出哈希值,然后进行取模运算(取余): + +```c +index = hash & dict->ht[x].sizemask +``` + +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时,就称这些键发生了哈希冲突(collision) + +Redis 的哈希表使用链地址法(separate chaining)来解决键哈希冲突, 每个哈希表节点都有一个 next 指针,多个节点通过 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题 + +dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(**头插法**),时间复杂度为 O(1) + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典解决哈希冲突.png) + + + +**** + + + +#### 负载因子 + +负载因子的计算方式:哈希表中的**节点数量** / 哈希表的大小(**长度**) + +```c +load_factor = ht[0].used / ht[0].size +``` + +为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时 ,程序会自动对哈希表的大小进行相应的扩展或者收缩 + +哈希表执行扩容的条件: + +* 服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 1 + +* 服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 5 + + 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 + +哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测) + + + +*** + + + +#### 重新散列 + +扩展和收缩哈希表的操作通过 rehash(重新散列)来完成,步骤如下: + +* 为字典的 ht[1] 哈希表分配空间,空间大小的分配情况: + * 如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$ + * 如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$ +* 将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上 +* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0] 变为空表),释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 + +如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 + +Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中式的完成,而是分多次,渐进式的完成,又叫**渐进式 rehash** + +* 为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表 +* 在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始 +* 在 rehash 进行期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** +* 随着字典操作的不断执行,最终在某个时间点 ht[0] 的所有键值对都被 rehash 至 ht[1],将 rehashidx 属性的值设为 -1 + +渐进式 rehash 采用**分而治之**的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 + +渐进式 rehash 期间的哈希表操作: + +* 字典的查找、删除、更新操作会在两个哈希表上进行,比如查找一个键会先在 ht[0] 上查找,查找不到就去 ht[1] 继续查找 +* 字典的添加操作会直接在 ht[1] 上添加,不在 ht[0] 上进行任何添加 + + + + + +**** + + + +### 跳跃表 + +#### 底层结构 + +跳跃表(skiplist)是一种有序(**默认升序**)的数据结构,在链表的基础上**增加了多级索引以提升查找的效率**,索引是占内存的,所以是一个**空间换时间**的方案,跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找,效率与平衡树相当但是实现更简单 + +原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点(占内存)则可以忽略 + +Redis 只在两个地方应用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构 + +```c +typedef struct zskiplist { + // 表头节点和表尾节点,O(1) 的时间复杂度定位头尾节点 + struct skiplistNode *head, *tail; + + // 表的长度,也就是表内的节点数量 (表头节点不计算在内) + unsigned long length; + + // 表中层数最大的节点的层数 (表头节点的层高不计算在内) + int level +} zskiplist; +``` + +```c +typedef struct zskiplistNode { + // 层 + struct zskiplistLevel { + // 前进指针 + struct zskiplistNode *forward; + // 跨度 + unsigned int span; + } level[]; + + // 后退指针 + struct zskiplistNode *backward; + + // 分值 + double score; + + // 成员对象 + robj *obj; +} zskiplistNode; +``` + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-跳表底层结构.png) + + + +*** + + + +#### 属性分析 + +层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 + +前进指针:forward 用于从表头到表尾方向**正序(升序)遍历节点**,遇到 NULL 停止遍历 + +跨度:span 用于记录两个节点之间的距离,用来计算排位(rank): + +* 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 + +* 在查找某个节点的过程中,**将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位**,按照上图所示: + + 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 + + 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 + +后退指针:backward 用于从表尾到表头方向**逆序(降序)遍历节点** + +分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 + +成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的**成员对象必须是唯一的**,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) + + + +个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表 + + + +**** + + + +### 整数集合 + +#### 底层结构 + +整数集合(intset)是用于保存整数值的集合数据结构,是 Redis 集合键的底层实现之一 + +```c +typedef struct intset { + // 编码方式 + uint32_t encoding; + + // 集合包含的元素数量,也就是 contents 数组的长度 + uint32_t length; + + // 保存元素的数组 + int8_t contents[]; +} intset; +``` + +encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 + +整数集合的每个元素都是 contents 数组的一个数组项(item),在数组中按值的大小从小到大**有序排列**,并且数组中**不包含任何重复项**。虽然 contents 属性声明为 int8_t 类型,但实际上数组并不保存任何 int8_t 类型的值, 真正类型取决于 encoding 属性 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合底层结构.png) + +说明:底层存储结构是数组,所以为了保证有序性和不重复性,每次添加一个元素的时间复杂度是 O(N) + + + +**** + + + +#### 类型升级 + +整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: + +* 根据新元素的类型长度以及集合元素的数量(包括新元素在内),扩展整数集合底层数组的空间大小 + +* 将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放入正确的位置,放置过程保证数组的有序性 + + 图示 32 * 4 = 128 位,首先将 3 放入索引 2(64 位 - 95 位),然后将 2 放置索引 1,将 1 放置在索引 0,从后向前依次放置在对应的区间,最后放置 65535 元素到索引 3(96 位- 127 位),修改 length 属性为 4 + +* 将新元素添加到底层数组里 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合升级.png) + +每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N) + +引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素,升级之后新元素的摆放位置: + +* 在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引 0) +* 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引 length-1) + +整数集合升级策略的优点: + +* 提升整数集合的灵活性:C 语言是静态类型语言,为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面,整数集合可以自动升级底层数组来适应新元素,所以可以随意的添加整数 + +* 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 + +整数集合**不支持降级操作**,一旦对数组进行了升级,编码就会一直保持升级后的状态 + + + + + +***** + + + +### 压缩列表 + +#### 底层结构 + +压缩列表(ziplist)是 Redis 为了节约内存而开发的,是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) + +* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算 zlend 的位置时使用 +* zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 +* zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 +* entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** +* zlend:uint8_t 类型 1 字节,是一个特殊值 0xFF (255),用于标记压缩列表的末端 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表示例.png) + +列表 zlbytes 属性的值为 0x50 (十进制 80),表示压缩列表的总长为 80 字节,列表 zltail 属性的值为 0x3c (十进制 60),假设表的起始地址为 p,计算得出表尾节点 entry3 的地址 p + 60 + + + +**** + + + +#### 列表节点 + +列表节点 entry 的数据结构: + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) + +previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成**从表尾向表头遍历**操作 + +* 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 +* 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 + +encoding:记录了节点的 content 属性所保存的数据类型和长度 + +* **长度为 1 字节、2 字节或者 5 字节**,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表字节数组编码.png) + +* 长度为 1 字节,值的最高位为 11 的是整数编码,整数值的类型和长度由编码除去最高两位之后的其他位记录 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表整数编码.png) + +content:每个压缩列表节点可以保存一个字节数组或者一个整数值 + +* 字节数组可以是以下三种长度的其中一种: + + * 长度小于等于 $63 (2^6-1)$ 字节的字节数组 + + * 长度小于等于 $16383(2^{14}-1)$ 字节的字节数组 + + * 长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组 + +* 整数值则可以是以下六种长度的其中一种: + + * 4 位长,介于 0 至 12 之间的无符号整数 + + * 1 字节长的有符号整数 + + * 3 字节长的有符号整数 + + * int16_t 类型整数 + + * int32_t 类型整数 + + * int64_t 类型整数 + + + +*** + + + +#### 连锁更新 + +Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新(cascade update) + +假设在一个压缩列表中,有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点,new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节,无法保存新节点 new 的长度,所以要对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间,所以扩展后 e1 的长度就变成了 254 至 257 字节之间,导致 e2 的 previous_entry_length 属性无法保存 e1 的长度,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新1.png) + + 删除节点也可能会引发连锁更新,big.length >= 254,small.length < 254,删除 small 节点 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新2.png) + +连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配,每次重分配的最坏复杂度为 O(N),所以连锁更新的最坏复杂度为 O(N^2) + +说明:尽管连锁更新的复杂度较高,但出现的记录是非常低的,即使出现只要被更新的节点数量不多,就不会对性能造成影响 + + + + + +**** + + + + + +## 数据类型 + +### redisObj + +#### 对象系统 + +Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(**键对象**),另一个对象用作键值对的值(**值对象**) + +Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: + +```c +typedef struct redisObiect { + // 类型 + unsigned type:4; + // 编码 + unsigned encoding:4; + // 指向底层数据结构的指针 + void *ptr; + + // .... +} robj; +``` + +Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 + +Redis 是一个 Map 类型,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) + +* 对一个数据库键执行 TYPE 命令,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型 +* 对一个数据库键执行 OBJECT ENCODING 命令,查看数据库键对应的值对象的编码 + + + +**** + + + +#### 命令多态 + +Redis 中用于操作键的命令分为两种类型: + +* 一种命令可以对任何类型的键执行,比如说 DEL 、EXPIRE、RENAME、 TYPE 等(基于类型的多态) +* 只能对特定类型的键执行,比如 SET 只能对字符串键执行、HSET 对哈希键执行、SADD 对集合键执行,如果类型步匹配会报类型错误: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + +Redis 为了确保只有指定类型的键可以执行某些特定的命令,在执行类型特定的命令之前,先通过值对象 redisObject 结构 type 属性检查操作类型是否正确,然后再决定是否执行指定的命令 + +对于多态命令,比如列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) + + + +*** + + + +#### 内存回收 + +对象的整个生命周期可以划分为创建对象、 操作对象、 释放对象三个阶段 + +C 语言没有自动回收内存的功能,所以 Redis 在对象系统中构建了引用计数(reference counting)技术实现的内存回收机制,程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收 + +```c +typedef struct redisObiect { + // 引用计数 + int refcount; +} robj; +``` + +对象的引用计数信息会随着对象的使用状态而不断变化,创建时引用计数 refcount 初始化为 1,每次被一个新程序使用时引用计数加 1,当对象不再被一个程序使用时引用计数值会被减 1,当对象的引用计数值变为 0 时,对象所占用的内存会被释放 + + + +*** + + + +#### 对象共享 + +对象的引用计数属性带有对象共享的作用,共享对象机制更节约内存,数据库中保存的相同值对象越多,节约的内存就越多 + +让多个键共享一个对象的步骤: + +* 将数据库键的值指针指向一个现有的值对象 + +* 将被共享的值对象的引用计数增一 + + + +Redis 在初始化服务器时创建一万个(配置文件可以修改)字符串对象,包含了**从 0 到 9999 的所有整数值**,当服务器需要用到值为 0 到 9999 的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象 + +比如创建一个值为 100 的键 A,并使用 OBJECT REFCOUNT 命令查看键 A 的值对象的引用计数,会发现值对象的引用计数为 2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键 A + +共享对象在嵌套了字符串对象的对象(linkedlist 编码的列表、hashtable 编码的哈希、zset 编码的有序集合)中也能使用 + +Redis 不共享包含字符串对象的原因:验证共享对象和目标对象是否相同的复杂度越高,消耗的 CPU 时间也会越多 + +* 整数值的字符串对象, 验证操作的复杂度为 O(1) +* 字符串值的字符串对象, 验证操作的复杂度为 O(N) +* 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,验证操作的复杂度为 O(N^2) + + + +**** + + + +#### 空转时长 + +redisObject 结构包含一个 lru 属性,该属性记录了对象最后一次被命令程序访问的时间 + +```c +typedef struct redisObiect { + unsigned lru:22; +} robj; +``` + +OBJECT IDLETIME 命令可以打印出给定键的空转时长,该值就是通过将当前时间减去键的值对象的 lru 时间计算得出的,这个命令在访问键的值对象时,不会修改值对象的 lru 属性 + +```sh +redis> OBJECT IDLETIME msg +(integer) 10 +# 等待一分钟 +redis> OBJECT IDLETIME msg +(integer) 70 +# 访问 msg +redis> GET msg +"hello world" +# 键处于活跃状态,空转时长为 0 +redis> OBJECT IDLETIME msg +(integer) 0 +``` + +空转时长的作用:如果服务器开启 maxmemory 选项,并且回收内存的算法为 volatile-lru 或者 allkeys-lru,那么当服务器占用的内存数超过了 maxmemory 所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存(LRU 算法) + + + + + +*** + + + +### string + +#### 简介 + +存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型,实质上是存一个字符串,string 类型是二进制安全的,可以包含任何数据,比如图片或者序列化的对象 + +存储数据的格式:一个存储空间保存一个数据,每一个空间中只能保存一个字符串信息 + +存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 + + + +Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败 + +字符串对象可以是 int、raw、embstr 三种实现方式 + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作: + + ```sh + set key value #添加/修改数据添加/修改数据 + del key #删除数据 + setnx key value #判定性添加数据,键值为空则设添加 + mset k1 v1 k2 v2... #添加/修改多个数据,m:Multiple + append key value #追加信息到原始信息后部(如果原始信息存在就追加,否则新建) + ``` + +* 查询操作 + + ```sh + get key #获取数据,如果不存在,返回空(nil) + mget key1 key2... #获取多个数据 + strlen key #获取数据字符个数(字符串长度) + ``` + +* 设置数值数据增加/减少指定范围的值 + + ```sh + incr key #key++ + incrby key increment #key+increment + incrbyfloat key increment #对小数操作 + decr key #key-- + decrby key increment #key-increment + ``` + +* 设置数据具有指定的生命周期 + + ```sh + setex key seconds value #设置key-value存活时间,seconds单位是秒 + psetex key milliseconds value #毫秒级 + ``` + +注意事项: + +1. 数据操作不成功的反馈与数据正常操作之间的差异 + + * 表示运行结果是否成功 + + * (integer) 0 → false ,失败 + + * (integer) 1 → true,成功 + + * 表示运行结果值 + + * (integer) 3 → 3 个 + + * (integer) 1 → 1 个 + +2. 数据未获取到时,对应的数据为(nil),等同于null + +3. **数据最大存储量**:512MB + +4. string 在 Redis 内部存储默认就是一个字符串,当遇到增减类操作 incr,decr 时**会转成数值型**进行计算 + +5. 按数值进行操作的数据,如果原始数据不能转成数值,或超越了Redis 数值上限范围,将报错 + 9223372036854775807(java 中 Long 型数据最大值,Long.MAX_VALUE) + +6. Redis 可用于控制数据库表主键 ID,为数据库表主键提供生成策略,保障数据库表的主键唯一性 + + +单数据和多数据的选择: + +* 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3 次返回 +* 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1 次返回(发送和返回的事件略高于单数据) + + + + + + + +*** + + + +#### 实现 + +字符串对象的编码可以是 int、raw、embstr 三种 + +* int:字符串对象保存的是**整数值**,并且整数值可以用 long 类型来表示,那么对象会将整数值保存在字符串对象结构的 ptr 属性面(将 void * 转换成 long),并将字符串对象的编码设置为 int(浮点数用另外两种方式) + + + +* raw:字符串对象保存的是一个字符串值,并且值的长度大于 39 字节,那么对象将使用简单动态字符串(SDS)来保存该值,并将对象的编码设置为 raw + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象raw编码.png) + +* embstr:字符串对象保存的是一个字符串值,并且值的长度小于等于 39 字节,那么对象将使用 embstr 编码的方式来保存这个字符串值,并将对象的编码设置为 embstr + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字符串对象embstr编码.png) + + 上图所示,embstr 与 raw 都使用了 redisObject 和 sdshdr 来表示字符串对象,但是 raw 需要调用两次内存分配函数分别创建两种结构,embstr 只需要一次内存分配来分配一块**连续的空间** + +embstr 是用于保存短字符串的一种编码方式,对比 raw 的优点: + +* 内存分配次数从两次降低为一次,同样释放内存的次数也从两次变为一次 +* embstr 编码的字符串对象的数据都保存在同一块连续内存,所以比 raw 编码能够更好地利用缓存优势(局部性原理) + +int 和 embstr 编码的字符串对象在条件满足的情况下,会被转换为 raw 编码的字符串对象: + +* int 编码的整数值,执行 APPEND 命令追加一个字符串值,先将整数值转为字符串然后追加,最后得到一个 raw 编码的对象 +* Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以 embstr 对象实际上**是只读的**,执行修改命令会将对象的编码从 embstr 转换成 raw,操作完成后得到一个 raw 编码的对象 + +某些情况下,程序会将字符串对象里面的字符串值转换回浮点数值,执行某些操作后再将浮点数值转换回字符串值: + +```sh +redis> SET pi 3.14 +OK +redis> OBJECT ENCODING pi +"embstr" +redis> INCRBYFLOAT pi 2.0 # 转为浮点数执行增加的操作 +"5. 14" +redis> OBJECT ENCODING pi +"embstr" +``` + + + + + + + +**** + + + +#### 应用 + +主页高频访问信息显示控制,例如新浪微博大 V 主页显示粉丝数与微博数量 + +* 在 Redis 中为大 V 用户设定用户信息,以用户主键和属性值作为 key,后台设定定时刷新策略 + + ```sh + set user:id:3506728370:fans 12210947 + set user:id:3506728370:blogs 6164 + set user:id:3506728370:focuses 83 + ``` + +* 使用 JSON 格式保存数据 + + ```sh + user:id:3506728370 → {"fans":12210947,"blogs":6164,"focuses":83} + ``` + +* key的设置约定:表名 : 主键名 : 主键值 : 字段名 + + | 表名 | 主键名 | 主键值 | 字段名 | + | ----- | ------ | --------- | ------ | + | order | id | 29437595 | name | + | equip | id | 390472345 | type | + | news | id | 202004150 | title | + + + + + +*** + + + +### hash + +#### 简介 + +数据存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息 + +数据存储结构:一个存储空间保存多个键值对数据 + +hash 类型:底层使用**哈希表**结构实现数据存储 + + + +Redis 中的 hash 类似于 Java 中的 `Map>`,左边是 key,右边是值,中间叫 field 字段,本质上 **hash 存了一个 key-value 的存储空间** + +hash 是指的一个数据类型,并不是一个数据 + +* 如果 field 数量较少,存储结构优化为**压缩列表结构**(有序) +* 如果 field 数量较多,存储结构使用 HashMap 结构(无序) + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + hset key field value #添加/修改数据 + hdel key field1 [field2] #删除数据,[]代表可选 + hsetnx key field value #设置field的值,如果该field存在则不做任何操作 + hmset key f1 v1 f2 v2... #添加/修改多个数据 + ``` + +* 查询操作 + + ```sh + hget key field #获取指定field对应数据 + hgetall key #获取指定key所有数据 + hmget key field1 field2... #获取多个数据 + hexists key field #获取哈希表中是否存在指定的字段 + hlen key #获取哈希表中字段的数量 + ``` + +* 获取哈希表中所有的字段名或字段值 + + ```sh + hkeys key #获取所有的field + hvals key #获取所有的value + ``` + +* 设置指定字段的数值数据增加指定范围的值 + + ```sh + hincrby key field increment #指定字段的数值数据增加指定的值,increment为负数则减少 + hincrbyfloat key field increment#操作小数 + ``` + + +注意事项 + +1. hash 类型中 value 只能存储字符串,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为(nil) +2. 每个 hash 可以存储 2^32 - 1 个键值对 +3. hash 类型和对象的数据存储形式相似,并且可以灵活添加删除对象属性。但 hash 设计初衷不是为了存储大量对象而设计的,不可滥用,不可将 hash 作为对象列表使用 +4. hgetall 操作可以获取全部属性,如果内部 field 过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈 + + + +*** + + + +#### 实现 + +哈希对象的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典) + +* 压缩列表实现哈希对象:同一键值对的节点总是挨在一起,保存键的节点在前,保存值的节点在后 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希对象ziplist.png) + +* 字典实现哈希对象:字典的每一个键都是一个字符串对象,每个值也是 + + + +当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型,具体需要满足两个条件: + +- 当键值对数量小于 hash-max-ziplist-entries 配置(默认 512 个) +- 所有键和值的长度都小于 hash-max-ziplist-value 配置(默认 64 字节) + +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 + +ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀,当 ziplist 无法满足哈希类型时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) + + + +*** + + + +#### 应用 + +```sh +user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} +``` + +对于以上数据,使用单条去存的话,存的条数会很多。但如果用 json 格式,存一条数据就够了。 + +假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 + + + +可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息 + + + + + +*** + + + +### list + +#### 简介 + +数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分 + +数据存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序,允许重复元素 + +list 类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList + + + +如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈 + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + lpush key value1 [value2]...#从左边添加/修改数据(表头) + rpush key value1 [value2]...#从右边添加/修改数据(表尾) + lpop key #从左边获取并移除第一个数据,类似于出栈/出队 + rpop key #从右边获取并移除第一个数据 + lrem key count value #删除指定数据,count=2删除2个,该value可能有多个(重复数据) + ``` + +* 查询操作 + + ```sh + lrange key start stop #从左边遍历数据并指定开始和结束索引,0是第一个索引,-1是终索引 + lindex key index #获取指定索引数据,没有则为nil,没有索引越界 + llen key #list中数据长度/个数 + ``` + +* 规定时间内获取并移除数据 + + ```sh + b #代表阻塞 + blpop key1 [key2] timeout #在指定时间内获取指定key(可以多个)的数据,超时则为(nil) + #可以从其他客户端写数据,当前客户端阻塞读取数据 + brpop key1 [key2] timeout #从右边操作 + ``` + +* 复制操作 + + ```sh + brpoplpush source destination timeout #从source获取数据放入destination,假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长 + ``` + +注意事项 + +1. list 中保存的数据都是 string 类型的,数据总容量是有限的,最多 2^32 - 1 个元素(4294967295) +2. list 具有索引的概念,但操作数据时通常以队列的形式进行入队出队,或以栈的形式进行入栈出栈 +3. 获取全部数据操作结束索引设置为 -1 +4. list 可以对数据进行分页操作,通常第一页的信息来自于 list,第 2 页及更多的信息通过数据库的形式加载 + + + +**** + + + +#### 实现 + +在 Redis3.2 版本以前列表对象的内部编码有两种:ziplist(压缩列表)和 linkedlist(链表) + +* 压缩列表实现的列表对象:PUSH 1、three、5 三个元素 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象ziplist.png) + +* 链表实现的列表对象:为了简化字符串对象的表示,使用了 StringObject 的结构,底层其实是 sdshdr 结构 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-列表对象linkedlist.png) + +列表中存储的数据量比较小的时候,列表就会使用一块连续的内存存储,采用压缩列表的方式实现的条件: + +* 列表对象保存的所有字符串元素的长度都小于 64 字节 +* 列表对象保存的元素数量小于 512 个 + +以上两个条件的上限值是可以通过配置文件修改的,当两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行 + +在 Redis3.2 版本 以后对列表数据结构进行了改造,使用 **quicklist(快速列表)**代替了 linkedlist,quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余 + + + + + +*** + + + +#### 应用 + +企业运营过程中,系统将产生出大量的运营数据,如何保障多台服务器操作日志的统一顺序输出? + +* 依赖 list 的数据具有顺序的特征对信息进行管理,右进左查或者左近左查 +* 使用队列模型解决多路信息汇总合并的问题 +* 使用栈模型解决最新消息的问题 + +微信文章订阅公众号: + +* 比如订阅了两个公众号,它们发布了两篇文章,文章 ID 分别为 666 和 888,可以通过执行 `LPUSH key 666 888` 命令推送给我 + + + + + +*** + + + +### set + +#### 简介 + +数据存储需求:存储大量的数据,在查询方面提供更高的效率 + +数据存储结构:能够保存大量的数据,高效的内部存储机制,便于查询 + +set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且**值是不允许重复且无序的** + + + + + +*** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + sadd key member1 [member2] #添加数据 + srem key member1 [member2] #删除数据 + ``` + +* 查询操作 + + ```sh + smembers key #获取全部数据 + scard key #获取集合数据总量 + sismember key member #判断集合中是否包含指定数据 + ``` + +* 随机操作 + + ```sh + spop key [count] #随机获取集中的某个数据并将该数据移除集合 + srandmember key [count] #随机获取集合中指定(数量)的数据 + +* 集合的交、并、差 + + ```sh + sinter key1 [key2...] #两个集合的交集,不存在为(empty list or set) + sunion key1 [key2...] #两个集合的并集 + sdiff key1 [key2...] #两个集合的差集 + + sinterstore destination key1 [key2...] #两个集合的交集并存储到指定集合中 + sunionstore destination key1 [key2...] #两个集合的并集并存储到指定集合中 + sdiffstore destination key1 [key2...] #两个集合的差集并存储到指定集合中 + ``` + +* 复制 + + ```sh + smove source destination member #将指定数据从原始集合中移动到目标集合中 + ``` + + +注意事项 + +1. set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份 +2. set 虽然与 hash 的存储结构相同,但是无法启用 hash 中存储值的空间 + + + +*** + + + +#### 实现 + +集合对象的内部编码有两种:intset(整数集合)、hashtable(哈希表、字典) + +* 整数集合实现的集合对象: + + + +* 字典实现的集合对象:键值对的值为 NULL + + + +当集合对象可以同时满足以下两个条件时,对象使用 intset 编码: + +* 集合中的元素都是整数值 +* 集合中的元素数量小于 set-maxintset-entries配置(默认 512 个) + +以上两个条件的上限值是可以通过配置文件修改的 + + + +**** + + + +#### 应用 + +应用场景: + +1. 黑名单:资讯类信息类网站追求高访问量,但是由于其信息的价值,往往容易被不法分子利用,通过爬虫技术,快速获取信息,个别特种行业网站信息通过爬虫获取分析后,可以转换成商业机密。 + + 注意:爬虫不一定做摧毁性的工作,有些小型网站需要爬虫为其带来一些流量。 + +2. 白名单:对于安全性更高的应用访问,仅仅靠黑名单是不能解决安全问题的,此时需要设定可访问的用户群体, 依赖白名单做更为苛刻的访问验证 + +3. 随机操作可以实现抽奖功能 + +4. 集合的交并补可以实现微博共同关注的查看,可以根据共同关注或者共同喜欢推荐相关内容 + + + + + +*** + + + +### zset + +#### 简介 + +数据存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式 + +数据存储结构:新的存储模型,可以保存可排序的数据 + + + +**** + + + +#### 操作 + +指令操作: + +* 数据操作 + + ```sh + zadd key score1 member1 [score2 member2] #添加数据 + zrem key member [member ...] #删除数据 + zremrangebyrank key start stop #删除指定索引范围的数据 + zremrangebyscore key min max #删除指定分数区间内的数据 + zscore key member #获取指定值的分数 + zincrby key increment member #指定值的分数增加increment + ``` + +* 查询操作 + + ```sh + zrange key start stop [WITHSCORES] #获取指定范围的数据,升序,WITHSCORES 代表显示分数 + zrevrange key start stop [WITHSCORES] #获取指定范围的数据,降序 + + zrangebyscore key min max [WITHSCORES] [LIMIT offset count] #按条件获取数据,从小到大 + zrevrangebyscore key max min [WITHSCORES] [...] #从大到小 + + zcard key #获取集合数据的总量 + zcount key min max #获取指定分数区间内的数据总量 + zrank key member #获取数据对应的索引(排名)升序 + zrevrank key member #获取数据对应的索引(排名)降序 + ``` + + * min 与 max 用于限定搜索查询的条件 + * start 与 stop 用于限定查询范围,作用于索引,表示开始和结束索引 + * offset 与 count 用于限定查询范围,作用于查询结果,表示开始位置和数据总量 + +* 集合的交、并操作 + + ```sh + zinterstore destination numkeys key [key ...] #两个集合的交集并存储到指定集合中 + zunionstore destination numkeys key [key ...] #两个集合的并集并存储到指定集合中 + ``` + +注意事项: + +1. score 保存的数据存储空间是 64 位,如果是整数范围是 -9007199254740992~9007199254740992 +2. score 保存的数据也可以是一个双精度的 double 值,基于双精度浮点数的特征可能会丢失精度,慎重使用 +3. sorted_set 底层存储还是基于 set 结构的,因此数据不能重复,如果重复添加相同的数据,score 值将被反复覆盖,保留最后一次修改的结果 + + + +*** + + + +#### 实现 + +有序集合对象的内部编码有两种:ziplist(压缩列表)和 skiplist(跳跃表) + +* 压缩列表实现有序集合对象:ziplist 本身是有序、不可重复的,符合有序集合的特性 + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象ziplist.png) + +* 跳跃表实现有序集合对象:**底层是 zset 结构,zset 同时包含字典和跳跃表的结构**,图示字典和跳跃表中重复展示了各个元素的成员和分值,但实际上两者会**通过指针来共享相同元素的成员和分值**,不会产生空间浪费 + + ```c + typedef struct zset { + zskiplist *zsl; + dict *dict; + } zset; + ``` + + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-有序集合对象zset.png) + +使用字典加跳跃表的优势: + +* 字典为有序集合创建了一个**从成员到分值的映射**,用 O(1) 复杂度查找给定成员的分值 +* **排序操作使用跳跃表完成**,节省每次重新排序带来的时间成本和空间成本 + +使用 ziplist 格式存储需要满足以下两个条件: + +- 有序集合保存的元素个数要小于 128 个; +- 有序集合保存的所有元素大小都小于 64 字节 + +当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) + +为什么用跳表而不用平衡树? + +* 在做范围查找的时候,跳表操作简单(前进指针或后退指针),平衡树需要回旋查找 +* 跳表比平衡树实现简单,平衡树的插入和删除操作可能引发子树的旋转调整,而跳表的插入和删除只需要修改相邻节点的指针 + + + +*** + + + +#### 应用 + +* 排行榜 +* 对于基于时间线限定的任务处理,将处理时间记录为 score 值,利用排序功能区分处理的先后顺序 +* 当任务或者消息待处理,形成了任务队列或消息队列时,对于高优先级的任务要保障对其优先处理,采用 score 记录权重 + + + + + +*** + + + +### Bitmaps + +#### 基本操作 + +Bitmaps 是二进制位数组(bit array),底层使用 SDS 字符串表示,因为 SDS 是二进制安全的 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-位数组结构.png) + +buf 数组的每个字节用一行表示,buf[1] 是 `'\0'`,保存位数组的顺序和书写位数组的顺序是完全相反的,图示的位数组 0100 1101 + +数据结构的详解查看 Java → Algorithm → 位图 + + + + + +*** + + + +#### 命令实现 + +##### GETBIT + +GETBIT 命令获取位数组 bitarray 在 offset 偏移量上的二进制位的值 + +```sh +GETBIT +``` + +执行过程: + +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,并返回这个位的值 + +GETBIT 命令执行的所有操作都可以在常数时间内完成,所以时间复杂度为 O(1) + + + +*** + + + +##### SETBIT + +SETBIT 将位数组 bitarray 在 offset 偏移量上的二进制位的值设置为 value,并向客户端返回二进制位的旧值 + +```sh +SETBIT +``` + +执行过程: + +* 计算 `len = offset/8 + 1`,len 值记录了保存该数据至少需要多少个字节 +* 检查 bitarray 键保存的位数组的长度是否小于 len,成立就会将 SDS 扩展为 len 字节(注意空间预分配机制),所有新扩展空间的二进制位的值置为 0 +* 计算 `byte = offset/8`(向下取整), byte 值记录数据保存在位数组中的索引 +* 计算 `bit = (offset mod 8) + 1`,bit 值记录数据在位数组中的第几个二进制位 +* 根据 byte 和 bit 值,在位数组 bitarray 中定位 offset 偏移量指定的二进制位,首先将指定位现存的值保存在 oldvalue 变量,然后将新值 value 设置为这个二进制位的值 +* 向客户端返回 oldvalue 变量的值 + + + +*** + + + +##### BITCOUNT + +BITCOUNT 命令用于统计给定位数组中,值为 1 的二进制位的数量 + +```sh +BITCOUNT [start end] +``` + +二进制位统计算法: + +* 遍历法:遍历位数组中的每个二进制位 +* 查表算法:读取每个字节(8 位)的数据,查表获取数值对应的二进制中有几个 1 +* variable-precision SWAR算法:计算汉明距离 +* Redis 实现: + * 如果二进制位的数量大于等于 128 位, 那么使用 variable-precision SWAR 算法来计算二进制位的汉明重量 + * 如果二进制位的数量小于 128 位,那么使用查表算法来计算二进制位的汉明重量 + + + +**** + + + +##### BITOP + +BITOP 命令对指定 key 按位进行交、并、非、异或操作,并将结果保存到指定的键中 + +```sh +BITOP OPTION destKey key1 [key2...] +``` + +OPTION 有 AND(与)、OR(或)、 XOR(异或)和 NOT(非)四个选项 + +AND、OR、XOR 三个命令可以接受多个位数组作为输入,需要遍历输入的每个位数组的每个字节来进行计算,所以命令的复杂度为 O(n^2);与此相反,NOT 命令只接受一个位数组输入,所以时间复杂度为 O(n) + + + +*** + + + +#### 应用场景 + +- **解决 Redis 缓存穿透**,判断给定数据是否存在, 防止缓存穿透 + + + +- 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 + +- 爬虫去重,爬给定网址的时候对已经爬取过的 URL 去重 + +- 信息状态统计 + + + + + +*** + + + +### Hyper + +基数是数据集去重后元素个数,HyperLogLog 是用来做基数统计的,运用了 LogLog 的算法 + +```java +{1, 3, 5, 7, 5, 7, 8} 基数集: {1, 3, 5 ,7, 8} 基数:5 +{1, 1, 1, 1, 1, 7, 1} 基数集: {1,7} 基数:2 +``` + +相关指令: + +* 添加数据 + + ```sh + pfadd key element [element ...] + ``` + +* 统计数据 + + ```sh + pfcount key [key ...] + ``` + +* 合并数据 + + ```sh + pfmerge destkey sourcekey [sourcekey...] + ``` + +应用场景: + +* 用于进行基数统计,不是集合不保存数据,只记录数量而不是具体数据,比如网站的访问量 +* 核心是基数估算算法,最终数值存在一定误差 +* 误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值 +* 耗空间极小,每个 hyperloglog key 占用了12K的内存用于标记基数 +* pfadd 命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大 +* Pfmerge 命令合并后占用的存储空间为12K,无论合并之前数据量多少 + + + +*** + + + +### GEO + +GeoHash 是一种地址编码方法,把二维的空间经纬度数据编码成一个字符串 + +* 添加坐标点 + + ```sh + geoadd key longitude latitude member [longitude latitude member ...] + georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 获取坐标点 + + ```sh + geopos key member [member ...] + georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] + ``` + +* 计算距离 + + ```sh + geodist key member1 member2 [unit] #计算坐标点距离 + geohash key member [member ...] #计算经纬度 + ``` + +Redis 应用于地理位置计算 + + + + + +**** + + + + + +## 持久机制 + +### 概述 + +持久化:利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化 + +作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘 + +计算机中的数据全部都是二进制,保存一组数据有两种方式 + + +RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 + +AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂 + + + +*** + + + +### RDB + +#### 文件创建 + +RDB 持久化功能所生成的 RDB 文件是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE + + + +##### SAVE + +SAVE 指令:手动执行一次保存操作,该指令的执行会阻塞当前 Redis 服务器,客户端发送的所有命令请求都会被拒绝,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用 + +工作原理:Redis 是个**单线程的工作模式**,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时 + +配置 redis.conf: + +```sh +dir path #设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data +dbfilename "x.rdb" #设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb +rdbcompression yes|no #设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间 +rdbchecksum yes|no #设置读写文件过程是否进行RDB格式校验,默认yes +``` + + + +*** + + + +##### BGSAVE + +BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进程,**进程之间不相互影响**,所以持久化期间 Redis 正常工作 + +工作原理: + + + +流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会异步执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件 + +```python +# 创建子进程 +pid = fork() +if pid == 0: + # 子进程负责创建 RDB 文件 + rdbSave() + # 完成之后向父进程发送信号 + signal_parent() +elif pid > 0: + # 父进程继续处理命令请求,并通过轮询等待子进程的信号 + handle_request_and_wait_signal() +else: + # 处理出错恃况 + handle_fork_error() +``` + +配置 redis.conf + +```sh +stop-writes-on-bgsave-error yes|no #后台存储过程中如果出现错误,是否停止保存操作,默认yes +dbfilename filename +dir path +rdbcompression yes|no +rdbchecksum yes|no +``` + +注意:BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用 + +在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同 + +* SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件 +* BGSAVE 命令也会被服务器拒绝,也会产生竞争条件 +* BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行 + * 如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被**延迟**到 BGSAVE 命令执行完毕之后执行 + * 如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝 + + + +*** + + + +##### 特殊指令 + +RDB 特殊启动形式的指令(客户端输入) + +* 服务器运行过程中重启 + + ```sh + debug reload + ``` + +* 关闭服务器时指定保存数据 + + ```sh + shutdown save + ``` + + 默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能) + +* 全量复制:主从复制部分详解 + + + + + +*** + + + +#### 文件载入 + +RDB 文件的载入工作是在服务器启动时自动执行,期间 Redis 会一直处于阻塞状态,直到载入完成 + +Redis 并没有专门用于载入 RDB 文件的命令,只要服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件 + +```sh +[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds # 服务器在成功载入 RDB 文件之后打印 +``` + +AOF 文件的更新频率通常比 RDB 文件的更新频率高: + +* 如果服务器开启了 AOF 持久化功能,那么会优先使用 AOF 文件来还原数据库状态 +* 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态 + + + + + +**** + + + +#### 自动保存 + +##### 配置文件 + +Redis 支持通过配置服务器的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令 + +配置 redis.conf: + +```sh +save second changes #设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave) +``` + +* second:监控时间范围 +* changes:监控 key 的变化量 + +默认三个条件: + +```sh +save 900 1 # 900s内1个key发生变化就进行持久化 +save 300 10 +save 60 10000 +``` + +判定 key 变化的依据: + +* 对数据产生了影响,不包括查询 +* 不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化 + +save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的 + + + +*** + + + +##### 自动原理 + +服务器状态相关的属性: + +```c +struct redisServer { + // 记录了保存条件的数组 + struct saveparam *saveparams; + + // 修改计数器 + long long dirty; + + // 上一次执行保存的时间 + time_t lastsave; +}; +``` + +* Redis 服务器启动时,可以通过指定配置文件或者传入启动参数的方式设置 save 选项, 如果没有自定义就设置为三个默认值(上节提及),设置服务器状态 redisServe.saveparams 属性,该数组每一项为一个 saveparam 结构,代表 save 的选项设置 + + ```c + struct saveparam { + // 秒数 + time_t seconds + // 修改数 + int changes; + }; + ``` + +* dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后,服务器中的所有数据库进行了多少次修改(包括写入、删除、更新等操作),当服务器成功执行一个修改指令,该命令修改了多少次数据库, dirty 的值就增加多少 + +* lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间 + +Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护 + +serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足,会遍历 saveparams 数组中的**所有保存条件**,只要有任意一个条件被满足服务器就会执行 BGSAVE 命令 + +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-BGSAVE执行原理.png) + + + + + +*** + + -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表节点底层结构.png) +#### 文件结构 -list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len +RDB 的存储结构:图示全大写单词标示常量,用全小写单词标示变量和数据 -```c -typedef struct list { - // 表头节点 - listNode *head; - // 表尾节点 - listNode *tail; - - // 链表所包含的节点数量 - unsigned long len; - - // 节点值复制函数,用于复制链表节点所保存的值 - void *(*dup) (void *ptr); - // 节点值释放函数,用于释放链表节点所保存的值 - void (*free) (void *ptr); - // 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等 - int (*match) (void *ptr, void *key); -} list; -``` +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-RDB文件结构.png) -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表底层结构.png) +* REDIS:长度为 5 字节,保存着 `REDIS` 五个字符,是 RDB 文件的开头,在载入文件时可以快速检查所载入的文件是否 RDB 文件 +* db_version:长度为 4 字节,是一个用字符串表示的整数,记录 RDB 的版本号 +* database:包含着零个或任意多个数据库,以及各个数据库中的键值对数据 +* EOF:长度为 1 字节的常量,标志着 RDB 文件正文内容的结束,当读入遇到这个值时,代表所有数据库的键值对都已经载入完毕 +* check_sum:长度为 8 字节的无符号整数,保存着一个校验和,该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,来检查 RDB 文件是否有出错或者损坏 -Redis 链表的特性: +Redis 本身带有 RDB 文件检查工具 redis-check-dump -* 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是 O(1) -* 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点 -* 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为 O(1) -* 带链表长度计数器:使用 len 属性来对 list 持有的链表节点进行计数,获取链表中节点数量的时间复杂度为 O(1) -* 多态:链表节点使用 void * 指针来保存节点值, 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数,所以链表可以保存各种**不同类型的值** +*** -**** +### AOF -### 字典 +#### 基本概述 -#### 哈希表 +AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读)来记录数据库状态,**增量保存**只许追加文件但不可以改写文件,**与 RDB 相比可以理解为由记录数据改为记录数据的变化** -Redis 字典使用的哈希表结构: +AOF 主要作用是解决了**数据持久化的实时性**,目前已经是 Redis 持久化的主流方式 -```c -typedef struct dictht { - // 哈希表数组,数组中每个元素指向 dictEntry 结构 - dictEntry **table; - - // 哈希表大小,数组的长度 - unsigned long size; - - // 哈希表大小掩码,用于计算索引值,总是等于 【size-1】 - unsigned long sizemask; - - // 该哈希表已有节点的数量 - unsigned long used; -} dictht; +AOF 写数据过程: + + + +Redis 只会将对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器,但是 PUBSUB 和 SCRIPT LOAD 命令例外: + +* PUBSUB 命令虽然没有修改数据库,但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变,所以服务器需要使用 REDIS_FORCE_AOF 标志强制将这个命令写入 AOF 文件。这样在将来载入 AOF 文件时,服务器就可以再次执行相同的 PUBSUB 命令,并产生相同的副作用 +* SCRIPT LOAD 命令虽然没有修改数据库,但它修改了服务器状态,所以也是一个带有副作用的命令,需要使用 REDIS_FORCE_AOF + + + +*** + + + +#### 持久实现 + +AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤 + + + +##### 命令追加 + +启动 AOF 的基本配置: + +```sh +appendonly yes|no #开启AOF持久化功能,默认no,即不开启状态 +appendfilename filename #AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof +dir #AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可 ``` -哈希表节点结构: +当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令**追加**到服务器状态的 aof_buf 缓冲区的末尾 ```c -typedef struct dictEntry { - // 键 - void *key; - - // 值,可以是一个指针,或者整数 - union { - void *val; // 指针 - uint64_t u64; - int64_t s64; - } - - // 指向下个哈希表节点,形成链表,用来解决冲突问题 - struct dictEntry *next; -} dictEntry; +struct redisServer { + // AOF 缓冲区 + sds aof_buf; +}; ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哈希表底层结构.png) - *** -#### 字典结构 +##### 文件写入 -字典,又称为符号表、关联数组、映射(Map),用于保存键值对的数据结构,字典中的每个键都是独一无二的。底层采用哈希表实现,一个哈希表包含多个哈希表节点,每个节点保存一个键值对 +服务器在处理文件事件时会执行**写命令,追加一些内容到 aof_buf 缓冲区**里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要**将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件**里 -```c -typedef struct dict { - // 类型特定函数 - dictType *type; - - // 私有数据 - void *privdata; - - // 哈希表,数组中的每个项都是一个dictht哈希表, - // 一般情况下字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用 - dictht ht[2]; - - // rehash 索引,当 rehash 不在进行时,值为 -1 - int rehashidx; -} dict; +flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定 + +```sh +appendfsync always|everysec|no #AOF写数据策略:默认为everysec ``` -type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的: +- always:每次写入操作都将 aof_buf 缓冲区中的所有内容**写入并同步**到 AOF 文件 -* type 属性是指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数 -* privdata 属性保存了需要传给那些类型特定函数的可选参数 + 特点:安全性最高,数据零误差,但是性能较低,不建议使用 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典底层结构.png) + +- everysec:先将 aof_buf 缓冲区中的内容写入到操作系统缓存,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次进行同步 fsync,这个同步操作是由一个(子)线程专门负责执行的 + + 特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置 + + +- no:将 aof_buf 缓冲区中的内容写入到操作系统缓存,但并不进行同步,何时同步由操作系统来决定 + + 特点:**整体不可控**,服务器宕机会丢失上次同步 AOF 后的所有写指令 @@ -9536,23 +12670,48 @@ type 属性和 privdata 属性是针对不同类型的键值对, 为创建多 -#### 哈希冲突 +##### 文件同步 -Redis 使用 MurmurHash 算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快 +在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区空间,等到缓冲区**写满或者到达特定时间周期**,才真正地将缓冲区中的数据写入到磁盘里面(刷脏) -将一个新的键值对添加到字典里,需要先根据键 key 计算出哈希值,然后进行取模运算(取余): +* 优点:提高文件的写入效率 +* 缺点:为写入数据带来了安全问题,如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失 -```c -index = hash & dict->ht[x].sizemask +系统提供了 fsync 和 fdatasync 两个同步函数做**强制硬盘同步**,可以让操作系统立即将缓冲区中的数据写入到硬盘里面,函数会阻塞到写入硬盘完成后返回,保证了数据持久化 + +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 + + + + + +*** + + + +#### 文件载入 + +AOF 文件里包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里的命令,就还原服务器关闭之前的数据库状态,服务器在启动时,还原数据库状态打印的日志: + +```sh +[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds ``` -当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时,就称这些键发生了哈希冲突(collision) +AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的,其他都是通过客户端发送的命令 -Redis 的哈希表使用链地址法(separate chaining)来解决键哈希冲突, 每个哈希表节点都有一个 next 指针,多个节点通过 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题 +```sh +* 2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n # 服务器自动添加 +* 3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n +* 5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n +``` + +Redis 读取 AOF 文件并还原数据库状态的步骤: + +* 创建一个**不带网络连接的伪客户端**(fake client)执行命令,因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接 +* 从 AOF 文件分析并读取一条写命令 +* 使用伪客户端执行被读出的写命令,然后重复上述步骤 -dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(**头插法**),时间复杂度为 O(1) -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-字典解决哈希冲突.png) @@ -9560,25 +12719,59 @@ dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度 -#### 负载因子 +#### 重写实现 -负载因子的计算方式:哈希表中的**节点数量** / 哈希表的大小(**长度**) +##### 重写策略 -```c -load_factor = ht[0].used / ht[0].size +AOF 重写:读取服务器当前的数据库状态,**生成新 AOF 文件来替换旧 AOF 文件**,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多 + +AOF 重写规则: + +- 进程内具有时效性的数据,并且数据已超时将不再写入文件 + + +- 对同一数据的多条写命令合并为一条命令,因为会读取当前的状态,所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等集合类型,**单条指令**最多写入 64 个元素 + + 如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c + +- 非写入类的无效指令将被忽略,只保留最终数据的写入命令,但是 select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录 + +AOF 重写作用: + +- 降低磁盘占用量,提高磁盘利用率 +- 提高持久化效率,降低持久化写时间,提高 IO 性能 +- 降低数据恢复的用时,提高数据恢复效率 + + + +*** + + + +##### 重写原理 + +AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件, 但是该函数会进行大量的写入操作,调用这个函数的线程将被长时间阻塞,所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行,不会阻塞父进程,重写命令: + +```sh +bgrewriteaof ``` -为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时 ,程序会自动对哈希表的大小进行相应的扩展或者收缩 +* 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求 -哈希表执行扩容的条件: +* 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据安全性 -* 服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 1 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) -* 服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 5 +子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致,所以 Redis 设置了 AOF 重写缓冲区 + +工作流程: + +* Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入) +* 当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会**对服务器进程(父进程)造成阻塞**(影响很小,类似 JVM STW),主要工作: + * 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致 + * 对新的 AOF 文件进行改名,**原子地(atomic)覆盖**现有的 AOF 文件,完成新旧两个 AOF 文件的替换 - 原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on­-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存 -哈希表执行收缩的条件:负载因子小于 0.1(自动执行),缩小为字典中数据个数的 50% 左右 @@ -9586,343 +12779,426 @@ load_factor = ht[0].used / ht[0].size -#### 重新散列 +##### 自动重写 -扩展和收缩哈希表的操作通过 rehash(重新散列)来完成,步骤如下: +触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发 -* 为字典的 ht[1] 哈希表分配空间,空间大小的分配情况: - * 如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$ - * 如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$ -* 将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上 -* 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0]变为空表), 释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备 +```sh +auto-aof-rewrite-min-size size #设置重写的基准值,最小文件 64MB,达到这个值开始重写 +auto-aof-rewrite-percentage percent #触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发 +``` -如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务 +自动重写触发比对参数( 运行指令 `info Persistence` 获取具体信息 ): -Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中式的完成,而是分多次,渐进式的完成,又叫**渐进式 rehash** +```sh +aof_current_size #AOF文件当前尺寸大小(单位:字节) +aof_base_size #AOF文件上次启动和重写时的尺寸大小(单位:字节) +``` -* 为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表 -* 在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始 -* 在 rehash 进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后**将 rehashidx 属性的值增一** -* 随着字典操作的不断执行,最终在某个时间点上 ht[0] 的所有键值对都被 rehash 至 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成 +自动重写触发条件公式: -渐进式 rehash 采用分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量 +- aof_current_size > auto-aof-rewrite-min-size +- (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage -渐进式 rehash 期间的哈希表操作: -* 字典的查找、删除、更新操作会在两个哈希表上进行,比如查找一个键会先在 ht[0] 上查找,查找不到就去 ht[1] 继续查找 -* 字典的添加操作会直接在 ht[1] 上添加,不在 ht[0] 上进行任何添加 +**** + + + +### 对比 + +RDB 的特点 + +* RDB 优点: + - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 + - RDB 内部存储的是 Redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制、灾难恢复** + - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 +* RDB 缺点: + + - BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 + - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 + - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 + +AOF 特点: + +* AOF 的优点:数据持久化有**较好的实时性**,通过 AOF 重写可以降低文件的体积 +* AOF 的缺点:文件较大时恢复较慢 + +AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失) + +应用场景: + +- 对数据**非常敏感**,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能 + + 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 + +- 数据呈现**阶段有效性**,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快 + + 注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低 + +综合对比: + +- RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊 +- 灾难恢复选用 RDB +- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 + + + +*** + + + +### fork + +#### 介绍 + +fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把父进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程 + +在完成对其调用之后,会产生 2 个进程,且每个进程都会**从 fork() 的返回处开始执行**,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段 + +```c +#include +pid_t fork(void); +// 父进程返回子进程的pid,子进程返回0,错误返回负值,根据返回值的不同进行对应的逻辑处理 +``` + +fork 调用一次,却能够**返回两次**,可能有三种不同的返回值: + +* 在父进程中,fork 返回新创建子进程的进程 ID +* 在子进程中,fork 返回 0 +* 如果出现错误,fork 返回一个负值,错误原因: + * 当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN + * 系统内存不足,这时 errno 的值被设置为 ENOMEM + +fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0 +创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略 -**** +每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值 -### 跳跃表 +*** -#### 底层结构 -跳跃表(skiplist)是一种有序(**默认升序**)的数据结构,在链表的基础上**增加了多级索引以提升查找的效率**,索引是占内存的,所以是一个**空间换时间**的方案,跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找,效率与平衡树相当但是实现更简单 -原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点(占内存)则可以忽略 +#### 使用 -Redis 只在两个地方应用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构 +基本使用: ```c -typedef struct zskiplist { - // 表头节点和表尾节点,O(1) 的时间复杂度定位头尾节点 - struct skiplistNode *head, *tail; - - // 表的长度,也就是表内的节点数量 (表头节点不计算在内) - unsigned long length; - - // 表中层数最大的节点的层数 (表头节点的层高不计算在内) - int level -} zskiplist; +#include +#include +int main () +{ + pid_t fpid; // fpid表示fork函数返回的值 + int count = 0; + fpid = fork(); + if (fpid < 0) + printf("error in fork!"); + else if (fpid == 0) { + printf("i am the child process, my process id is %d/n", getpid()); + count++; + } + else { + printf("i am the parent process, my process id is %d/n", getpid()); + count++; + } + printf("count: %d/n",count);// 1 + return 0; +} +/* 输出内容: + i am the child process, my process id is 5574 + count: 1 + i am the parent process, my process id is 5573 + count: 1 +*/ ``` +进阶使用: + ```c -typedef struct zskiplistNode { - // 层 - struct zskiplistLevel { - // 前进指针 - struct zskiplistNode *forward; - // 跨度 - unsigned int span; - } level[]; - - // 后退指针 - struct zskiplistNode *backward; - - // 分值 - double score; - - // 成员对象 - robj *obj; -} zskiplistNode; +#include +#include +int main(void) +{ + int i = 0; + // ppid 指当前进程的父进程pid + // pid 指当前进程的pid, + // fpid 指fork返回给当前进程的值,在这可以表示子进程 + for(i = 0; i < 2; i++){ + pid_t fpid = fork(); + if(fpid == 0) + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); + else + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); + } + return 0; +} +/*输出内容: + i 父id id 子id + 0 parent 2043 3224 3225 + 0 child 3224 3225 0 + 1 parent 2043 3224 3226 + 1 parent 3224 3225 3227 + 1 child 1 3227 0 + 1 child 1 3226 0 +*/ ``` -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-跳表底层结构.png) - + +在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1 的 init 进程(笔记 Tool → Linux → 进程管理详解) -*** +参考文章:https://blog.csdn.net/love_gaohz/article/details/41727415 -#### 属性分析 +*** -层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)**随机**生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1 -前进指针:forward 用于从表头到表尾方向正序(升序)遍历节点,遇到 NULL 停止遍历 -跨度:span 用于记录两个节点之间的距离,用来**计算排位(rank)**: +#### 内存 -* 两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0 +fork() 调用之后父子进程的内存关系 -* 在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位,按照上图所示: +早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法: - 查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3 +* 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 - 查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2 + -后退指针:backward 用于从表尾到表头方向逆序(降序)遍历节点 +* 对于父进程的数据段,堆段,栈段中的各页,由于父子进程相互独立,采用**写时复制 COW** 的技术,来提高内存以及内核的利用率 -分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 + 在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,**两者的虚拟空间不同,但其对应的物理空间是同一个**,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。 -成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大) + fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 + +补充知识: -个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表 +vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的 -**** +参考文章:https://blog.csdn.net/Shreck66/article/details/47039937 -### 整数集合 -#### 底层结构 -整数集合(intset)是用于保存整数值的集合数据结构,是 Redis 集合键的底层实现之一 +**** -```c -typedef struct intset { - // 编码方式 - uint32_t encoding; - - // 集合包含的元素数量,也就是 contents 数组的长度 - uint32_t length; - - // 保存元素的数组 - int8_t contents[]; -} intset; -``` -encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64 -整数集合的每个元素都是 contents 数组的一个数组项(item),在数组中按值的大小从小到大**有序排列**,并且数组中**不包含任何重复项**。虽然 contents 属性声明为 int8_t 类型,但实际上数组并不保存任何 int8_t 类型的值, 真正类型取决于 encoding 属性 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合底层结构.png) -说明:底层存储结构是数组,所以为了保证有序性和不重复性,每次添加一个元素的时间复杂度是 O(N) +## 事务机制 +### 事务特征 +Redis 事务就是将多个命令请求打包,然后**一次性、按顺序**地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务去执行其他的命令请求,会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求,Redis 事务的特性: -**** +* Redis 事务**没有隔离级别**的概念,队列中的命令在事务没有提交之前都不会实际被执行 +* Redis 单条命令式保存原子性的,但是事务**不保证原子性**,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 -#### 升级降级 -整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程: -* 根据新元素的类型长度以及集合元素的数量(包括新元素在内),扩展整数集合底层数组的空间大小 +*** -* 将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放入正确的位置,放置过程保证数组的有序性 - 图示 32 * 4 = 128 位,首先将 3 放入索引 2(64 位 - 95 位),然后将 2 放置索引 1,将 1 放置在索引 0,从后向前依次放置在对应的区间,最后放置 65535 元素到索引 3(96 位- 127 位),修改 length 属性为 4 -* 将新元素添加到底层数组里 +### 工作流程 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-整数集合升级.png) +事务的执行流程分为三个阶段: -每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N) +* 事务开始:MULTI 命令的执行标志着事务的开始,通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识,将执行该命令的客户端从非事务状态切换至事务状态 -引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素,升级之后新元素的摆放位置: + ```sh + MULTI # 设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 + ``` -* 在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引 0) -* 在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引 length-1) +* 命令入队:事务队列以先进先出(FIFO)的方式保存入队的命令,每个 Redis 客户端都有事务状态,包含着事务队列: -整数集合升级策略的优点: + ```c + typedef struct redisClient { + // 事务状态 + multiState mstate; /* MULTI/EXEC state */ + } + + typedef struct multiState { + // 事务队列,FIFO顺序 + multiCmd *commands; + + // 已入队命令计数 + int count; + } + ``` -* 提升整数集合的灵活性:C 语言是静态类型语言,为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面,整数集合可以自动升级底层数组来适应新元素,所以可以随意的添加整数 + * 如果命令为 EXEC、DISCARD、WATCH、MULTI 四个命中的一个,那么服务器立即执行这个命令 + * 其他命令服务器不执行,而是将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复 -* 节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存 +* 事务执行:EXEC 提交事务给服务器执行,服务器会遍历这个客户端的事务队列,执行队列中的命令并将执行结果返回 -整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态 + ```sh + EXEC # Commit 提交,执行事务,与multi成对出现,成对使用 + ``` +事务取消的方法: +* 取消事务: + ```sh + DISCARD # 终止当前事务的定义,发生在multi之后,exec之前 + ``` + 一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚 -***** -### 压缩列表 -#### 底层结构 +*** -压缩列表(ziplist)是 Redis 为了节约内存而开发的,是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表底层结构.png) -* zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分或者计算 zlend 的位置时使用 -* zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址 -* zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出 -* entryX:列表节点,压缩列表中的各个节点,**节点的长度由节点保存的内容决定** -* zlend:uint8_t 类型 1 字节,是一个特殊值 0xFF (255),用于标记压缩列表的末端 +### WATCH -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表示例.png) +#### 监视机制 -列表 zlbytes 属性的值为 0x50 (十进制 80),表示压缩列表的总长为 80 字节,列表 zltail 属性的值为 0x3c (十进制 60),假设表的起始地址为 p,计算得出表尾节点 entry3 的地址 p + 60 +WATCH 命令是一个乐观锁(optimistic locking),可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复 +* 添加监控锁 + ```sh + WATCH key1 [key2……] #可以监控一个或者多个key + ``` -**** +* 取消对所有 key 的监视 + ```sh + UNWATCH + ``` -#### 列表节点 -列表节点 entry 的数据结构: +*** -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表节点.png) -previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成从表尾向表头遍历操作 -* 如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里 -* 如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度 +#### 实现原理 -encoding:记录了节点的 content 属性所保存的数据类型和长度 +每个 Redis 数据库都保存着一个 watched_keys 字典,键是某个被 WATCH 监视的数据库键,值则是一个链表,记录了所有监视相应数据库键的客户端: -* 长度为 1 字节、2 字节或者 5 字节,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 `_` 表示留空,而 `b`、`x` 等变量则代表实际的二进制数据 +```c +typedef struct redisDb { + // 正在被 WATCH 命令监视的键 + dict *watched_keys; +} +``` - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表字节数组编码.png) +所有对数据库进行修改的命令,在执行后都会调用 `multi.c/touchWatchKey` 函数对 watched_keys 字典进行检查,是否有客户端正在监视刚被命令修改过的数据库键,如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,表示该客户端的事务安全性已经被破坏 -* 长度为 1 字节,值的最高位为 11 的是整数编码,整数值的类型和长度由编码除去最高两位之后的其他位记录 +服务器接收到个客户端 EXEC 命令时,会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识,如果打开了说明客户端提交事务不安全,服务器会拒绝执行 - ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表整数编码.png) -content:每个压缩列表节点可以保存一个字节数组或者一个整数值 -* 字节数组可以是以下三种长度的其中一种: - * 长度小于等于 $63 (2^6-1)$ 字节的字节数组 - * 长度小于等于 $16383(2^{14}-1)$ 字节的字节数组 +**** - * 长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组 -* 整数值则可以是以下六种长度的其中一种: - * 4 位长,介于 0 至 12 之间的无符号整数 +### ACID - * 1 字节长的有符号整数 +#### 原子性 - * 3 字节长的有符号整数 +事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) - * int16_t 类型整数 +原子性指事务队列中的命令要么就全部都执行,要么一个都不执行,但是在命令执行出错时,不会保证原子性(下一节详解) - * int32_t 类型整数 +Redis 不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止 - * int64_t 类型整数 +回滚需要程序员在代码中实现,应该尽可能避免: +* 事务操作之前记录数据的状态 + * 单数据:string -*** + * 多数据:hash、list、set、zset +* 设置指令恢复所有的被修改的项 -#### 连锁更新 + * 单数据:直接 set(注意周边属性,例如时效) -Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新(cascade update) + * 多数据:修改对应值或整体克隆复制 -假设在一个压缩列表中,有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点,new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节,无法保存新节点 new 的长度,所以要对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间,所以扩展后 e1 的长度就变成了 254 至 257 字节之间,导致 e2 的 previous_entry_length 属性无法保存 e1 的长度,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新1.png) - 删除节点也可能会引发连锁更新,big.length >= 254,small.length < 254,删除 small 节点 +*** -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-压缩列表连锁更新2.png) -连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配,每次重分配的最坏复杂度为 O(N),所以连锁更新的最坏复杂度为 O(N^2) -说明:尽管连锁更新的复杂度较高,但出现的记录是非常低的,即使出现只要被更新的节点数量不多,就不会对性能造成影响 +#### 一致性 +事务具有一致性指的是,数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的 +一致是数据符合数据库的定义和要求,没有包含非法或者无效的错误数据,Redis 通过错误检测和简单的设计来保证事务的一致性: +* 入队错误:命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 + -**** +* 执行错误:命令执行出现错误,例如对字符串进行 incr 操作,事务中正确的命令会被执行,运行错误的命令不会被执行 + +* 服务器停机: + * 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据库是一致的 + * 如果服务器运行在持久化模式下,重启之后将数据库还原到一致的状态 -## 数据类型 -### redisObj -#### 对象系统 +*** -Redis 使用对象来表示数据库中的键和值,当在 Redis 数据库中新创建一个键值对时至少会创建两个对象,一个对象用作键值对的键(**键对象**),另一个对象用作键值对的值(**值对象**) -Redis 中对象由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、 encoding、ptr: -```c -typedef struct redisObiect { - // 类型 - unsigned type:4; - // 编码 - unsigned encoding:4; - // 指向底层数据结构的指针 - void *ptr; - - // .... -} robj; -``` +#### 隔离性 -Redis 并没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,而每种对象又通过不同的编码映射到不同的底层数据结构 +Redis 是一个单线程的执行原理,所以对于隔离性,分以下两种情况: -Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 +* 并发操作在 EXEC 命令前执行,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证 +* 并发操作在 EXEC 命令后执行,隔离性可以保证 -![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象编码.png) -* 对一个数据库键执行 TYPE 命令,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型 -* 对一个数据库键执行 OBJECT ENCODING 命令,查看数据库键对应的值对象的编码 +*** -**** +#### 持久性 +Redis 并没有为事务提供任何额外的持久化功能,事务的持久性由 Redis 所使用的持久化模式决定 -#### 命令多态 +配置选项 `no-appendfsync-on-rewrite` 可以配合 appendfsync 选项在 AOF 持久化模式使用: -Redis 中用于操作键的命令分为两种类型: +* 选项打开时在执行 BGSAVE 或者 BGREWRITEAOF 期间,服务器会暂时停止对 AOF 文件进行同步,从而尽可能地减少 I/O 阻塞 +* 选项打开时运行在 always 模式的 AOF 持久化,事务也不具有持久性,所以该选项默认关闭 -* 一种命令可以对任何类型的键执行,比如说 DEL 、EXPIRE、RENAME、 TYPE 等(基于类型的多态) -* 只能对特定类型的键执行,比如 SET 只能对字符串键执行、HSET 对哈希键执行、SADD 对集合键执行,如果类型步匹配会报类型错误: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` +在一个事务的最后加上 SAVE 命令总可以保证事务的耐久性 -Redis 为了确保只有指定类型的键可以执行某些特定的命令,在执行类型特定的命令之前,先通过值对象 redisObject 结构 type 属性检查操作类型是否正确,然后再决定是否执行指定的命令 -对于多态命令,列表对象有 ziplist 和 linkedlist 两种实现方式,通过 redisObject 结构 encoding 属性确定具体的编码类型,底层调用对应的 API 实现具体的操作(基于编码的多态) @@ -9930,108 +13206,107 @@ Redis 为了确保只有指定类型的键可以执行某些特定的命令, -#### 内存回收 - -对象的整个生命周期可以划分为创建对象、 操作对象、 释放对象三个阶段 +## Lua 脚本 -C 语言没有自动回收内存的功能,所以 Redis 在对象系统中构建了引用计数(reference counting)技术实现的内存回收机制,程序可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收 +### 环境创建 -```c -typedef struct redisObiect { - // 引用计数 - int refcount; -} robj; -``` +#### 基本介绍 -对象的引用计数信息会随着对象的使用状态而不断变化,创建时引用计数 refcount 初始化为 1,每次被一个新程序使用时引用计数加 1,当对象不再被一个程序使用时引用计数值会被减 1,当对象的引用计数值变为 0 时,对象所占用的内存会被释放 +Redis 对 Lua 脚本支持,通过在服务器中嵌入 Lua 环境,客户端可以使用 Lua 脚本直接在服务器端**原子地执行**多个命令 +```sh +EVAL