本文是青训营课程--后端入门--《RDBMS 基本情况介绍》的笔记,讲师为杨洋
1 经典案例
老师举了个例子,在春节红包雨的场景下,用户抢到了5元红包,那简单来讲就是两个操作
- 从抖音账户上扣钱
- 给用户账户上加钱
用SQL表示为
UPDATE account_table SET balance = balance - 5 WHERE name = '抖音';
UPDATE account_table SET balance = balance + 5 WHERE name = '用户名';
1.1 事务的ACID特性
事务是由关系型数据库提出的,是由一组SQL语句组成的一个程序执行单元,需要满足ACID特性。
将刚才提到的SQL语句用begin和commit包裹起来,就成了一个事务。
BEGIN;
UPDATE account_table SET balance = balance - 5 WHERE name = '抖音';
UPDATE account_table SET balance = balance + 5 WHERE name = '用户名';
COMMIT;
ACID特性:
- 原子性(Atomicity): 事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency): 数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
- 隔离性(Isolation):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果.
- 持久性(Durability):在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚.
除了这些八股以外,老师贴心的给出了例子以便深入理解ACID。
原子性
抖音账户扣完钱之后,服务器忽然挂了,用户账户的钱没加上。 也就是说,这两个操作被分割开了,有的执行了,有的没有执行。这就是不遵循原子性的例子。 原子性能够捆绑销售,所有的操作要么一个都不做,要么全部都得做完。
一致性
抖音的账户只有1块,但是扣减5块的操作却成功了,直接把余额口成了负数。 这就破坏了一致性,一致性要保证每个操作都必须是合法的,数据库应该从一个有效状态变为另一个有效的状态。
隔离性
用户同时抢到两个5元红包,这两个增加余额的操作同时到达,他们都在0的基础上加了5,结果最终用户的账户里只多了一个红包的钱。 这就是没处理好操作的并发执行,破坏了隔离性。 隔离性保证并行操作之间互不影响,表现的好像串行操作。
持久性
余额增加的操作还没更改到硬盘,服务器突然挂了,但是返回了执行成功的结果。 持久性保证更新成功后,结果应该用就行的保留下来,不会因为宕机而丢失。
1.2 好的数据库应该有的特点
高并发Concurrency
假设10亿人同时抢红包,每秒处理一个请求,那需要31年才能完成。
高可靠High Availability
保证稳定可用,避免宕机
2 发展历史
2.1 数据库发展历史
- 无计算机时代:还没有电脑,手写
- 前DBMS时代:文件系统,可以理解为打开记事本人工敲键盘记录
- DBMS时代:按照数据模型分为三类
- 网状模型:虽然能直接描述现实世界,但是结构复杂,用户不宜使用
- 层次模型:树状结构,同样,访问程序设计复杂,访问子结点必须访问父结点
- 关系模型:使用二维表描述数据。
2.2 SQL发展历史
使用类似Java这种编程语言时,我们需要细致的实现每一步要怎么做。这叫特点做过程化语言 而SQL是结构化查询语言,需要告诉他要做什么,但是不关心过程。
3 关键技术
- SQL引擎
- 事务引擎
- 存储引擎
3.1 一条SQL的一生
- SQL通过Request路由到服务器,进入等待队列,等到线程池中有Idle worker来处理请求
- SQL解析器生成AST(抽象语法树),然后经过优化器成为执行计划plan交给执行器。
- 执行器负责读写数据并产生日志。
3.2 SQL引擎
Parser
Parser能够进行词法分析、语法分析、语义分析。
多说无益,看老师的图。
能够看到,Parser阶段就能检测出明显的错误了。
Optimizer
RBO(基于规则的优化器),比如:
- 条件化简
- 表连接优化,总是小表优先连接
- Scan优化
CBO(基于代价的优化器),单个查询的代价是查询时间,总体的代价比如硬件资源利用率、吞吐量等
Executor
火山模型
执行器用的最多的是火山模型,如下图
每个Operator(算子)逐层调用Next访问下层Operator 每层的Operator计算完成后将这行数据返回给上层。 优点:
- 每个算子独立抽象实现,互相不耦合,逻辑结构简单
- 每计算一条数据又多次函数调用开销,导致CPU效率不高
向量化模型
和火山模型的差别是,算子返回的不再是Row,而是一批数据Batch。 优点:
- 函数调用次数降为
- CPU cache命中率更高
- 可以利用CPU提供的SIMD(Single Instruction Multi Data)机制
编译模型
动态编译执行技术LLVM能够将用户用到的算子打包成一个执行函数,这样就没有调用的问题。
3.3 存储引擎InnoDB
内存态
做数据缓存
Buffer Pool
以128M为单位向系统申请内存(一个Chunk)
每个Page都是16K的大小,每个Chunk128M。
将Buffer Pool划分为一个个Instance,降低Page访问冲突。
Buffer Pool在实现时需要两个重要的结构
- 哈希表
HashMap<page_id,block*>,能够通过page_id算出block在哪个哈希桶里面。 - LRU,MySQL通过优化版的LRU释放内存空间
硬盘态
系统表空间:存储元数据(比如表名、列名、权限等) 通用表空间:存储一般的表 Undo表:存undo事务日志 Redo日志:存redo事务日志。 一些临时表
Page
在MySQL中,表格是由若干个Page组成的。每个Page是一个二进制文件,其中包含了一定数量的记录。每个记录都有一个唯一的索引值,用于在表格中进行定位。Page的大小可以通过设置参数来调整,一般情况下,每个Page的大小为1MB左右。 当我们执行查询操作时,MySQL会根据查询条件来确定需要查询的Page,并将这些Page加载到内存中,然后对这些Page中的记录进行筛选,最终返回符合条件的记录。 在MySQL中,表格的数据存储是按照B+树的结构来组织的。每个节点(即Page)都是一个B+树的节点,包含了一定数量的子节点。每个子节点可以包含多个记录,而每个记录都有一个唯一的索引值。通过B+树的结构,MySQL可以快速地定位到需要查询的Page,并从中提取出需要的记录。
B+树
页面内:页目录内使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定记录。
从根到叶:中间节点存储。
3.4 事务引擎
Undo Log与原子性
MySQL中事务的回滚是通过undo日志实现的,Undo Log是逻辑日志,记录的是数据的增量变化。利用Undo Log可以进行事务回滚,从而保证事务的原子性。同时也实现了多版本并发控制(MVCC)解决读写冲突和一致性读的问题。
MVCC与隔离性
考虑三种场景:
- 多名读者,此时使用多个共享锁
- 多名写者,此时使用1个排他锁
- 有读者有写者,这时该怎么办呢?
MVCC就是数据的多版本。 新版本在page里,老版本在undo segment里,使用roll pointer组成链表,这样有读写冲突时,读者就能先读老版本数据了。 MVCC的意义:
- 读写互不阻塞
- 降低死锁概率
- 实现一致性读
Redo Log和持久性
- 如果在事务提交前写盘,会存在两个问题:
- 随机IO:数据在磁盘上是随机分布的,对于最新的SSD、NVMe这种磁盘的随机访问能力还可以,但是对于HDD这种磁盘来讲随机访问的能力就很差
- 写放大:写数据要修改Page(16K),可能本身只修改了几个字节,但是现在要更新16K这么大的数据。
- WAL(Write-ahead logging):redo log是物理日志,记录页面变化,作用是保持事务持久化。如果数据写入磁盘前发生故障,重启MySQL后会根据redo log重做。
4 企业实践案例
4.1 大流量问题
最常使用Sharding解决大流量问题。 Sharding能够分库、分表。 单节点读写能力(qps每秒查询数)、容量都有上限。 Sharding能够对业务数据进行水平拆分(比如通过对主键进行哈希,或者简单根据奇偶进行分流) 用户的数据请求发送给代理层,代理层再决定将请求发送给哪个结点,整个数据库集群对外表现仍为一个数据库
4.2 流量突增
扩容
- 问题背景:活动流量上涨,集群性能不能满足要求
- 解决方案:扩容DB物理节点数量,利用影子表进行压测
如何实现扩容(传统方法,云原生架构下已经采用更快的方法):
- 首先用将老服务器数据copy到新服务器
- 使用binlog实时同步修改,保证二者始终一致。
- 代理层更改路由逻辑,将流量分配到不同服务器上
- 删除每个结点的冗余数据
代理连接池
用户请求变多后,连接数也会变多,代理侧会使用连接缓存。 平时会缓存一些链接,当有突发流量后,可以直接使用缓存的连接而不用新建连接。 代理连接池可以有效避免DB被徒增流量打死,避免代理和DB被大量建联打死。
4.3 稳定性&可靠性
3AZ高可用
- 字节采用3AZ高可用,在不同的3个城市部署。
- 使用代理,实现读写分流,分库分表,流量调度
- 监控报警
HA管理
某个db宕机,能够快速被检测并有应对措施。