- 这是我参与「第五届青训营 」伴学笔记创作活动的第 16 天
RDBMS 事务 ACID
事务(Transaction):是由一组SQL语句组成的一个程序执行单元(Unit),它需要满足ACID特性
- 原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency) 事务前后数据的完整性必须保持一致。
- 隔离性(Isolation) 事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
高并发、高可靠
1. 发展历史
1.1 前DBMS时代
人工管理:
- 在现代计算机发明出来之前,通过人工的方式进行数据记录和管理,如结绳记事,账本,打孔机
- 效率太低
文件系统:
- 数据存储进入磁盘时代。数据管理直接通过文件系统来实现
1.2 DBMS时代
传统的文件系统难以应对数据增长的挑战,也无法满足多用户共享数据和快速检索数据的需求。
层次型、网状型和关系型数据库划分的原则是数据之间的联系方式。层次数据库按记录来存取数据;网状数据库采用网状原理和方法来存储数据;关系型数据库以行和列的形式存储数据。
网状模型:
- 网状数据模型是以记录类型为结点的网络结构,即一个结点可以有一个或多个下级结点,也可以有一个或多个上级结点,两个结点之间甚至可以有多种联系,例如“教师”与“课程”两个记录类型,可以有“任课”和“辅导”两种联系,称之为复合链
- 两个记录类型之间的值可以是多对多的联系,例如一门课程被多个学生修读,一个学生选修多门课程
层次模型:
- 层次数据库就是树结构。每棵树都有且仅有一个根节点,其余的节点都是非根节点。每个节点表示一个记录类型对应与实体的概念,记录类型的各个字段对应实体的各个属性。各个记录类型及其字段都必须记录
关系模型:
- 使用表格表示实体和实体之间关系的数据模型称之为关系数据模型
- 关系数据模型中,无论是是实体、还是实体之间的联系都是被映射成统一的关系——一张二维表,在关系模型中,操作的对象和结果都是一张二维表,它由行和列组成
- 关系型数据库可用于表示实体之间的多对多的关系,只是此时要借助第三个关系—表,来实现多对多的关系
| 网状模型 | 层次模型 | 关系模型 | |
|---|---|---|---|
| 优势 | - 能直接描述现实世界 - 存取效率高 | - 结构简单 - 查询效率高 - 可以提供较好的完整性支持 | - 实体及实体间的联系都通过二维表结构表示 - 可以方便的表示M:N关系 - 数据访问路径对用户透明 |
| 劣势 | - 结构复杂 - 用户不易使用 - 访问程序设计复杂 | - 无法表示M:N的关系 - 插入、删除限制多 - 遍历子节点必须经过父节点 - 访问程序设计复杂 | - 关联查询效率不够高 - 关系必须规范化 |
1.3 SQL语言
- 语法风格接近自然语言
- 高度非过程化 非关系数据模型的数据操纵语言是面向过程的语言,用其完成用户请求时,必须指定存取路径。而用SQL进行数据操作,用户只需提出“做什么”,而不必指明“怎么做”,因此用户无须了解存取路径,存取路径的选择以及SQL语句的操作过程由系统自动完成。这不仅大大减轻了用户负担,而且有利于提高数据独立性。
- 面向集合的操作方式 SQL采用集合操作方式,不仅查找结果可以是元组的集合,而且一次插入、删除、更新操作的对象也可以是元组的集合。
- 语言简洁,易学易用 SQL功能极强,但由于设计巧妙,语言十分简洁,完成数据定义、数据操纵、数据控制的核心功能只用了9个动词:CREATE、ALTER、DROP、SELECT、INSERT、UPDATE、DELETE、GRANT、REVOKE。且SQL语言语法简单,接近英语口语,因此容易学习,也容易使用。
2. 关键技术
- 查询解析:SQL语言接近自然语言,入门容易。但是各种关键字、操作符组合起来,可以表达丰富的语意。因此想要处理SQL命令,首先将文本解析成结构化数据,也就是抽象语法树(AST)
- 查询优化:SQL是一门表意的语言,只是说要做什么,而不说怎么做。所以需要一些复杂的逻辑选择如何拿数据,也就是选择一个好的查询计划。优化器的作用根据AST优化产生最优执行计划(Plan Tree)
- 查询执行:根据查询计划,完成数据读取、处理、写入等操作
- 事务引擎:处理事务一致性、并发、读写隔离等
- 存储引擎:内存中的数据缓存区、数据文件、日志文件
2.1 SQL引擎
解析器:
- 解析器(Parser)一般分为词法分析(Lexical analysis)、语法分析(Syntax analysis)、语义分析(Semantic analyzer)
- 词法分析:将一条SQL语句对应的字符串分割为一个个token,这些token可以简单分类
- 语法分析:把词法分析的结果转为语法树。根据tocken序列匹配不同的语法规则,比如这里匹配的是update语法规则,类似的还有insert、delete、select、create、drop等等语法规则。根据语法规则匹配SQL语句中的关键字,最终输出一个结构化的数据结构
- 语义分析:对语法树中的信息进行合法性校验
优化器:
基于规则的优化(RBO Rule Base Optimizer)
-
条件化简
-
表连接优化
- 总是小表先进行连接
-
Scan优化
- 唯一索引
- 普通索引
- 全表扫描
数据库索引:
- 是数据库管理系统中辅助数据结构,以协助快速查询、更新数据库表中数据。目前数据库中最常用的索引是通过B+树实现的
基于代价的优化(CBO cost Base Optimizer)
- 一个查询有多种执行方案,CBO会选择其中代价最低的方案去真正的执行
- 对于InnoDB存储引擎来说,全表扫描的意思就是把聚簇索引中的记录都依次和给定的搜索条件做一下比较,把符合搜索条件的记录加入到结果集,所以需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。
- 对于使用二级索引 + 回表方式的查询,设计MySQL的大叔计算这种查询的成本依赖两个方面的数据:范围区间数量,需要回表数据量
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到一个函数中去 - LLVM动态编译执行技术,根据优化器产生的计划,动态的生成执行代码
2.2 存储引擎
InnoDB:
-
In-Memory:
- Buffer Pool
- Change Buffer
- Adaptive Hash Index
- Log Buffer
-
On-Disk:
- System Tablespace (ibdata1)
- General Tablespaces (xxx.ibd)
- Undo Tablespaces (xxx.ibu)
- Temporary Tablespaces (xxx.ibt)
- Redo Log (ib_logfileN)
Buffer Pool:
- MySQL中每个chunk的大小一般为128M,每个block对应一个page,一个chunk下面有8192个block。这样可以避免内存碎片化。
- 分成多个instance,可以有效避免并发冲突。
当buffer pool里的页面都被使用之后,再需要换存其他页面怎么办:淘汰已有的页面
- 基于什么规则:淘汰最近一段时间最少被访问过的缓存页,这种思想就是典型的LRU算法
- 普通的LRU算法存在缺陷,考虑扫描100GB的表,而buffer pool只有1GB,这样就会因为全表扫描的数据量大,需要淘汰的缓存页多,导致在淘汰的过程中,极有可能将需要频繁使用到的缓存页给淘汰了,而放进来的新数据却是使用频率很低的数据。
- MySQL确实没有直接使用LRU算法,而是在LRU算法上进行了优化,MySQL的优化思路就是:对数据进行冷热分离,将LRU链表分成两部分,一部分用来存放冷数据,也就是刚从磁盘读进来的数据,另一部分用来存放热点数据,也就是经常被访问到数据
- 当从磁盘读取数据页后,会先将数据页存放到LRU链表冷数据区的头部,如果这些缓存页在1秒之后被访问,那么就将缓存页移动到热数据区的头部;如果是1秒之内被访问,则不会移动,缓存页仍然处于冷数据区中
- 淘汰时,首先淘汰冷数据区
Page:
- delete_mask:标识此条数据是否被删除
- next_record:下一条数据的位置
- record_type:表示当前记录的类型
- User Records 在页面上实际是无序的,通过一个单向链表连接
B+ Tree:
页面内:
- 页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
从根到页:
- 中间节点存储
2.3 事务引擎
Atomicity与Undo Log:
如何将数据库退到修改之前的状态?
- Undo Log是逻辑日志,记录的是数据的增量变化。利用Undo Log可以进行事务回滚,从而保证事务的原子性。同时也实现了多版本并发控制(MVCC),解决读写冲突和一致性读的问题
Isolation与MVCC:
MVCC的意义:
- 读写互不阻塞
- 降低死锁的概率
- 实现一致性读
Undo Log在MVCC的作用:
- 每个事务有一个单增的事务ID
- 数据页的行记录包含了DB_ROW_ID,DB_TRX_ID,DB_ROLL_PTR
- DB_ROLL_PTR将数据行的所有快照记录通过链表的结构串联了起来
Durability与Redo Log:
如何保证事务结束后,对数据的修改永久的保存?
-
事务提交前页面写盘
- 随机IO,写放大
-
WAL(Write-ahead logging)
- 修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改
- 优点: 只记录增量变化,没有写放大 Append only,没有随机IO
3. 企业实践
3.1 大流量-Sharding
问题背景:
- 单节点写容易成为瓶颈
- 单机数据容量上限
解决方案:
- 业务数据进行水平拆分
- 代理层进行分片路由
实施效果:
- 数据库写入性能线性扩展
- 数据库容量线性扩展
3.2 流量突增
扩容:
问题背景:
- 活动流量上涨
- 集群性能不满足要求
解决方案:
- 扩容DB物理节点数量
- 利用影子表进行压测
实施效果:
- 数据库集群提供更高的吞吐
- 保证集群可以承担预期流量
代理连接池:
问题背景:
- 突增流量导致大量建联
- 大量建联导致负载变大,延时上升
解决方案:
- 业务侧预热连接池
- 代理侧预热连接池
- 代理侧支持连接队列
实施效果:
- 避免DB被突增流量打死
- 避免代理和DB被大量建联打死
3.3 稳定性和可靠性
3AZ高可用:
-
三机房部署:
- 机房级别容灾
- 机房级别流量调度
-
proxy:
- 读写分离,分库分表
- 限流,流量调度
-
监控报警:
- 实时监控集群运行状态
- 提前上报集群风险
-
HA:
- High Availability
- 实时监控DB运行状态
- 宕机快速切换
-
BinLog: binlog是mysql用来记录数据库表结构变更以及表数据修改的的二进制日志,它只会记录表的变更操作,但不会记录select和show这种查询操作。
-
数据恢复:误删数据之后可以通过mysqlbinlog工具恢复数据
-
主从复制:主库将binlog传给从库,从库接收到之后读取内容写入从库,实现主库和从库数据一致性
-
审计:可以通过二进制日志中的信息进行审计,判断是否对数据库进行注入攻击
HA管理:
问题背景:
- DB所在机器异常宕机
- DB节点异常宕机
解决方案:
- HA服务监管、切换宕机节点
- 代理支持配置热加载
- 代理自动屏蔽宕机读节点
实施效果:
- 读节点宕机秒级恢复
- 写节点宕机30s内恢复服务