再识【关系型数据库 RDBMS】 | 青训营笔记

41 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天

RDBMS :关系型数据库

课程回顾

  • 存储系统

    • 块存储、文件存储、对象存储、key-value 存储
  • 数据库系统

    • 关系型数据库、非关系型数据库
  • 分布式架构

    • 数据分布策略、数据复制协议、分布式事务算法

1 经典案例

事务 ACID

  • 事务(Transaction)

    • 是由一组 SQL 语句组成的一个程序执行单元(Unit),它需要满足 ACID 特性。
  • ACID

    • 原子性(Atomicity): 事务是一个不可再分的工作单元,事务中的操作要么都发生,要么都不发生。

    • 一致性(Consistengy): 数据库事务不能坏关系数据的完整性以及业务逻辑上的一致性。

    • 隔离性(Isolation): 多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。

    • 持久性(Durability): 在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。

case:红包雨

2 发展历史

2.1 前 DBMS 时代

人工管理--文件系统--DBMS 时代

人工管理:人工进行数据记录和管理

2.2 DBMS 数据模型

  • 网状模型

image.png

  • 层次模型

image.png

  • 关系模型

image.png

  • 三者优缺点比较
标题网状模型层次模型关系模型
优势能直接描述现实世界;存取效率较高结构简单;查询效率高;可以提供较好的完整性支持实体及实体间的联系都通过二维表结构表示;可以方便的表示 M:N 关系;数据访问路径对用户透明
劣势结构复杂;用户不易使用;访问程序设计复杂无法表示M:N的关系;插入、删除限制多遍历子节点必须经过父节点;访问程序设计复杂关联查询效率不够高;关系必须规范化

2.3 SQL 语言

SQL (structured Query Language)语言

  • 语法风格接近自然语言;
  • 高度非过程化;
  • 面向集合的操作方式;
  • 语言简洁,易学易用。

2.4 历史回顾

image.png

3 关键技术

万变归宗:无论 RDBMS 如何演变,其核心都是 SQL 引擎、存储引擎、事务引擎。

image.png

3.1 一条 SQL 的一生

查询解析: SQL 语言接近自然语言,入门容易。但是各种关键字、操作符组合起来,可以表达丰富的语意。因此想要处理 SQL 命令,首先将文本解析成结构化数据,也就是抽象语法树 (AST)。

  • 查询优化

    • SOL 是一门表意的语言,只是说【要做什么】,而不说【怎么做】。所以需要一些复杂的逻辑选择【如何拿数据】,也就是选择一个好的查询计划。优化器的作用根据 AST 优化产生最优执行计划(Plan Tree)。
  • 查询执行

    • 根据查询计划,完成数据读取、处理、写入等操作。
  • 事务引警

    • 处理事务一致性、并发、读写隔离等
  • 存储引警

    • 内存中的数据缓存区、数据文件、日志文件

3.2 SQL 引擎

Parser

image.png

Optimizer

  • 基于规则的优化(RBO Rule Base Optimizer)

  • 表连接优化

    • 总是小表先进行连接
  • Scan 优化

    • 唯一索引
    • 普通索引
    • 全表扫描
  • 基于代价的优化(CBO Cost Base Optimizer)

一个查询有多种执行方案, CBO 会选择其中代价最低的方案去真正的执行。

什么是代价?

到达一个目的地,有不同的路线,选择不同的路线有不同的代价。

这里的代价可能是时间,也可能是路程。比如我们赶时间的时候,就会选择时间最短的。如果时间没那么赶,那么我们可能选择路程最短的。因为这样省油啊。

对于数据库也是这样,一个查询有不同的执行方案。

那对于数据库而言,什么是一条 SQL 执行的代价呢?

其实,对于用户只能感知到查询时间这个代价,底层用了多少资源他是不在乎的。但是在并发的情况下,就得考虑资源消耗了,这个用户的查询占用的资源多了,其他用户的资源就少了。所以资源也是必须考虑的一点。

对于 InnoDB 存储引警来说,全表扫描的意思就是把聚簇索引中的记录都依次和给定的搜索条件做一下比较,把符合搜索条件的记录加入到结果集,所以需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。

对于使用二级索引 + 回表方式的查询,设计 MySOL 的大叔计算这种查询的成本依赖两个方面的数据:范围区间数量,需要回表数据量

  • 数据库索引

    • 是数据库管理系统中辅助数据结构,以协助快速查询、更新数据库表中数据。目前数据库中最常用的索引是通过 B+ 树实现的。

Executor

每个 Operator 调用 Next 操作,访问下层 Operator,获得下层 Operator 返回的一行数据,经过计算之后,将这行数据返回给上层。

  • 优点

    • 每个算子独立抽象实现,相互之间没有耦合,逻辑结构简单
  • 缺点

    • 每计算一条数据有多次函数调用开销,导致 CPU 效率不高
  • 向量化 每个 Operator 每次操作计算的不再是一行数据,而是一批数据(Batch N 行数据),计算完成后向上层算子返回一个 Batch。

    • 优点:

            - 函数调用次数降低为 1/N;
            -  CPU cache命中率更高;
            - 可以利用 CPU 提供的 SIMD(Single Instruction Multi Data) 机制
            
      

向量化执行更适合于大批量数据处理,对于很多单行数据处理并没有优势。而且往往搭配列式存储使用。

  • 编译执行

将所有的操作封装到一个函数里面,函数调用的代价也能大幅度降低.

代码生成之后数据库运行时仍然是一个 for 循环,只不过这个循环内部的代码从简单的一个虚函数调用 plan.next() 展开成了一系列具体的运算逻辑,这样数据就不用再各个 operator 之间进行传递,而且有些数据还可以直接被存放在寄存器中,进一步提升系统性能。整个操作有点像 inline 函数,把所有的操作 inline 到一个函数中去。

  • 用户 SQL 千变万化怎么办? 难道要穷举用户的所有 SQL,给每一个 SQL 都预先写好一个执行函数吗?

LLVM 动态编译执行技术,根据优化器产生的计划,动态的生成执行代码。

3.3 存储引擎

InnoDB

Buffer Pool

MySQL 中每个 chunk 的大小一般为 128M,每 block 对应一个 page,一个 chunk 下面有 8192 个 block,这样可以避免内存碎片化。

分成多个 instance,可以有效避免并发冲实。

Page id % instance num 得到它属于哪个 instance

image.png

  • 当 buffer pool 里的页面都被使用之后,再需要换存其他页面怎么办?

淘汰已有的页面

  • 基于什么规则淘汰:

淘汰那个最近一段时间最少被访问过的缓存页,这种思想就是典型的 LRU 算法了。

  • 普通的 LRU 算法存在缺陷:
    • 考虑我们需要扫描 100GB 的表,而我们的 buffer pool 只有 1GB,这样就会因为全表扫描的数据量大,需要淘汰的缓存页多,导致在淘汰的过程中,极有可能将需要频繁使用到的缓存页给淘汰了,而放进来的新数据却是使用频率很低的数据。

MySQL 确实没有直接使用 LRU 算法,而是在 LRU 算法上进行了优化。

  • MySQL 的优化思路就是:

    • 对数据进行冷热分离,将 LRU 链表分成两部分,一部分用来存放冷数据,也就是刚从磁盘读进来的数据,另一部分用来存放热点数据,也就是经常被访问到数据。
    • 当从磁盘读取数据页后,会先将数据页存放到 LRU 链表冷数据区的头部,如果这些缓存页在 1 秒之后被访问,那么就将缓存页移动到热数据区的头部;如果是1 秒之内被访问,则不会移动,缓存页仍然处于冷数据区中。
    • 淘汰时,首先淘汰冷数据区。

Page

User Records 在页面上实际是无序的,通过一个单向链表连接

image.png

B+ Tree

image.png

  • 页面内:

    • 页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
  • 从根到叶:

    • 中间节点存储
  • 点查:

    • Select * from table wehre id = 2000;
  • 范围查询:

    • Select * from table wehre id > 2000;

3.4 事务引擎

ACID 与事务引擎

1. Atomicity 与 Undo Log

原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback) 到事务开始前的状态,就像这个事务从来没有执行过一样。

需要记录数据修改前的状态,一边在事务失败时进行回滚。

Undo Log 是逻辑日志,记录的是数据的增量变化。利用 Undo Log 可以进行事务回滚,从而保证事务的原子性。同时也实现了多版本并发控制 (MVCC) 解决读写冲突和一致性读的问题。

  1. Isolation 与锁

Isolation (隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

如果多个并发事务访问同一行记录,就需要锁机制来保证了。

读写是否冲突? 读写互不阻塞,MVCC 机制

  1. Isolation 与 MVCC

MVCC 的意义

- 读写互不阻塞;
- 降低死锁概率;
- 实现一致性读。

Undo Log 在 MVCC 的作用:

  • 每个事务有一个单增的事务ID;
  • 数据页的行记录中包含了 DB_ROW_ID,DB_TRX_ID,DB_ROLL_PTR;
  • DB_ROLL_PTR将数据行的所有快照记录都通过链表的结构串联了起来.

脏读:事务还没提交之前,它对数据做的修改,不应该被其他人看到。

  1. Durability 与 Redo Log

持久化: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

如何保证事务结束后,对数据的修改永久的保存?

  • 方案一:事务提交前页面写盘。

缺点:随机 IO、写放大。

  • 方案二:WAL(Write-ahead logging)

WAL:修改并不直接写入到数据库文件中,而是写入到另外一个称为 WAL 的文件中;

redo log 是物理日志,记录的是页面的变化,它的作用是保证事务持久化。如果数据写入磁盘前发生故障,重启MySQL后会根据 redo log 重做。

如果事务失败,WAL 中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。

优点:

  - 只记录增量变化,没有写放大
  - Append only,没有随机 IO

扩展阅读: