指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性
- 原子性(atomicity,或称不可分割性):多个操作同时成功或者同时失败
- 一致性(consistency):事务执行结果符合数据库设置的约束、触发器、级联回滚等限制条件
- 隔离性(isolation,又称独立性):多个事务并行的结果,应该和多个事务串行的结果一致
- 持久性(durability):事务结果永久有效
- B 树就是常说的“B 减树(B- 树)”,又名平衡多路(即不止两个子树)查找树,
- B 树的定义:
- 若根结点不是终端结点,则至少有 2 棵子树
- 除根节点以外的所有非叶结点至少有 M/2 棵子树,至多有 M 个子树(关键字数为子树减一)
- 所有的叶子结点都位于同一层
- B 树的平衡条件:
- 叶子节点都在同一层
- 每个节点的关键字数为子树个数减一(子树个数 k 介于树的阶 M 和它的二分之一)
- 子树的关键字保证左小右大的顺序
- 从根节点开始,如果查找的数据比根节点小,就去左子树找,否则去右子树
- 和子树的多个关键字进行比较,找到它所处的范围,然后去范围对应的子树中继续查找
- 以此循环,直到找到或者到叶子节点还没找到为止
- 平衡二叉树节点最多有两个子树,而 B 树每个节点可以有多个子树,M 阶 B 树表示该树每个节点最多有 M 个子树
- 平衡二叉树每个节点只有一个数据和两个指向孩子的指针,而 B 树每个中间节点有 k-1 个关键字(可以理解为数据)和 k 个子树( **k 介于阶数 M 和 M/2 之间,M/2 向上取整)
- B 树的所有叶子节点都在同一层,并且叶子节点只有关键字,指向孩子的指针为 null
- B+树是 B 树的一种变形树
- B+树需要满足的条件:
- 节点的子树数和关键字数相同(B 树是关键字数比子树数少一)
- 非叶子节点的关键字表示的是子树中的最大数,在子树中同样含有这个数据
- 叶子节点包含了全部数据,同时符合左小右大的顺序
- 叶子节点用指针连在一起
- B+树中只有叶子节点会带有指向记录的指针(ROWID),而 B 树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中
- B+树中所有叶子节点都是通过指针连接在一起,而 B 树不会
- B+树的优点:
- 非叶子节点不会带上指针,一个块中可以容纳更多的索引项,所以树的层级更低,一个内部节点可以定位更多的叶子节点,所以 IO 次数更少
- 每次都需要查询到叶子节点,查询性能稳定
- 叶子节点之间通过指针来连接,形成有序链表,范围查询更方便
- B 树的优点:
- 内部节点可直接得到数据,不必再根据叶子节点来定位
- LSM-Tree 全称是 Log Structured Merge Tree,是一种分层,有序,面向磁盘的数据结构,其核心思想是充分了利用了“磁盘批量的顺序写要远比随机写性能高出很多”的特性
- 在 LSM-Tree 里面,核心的数据结构就是 SSTable,全称是 Sorted String Table,SSTable 的概念其实也是来自于 Google 的 Bigtable 论文
- SSTable 是一种拥有持久化,有序且不可变的的键值存储结构,它的 key 和 value 都是任意的字节数组,并且了提供了按指定 key 查找和指定范围的 key 区间迭代遍历的功能
- 当收到一个写请求时,会先把该条数据记录在 WAL(write ahead log)里面,用作故障恢复
- 当写完 WAL Log 后,会把该条数据写入内存的 SSTable 里面(删除是墓碑标记,更新是新记录一条的数据),也称 Memtable。注意为了维持有序性在内存里面可以采用红黑树或者跳跃表相关的数据结构
- 当 Memtable 超过一定的大小后,会在内存里面冻结,变成不可变的 Memtable,同时为了不阻塞写操作需要新生成一个 Memtable 继续提供服务
- 把内存里面不可变的 Memtable 给 dump 到到硬盘上的 SSTable 层中,此步骤也称为 Minor Compaction,这里需要注意在 L0 层的 SSTable 是没有进行合并的,所以这里的 key range 在多个 SSTable 中可能会出现重叠,在层数大于 0 层之后的 SSTable,不存在重叠 key
- 当每层的磁盘上的 SSTable 的体积超过一定的大小或者个数,也会周期的进行合并。此步骤也称为 Major Compaction,这个阶段会真正 的清除掉被标记删除掉的数据以及多版本数据的合并,避免浪费空间,注意由于 SSTable 都是有序的,我们可以直接采用 merge sort 进行高效合并
- 当收到一个读请求的时候,会直接先在内存里面查询,如果查询到就返回
- 如果没有查询到就会依次下沉,知道把所有的 Level 层查询一遍得到最终结果
- LSM-Tree 的设计思路是,将数据拆分为几百 M 大小的 Segments,并且顺序写入
- B+Tree 则是将数据拆分为固定大小的 Block 或 Page, 一般是 4KB 大小,和磁盘一个扇区的大小对应,Page 是读写的最小单位
- 在数据的更新和删除方面,B+Tree 可以做到原地更新和删除,这种方式对数据库事务支持更加友好,因为一个 key 只会出现一个 Page 页里面,但由于 LSM-Tree 只能追加写,并且在 L0 层 key 的 rang 会重叠,所以对事务支持较弱,只能在 Segment Compaction 的时候进行真正地更新和删除
- 因此 LSM-Tree 的优点是支持高吞吐的写(可认为是 O(1)),这个特点在分布式系统上更为看重,当然针对读取普通的 LSM-Tree 结构,读取是 O(N)的复杂度,在使用索引或者缓存优化后的也可以达到 O(logN)的复杂度
- 而 B+tree 的优点是支持高效的读(稳定的 OlogN),但是在大规模的写请求下(复杂度 O(LogN)),效率会变得比较低,因为随着 insert 的操作,为了维护 B+树结构,节点会不断的分裂和合并。操作磁盘的随机读写概率会变大,故导致性能降低
- 基于 LSM-Tree 分层存储能够做到写的高吞吐,带来的副作用是整个系统必须频繁的进行 compaction,写入量越大,Compaction 的过程越频繁。而 compaction 是一个 compare & merge 的过程,非常消耗 CPU 和存储 IO,在高吞吐的写入情形下,大量的 compaction 操作占用大量系统资源,必然带来整个系统性能断崖式下跌,对应用系统产生巨大影响
- RocksDB 是使用 C++编写的嵌入式 kv 存储引擎,其键值均允许使用二进制流。由 Facebook 基于 levelDB 开发, 提供向后兼容的 levelDB API,包括 TIDB、MyRocks (Facebook 基于 RocksDB 的 MySQL 存储引擎)、Nebula(图数据库 )等数据库都使用到了 RocksDB
- RocksDB 针对 Flash 存储进行优化,延迟极小。RocksDB 使用 LSM(LSM-tree)存储引擎,纯 C++编写
- RocksDB 依靠大量灵活的配置,使之能针对不同的生产环境进行调优,包括直接使用内存,使用 Flash,使用硬盘或者 HDFS。支持使用不同的压缩算法,并且有一套完整的工具供生产和调试使用
在 MySQL 5.5 之前的版本中,默认的搜索引擎是 MyISAM,从 MySQL 5.5 之后的版本中,默认的搜索引擎变更为 InnoDB
- MyISAM
- 不支持事务,不具备 AICD 特性
- 最小的锁粒度是表级锁(更新数据会锁定整个表,导致其他查询和更新都会被阻塞,性能低)
- 不支持外键
- 非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和其他索引是独立的
- 支持全文索引
- 用一个变量保存了整个表的行数,执行 select count(*)时只需要读出该变量即可
- InnoDB
- 支持事务,具备 ACID 特性
- 最小的锁粒度是行级锁(更新数据是锁定行,只会锁定修改的行,性能高)
- 支持外键
- 聚簇索引,数据文件存放在主键索引的叶子节点上
- 因此 InnoDB 必须要有主键,通过主键索引的查询效率很高
- 如果没有设定主键或者非空唯一索引,就会自动生成一个 6 字节的主键(用户不可见)
- 但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据
- 因此,主键不应该过大,因为主键太大,其他索引也都会很大
- 不支持全文索引,5.6.4 版本开始支持全文索引,也可以使用 sphinx 插件支持全文索引,并且效果更好
- 不保存表的具体行数,执行 select count(*)时需要扫描全表
- 脏读:一个事务中访问到了另外一个事务未提交的数据
- 不可重复读:一个事务内读取同一条记录 2 次,得到的结果不一致
- 在可重复读中,事务内第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读
- 但因为无法锁住 insert 的数据,所以可重复读级别无法避免幻读
- 幻读:一个事务内范围读取 2 次,得到的记录条数不一致
- 事务 A 第一次按照范围读取数据之后,事务 B 接着 Insert 了一条满足事务 A 查询条件的数据
- 导致事务 A 第二次按照同样的查询条件,读取到的数据条数和第一次的不一致
- 读未提交(Read uncommitted)
- 能读取到其他事务未提交的数据(存在脏读、不可重复读、幻读)
- 读不会加任何锁。而写会加排他锁,并到事务结束之后释放
- 读已提交(Read committed)
- 只能读取到事务已经提交的数据(存在不可重复读、幻读)
- 写数据时,使用排它锁, 读取数据不加锁而是使用了 MVCC 机制
- 可重复读(Repeatable read)
- 同一个事务内,对同一条数据的多次查询结果是一致的(存在幻读)
- 写数据时,使用排它锁,读取数据时也使用 MVCC,但有区别, 一次事务中只在第一次 select 时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读
- 串行化(Serializable )
- 所有事务串行执行
- 会自动为 select 操作加上共享锁(select ... lock in share mode),并且写入也会加入排他锁,实现读-写、写-写均互斥
- 支持 checkpoint 和 mysqlbinlog 两种方式恢复数据
- checkpoint 是数据库某一时刻的快照
- sharp checkpoint:在关闭数据库时,将 buffer pool 中的脏页全部刷入磁盘
- fuzzy checkpoint:在数据库正常运行时,找到不同时机将脏页写入磁盘,一部分一部分的刷入磁盘,不会因为一次性刷入磁盘造成性能问题
- Server 层
- 连接器:管理连接权限验证
- 查询缓存:命中缓存直接换回查询结果
- 分析器:分析语法
- 优化器:生成执行计划,选择索引
- 执行器:操作索引返回结果
- 存储引擎
- 存储引擎负责数据的存储和提取;其架构是插件式的。innodb 在 mysql5.5.5 版本开始成为 mysql 默认存储引擎
- 数据文件(datafile):存放表中的具体数据的文件
- 数据字典:记录数据库中所有 innodb 表的信息
- 重做日志 (redo log):记录数据库变更记录的文件,用于系统异常 crash(掉电)后的恢复操作,可以配置多个(配置这个参数 innodb_log_files_in_group)比如 ib_logfile0、 ib_logfile1
- 回滚日志(undolog):也存在于 mysql 的 ibdata 文件,用于记录事务的回滚操作。注在 mysql5.6 以上版本可以拆开出来,单独文件夹存在
- 归档日志(binlog):记录数据更新日志
- 中继日志(relay log):从 master 获取到 slave 的中转日志文件,sql_thread 则会应用 relay log 并重放于从机器
- 慢查询日志(slowlolg):记录慢查询日志
- 错误日志(errorlog)、查询日志(querylog)
- MySQL 中的 binlog 是一个二进制日志,主要是用来记录对 mysql 数据更新或潜在发生更新的 SQL 语句,并以"事务"的形式保存在磁盘中
- 当需要同步的时候会主动通知 slave 节点,slave 收到通知后使用 IO Thread 主动去 master 读取 binlog 日志,然后异步写入 relay 日志(中转日志),然后使 SQL Thread 完成对`relay 日志 的解析然后入库操作,完成同步
- binlog 作用
- 复制:MySQL Replication 在 Master 端开启 binlog,Master 把它的二进制日志传递给 slaves 并回放来达到 master-slave 数据一致的目的
- 数据恢复:通过 mysqlbinlog 工具恢复数据
- 增量备份
- binlog 具有三种模式
- Row 模式:
- 仅保存记录被修改细节,不记录 sql 语句上下文相关信息
- 优点:能非常清晰的记录下每行数据的修改细节,不需要记录上下文相关信息,因此不会发生某些特定情况下的 procedure、function、及 trigger 的调用触发无法被正确复制的问题,任何情况都可以被复制,且能加快从库重放日志的效率,保证从库数据的一致性
- 缺点:由于所有的执行的语句在日志中都将以每行记录的修改细节来记录,因此,可能会产生大量的日志内容,干扰内容也较多;比如一条 update 语句,如修改多条记录,则 binlog 中每一条修改都会有记录,这样造成 binlog 日志量会很大,特别是当执行 alter table 之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中,实际等于重建了表
- Statement 模式(早期的默认模式):
- 每一条会修改数据的 sql 都会记录到 master 的 binlog 中。slave 在复制的时候 sql Thread 会解析成和原来 master 端执行过的相同的 sql 来再次执行
- 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO,提高性能
- 缺点:由于记录的只是执行语句,为了这些语句能在 slave 上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在 slave 得到和在 master 端执行时候相同的结果。部分有状态的函数,同步到 slave 节点时会出现数据不一致的问题
- Mixed 模式:
- Mixed 即混合模式,MySQL 会根据执行的每一条具体的 sql 语句来区分对待记录的日志形式,也就是在 Statement 和 Row 之间选择一种
- 一般的语句修改使用 statment 格式保存 binlog
- 一些有状态的函数,statement 无法完成主从复制的操作,则采用 row 格式保存 binlog
- Row 模式:
- 如何选择 binlog 模式
- 互联网公司,使用 MySQL 的功能相对少(存储过程、触发器、函数),选择默认的 Statement Level 模式(默认)
- 公司如果用到使用 MySQL 的特殊功能(存储过程、触发器、函数),则选择 Mixed 模式
- 公司如果用到使用 MySQL 的特殊功能(存储过程、触发器、函数)又希望数据最大化一直,此时最好选择 Row level 模式
- 原子性:
- 通过回滚日志(undo log)实现事务的原子性
- 一致性:
- 通过原子性、隔离性、持久性来保证一致性,也就是说 ACID 四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段
- 隔离性:
- 引入锁机制来保证(表锁、行锁、间隙锁、net-key 锁、意向锁)
- MVCC(多版本并发控制)
- 持久性:
- Innodb 通过force log at commit 机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的 redo log file 和 undo log file 中进行持久化
- 对于在内存中未写入磁盘的脏页,通过 checkpoint 机制落盘
- 先顺序写回滚日志(undo log)
- 在内存更新数据,这步操作就在内存中形成了脏页,如果脏页过多,checkpoint 机制进行刷新
- 顺序写重做日志(redo log),redolog 状态为 prepare
- 顺序写归档日志(binlog)
- 提交事务
- 重做日志(redo log)第二阶段,这里会进行判断前 2 步是否成功,成功则默认 commit,否则 rollback,redolog 状态为 commit 或者 roolback
- redolog 的提交方式为“两段式提交”,这样做的目的是为了数据恢复的时候确保数据恢复的准确性,因为数据恢复是通过备份的 binlog 来完成的,所以要确保 redolog 要和 binlog 一致
- 查看缓存中是否存在 id,如果有则直接返回内存中的数据
- 如果没有则访问磁盘获取数据,并将数据和索引数据存入内存
- 原理(B+树、Hash)
- B+树索引:
- 详见 B+树的原理
- Hash 索引:
- 实现原理跟 JAVA 的 HashMap 类似
- 优缺点:
- 精确查找效率高(同一个 hash 冲突的链表过长时,效率会低)
- 不支持范围查询
- 不支持按索引排序
- 不支持部分字段查询(字段值合并后取 hash 做的索引)
- B+树索引:
- 类型
- 普通索引(key)
- 唯一索引(unique key):查询到第一个满足条件的记录后就停止检索,索引效率高
- 主键索引(primary key)
- 主键索引和普通索引的区别
- 主键索引也被称为聚簇索引,叶子节点存放的是整行数据
- 普通索引被称为二级索引,叶子节点存放的是主键的值
- 主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值,索引列的所有值都只能出现一次,即必须唯一
- 普通索引没有任何限制,值可以为空,可以重复,一个表中可以有多个普通索引
- 如果通过非主键索引查询, 需要先搜索 k 索引树, 找到对应的主键, 然后再到 ID 索引树搜索一次, 这个过程叫做回表
- 粒度锁(从锁定资源的角度):
- 表级锁(table-level locking)
- 对整张表加锁。开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低
- 行级锁(row-level locking)
- 对某行记录加锁。开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高
- 页面锁(page-level locking)
- 开销和加锁时间界于表锁和行锁之间,会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般
- 表级锁(table-level locking)
- 行级锁(共享锁(S)、排他锁(X))
- 共享锁(S)
- 当一个事务读取一条记录的时候,不会阻塞其他事务对同一记录的读请求,但会阻塞对其的写请求。当读锁释放后,才会执行其他事务的写操作
- SQL 实现:select * from table lock in share mode
- 排他锁(X)
- 当一个事务对一条记录进行写操作时,会阻塞其他事务对同一表的读写操作,当该锁释放后,才会执行其他事务的读写操作
- SQL 实现:select * from table for update
- 共享锁(S)
- 间隙锁
- 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时(在事务中使用范围性的条件查询数据),InnoDB 会给符合条件的已有数据记录的索引项加锁(Innodb 会将索引数据分为几个区间,并给每个区间的间隙加锁,所以叫间隙锁)
- MySQL 只在可重复读(REPEATABLE READ)隔离级别下的特定操作才会获取间隙锁
- UPDATE/DELETE/SELECT FOR UPDATE 时,除了对唯一索引的唯一搜索外,都会获取间隙锁
- 作用:
- 防止幻读,以满足相关隔离级别的要求
- Next-Key 锁
- Next-Key 锁是行锁和 GAP(间隙锁)的合并
- 乐观锁、悲观锁
- 乐观锁(CAS)
- 增加 version 字段,修改之前先读出 version,修改时判断 version 的值是否发生过变化,变化了则更新失败
- 悲观锁(排他锁)
- select * from table for update
- 乐观锁(CAS)
- 意向锁(加在表上的锁)
- 意向共享锁(IS)
- 事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁
- 意向排他锁(IX)
- 事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁
- 作用:
- 为了允许行锁和表锁共存
- 当 A 事务获取到表的行锁后,B 事务如果要获取到表级锁,需要先遍历判断表中的数据是否有被其他事务获取到行锁(表级锁可以修改表内任何数据,和行级锁冲突),意向锁则可以解决该问题,如果表持有意向锁,则表示表中存在被其他事务获取到的行锁
- 意向共享锁(IS)
- MVCC(InnoDB)
- MVCC(Multi-Version Concurrency Control):多版本并发控制,是一种并发控制的方法
- 是 同一份数据临时保留多版本的一种方式,进而实现并发控制
- MVCC 提供了时点(point in time)一致性视图。MVCC 并发控制下的读事务一般使用时间戳或者事务 ID 去标记当前读的数据库的状态(版本),读取这个版本的数据
- 读、写事务相互隔离,不需要加锁,MVCC 比单纯的加行锁更有效, 开销更小
- 读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据
- MVCC 缺点是会保存多个快照版本,造成了空间的冗余,但是保证了每个线程的独立操作
- MVCC 在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用
- MVCC 既可以基于乐观锁又可以基于悲观锁来实现
- 实现原理
- InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果没有主键,则还会多一个隐藏的主键列)
- DATA_TRX_ID:记录最近更新这条行记录的事务 ID,大小为 6 个字节
- DATA_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织
- Redo Log 和 Undo Log:
- 多个事务并行操作某行数据的情况下,不同事务对该行数据的 UPDATE 会产生多个版本,然后通过回滚指针组织成一条 Undo Log 链
- 死锁
- 死锁的根本原因是有两个或多个事务之间加锁顺序的不一致导致的
- 经典场景:
事务A: update test set count=10 where id=10; update test set count=10 where id=20; 事务B: update test set count=10 where id=20; update test set count=10 where id=10;
- 间隙锁死锁:
- 如果一个事务 A 获取到了一个范围之间的间隙锁,另一个事务 B 也可以获取到这个范围之间的间隙锁。这时就可能会发生死锁问题
事务A: select * from t where id = 9 for update; insert into user value(7,7,7); 事务B: select * from t where id = 6 for update; insert into user value(7,7,7);
- 加锁机制
- 意向锁是 InnoDB 自动加的, 不需要干预
- 对于普通 SELECT 语句,InnoDB 不会加任何锁
- InnoDB 在事务执行过程中,使用两阶段锁协议,InnoDB 会根据隔离级别在需要的时候自动加锁
- 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB 会自动给涉及数据集加排他锁(X)
- 事务可以通过 SQL 语句显式给记录集加共享锁或排他锁
- 锁释放机制
- 锁只有事务在执行 commit 或者 rollback 的时候才会释放,并且所有的锁是在同一时刻被释放
- 查看锁状况的方法
- 在 MySQL 5.5 至 5.7.14 的版本中,用户可以通过 INFORMATION_SCHEMA 下的 INNODB_TRX、INNODB_LOCKS 以及 INNODB_LOCK_WAITS 这三张表简单地监控并分析可能存在的锁问题
- 从 MySQL 8.0 版本开始,需要使用 performance_schema 下的 data_locks 以及 data_lock_waits 获取相关的锁以及锁等待信息
- 作用
- 描述 MySQL 如何执行查询操作、执行顺序,使用到的索引,以及 MySQL 成功返回结果集需要执行的行数。常用于分析 select 语句,优化查询效率
- 使用示例
- explain select * from servers
- explain 结果
- MySQL5.7 版本之前:id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra
- MySQL5.7 版本之后:id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra(5.7 之后的版本多出了 partitions 和 filtered 两列)
- 结果字段释义
- id:SQL 执行的成功的标识,SQL 从大到小的执行
- select_type:select 类型
- table:输出行所引用的表名,有时不是真实的表名
- partitions:使用的表分区
- type:访问类型,表示 MySQL 在表中找到所需行的方式,const、eq_reg、ref、range、indexhe、ALL(从左到右,性能从好到差)
- const、system: 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用这些类型访问,将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量,system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system
- eq_ref: 多表连接中使用 primary key 或者 unique key 作为关联条件
- ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
- range:只检索给定范围的行,使用一个索引来选择行
- indexhe:全索引扫描。index 与 ALL 区别为 index 类型只遍历索引树
- ALL:全表扫描
- possible_keys:指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
- key:显示 MySQL 实际决定使用的键(索引),如果没有选择索引,键是 NULL。要想强制 MySQL 使用或忽视 possible_keys 列中的索引,在查询中使用FORCE INDEX、USE INDEX 或者 IGNORE INDEX
- key_len:显示 MySQL 决定使用的键(索引)长度。如果键是 NULL,则长度为 NULL
- ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
- rows:rows 列显示 MySQL 认为它执行查询时必须检查的行数
- filtered:通过过滤条件之后对比总数的百分比
- Extra:额外的信息,例如:using file sort、Using temporary、using where、using join buffer、using index、distinct 等
- 分库:单库并发有限制(1 千~2 千),为了实现更高并发,可以将数据拆分到多个库中
- 分表:单表数据量太大,会极大影响你的 sql 执行的性能,一般超过五百万数据量,就要分表
- 常见分库分表中间件:
- TDDL(淘宝团队开发,属于 client 层方案)
- Sharding-jdbc(当当开源,已经成为 Apache 顶级项目,属于 client 层方案)
- Mycat(基于阿里 b2b 团队开源的 Cobar 改造的,属于 proxy 层方案)
- 停机扩容(不建议使用):直接停机将数据迁移到更多的库表中
- 动态扩容(将多个库拆分到更多机器上):
- 设定数据拆分规则,先将现有数据用同步方案同步到其他库,并保持一直同步
- 程序实现设定好的数据拆分规则,发布上线
- 停机迁移(不建议使用)
- 不停机迁移
- 在请求量少的时间段迁移
- 同步双写
- 稳定后再废除老库
- 一个主库,多个从库,数据只写入主库,查询只经过从库
- 数据写入主库后,通过主从复制技术同步到从库
- 普通主从复制:从库 copy 主库的 binlog,保存到日志中,并异步执行,保证数据跟主库一致
- 半同步主从复制:主库写入 binlog 之后强制将 binlog 刷到从库的日志中,从库写完日志返回 ack,主库收到 ack 之后才会认为操作完成
- 并行复制:从库开启多个线程,并行读取从主库同步来的不同库的日志,然后并行重放不同库的日志,这是库级别的并行
- 主从同步延时问题解决方案:
- 水平分库+并行复制
- 写入后不能立即读
- 写入后直连主库立即读
- count(*),count(字段),count(1)的区别
- count(*):
- InnoDB 在执行 count(*)时,就会判断使用哪个索引,会选择最小的树来进行遍历
- count(1):
- InnoDB 遍历全表,但是不取值,server 层对返回的每一行数据新增一个 1,然后进行判断累加
- count(1) 会统计表中的所有的记录数,包含字段为 null 的记录
- count(字段):
- count(字段) 会统计该字段在表中出现的次数,忽略字段为 null 的情况。即不统计字段为 null 的记录
- 查询照效率:
- 如果表多个列但是没有索引,则 count(*)<count(字段)<count(主键 id)≈count(1)
- 如果表有多个列并且有索引,count(字段)<count(主键 id)<count(1)≈count(*)
- count(*):
- 聚集索引和非聚集索引的区别
- 聚集索引就是以主键创建的索引
- 非聚集索引就是以非主键创建的索引
- 覆盖索引
- 覆盖索引(covering index)指一个查询语句的执行只用从索引页中就能够取得(如果不是聚集索引,叶子节点存储的是主键+列值,最终还是要回表,也就是要通过主键再查找一次),避免了查到索引后,再做回表操作,减少 I/O 提高效率
- 前缀索引
- 前缀索引就是对文本的前几个字符(具体是几个字符在创建索引时指定)创建索引,这样创建起来的索引更小。但是 MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引
- UNION ALL 与 UNION 的区别
- UNION 在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果
- 而 UNION ALL 只是简单的将两个结果合并后就返回
- 由于 UNION 需要排序去重,所以 UNION ALL 的效率比 UNION 好很多
- TRUNCATE 与 DELETE 区别
- TRUNCATE 是先把整张表 drop 调,然后重建该表。而 DELETE 是一行一行的删除
- TRUNCATE 不可以回滚,DELETE 可以
- TRUNCATE 会重置水平线(自增长列起始位),DELETE 不会
- TRUNCATE 只能清理整张表,DELETE 可以按照条件删除
- TIMESTAMP 与 DATETIME 的区别
- TIMESTAMP 是 4 个字节存储,时间范围:1970-01-01 08:00:01~2038-01-19 11:14:07
- TIMESTAMP 的值以 UTC 格式保存,涉及时区转化,存储时对当前的时区进行转换,检索时再转换回当前的时区
- DATETIME 是 8 个字节存储,时间范围:1000-10-01 00:00:00~9999-12-31 23:59:59
- 最左匹配原则
- 在 MySQL 建立联合索引时会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
- MySQL 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配(比如 a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d 的顺序可以任意调整)
- = 和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式
- int 类型的长度的意义
- int 固定占 4 个字节
- 长度只代表宽度
- 无论是 int(4), int(5), 存储的都是 4 字节无符号整数, 也就是 0~2^32,但是,当数字不足 4 位或 5 位时,前面会用 0 补齐
- MySQL 高效分页
- 例如:SELECT * FROM t ORDER BY id LIMIT N,M
- 上面 SQL 中 LIMIT N,M,是取出 N+M 行,丢弃前 N 行,返回 N ~ N+M 行的记录,如果 N 值非常大,效率极差
- 解决办法:SQL:SELECT id FROM t WHERE id > N LIMIT M
- MySQL 百万级数据分页查询及优化
- 建立主键或唯一索引, 利用索引查询
- WHERE id_pk > (pageNo*pageSize)
- 基于索引再排序
- WHERE id_pk > (pageNo*pageSize) ORDER BY id_pk ASC LIMIT M
- 利用"子查询/连接+索引"快速定位元组的位置,然后再读取元组 - SELECT * FROM your_table WHERE id <= (SELECT id FROM t ORDER BY id desc LIMIT (pageNo-1)*pageSize ORDER BY id desc LIMIT pageSize
- 建立主键或唯一索引, 利用索引查询
- Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立,却又能够混合部署配合使用的产品组成。 它们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景,已于 2020 年 4 月 16 日成为 Apache 软件基金会的顶级项目
- 组成:
- ShardingSphere-JDBC
- 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务
- 支持任意实现 JDBC 规范的数据库
- ShardingSphere-JDBC 采用无中心化架构,适用于 Java 开发的高性能的轻量级 OLTP 应用
- ShardingSphere-Proxy
- 定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持,目前提供 MySQL 和 PostgreSQL 版本
- ShardingSphere-Proxy 提供静态入口以及异构语言的支持,适用于 OLAP 应用以及对分片数据库进行管理和运维的场景
- ShardingSphere-Sidecar
- 定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的的啮合层,即 Database Mesh,又可称数据库网格
- Apache ShardingSphere 是多接入端共同组成的生态圈。 通过混合使用 ShardingSphere-JDBC 和 ShardingSphere-Proxy,并采用同一注册中心统一配置分片策略,能够灵活的搭建适用于各种场景的应用系统
- ShardingSphere-JDBC
- 功能列表:
- 数据分片
- 分库 & 分表
- 读写分离
- 分片策略定制化
- 无中心化分布式主键
- 分布式事务
- 标准化事务接口
- XA 强一致事务
- 柔性事务
- 数据库治理
- 分布式治理
- 弹性伸缩
- 可视化链路追踪
- 数据加密
- 数据分片
- Redis 服务器中的所有数据库保存在 db 数组中,数据库的结构是 redis.h/redisDb,其中,redisDb 结构的 dict 字典(哈希表)保存了数据库中的所有键值对
- 扩容机制(rehash):
- 负载因子:负载因子=哈希表已保存节点数量 / 哈希表大小 (load_factor = ht[0].used / ht[0].size)
- 缩扩容条件:
- Redis 服务器目前没有在执行 BGSAVE 命令或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1
- Redis 服务器目前在执行 BGSAVE 命令或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5
- 当哈希表的负载因子小于 0.1 时,对哈希表执行收缩操作
- rehash 的操作步骤:
- 为字典的 ht[1]哈希表分配空间(扩容翻倍,缩容减半)
- 将 ht[0]中的数据转移到 ht[1]中,在转移的过程中,重新计算键的哈希值和索引值,然后将键值对放置到 ht[1]的指定位置(类似 HashMap,重新取模,将一个链表分为高低位两个链表)
- 当 ht[0]的所有键值对都迁移到了 ht[1]之后(ht[0]变为空表),将 ht[0]释放,然后将 ht[1]设置成 ht[0],最后为 ht[1]分配一个空白哈希表
- 渐近式 rehash
- rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的
- 渐进式 rehash 的详细步骤:
- 为 ht[1] 分配空间,让字典同时持有 ht[0]和 ht[1]两个哈希表
- 在字典中维持一个索引计数器变量 rehashidx,并将它的值设置为 0,表示 rehash 开始
- 在 rehash 进行期间,每次对字典执行 CRUD 操作时,程序除了执行指定的操作以外,还会将 ht[0]中的数据 rehash 到 ht[1]表中,并且将 rehashidx 加一
- 当 ht[0]中所有数据转移到 ht[1]中时,将 rehashidx 设置成-1,表示 rehash 结束
- 采用渐进式 rehash 的好处在于它采取分而治之的方式,避免了集中式 rehash 带来的庞大计算量
- sdshdr:SDS 对象用于保存字符串和 AOF 持久化时的缓冲区,SDS 对象内存储了字符数组、实际字符大小、数组空闲长度(为什么不用 String:因为 C 语言用 N+1 的字符数组来表示长度为 N 的字符串,这样做在获取字符串长度,字符串扩展等操作方面效率较低,并且无法满足 redis 对字符串在安全性、效率以及功能方面的要求)
- dictht:字典,实现和 JAVA1.7 的 HashMap 类似,都是数组+链表形式
- list:双向链表
- ziplist:经过特殊编码(按照固定顺序和分隔符规则保存数据)的双向链表,ziplist 可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以 O(1)的时间复杂度在表的两端提供 push 和 pop 操作
- skiplist:跳跃表,是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美,详见跳跃表介绍
- intset:有序整数集合,可以进行二分查找
- String:底层使用 SDS 对象实现
- Hash:基于 ziplist+dictht 实现,数据量少且数据都是整数或者短字符串时使用 ziplist 保存数据,否则使用 dictht 保存
- List:基于 ziplist+list 实现,数据量少且数据都是整数或者短字符串时使用 ziplist 保存数据,否则使用 list 保存
- Set:基于 intset+dictht 实现,数据量少且数据都是整数时使用 intset 保存数据,否则使用 dictht 保存
- Sorted Set:基于 skiplist 实现
- Bitmap:使用 byte 数组实现
- HyperLogLogs:基于 HyperLogLog 算法(hash 分桶平均)实现,是 LogLog 算法的升级版,只需要 12K 内存就能统计 2^64 个数据,计数存在一定的误差,误差率整体较低。标准误差为 0.81%,可以设置辅助计算因子降低误差。redis 采用的是 hash 分桶平均方法,先设定 2^14 = 16384 个桶,每个桶有 6 位,占用内存为=16834 * 6 / 8 / 1024 = 12K,在存入时,value 会被 hash 成 64 位 bit 数组,前 14 位用来分桶,前 14 位的二进制转为 10 进制就是桶标号,后面 50 位为伯努利过程,每个桶有 6bit,记录第一次出现 1 的位置 count,如果 count>oldcount,就用 count 替换 oldcount
- Streams:5.0 新增,是一个新的强大的支持多播的可持久化的消息队列,内部有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容,消息会持久化。Stream 实现了类似 Kafka 的多消费组和 ack 机制,可以同时多个消费组消费,且能保证消息不丢失
- GeoHash:基于 Sorted Set 实现的 GeoHash 算法
- 定期删除:指的是 Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除
- 惰性删除:在你获取某个 key 的时候,Redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何值
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错
- allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除
- 当 master 服务器与 slave 服务器正常连接时,master 服务器会发送数据命令流给 slave 服务器,将自身数据的改变复制到 slave 服务器
- 当因为各种原因 master 服务器与 slave 服务器断开后,slave 服务器在重新连上 master 服务器时会尝试重新获取断开后未同步的数据即部分同步,或者称为部分复制
- 如果无法部分同步(比如初次同步),则会请求进行全量同步,这时 master 服务器会将自己的 rdb 文件发送给 slave 服务器进行数据同步,并记录同步期间的其他写入,再发送给 slave 服务器,以达到完全同步的目的,这种方式称为全量复制
- 主从
- master-slave,一主多从,读写分离,主从复制,故障转移主备切换
- 哨兵(Sentinel)
- 集群监控、故障转移、配置中心(pub/sub 模式通信,,用于故障转移时通知 client 客户端新的 master 地址,以及多个哨兵之间的同步)、选举(选举算法:跟 master 断开连接的时长、slave 优先级、主从复制的 offset、run id)
- 分片(Sharding)
- 客户端实现数据分片(一致性 Hash 等算法)
- 集群(Cluster)
- cluster bus
- 集群元数据管理,每个节点多暴露一个端口
- 通过 Gossip 协议进行通信,来进行故障检测、配置更新、故障转移授权
- hash slot 算法
- 2^14=16384 个 slot,使用 crc16 算法计算值,再对 16384 取模,得到对应的 hash slot
- 增减机器时,slot 会自动迁移
- 主从复制、故障转移
- 选举机制:
- slave 发现自己的 master 变为 FAIL,将自己记录的集群 currentEpoch(选举轮次标记)加 1,并广播信息给集群中其他节点
- 其他节点收到该信息,只有 master 响应,判断请求者的合法性,并发送结果
- 尝试选举的 slave 收集 master 返回的结果,收到超过半数 master 的统一后变成新 Master,并广播 Pong 消息通知其他集群节
- 缺点:
- 可能数据倾斜
- 批处理命令(pipeline、multi-keys 等)复杂性增大
- 主从节点的数据同步方式为异步复制,不保证数据的强一致性
- 不支持多数据库空间,单机下的 redis 可以支持到 16 个数据库,集群模式下只能使用 1 个数据库空间,即 db 0
- 可能会有热点 Key 问题(Hot Key)
- cluster bus
- RDB 持久化:周期性保存全量 Redis 数据,如果数据文件特别大,可能导致 Redis 短暂无法提供服务
- AOF 持久化:每隔 1 秒存储 Redis 命令到日志文件,日志文件以 append-only 模式写入
- 多数使用 AOF+RDB 方式来持久化,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,AOF 文件都丢失或损坏时可以使用 RDB 恢复数据
- 缓存雪崩
- 缓存机器宕机,导致全部请求拥往数据库
- 解决方案:Redis 高可用+Redis 持久化+应用前置内存缓存+应用限流
- 缓存穿透
- 大量请求无法命中缓存,导致大量请求拥往数据库
- 解决方案 1:从数据库中没查到数据时,在缓存中设置一个有过期时间的空值
- 解决方案 2:将所有 Key 存储到一个布隆过滤器中,查询数据时,先查询布隆过滤器,判断 Key 是否存在,如果不存在,则表示库中没有数据,因此可以直接返回默认值给请求方,而不再查询数据库
- 缓存击穿
- 热点 key 失效,导大量请求拥往数据库
- 解决方案:设置数据永不过期、使用互斥锁、查询时延长缓存的过期时间
- Lua 脚本保证多个操作的原子性
- Redis 事务(watch、unwatch、multi、exec、discard)
- 使用 set 的 NX 属性实现分布式锁,释放锁时需要使用 luna 脚本判断锁上值的是否是自己设置的值,是的话才能删除锁,否则可能会删除别人的锁
- 该算法存在缺点,主节点如果在 setnx 之后宕机,数据可能还没同步到从节点,导致重复创建分布式锁,其次,如果服务器在获取到分布式锁之后宕机,可能导致锁没有被 release,其他节点需要等到 key 过期后才能获取到新的锁
- 相对而言,不如 Zookeeper 的分布式锁好用
- 因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案
- Redis 复杂的数据结构需要进行很细粒度的操作,多线程的话需要操作各种锁,而在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- “多路”指的是多个网络连接,“复用”指的是复用同一个线程
- redis 非阻塞 IO 内部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)
- Redis 中的有序集合(ZSet)支持插入、删除、查询、全表有序查询、按照范围区间查找元素
- 前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的
- 但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高
mongodb 3.0 默认存储引擎为 MMAPV1,3.2 开始,已将 WiredTiger 作为默认存储引擎
- MongoDB3.2 之前版本的默认引擎
- mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址
- Padding 机制
- 由于文档在磁盘中连续存放,当文档大小增长时,可能需要重新分配文档空间,并更新索引。这会使写入效率降低,因此通常 MongoDB 为文档分配的 record 空间会包括 document 数据和 padding 空间。这样减少了文档移动的可能性,提高了写入效率
- 在 MongoDB3.0 之前,MMAPv1 使用填充因子(padding factor)来决定空间分配,填充因子会根据文档移动的频繁度动态调整(初始时为 1.0),当 padding factor = 1.5 时,MMAPv1 将为文档分配 sizeof(record) = 1.5 * sizeof(document)的空间,其中 0.5*sizeof(document)用作 padding
- padding factor 这种方式看起来很智能,但是由于文档的 record 大小不一,在文档删除或移动之后,文档原来分配的空间很难被再次利用,从而造成了磁盘碎片,这也是 MongoDB3.0 之前数据占用磁盘空间大的主要原因之一
- 在 MongoDB3.0 之后,不再使用 padding factor 填充机制,而使用 Power of 2 Sized Allocations,为每个文档分配 2 的 N 次方的空间(超过 2MB 则变为 2MB 的倍数增长),这样做既可以减少文档的移动,文档被删除或移动后的空间也可以被有序地组织起来,达成复用(只能被其所在 collection 的文档复用)
- 虽然 Power of 2 Sized Allocations 解决了磁盘碎片的问题,但改进后的 MMAPv1 引擎仍然在数据库级别分配文件,数据库中的所有集合和索引都混合存储在数据库文件中,并且删除或移动文档后的空间会被保留用以复用,因此磁盘空间无法无法即时自动回收的问题仍然存在(即使 drop collection)
- 并发能力
- 在 MongoDB3.0 之前,MMAPv1 存储引擎只支持 Database 级的锁,有时候不得不刻意将数据分到多个数据库中提升并发能力。在 MongoDB3.0 之后,MMAPv1 终于支持 collection 级的并发,并发效率提升了一个档次
- 故障恢复
- 支持 journal(变更操作日志,类似 MySQL 的 binlog)持久化
- MongoDB 默认记录所有的变更操作日志(journal)并写入磁盘,MongoDB flush 变更日志的频率(默认 100ms)比 flush 数据的频率(默认 60s)要高,因此 journal 是 MongoDB 故障恢复的重要保障
- 内存占用
- 由于 MMAPv1 使用 mmap 来将数据库文件映射到内存中,MongoDB 总是尽可能的多吃内存,以映射更多的数据文件
- 在 MongoDB3.0 中引入,在 MongoDB3.2 中,已将 WiredTiger 作为默认存储引擎
- 并发能力
- 支持文档级别的并发,WiredTiger 通过 MVCC(多版本并发控制,类似 MySQL)实现文档级别的并发控制,即文档级别锁。这就允许多个客户端请求同时更新一个集合内存的多个文档
- 故障恢复
- 支持 checkpoint 和 journal 两种方式进行持久化
- checkpoint 是数据库某一时刻的快照,每 60s 或超过 2GB 的变更日志执行一次,在写最新快照时,上一个快照仍然有效,防止 MongoDB 在快照落地时挂掉,在快照落地完成后,上一个快照将被删除
- journal 可与 checkpoint 集合使用,提供快速,可靠的数据恢复
- MongoDB3.2 之前的版本中,WiredTiger journal 默认在日志超过 100MB 时持久化 journal 一次,系统宕机最多会丢失 100MB journal 数据。在 3.2 版本中,加入了默认 50ms 时间间隔刷盘条件
- 磁盘占用
- 不同于 MMAPv1 在数据库级别分配数据文件,WiredTiger 将每个 collection 的数据和索引单独存放,并且会即时回收文档和集合占用空间
- WiredTiger 支持日志、文档数据块、索引的压缩,可配置或关闭压缩算法,大幅度节省了磁盘空间
- 内存占用
- WiredTiger 支持内存使用容量配置,用户可通过 WiredTiger CacheSize 配置 MongoDB WiredTiger 所能使用的最大内存,在 3.2 版本中,该参数默认值为 max(60%Ram-1GB, 1GB)。这个内存限制的并不是 MongoDB 所占用的内存,MongoDB 还使用 OS 文件系统缓存(文件可能是被压缩过的)
- 原理
- 每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里快速读出该文档
- mmapv1 引擎里,位置信息是『文件 id + 文件内 offset 』, 在 wiredtiger 存储引擎(一个 KV 存储引擎)里,位置信息是 wiredtiger 在存储文档时生成的一个 key,通过这个 key 能访问到对应的文档
- 建立索引后,MongoDB 会额外存储一份按索引字段升序排序的索引数据,索引数据中存储了字段值和对应文档的位置信息,通常采用类似 B 树的结构持久化存储,以保证从索引里快速(O(logn)的时间复杂度)找出某个索引值对应的位置信息
- MongoDB 默认会为集合创建_id 字段的索引
- 覆盖查询:当查询标准和查询投影 仅包含索引字段,MongoDB 直接从索引返回结果,无需扫描任何文档
- 交叉索引:对于指定复合查询条件的查询,如果一个索引可以满足查询条件的一部分,而另一个索引可以满足查询条件的另一部分,则 MongoDB 可以使用两个索引的交集来满足查询。使用复合索引还是使用索引交集更有效取决于特定查询和系统
- 类型
- 单键索引 (Single Field Index):存储单字段值和对应文档位置信息
- 复合索引 (Compound Index):存储多字段值和对应文档位置信息
- 多 key 索引 (Multikey Index):当索引的字段为数组时,创建出的索引称为多 key 索引,多 key 索引会为数组的每个元素建立一条索引
- 文本索引(Text Index):文本索引会将文本拆分成关键字来保存索引(类似倒排),支持权重和通配符两种类型
- 哈希索引(Hashed Index):按照某个字段的 hash 值来建立索引,目前主要用于 MongoDB Sharded Cluster 的 Hash 分片,hash 索引只能满足字段完全匹配的查询,不能满足范围查询
- 地理位置索引(Geospatial Index):为了支持对地理空间坐标数据的高效查询,MongoDB 提供了两个特殊的索引:返回结果时使用平面几何的 2d 索引和使用球面几何的 2dsphere 索引
- 属性
- 唯一索引:索引字段的值全局唯一
- 部分索引:部分索引仅索引集合中符合指定过滤表达式的文档。通过索引集合中文档的子集,部分索引具有较低的存储需求,并降低了索引创建和维护的性能成本。部分索引提供了稀疏索引功能的超集,应优先于稀疏索引
- 稀疏索引:索引的稀疏属性可确保索引仅包含具有索引字段的文档的条目。索引会跳过没有索引字段的文档
- TTL 索引:可以在指定时间后自动从集合中删除文档使用的特殊索引
- 限制
- 索引名称长度不能超过 128 位
- 复合索引不能超过 32 个属性
- 每个集合 Collection 不能超过 64 个索引
- 不同类型索引还具有各自的限制条件
- Replica set(复制集)
- 通常是三个对等的节点构成一个“复制集”集群,有“primary”和 secondary 等多中角色)
- 其中 primary 负责读写请求,secondary 可以负责读请求,可以自定义配置
- 其中 secondary 紧跟 primary 并应用 write 操作
- 如果 primay 失效,则集群进行“多数派”选举,选举出新的 primary,即 failover 机制,即 HA 架构
- 缺点:
- 集群数据容量”受限于单个节点的磁盘大小,扩容困难
- Sharding cluster(分片集群)
- 将整个 collection 的数据将根据 sharding key 被 sharding 到多个 mongod 节点上
- 每个节点持有 collection 的一部分数据,这个集群持有全部数据,原则上 sharding 可以支撑数 TB 的数据
- 每个索引可以拆分成多个 shard ,每个 shard 存储部分数据
- 每个 shard 都有一个 primary shard ,负责写入数据,但是还有几个 replica shard 。 primary shard 写入数据之后,会将数据同步到其他几个 replica shard 上去
- ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。如果 master 节点宕机了,那么会重新选举一个节点为 master 节点
- 如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常
- 客户端发送请求到一个 coordinate node
- 协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard ,都可以
- query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果(doc id)
- fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端
- 客户端发送请求到任意一个 node,成为 coordinate node
- coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡
- 接收请求的 node 返回 document 给 coordinate node
- coordinate node 返回 document 给客户端
- 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)
- coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)
- 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node 4, coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端
- buffer->os cache->segment file,buffer->translog
- 一定时长或者 translog 达到一定代销后触发 segment file 的批量 flush 操作
- 删除时只做标记
- 修改时先删除再插入
- segment file 定期 merge 成一个 file
- merge 的同时删除带删除标记的 document
- 将数据分词,得到 N 个关键词
- 保存关键词和出现的文档 ID 映射关系,每个关键词都会对应一个或多个文档 ID
- 倒排索引中的词项根据字典顺序升序排列
- filesystem cache 尽量大于等于真实数据量
- ES 中只存作为搜索条件的字段并配合 Hbase 使用
- 定时查询热数据使热数据被其他业务查询时经过 filesystem cache
- 冷热数据分不同索引
- 通过冗余字段来避免 join
- 避免深度分页以及使用 scroll api 或者 search_after 来优化分页查询性能