数据库
2024-03-18 12:22:07 0 举报
AI智能生成
这个数据库是一个结构化的信息集合,用于存储、管理、检索和操作各种类型的数据。
作者其他创作
大纲/内容
关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。
MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。
数据库
MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。
定义
成熟稳定,功能完善。
开源免费。
文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。
开箱即用,操作简单,维护成本低。
兼容性好,支持常见的操作系统,支持多种开发语言。
社区活跃,生态完善。
事务支持优秀, InnoDB 存储引擎不会有任何性能损失,并且可以解决幻读问题发生的。
支持分库分表、读写分离、高可用。
优点
数值类型:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL)
字符串类型:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等。
日期时间类型:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。
表示不允许负值的无符号整数,将正整数的上限提高一倍。
UNSIGNED
CHAR 是定长字符串,VARCHAR 是变长字符串。
CHAR 和 VARCHAR
DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
DECIMAL 和 FLOAT/DOUBLE
BLOB 类型主要用于存储二进制大对象,例如图片、音视频等文件。
不推荐使用,很少使用 TEXT 类型,只偶尔会用到,而 BLOB 类型则基本不常用。
TEXT 和 BLOB
DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。
DATETIME 和 TIMESTAMP
NULL 代表一个不确定的值,就算是两个 NULL,它俩也不一定相等。
''的长度是 0,是不占用空间的,而 NULL 是需要占用空间的。
NULL 会影响聚合函数的结果。
查询 NULL 值时,必须使用 IS NULL 或 IS NOT NULLl 来判断。
NULL 和 ''
字段类型
MySQL 主要分为 Server 层和引擎层。
连接器: 身份认证和权限相关(登录 MySQL 的时候)。
查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器分为词法分析和语法分析。
优化器: 按照 MySQL 认为最优的方案去执行。
执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
Server 层
插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
引擎层
查询语句的执行流程:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎。
更新语句执行流程:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit 状态)。
结构
MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
存储引擎采用的是 插件式架构 ,支持多种存储引擎。存储引擎是基于表的,而不是数据库。
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
MyISAM 不支持事务,而 InnoDB 支持,并实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。
MyISAM 不支持外键,而 InnoDB 支持。
MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。
MyISAM 不支持 MVCC,而 InnoDB 支持。
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的。
InnoDB 的性能比 MyISAM 更强大,随着 CPU 核数的增加,差距更明显。
MyISAM 和 InnoDB
存储引擎
索引(Index)是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
创建索引和维护索引需要耗费许多时间。
索引需要使用物理文件存储,也会耗费一定空间。
缺点
B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
哈希算法(也叫散列算法)
原理
InnoDB 存储引擎中存在一种特殊的“自适应哈希索引”(Adaptive Hash Index),结合了 B+Tree 和哈希索引的特点。
Hash 冲突
因为 Hash 索引不支持顺序和范围查询,MySQL 没有使用其作为索引的数据结构。
Hash 表
二叉查找树(Binary Search Tree)是一种基于二叉树的数据结构。
左子树所有节点的值均小于根节点的值。
右子树所有节点的值均大于根节点的值。
左右子树也分别为二叉查找树。
特点
二叉查找树的性能非常依赖于它的平衡程度,这就导致其不适合作为 MySQL 底层索引的数据结构。
二叉查找树(BST)
计算机科学中最早被发明的自平衡二叉查找树。
保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树。
由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了查询性能。
AVL 树
红黑树是一种自平衡二叉查找树,通过在插入和删除节点时进行颜色变换和旋转操作,使得树始终保持平衡状态。
每个节点非红即黑。
根节点总是黑色的。
每个叶子节点都是黑色的空节点(NIL 节点)。
如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
因为红黑树的平衡性相对较弱,高度较高的树的查询可能会导致多次磁盘 IO 操作,这也是 MySQL 没有选择红黑树的主要原因。
红黑树
B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。
大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
B 树 & B+树
BTree 索引:MySQL 里默认和最常用的索引类型。
哈希索引:类似键值对的形式,一次即可定位。
RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
对文本的内容进行分词,进行搜索。目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
按数据结构
聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。
按底层存储方式
主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。
普通索引:仅加速查询。
唯一索引:加速查询 + 列值唯一(可以有 NULL)。
覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
全文索引:对文本的内容进行分词,进行搜索。
按应用维度
类型
隐藏索引:也称为不可见索引,不会被优化器使用,但是仍然需要维护,通常会软删除和灰度发布的场景中使用。主键不能设置为隐藏。
降序索引:MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。
函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
MySQL 8.x 新特性
数据表的主键列使用的就是主键索引。
一张数据表有只能有一个主键,并且主键不能为 null,不能重复。
主键索引(Primary Key)
二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。
唯一索引,普通索引,前缀索引等索引属于二级索引。
唯一索引(Unique Key):唯一索引也是一种约束。不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。
普通索引(Index):普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
前缀索引(Prefix):前缀索引只适用于字符串类型的数据。
全文索引(Full Text):全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。
二级索引
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
查询速度非常快、对排序查找和范围查找优化。
依赖于有序的数据、更新代价大。
聚簇索引(聚集索引)
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
更新代价比聚簇索引要小 。
依赖于有序的数据、可能会二次查询(回表)。
非聚簇索引(非聚集索引)
覆盖索引(Covering Index),一个索引包含(或者说覆盖)所有需要查询的字段的值。
覆盖索引
使用表中的多个字段创建索引,就是 联合索引,也叫 组合索引 或 复合索引。
联合索引
在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >、< )才会停止匹配。对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
最左前缀匹配原则
索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
索引下推
不为 NULL 的字段。
被频繁查询的字段。
被作为条件查询的字段。
频繁需要排序的字段。
被经常频繁用于连接的字段。
选择合适的字段创建索引
被频繁更新的字段应该慎重建立索引。
建议单张表索引不超过 5 个!
限制每张表上的索引数量
尽可能的考虑建立联合索引而不是单列索引。
注意避免冗余索引。
字符串类型的字段使用前缀索引代替普通索引。
SELECT * 不会直接导致索引失效,但它可能会带来浪费、无法使用索引覆盖。
创建了组合索引,但查询条件未遵守最左匹配原则。
在索引列上进行计算、函数、类型转换等操作。
以 % 开头的 LIKE 查询比如 LIKE '%abc'。
查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到。
IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同)。
发生隐式转换。
避免索引失效
删除长期未使用的索引。
知道如何分析语句是否走索引查询,EXPLAIN 执行计划。
使用建议
索引
主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
redo log 是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。
作用
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页。
数据页
过程
每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成。
组成
事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘。
log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。
Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页刷新到磁盘,并将相关的重做日志一同刷新。
正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。
刷盘时机
设置参数 innodb_flush_log_at_trx_commit,默认值为1。
0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。性能最高,但也最不安全。
1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。性能最低,但也最安全。
2:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。
刷盘策略
硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写。
write pos:是当前记录的位置,一边写一边后移。
checkpoint:是当前要擦除的位置,也是往后推移。
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log 记录。MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
重要属性
日志文件组
redo log(重做日志)
binlog(归档日志)保证了 MySQL 集群架构的数据一致性。
MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。
binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写。
binlog 日志有三种格式,可以通过 binlog_format 参数指定。
记录的内容是SQL语句原文。
update_time=now() 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。
问题
statement
记录的内容不再是简单的SQL语句了,还包含操作的具体数据。
需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。
row
statement 和 row 的混合,属于折中方案。
MySQL会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。
mixed
记录格式
事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
fsync,才是将数据持久化到磁盘的操作。
write 和 fsync 的时机,可以由参数 sync_binlog 控制,默认是1。为0时,表示每次提交事务都只 write,由系统自行判断什么时候执行 fsync。为1时,表示每次提交事务都会执行 fsync,就如同 redo log 日志刷盘流程 一样。为N(N>1)时,表示每次提交事务都 write,但累积N个事务后才 fsync。
写入机制
binlog(归档日志)
为了解决两份日志(redo log 和 binlog)之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。
将 redo log 的写入拆成了两个步骤 prepare 和 commit,这就是两阶段提交。
MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于 prepare 阶段,并且没有对应 binlog 日志,就会回滚该事务。
两阶段提交
在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。
保证事务的原子性。
目的
undo log(回滚日志)
日志
事务是逻辑上的一组操作,要么都执行,要么都不执行。
原子性(Atomicity):事务是最小的执行单位,不允许分割。
一致性(Consistency):执行事务前后,数据保持一致。
隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
持久性(Durability):一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
ACID特性
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据。
脏读(Dirty read)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
丢失修改(Lost to modify)
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
不可重复读(Unrepeatable read)
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
幻读(Phantom read)
锁可以看作是悲观控制的模式。
锁控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。
共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。
锁
多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。
通过在每个数据行上维护多个版本的数据来实现的。
MVCC 的实现依赖于:隐藏字段、Read View、undo log。
当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
MVCC
控制方式
READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化):最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,可以防止脏读、不可重复读以及幻读。
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。
InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:快照读:由 MVCC 机制来保证不出现幻读。当前读:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
隔离级别
事务
锁是一种常见的并发事务的控制方式。
MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。
MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁。实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
表级锁
MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。
记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
行锁定方式
行级锁
共享锁和排他锁
由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
分类
意向锁
快照读(一致性非锁定读)就是单纯的 SELECT 语句。
比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
快照读
当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。
当前读
当前读和快照读
InnoDB 中的自增主键会涉及一种比较特殊的表级锁。
自增锁(AUTO-INC Locks)
不建议用 MySQL 直接存储文件(比如图片),推荐使用 FastDFS、MinIO(推荐)。
INET_ATON(),INET_NTOA()
可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。
MySQL 为我们提供了 EXPLAIN 命令,来获取执行计划的相关信息。
EXPLAIN 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。
EXPLAIN 执行计划支持 SELECT、DELETE、INSERT、REPLACE 以及 UPDATE 语句。
SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。
id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。
id
查询的类型,主要用于区分普通查询、联合查询、子查询等复杂的查询。
SIMPLE:简单查询,不包含 UNION 或者子查询。
PRIMARY:查询中如果包含子查询或其他部分,外层的 SELECT 将被标记为 PRIMARY。
SUBQUERY:子查询中的第一个 SELECT。
UNION:在 UNION 语句中,UNION 之后出现的 SELECT。
DERIVED:在 FROM 中出现的子查询将被标记为 DERIVED。
UNION RESULT:UNION 查询的结果。
select_type
查询用到的表名,每行都有对应的表名。
<derivedN> : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。
<subqueryN> : 本行引用了 id 为 N 的表所产生的的物化子查询结果。
table
查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。
const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。
eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式。
ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。
index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。
range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。
index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。
ALL:全表扫描。
type
possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引。
possible_keys
key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。
key
key_len 列表示 MySQL 实际使用的索引的最大长度;当使用到联合索引时,有可能是多个列的长度和。
在满足需求的前提下越短越好。如果 key 列显示 NULL ,则 key_len 列也显示 NULL 。
key_len
rows 列表示根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好。
rows
包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。
Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。
Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。
Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。
Using index condition:表示查询优化器选择使用了索引条件下推这个特性。
Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。
Using join buffer (Block Nested Loop):连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。
当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。
注意
Extra
结果分析
执行计划
所有数据库对象名称必须使用小写字母并用下划线分割。
所有数据库对象名称禁止使用 MySQL 保留关键字。
数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符。
临时库表必须以 tmp_ 为前缀并以日期为后缀,备份表必须以 bak_ 为前缀并以日期 (时间戳) 为后缀。
所有存储相同数据的列名和列类型必须一致。
数据库命名规范
所有表必须使用 InnoDB 存储引擎。
数据库和表的字符集统一使用 UTF8,支持emoji,则需要采用 utf8mb4。
所有表和字段都需要添加注释。
尽量控制单表数据量的大小,建议控制在 500 万以内(并不是数据库限制)。
谨慎使用 MySQL 分区表,建议采用物理分表的方式管理大数据。
经常一起使用的列放到一个表中。
禁止在表中建立预留字段。
禁止在数据库中存储文件(比如图片)这类大的二进制数据。
不要被数据库范式所束缚。
禁止在线上做数据库压力测试。
禁止从开发环境,测试环境直接连接生产环境数据库。
数据库基本设计规范
优先选择符合存储需要的最小的数据类型。
避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据。
避免使用 ENUM 类型。
尽可能把所有列定义为 NOT NULL。
一定不要用字符串存储日期。
同财务相关的金额类数据必须使用 decimal 类型。
单表不要包含过多字段。
数据库字段设计规范
限制每张表上的索引数量,建议单张表索引不超过 5 个。
禁止使用全文索引。
禁止给表中的每一列都建立单独的索引。
不要使用 UUID、MD5、HASH 字符串列作为主键,主键建议使用自增 ID 值。
每个 InnoDB 表必须有个主键。
1、出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。
2、包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。
3、并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好。
4、多表 join 的关联列。
常见索引列建议
区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)。
尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)。
使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。
选择索引列的顺序
避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)。
避免 InnoDB 表进行索引的二次查询,也就是回表操作。
可以把随机 IO 变成顺序 IO 加快查询效率。
好处
对于频繁的查询优先考虑使用覆盖索引
不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。
外键可用于保证数据的参照完整性,但建议在业务端实现。
外键会影响父表和子表的写操作从而降低性能。
索引 SET 规范
索引设计规范
尽量不在数据库做运算,复杂运算需移到业务应用里完成。
优化对性能影响较大的 SQL 语句。
充分利用表上已经存在的索引。
禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询。
禁止使用不含字段列表的 INSERT 语句。
建议使用预编译语句进行数据库操作。
避免数据类型的隐式转换。
避免使用子查询,可以把子查询优化为 join 操作。
避免使用 JOIN 关联太多的表。
减少同数据库的交互次数。
in 的值不要超过 500 个。
对应同一列进行 or 判断时,使用 in 代替 or。
禁止使用 order by rand() 进行随机排序。
WHERE 从句中禁止对列进行函数转换和计算。
在明显不会有重复值时使用 UNION ALL 而不是 UNION。
拆分复杂的大 SQL 为多个小 SQL。
程序连接不同的数据库使用不同的账号,禁止跨库查询。
数据库 SQL 开发规范
对于大表使用 pt-online-schema-change 修改表结构。
禁止为程序使用的账号赋予 super 权限。
对于程序连接数据库账号,遵循权限最小原则。
数据库操作行为规范
规范
执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用。
查询缓存
让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。
MyISAM 引擎的自增值保存在数据文件中。
InnoDB 引擎的自增值,其实是保存在了内存里,并没有持久化。首次打开表时,会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。
MySQL 8.0 版本后,自增值的变更记录被放在了 redo log 中,提供了自增值持久化的能力。
存储位置
在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为。
如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段。
如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设某次要插入的值是 insert_num,当前的自增值是 autoIncrement_num:如果 insert_num < autoIncrement_num,那么这个表的自增值不变。如果 insert_num >= autoIncrement_num,就需要把当前自增值修改为新的自增值,
自增行为
分布式 id 为了避免两个库生成的主键发生冲突,我们可以让一个库的自增 id 都是奇数,另一个库的自增 id 都是偶数。
自增值不连续场景 1
自增初始值和自增步长设置不为 1。
唯一键冲突。
事务回滚。
自增值不连续场景 2
批量插入(如 insert...select 语句)。
自增值不连续场景 3
自增主键
当操作符左右两边的数据类型不一致时,会发生隐式转换。
当 where 查询操作符左边为数值类型时发生了隐式转换,那么对效率影响不大,但还是不推荐这么做。
当 where 查询操作符左边为字符类型时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。
字符串转换为数值类型时,非数字开头的字符串会转化为0,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。
隐式转换
其它
MySQL
Redis (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。
Redis 基于内存,内存的访问速度是磁盘的上千倍。
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用。
Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
快的原因
首选
Redis
分布式缓存最开始兴起的那会,比较常用的。
Memcached
针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
Dragonfly
Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
KeyDB
腾讯开源,基于 RocksDB,兼容 Redis,但关注度不高。
Tendis
分布式缓存选型
都是基于内存的数据库,一般都用来当做缓存使用。
都有过期策略。
两者的性能都非常高。
共同点
Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Memcached 只支持最简单的 k/v 数据类型。
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
区别
Redis 和 Memcached
使用内存,访问速度非常快。
高性能
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数。
高并发
用 Redis 原因
服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
写
读
首次请求数据一定不在 cache 的问题。解决:可以将热点数据可以提前放入 cache 中。
写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。解决:数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间。
缺陷
Cache Aside Pattern(旁路缓存模式)
服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。
在平时在开发过程中非常少见,因为 Redis 并没有提供 cache 将数据写入 db 的功能。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。
写(Write Through)
读(Read Through)
Read/Write Through Pattern(读写穿透)
与 Read/Write Through Pattern 很相似,但其只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
在平时开发过程中也非常少见,因其更新db方式对数据一致性带来了更大的挑战。
消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制。
db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
应用
Write Behind Pattern(异步缓存写入)
读写策略
Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中。
实现搜索引擎、处理 JSON 数据、实现图形数据库、处理时间序列数据、实现布隆过滤器、实现分布式限流模块等等。
常用
模块化
分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。基于 Redisson 来实现分布式锁。
限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
消息队列:Redis 自带的 List/Stream 数据结构可以作为一个简单的队列使用。不建议。
延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景。
RediSearch + RedisJSON 可实现小型项目的简单搜索场景。
比较复杂或者数据规模较大的搜索场景还是建议使用 Elasticsearch。
搜索引擎
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
其它特殊数据类型:Bloom filter(布隆过滤器)、Bitfield(位域)。
基础数据类型的底层 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
概要
String 是 Redis 中最简单同时也是最常用的一个数据类型。
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(base64 编码或路径)、序列化后的对象。
简单动态字符串(Simple Dynamic String,SDS)
底层实现
常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存。
计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
分布式锁(利用 SETNX key value 命令可以实现一个最简易的分布式锁)。
对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 非常适合。
系统对性能和资源消耗非常敏感的话,String 非常适合。
在绝大部分情况,推荐使用 String 来存储对象数据。
与 Hash 对比
String(字符串)
Redis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
LinkedList(双向链表)/ ZipList(压缩列表)/ QuickList(快速列表)
信息流展示(最新文章、最新动态)
消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
List(列表)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象。
Dict(哈希表/字典)、ZipList(压缩列表)
对象数据存储场景(用户信息、商品信息、文章信息、购物车信息)
Hash(哈希)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一。
Dict(哈希表/字典)、Intset(整数集合)
需要存放的数据不能重复的场景(网站 UV 统计、文章点赞、动态点赞)
需要获取多个数据源交集、并集和差集的场景(共同好友(交集)、好友推荐(差集)、订阅号推荐(差集+交集))
需要随机获取数据源中的元素的场景(抽奖系统、随机点名)
Set(集合)
类似于 Set,但增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
ZipList(压缩列表)、SkipList(跳跃表)
需要随机获取数据源中的元素根据某个权重进行排序的场景(各种排行榜)
需要存储的数据有优先级或者重要程度的场景(优先级任务队列)
Sorted Set(有序集合)
Bitmap 存储的是连续的二进制数字(0 和 1),只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。
需要保存状态信息(0/1 即可表示)的场景(用户签到情况、活跃用户情况、用户行为统计)
Bitmap (位图)
HyperLogLog 是一种有名的基数计数概率算法 ,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64个不同元素。
稀疏矩阵:计数较少的时候,占用空间很小。
稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间。
计数方式
数量量巨大(百万、千万级别以上)的计数场景(热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计)
HyperLogLog(基数统计)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
需要管理使用地理空间数据的场景(附近的人)
Geospatial (地理位置)
数据类型
使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。
快照(snapshotting,RDB)
只追加文件(append-only file,AOF)
RDB 和 AOF 的混合持久化(Redis 4.0 新增)
方式
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。默认方式。
将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能)。
将快照留在原地以便重启服务器的时候使用。
save : 同步保存操作,会阻塞 Redis 主线程。
bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
生成 RDB 快照命令
RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。
使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。
优势
RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
劣势
RDB 持久化
与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了)。
命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。
文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。
文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
write:写入系统内核缓冲区之后直接返回(仅仅是写到缓冲区),不会立即同步到硬盘。
fsync:用于强制刷新系统内核缓冲区(同步到到磁盘),确保写磁盘操作结束才会返回。
流程
appendfsync always:write + fsync,这样会严重降低 Redis 的性能。
appendfsync everysec:write + fsync,fsync 间隔为 1 秒,兼顾数据和写入性能。
appendfsync no:write 但不 fsync,fsync 的时机由操作系统决定。
这 3 种持久化方式的主要区别在于 fsync 同步 AOF 文件的时机(刷盘)。
从 Redis 7.0.0 开始,Redis 使用了 Multi Part AOF 机制。顾名思义,Multi Part AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。在 Multi Part AOF 中,AOF 文件被分为三种类型,分别为:BASE:表示基础 AOF 文件,它一般由子进程通过重写产生,该文件最多只有一个。INCR:表示增量 AOF 文件,它一般会在 AOFRW 开始执行时被创建,该文件可能存在多个。HISTORY:表示历史 AOF 文件,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,HISTORY 类型的 AOF 会被 Redis 自动删除。
方式( fsync策略)
避免额外的检查开销,AOF 记录日志不会对命令进行语法检查。
在命令执行完之后再记录,不会阻塞当前的命令执行。
如果刚执行完命令 Redis 就宕机会导致对应的修改丢失。
可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
风险
执行完命令之后记录日志原因
当 AOF 变得太大时,Redis 在后台自动重写 AOF 产生一个新的 AOF 文件,新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
AOF 重写
Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。
校验和(checksum),通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。
AOF 校验机制
RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。
AOF 以一种易于理解和解析的格式包含所有操作的日志。
AOF 持久化
由于 RDB 和 AOF 各有优势,于是,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
混合持久化
Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。
RDB 和 AOF
持久化
对于读写命令来说,Redis 一直是单线程模型。
Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作。
Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 ,该模型对应的是 Redis 中的文件事件处理器(file event handler)。
由于文件事件处理器(file event handler)是单线程方式运行的,所以一般都说 Redis 是单线程模型。
多个 socket(客户端连接)
IO 多路复用程序(支持多个客户端连接的关键)
文件事件分派器(将 socket 关联到相应的事件处理器)
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
文件事件处理器
单线程模型
Redis6.0 虽然引入了多线程,但是只是在网络数据的读写这类耗时操作上使用,执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。并且开启后,性能不能有太大提升,因此一般情况下并不建议开启。
用于执行一些比较耗时的操作。
通过 bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
通过 bio_lazy_free 后台线程释放大对象(已删除)占用的内存空间。
后台线程
线程模型
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
惰性删除:只会在取出 key 的时候才对数据进行过期检查。
定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
过期数据的删除策略
解决可能存在定期删除和惰性删除漏掉了很多过期 key 的情况,导致大量过期 key 堆积在内存里而 Out of memory 问题。
内存淘汰机制
volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。不推荐!
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
数据淘汰策略
内存管理
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(Transaction)功能。
Redis 事务是不支持回滚(roll back)操作,Redis 事务不满足原子性。
Redis 事务的持久性也是没办法保证。
通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
Redis 事务实际开发中使用的非常少,功能比较鸡肋,不建议在日常开发中使用。
原生批量操作命令
pipeline(流水线)
Lua 脚本
使用批量操作减少网络传输
给 key 设置随机过期时间。推荐!
开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的。
大量 key 集中过期问题
String 类型的 value 超过 1MB。
复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个。
标准
客户端超时阻塞、网络阻塞、工作线程阻塞
使用 Redis 自带的 --bigkeys 参数来查找。
使用 Redis 自带的 SCAN 命令。
借助开源工具分析 RDB 文件。
借助公有云的 Redis 分析服务。
定位
分割 bigkey:将一个 bigkey 分割为多个小 key。
手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。
采用合适的数据结构。
开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的。
处理
Redis bigkey(大 Key)
如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。
如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。
使用 Redis 自带的 --hotkeys 参数来查找。
使用MONITOR 命令。
京东零售的 hotkey 项目。
借助开源项目。
根据业务情况提前预估。
业务代码中记录分析。
读写分离:主节点处理写请求,从节点处理读请求。
使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
公有云,通过代理查询缓存功能(Proxy Query Cache)优化热点 Key 问题。
Redis hotkey(热 Key)
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
KEYS *:会返回所有符合规则的 key。
HGETALL:会返回一个 Hash 中所有的键值对。
LRANGE:会返回 List 中指定范围内的元素。
SMEMBERS:返回 Set 中的所有元素。
SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
时间复杂度 O(n) 的命令
ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。
ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。
时间复杂度 O(n) 以上的命令
在 redis.conf 文件中,我们可以使用 slowlog-log-slower-than 参数设置耗时命令的阈值,并使用 slowlog-max-len 参数设置耗时命令的最大记录条数。
慢查询命令
那些不可用的空闲内存。虽然不会影响 Redis 性能,但是会增加内存消耗。
Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
频繁修改 Redis 中的数据也会产生内存碎片。
原因
使用 info memory 命令即可查看 Redis 内存相关的信息。
内存碎片率的计算公式:mem_fragmentation_ratio = used_memory_rss / used_memory
mem_fragmentation_ratio (内存碎片率)的值越大代表内存碎片率越严重。
mem_fragmentation_ratio > 1.5 才需要清理内存碎片。
查看
注意性能影响,可通过配置控制。
直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。
重启节点可以做到内存碎片重新整理。
清理
内存碎片
性能优化
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
缓存无效 key、布隆过滤器、接口限流
解决
缓存穿透
请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
设置热点数据永不过期或者过期时间比较长。
针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存击穿
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
限流,避免同时处理大量的请求。
多级缓存,例如:本地缓存 + Redis 缓存的组合。
服务不可用
设置不同的失效时间比如随机设置缓存的失效时间。
缓存永不失效(不太推荐,实用性太差)。
使用定时任务,比如 xxl-job。
使用消息队列,比如 Kafka。
缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中。
热点缓存失效
缓存雪崩
O(n) 命令
SAVE 创建 RDB 快照、AOF 日志记录阻塞、AOF 刷盘阻塞、AOF 重写阻塞
大 Key、清空数据库、集群扩容、Swap(内存交换)、CPU 竞争、网络问题
Redis 阻塞
生产问题
Redis 集群是一种通过将多个 Redis 节点连接在一起以实现高可用性、数据分片和负载均衡的技术。
主要有三种模式:主从复制模式(Master-Slave)、哨兵模式(Sentinel)和 Cluster 模式。
高可用性:Redis集群可以在某个节点发生故障时,自动进行故障转移,保证服务的持续可用。
负载均衡:Redis集群可以将客户端请求分发到不同的节点上,有效地分摊节点的压力,提高系统的整体性能。
容灾恢复:通过主从复制或哨兵模式,Redis集群可以在主节点出现故障时,快速切换到从节点,实现业务的无缝切换。
数据分片:在Cluster模式下,Redis集群可以将数据分散在不同的节点上,从而突破单节点内存限制,实现更大规模的数据存储。
易于扩展:Redis集群可以根据业务需求和系统负载,动态地添加或移除节点,实现水平扩展。
一种基本集群模式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点(从节点)来实现数据的冗余和备份。
配置简单,易于实现。
实现数据冗余,提高数据可靠性。
读写分离,提高系统性能。
主节点故障时,需要手动切换到从节点,故障恢复时间较长。
主节点承担所有写操作,可能成为性能瓶颈。
无法实现数据分片,受单节点内存限制。
数据备份和容灾恢复:通过从节点备份主节点的数据,实现数据冗余。
读写分离:将读操作分发到从节点,减轻主节点压力,提高系统性能。
在线升级和扩展:在不影响主节点的情况下,通过增加从节点来扩展系统的读取能力。
主从复制模式(Master-Slave)
在主从复制基础上加入了哨兵节点,实现了自动故障转移。
自动故障转移,提高系统的高可用性。
具有主从复制模式的所有优点,如数据冗余和读写分离。
配置和管理相对复杂。
依然无法实现数据分片,受单节点内存限制。
高可用性要求较高的场景:通过自动故障转移,确保服务的持续可用。
数据备份和容灾恢复:在主从复制的基础上,提供自动故障转移功能。
哨兵模式(Sentinel)
Redis的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。
在Cluster模式下,Redis将所有的键值对数据分散在多个节点上。每个节点负责一部分数据,称为槽位。
通过对数据的分片,Cluster模式可以突破单节点的内存限制,实现更大规模的数据存储。
Redis Cluster将数据分为16384个槽位,每个节点负责管理一部分槽位。
当客户端向Redis Cluster发送请求时,Cluster会根据键的哈希值将请求路由到相应的节点。
具体来说,Redis Cluster使用CRC16算法计算键的哈希值,然后对16384取模,得到槽位编号。
数据分片与槽位
数据分片,实现大规模数据存储。
负载均衡,提高系统性能。
自动故障转移,提高高可用性。
配置和管理较复杂。
一些复杂的多键操作可能受到限制。
大规模数据存储:通过数据分片,突破单节点内存限制。
高性能要求场景:通过负载均衡,提高系统性能。
高可用性要求场景:通过自动故障转移,确保服务的持续可用。
Cluster 模式
集群
使用连接池:避免频繁创建关闭客户端连接。
尽量不使用 O(n)指令,使用 O(n) 命令时要关注 n 的数量。
使用批量操作减少网络传输:原生批量操作命令(比如 MGET、MSET等等)、pipeline、Lua 脚本。
尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
禁止长时间开启 monitor:对性能影响比较大。
控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。
Elasticsearch 全称叫全文搜索引擎,简称 ES,一个分布式可扩展的实时搜索和分析引擎,一个建立在搜索引擎 Apache Lucene™ 基础上的搜索引擎。
使用 Java 编写的,它的内部使用 Lucene 做索引与搜索。
Lucene 就是一个 Jar 包,里面包含了各种建立倒排索引的方法。
一个分布式的实时文档存储,每个字段可以被索引与搜索。
一个分布式实时分析搜索引擎。
能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。
支持 Http 和 Tcp 两种协议去访问,不过目前 Tcp 只支持 Java。Http 和 Tcp 端口默认9200,如果是 Java 应用建议是 Tcp 方式访问 ES,性能更好。
Client(客户端)
ES 天生就是集群方式,哪怕只有一个节点。
一个集群有一个唯一的名字标志,默认为“elasticsearch”。
集群名称非常重要,具体相同集群名的节点才会组成一个集群,集群名称可以在配置文件中指定。
Cluster(集群)
EsMaster 负责存放 Elasticsearch 的元数据。
ES元数据包括(身份元数据、索引元数据、文档元数据、路由元数据以及其他类型的元数据),管理集群节点状态。
EsMaster(Master 节点)
Node 节点:存储集群的数据,参与集群的索引和搜索功能。
同一个集群内节点的名字不能重复,ES会自动分配,也可以在配置文件中指定。通常在一个节点上分配一个或者多个分片。
EsNode(Node 节点)
分片,就是把索引的数据做水平切分,类似于mysql的分区。
在一个多分片的索引中写入数据时,通过路由来确定具体写入那一个分片中,所以在创建索引时需要指定分片的数量,并且分片的数量一旦确定就不能更改。
分片还有可以有副本,解决当某个节点宕机后不影响索引正常访问。分片数据量可按照每个分片<30G设置,默认5个分片。
Shards(分片)
副本,是指对主分片的备份。主分片和备份分片都可以对外提供查询服务,写操作时先在主分片上完成,然后分发到备份上。
当主分片不可用时,会在备份的分片中选举出一个作为主分片,所以备份不仅可以提升系统的高可用性能,还可以提升搜索时的并发性能。
副本数不宜太多,会增加数据写入负担,副本数要 <= 集群节点数 - 1。
Replicas(副本)
索引: 一个索引是一个文档的集合。每个索引有唯一的名字,通过这个名字来操作它。
Index 可以理解为 RDBMS 里的数据库,也可理解一张表,要看我们怎么使用。
Index(索引-数据库/表)
类型,指索引内部的逻辑分区,通过Type的名字在索引内进行唯一标识。
在查询时如果没有该值,则表示在整个索引中查询。可以理解为表。
在 ES6.x 中一个 Index 只能有一个 Type,在 ES7.x 后就取消了 Type,逐渐减少 Type 是为了提高查询效率。
Type(类型-表)
文档,索引中的每一条数据叫作一个文档。
ES 中一个可以被检索的基本单位,每一 Document 都有一个唯一的 ID 作为区分,以 Json 格式来表示。
Document(文档-行)
好比关系型数据库中列的概念,一个 Document 有一个或者多个 Field 组成。
Field(字段-列)
类似于关系型数据库中的表结构信息,用于定义索引中字段(Field)的存储类型、分词方式、是否存储等信息。
ES 中的 Mapping 是可以动态识别的,根据插入的数据自动识别字段类型。
一个索引的 Mapping 一旦创建,若已经存储了数据,就不可修改了。
可以创建索引 Mapping 模板,只要索引名字匹配了就会按照该 Mapping 插入数据。
Mapping(映射-表结构)
Green(绿色):所有的主分片和副本分片都正常运行。
Yellow(黄色):所有的主分片都正常运行,但不是所有的副本分片都正常运行。
Red(红色):有主分片没能正常运行。
Status(集群状态)
ES数据架构的主要概念与关系数据库Mysql对比表。
组件
字符串按照一定规则分成多个独立的词元(token)。
Elasticsearch 内置的分词器对中文不友好,会把中文分成单个字来进行全文检索,不能达到想要的结果 。其中IK分词器对中文很好,一般都使用它。
分词
也可以称反向索引,倒排索引是搜索引擎到核心。
记录所有文档的单词,一般都比较大。
记录单词到倒排列表的关联信息(文档ID)。
单词词典(Term Dictionary)
记录了单词对应的文档集合,由倒排索引项(Posting)组成。
单词词典的实现一般是 B+Tree。
文档ID,用于获取原始信息。
位置(Position),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)。
偏移(Offset),记录单词在文档的开始和结束位置,用于做高亮显示。
倒排索引项组成
倒排列表(Posting List)
倒排索引
核心技术
天生分片和集群,从 ES 出生开始就天然的支持分布式的特征,且无需第三方组件,自带。
天生索引,ES 所有数据都是默认进行索引的,这点和 MySQL 正好相反,ES 只有不加索引才需要说明。
支持PB级海量数据实时全文搜索。
支持多语言访问,支持 TCP 和 RESTFUL API两种方式访问。
不适合做复杂聚合,会影响ES集群性能。
不支持高并发写入数据。
ES 耗 CPU 和内存资源,需要用高配置的机器来搭建集群,使用成本比较高。
ElasticSearch
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的,属于文档类型数据库。
文档(Document):MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。
集合(Collection):一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。
数据库(Database):一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。
MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
BSON [bee·sahn] 是 Binary JSON 的简称,是 JSON 文档的二进制表示,支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。
BJSON 的遍历速度优于 JSON,但 BJSON 需要更多的存储空间。
键不能含有 \\0(空字符)。这个字符用来表示键的结尾。
. 和 $ 有特别的意义,只有在特定环境下才能使用。
以下划线_开头的键是保留的(不是严格要求的)。
键命名规则
文档
MongoDB 集合存在于数据库中,没有固定的结构,也就是 无模式 的,这意味着可以往集合插入不同格式和类型的数据。
集合名不能是空字符串\"\"。
集合名不能含有 \\0 (空字符),这个字符表示集合名的结尾。
集合名不能以\"system.\"开头,这是为系统集合保留的前缀。
集合名必须以下划线或者字母符号开始,并且不能包含 $。
集合命名条件
集合
数据库用于存储所有集合,而集合又用于存储所有文档。一个 MongoDB 中可以创建多个数据库,每一个数据库都有自己的集合和权限。
admin : admin 数据库主要是保存 root 用户和角色。
local : local 数据库是不会被复制到其他分片的,因此可以用来存储本地单台服务器的任意 collection。
config : 当 MongoDB 使用分片设置时,config 数据库可用来保存分片的相关信息。
test : 默认创建的测试库,连接 mongod 服务时,如果不指定连接的具体数据库,默认就会连接到 test 数据库。
预留库
不能是空字符串\"\"。
不得含有' '(空格)、.、$、/、\\和 \\0 (空字符)。
应全部小写。
最多 64 字节。
数据库命名条件
存储结构
数据记录被存储为文档:MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
模式自由:集合的概念类似 MySQL 里的表,但它不需要定义任何模式,能够用更少的数据对象表现复杂的领域模型对象。
支持多种查询方式:MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。
支持 ACID 事务:NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。但MongoDB 支持事务,同样具有 ACID 特性。
高效的二进制存储:存储在集合中的文档,是以键值对的形式存在的。
自带数据压缩功能:存储同样的数据所需的资源更少。
支持 mapreduce:通过分治的方式完成复杂的聚合任务。从 MongoDB 5.0 开始,推荐其替代方案 聚合管道。
支持多种类型的索引:MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等。
支持 failover:提供自动故障恢复的功能,主节点故障时,自动选举新的主节点,客户无感知。
支持分片集群:MongoDB 支持集群自动切分数据,让集群存储更多的数据,具备更强的性能。
支持存储大文件:MongoDB 的单文档存储空间要求不超过 16MB。提供了 GridFS 来进行分块存储。
数据库的核心组件,负责管理数据在内存和磁盘中的存储方式。与 MySQL 一样,MongoDB 采用的也是 插件式的存储引擎架构。
最初默认是使用 MMAPV1 存储引擎,MongoDB4.x 版本不再支持 MMAPv1 存储引擎。
WiredTiger 存储引擎:自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎 。非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩等功能。
In-Memory 存储引擎:In-Memory 存储引擎 在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。
MongoDB 3.0 提供了 可插拔的存储引擎 API ,允许第三方为 MongoDB 开发存储引擎,这点和 MySQL 也比较类似。
默认使用 B+ 树作为其存储结构,还支持 LSM(Log Structured Merge) 树作为存储结构。
使用 B+ 树时,WiredTiger 以 page 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page。
root page(根节点):B+ 树的根节点。
internal page(内部节点):不实际存储数据的中间索引节点。
leaf page(叶子节点):真正存储数据的叶子节点,包含一个页头(page header)、块头(block header)和真正的数据(key/value)。
page分类
WiredTiger
将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为 聚合操作 。
聚合管道(Aggregation Pipeline):执行聚合操作的首选方法。
单一目的聚合方法(Single purpose aggregation methods):也就是单一作用的聚合函数比如 count()、distinct()、estimatedDocumentCount()。
mapreduce:通过分治的方式完成复杂的聚合任务。从 MongoDB 5.0 开始,推荐其替代方案 聚合管道。
由多个阶段组成,每个阶段在文档通过管道时转换文档。
接受一系列原始数据文档。
对这些文档进行一系列运算。
结果文档输出给下一个阶段。
工作流程
匹配操作符,用于对文档集合进行筛选。
$match
投射操作符,用于重构每一个文档的字段,可以提取字段,重命名字段,甚至可以对原有字段进行操作后新增字段。
$project
排序操作符,用于根据一个或多个字段对文档进行排序。
$sort
限制操作符,用于限制返回文档的数量。
$limit
跳过操作符,用于跳过指定数量的文档。
$skip
统计操作符,用于统计文档的数量。
$count
分组操作符,用于对文档集合进行分组。
$group
拆分操作符,用于将数组中的每一个值拆分为单独的文档。
$unwind
连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于 populate。
$lookup
操作符
聚合管道
聚合
NoSQL 数据库通常不支持事务,但 MongoDB 支持事务。
MongoDB 单文档原生支持原子性,也具备事务的特性。通常指的是 多文档 。
MongoDB 4.0 加入了对多文档 ACID 事务的支持,但只支持复制集部署模式下的 ACID 事务,也就是说事务的作用域限制为一个副本集内。
MongoDB 4.2 引入了 分布式事务 ,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
在大多数情况下,多文档事务比单文档写入会产生更大的性能成本。对于大部分场景来说,适当地对数据进行建模可以最大限度地减少对多文档事务的需求。
从 MongoDB 4.2 开始,多文档事务支持副本集和分片集群,其中:主节点使用 WiredTiger 存储引擎,同时从节点使用 WiredTiger 存储引擎或 In-Memory 存储引擎。在 MongoDB 4.0 中,只有使用 WiredTiger 存储引擎的副本集支持事务。
在 MongoDB 4.2 及更早版本中,你无法在事务中创建集合。从 MongoDB 4.4 开始,您可以在事务中创建集合和索引。
借助 WiredTiger 存储引擎,MongoDB 支持对所有集合和索引进行压缩。压缩以额外的 CPU 为代价最大限度地减少存储使用。
Snappy:谷歌开源的压缩算法(压缩比 3 ~ 5 倍),WiredTiger 默认使用其对所有集合使用块压缩,对所有索引使用前缀压缩。
zlib:高度压缩算法,压缩比 5 ~ 7 倍。
Zstandard(简称 zstd):Facebook 开源的一种快速无损压缩算法,针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。
算法
WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。
数据压缩
MongoDB 可以使用该索引来限制它必须检查的文档数量,以便提高查询效率。
单字段索引: 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。
复合索引: 建立在多个字段上的索引,也可以称之为组合索引、联合索引。字段的顺序非常重要,且遵循左前缀原则。
多键索引:MongoDB 的一个字段可能是数组,在对这种字段创建索引(为数组的每个值创建索引)时,就是多键索引。
哈希索引:按数据的哈希值索引,用在哈希分片集群上。
文本索引: 支持对字符串内容的文本搜索查询。支持全文索引,但性能低下,暂不推荐。
地理位置索引: 基于经纬度的索引,适合 2D 和 3D 的位置查询。
唯一索引:确保索引字段不会存储重复值。
TTL 索引:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。
TTL 索引是单字段索引。复合索引不支持 TTL。
_id字段不支持 TTL 索引。
无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。
如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。
TTL 索引限制
所有的查询字段是索引的一部分。
结果中返回的所有字段都在同一索引中。
查询中没有字段等于null。
覆盖索引查询
又称为副本集群,是一组维护相同数据集合的 mongod 进程。
一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。
主节点:整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。
从节点:从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。
仲裁节点:这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。
主节点与备节点之间是通过 oplog(操作日志) 来同步数据的。oplog 是 local 库下的一个特殊的 上限集合(Capped Collection) ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。
实现 failover:提供自动故障恢复的功能。
实现读写分离:减轻了主节点读写压力过大的问题。
复制集群
MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中。
不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。
Config Servers:配置服务器,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、Chunks 等。
Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
Shard:每个分片是整体数据的一部分子集,从 MongoDB3.6 版本开始,每个 Shard 必须部署为副本集(replica set)架构。
垂直扩展:通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等。
水平扩展:通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。
拓展方式
存储容量受单机限制,即磁盘资源遭遇瓶颈。
读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。
场景
分片键(Shard Key) 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。
分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求。
它必须在所有文档中都出现。
它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。
MongoDB 4.2 之前的版本,文档的分片键字段值不可变。MongoDB 4.2 版本开始,除非分片键字段是不可变的 _id 字段,否则您可以更新文档的分片键值。MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。
它的大小不能超过 512 字节。
要求
选择合适的片键对 sharding 效率影响很大,主要基于四个因素。
取值基数:取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。
取值分布:取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。
查询带分片:查询时建议带上分片,使用分片键进行条件查询时,可以直接定位到具体分片,否则需要将查询分发到所有分片,再等待响应返回。
避免单调递增或递减:单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。
选择条件
分片键
MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时,范围分片更高效。
Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。
可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。
分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。
基于范围的分片
MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。
可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。
不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。
分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。
基于 Hash 值的分片
除了上述两种分片策略,还可以配置 复合片键 ,例如由一个低基数的键和一个单调递增的键组成。
分片策略
MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中划分的概念。
Chunk(块)
分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。
默认情况下,一个 Chunk 的最大值默认为 64MB(可调整,取值范围为 1~1024 MB。如无特殊需求,建议保持默认值)。
进行数据插入、更新、删除时,如果此时 Mongos 感知到了目标 Chunk 的大小或者其中的数据量超过上限,则会触发 Chunk 分裂。
Chunk 分裂
数据的增长会让 Chunk 分裂得越来越多。此时各个分片上的 Chunk 数量可能会不平衡。Mongos 中的 均衡器(Balancer) 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 再平衡(Rebalance)。默认情况下,数据库和集合的 Rebalance 是开启的。
Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其影响。
再平衡(Rebalance)
MongoDB默认情况下会开启一个balancer模块用于定期检测各个shard上的chunk数量分布,当检测到各个shard上的chunk数量存在分布不均匀的情况时,就会触发chunk迁移。
chunk迁移操作通过moveChunk命令发起,可以被balancer自动调用(balancer每隔10s扫描哪些chunk需要被迁移),也支持用户主动发起。
迁移chunk的整个过程实际上就是一次两个shard进行数据交换的过程,分别有 chunk 的发送方和接收方。
Chunk 迁移
分片数据存储
分片集群
高可用
MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。
MongoDB
0 条评论
回复 删除
下一页