深入理解RDBMS(下)| 青训营笔记

110 阅读13分钟

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

一、本堂课重点知识

  1. 关键技术
  2. 企业实践

二、详细知识点介绍

3. 关键技术

3.1 一条SQL的一生

UPDATE account_table SET balance = balance - '小目标' WHERE name = '抖音';
  • SQL引擎
  • 事务引擎
  • 存储引擎

image.png

  • 查询解析
    • SQL语言接近自然语言,入门容易。但是各种关键字、操作符组合起来,可以表达丰富的语意。因此想要处理SQL命令,首先将文本解析成结构化数据,也就是抽象语法树(AST)
  • 查询优化
    • SQL是一门表意的语言,只能说“要做什么”,而不说“怎么做”。所以需要一些复杂的逻辑选择“如何拿数据”,也就是选择一个好的查询计划。优化器的作用根据AST优化产生最优执行计划(Plan Tree)
  • 查询执行
    • 根据查询计划,完成数据读取、处理、写入等操作
  • 事务引擎
    • 事务处理一致性、并发、读写隔离等
  • 存储引擎
    • 内存中的数据缓存区、数据文件、日志文件

3.2 SQL引擎

3.2.1 Parser

解析器(Parser)一般分为词法解析(Lexical analysis)、语法分析(Syntax analysis)、语义分析(Semantic analysis)等步骤

image.png

所有代码在执行之前,都存在一个解析编译过程,差异点无非在于是静态解析编译还是动态的。

SQL语言也类似,在 SQL 查询执行前的第一步就是查询解析。

  • 词法分析:将一条SQL语句对应的字符串分割为一个个token,这些token可以简单分类。
  • 语法分析:把此法分析的结果转为语法树。根据token序列匹配不同的语法规则,比如这里匹配的是update语法规则,类似的还有insert、delete、select、drop等等语法规则。根据语法规则匹配SQL语句中的关键字,最终输出一个结构化的数据结构。
  • 语义分析:对语法树中的信息进行合法性校验。

3.2.2 Optimizer

为什么需要一个优化器(Optimizer)?

SELECT * FROM A, B, C  WHERE A.a1 = B.b1 and A.a1 = C.b1;

image.png

  1. 基于规则的优化(RBO Rule Base Optimizer)
  • 条件简化

a = 5 and b > a ==> a = 5 and b > 5

a > 5 and a < b and b = 1 ==>FALSE

  • 表连接优化
    • 总是小表先进行连接
SELECT * FROM A,B,C  WHERE A.a1 = B.b1 and A.a1 = C.b1;
  • Scan 优化
    • 唯一索引
    • 普通索引
    • 全表扫描

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

  1. 基于代价的优化(CBO Cost Base Optimizer)

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

什么是代价?

image.png

到达一个目的地,有不同的路线,选择不同的路线有不同的代价。这里的代价可能是时间,也可能是路程。比如我们赶时间的时候,就会选择时间最短的。如果时间没那么赶,那么我们可能选择路程最短的。

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

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

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

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

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

3.2.3 Executor

火山模型:

image.png

Plan Tree 为基础,调用关系是由根到叶,数据流是从叶到根

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

优点:

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

缺点:

每计算一条数据有多次函数调用开销,导致CPU效率不高。

  • 向量化

image.png

每个Operator每次操作计算的不再是一行数据,而是一批数据(Batch N行数据),计算完成后向上层算子返回一个Batch

优点:

函数调用次数降低为1/N;

CPU cache命中率更高;

可以利用CPU提供的SIMD(Single Instruction Multi Data)机制

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

  • 编译执行

image.png

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

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

image.png

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

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

3.3 存储引擎

3.3.1 InnoDB

image.png

In-Memory:

  • Buffer Pool
  • Change Buffer
  • Adaptive Hash Index
  • Log Buffer

On-Disk:

  • System Tablespace(ibdata1)
  • General Tablespaces(xxx.ibd)
  • Undo Tabkespaces(xxx.ibu)
  • Temporary Tablespaces(xxx.ibt)
  • Redo Log(ib_logfileN)

3.3.2 Buffer Poll

image.png

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秒之内被访问,则不会移动,缓存页仍然处于冷数据区中。

淘汰时,首先淘汰冷数据区。

3.3.3 Page

image.png

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

3.3.4 B+ Tree

image.png

  • 页面内:

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

    • 中间节点存储
  • 点查:

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

3.4 事务引擎

前情提要:如何帮助羊老师从抖音薅一个亿的羊毛?

image.png

3.4.1 Atomicity 与 Undo Log

BEGIN;
UPDATE account_table SET balance = balance - '小目标' WHERE name = '抖音';
Server crush
UPDATE account_table SET balance = balance + '小目标' WHERE name = '杨洋';
COMMIT;

如何将数据库回退到修改之前的状态

Undo Log

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

SQL:

Insert into users(id, name, phone_num) value(100, jim, 6255123);

Undo:

Delete from users where id = 100;

SQL:

Update users set phone_num = 6789123 where id = 100;

Undo:

Update users set phone_num = 6255123 where id = 100;

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

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

3.4.2 Isolation

3.4.2.1 Isolation 与 锁

前情提要:羊老师从抖音抢了一个亿红包,又从头条抢了一个亿。抖音和头条都要往羊老师的账户转一个亿,如果两个操作同时进行,发生冲突怎么办?

image.png

Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,如果多个并发事务访问同一行记录,就需要锁机制来保证了。

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

3.4.2.2 Isolation 与 MVCC
  • MVCC的意义:
    • 读写互不阻塞
    • 降低死锁概率
    • 实现一致性读
  • Undo Log 在 MVCC 的作用:
    • 每个事务有一个单增的事务ID
    • 数据页的行记录中包含了DB_ROW_ID,DB_TRX_ID,DB_ROLL_PTR
    • DB_ROLL_PTR将数据行的所有快照记录都通过链表的结构串联了起来

image.png

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

万一抖音给我的账户转账的事务还没完成,羊老师就查到了账户上有一个亿,后来抖音发现不对,把这个事务回滚掉了。过一会儿羊老师发现自己账户的一个亿又没了,去银行要个说法,结果被保安赶了出来。

3.4.3 Durability 与 Redo Log

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

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

方案二:WAL(Write-ahead logging)

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

image.png

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

WAL:修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。

优点:

只记录增量变化,没有写放大

Append only,没有随机IO

4. 企业实践

4.1 春节红包雨挑战

  • 活动统计
    • 共计发放红包20亿元
    • 总计发放卡券24亿张
    • 百年红包补贴12.9亿
  • 流量统计
    • 活动钱包:400w/s
    • 发红包:300w/s
    • 发奖券:400w/s

image.png

4.2 大流量

  • 问题背景
    • 单节点写容易成为瓶颈
    • 单机数据容量上限
  • 解决方案
    • 业务数据进行水平拆分
    • 代理层进行分片路由
  • 实施效果
    • 数据库写入性能线性扩展
    • 数据库容量线性扩展

image.png

当数据库中的数据量越来越大时,不论是读还是写,压力都会变得越来越大。虽然上面的方案可以扩展读节点,但是对于写流量增加,以及数据量的增加却没有办法

4.3 流量突增

4.3.1 扩容

  • 问题背景
    • 活动流量上涨
    • 集群性能不满足要求
  • 解决方案
    • 扩容DB物理节点数量
    • 利用影子表进行压测
  • 实施效果
    • 数据库集群提供更高的吞吐
    • 保证集群可以承担预期流量

image.png

4.3.2 代理连接池

image.png

  • 问题背景
    • 突增流量导致大量建联
    • 大量建联导致负载变大,延时上升
  • 解决方案
    • 业务侧预热连接池
    • 代理侧预热连接池
    • 代理侧支持连接队列
  • 实施效果
    • 避免DB被突增流量打死
    • 避免代理和DB被大量建联打死

4.4 稳定性&可靠性

image.png

为什么要高可用:

恶意事故:程序员删库跑路?哪个程序员不想执行一把rm-rf*?

偶然事故:如果一个机房断电?断网?

某施工队,施工的时候挖掘机把某游戏公司的光纤挖断了,一下午的时间,保守估计损失一个亿。

4.4.1 3AZ高可用

image.png

  • 三机房部署
    • 机房级别容灾
    • 机房级别流量调度
  • proxy
    • 读写分离,分库分表
    • 限流,流量调度
  • 监控报警
    • 实时监控集群运行状态
    • 提前上报集群风险
  • HA
    • High Availability
    • 实时监控DB运行状态
    • 宕机快速切换 BinLog:binlog是mysql用来记录数据库表结构变更以及数据修改的二进制日志,它只会记录表的变更操作,但不会记录select和show这种查询操作。

数据恢复:误删数据之后可以通过mysqlbinlog工具恢复数据

主从复制:主库将binlog传给从库,从库接收到之后读取内容写入从库,实现主库和从库数据一致性

审计:可以通过二进制日志中的信息进行审计,判断是否对数据库进行注入攻击

4.4.2 HA管理

image.png

  • 问题背景
    • db 所在机器异常宕机
    • db 节点异常宕机
  • 解决方案
    • ha 服务监管、切换宕机节点
    • 代理支持配置热加载
    • 代理自动屏蔽宕机读节点
  • 实施效果
    • 读节点宕机秒级恢复
    • 写节点宕机 30s 内恢复服务

三、总结

image.png

四、参考文献

字节跳动内部课