第一部分-数据系统基础
1 可靠、可拓展与可维护的应用系统
2 数据模型与查询语言
3 数据存储与检索
3.1 日志结构存储
- 代表:Bitcask,LSM-Tree(Log Structed Merge Tree)
- 使用哈希索引实现
- 只允许追加更新的数据文件,以最后一次键值对为准
- 在内存存放key,以及value在硬盘中的偏移量,读取时只需一次磁盘寻址
- 如何防止耗尽磁盘空间?
- 将日志分解为小段,以文件大小限制,查找时依次向前查找
- 在多个段上实行压缩,只保留最新的键值对,对于相同的key
- 崩溃恢复:Bitcask将hashmap快照存放于磁盘,来加快恢复速度
- 删除记录: 墓碑机制,压缩时直接忽略
- 并发控制:单个线程负责追加,允许多个线程读取
- 缺点
- 哈希表必须全部放入内存
- 区间查询效率不高
- 改进:SSTables
- 名称:排序字符串表
- 内存中存放的叫稀疏索引,因为是间隔存放key及偏移的
- 要求段文件内按照key排序,且每个key在每个段文件只出现一次
- 优点:
- 合并更加简单高效
- 不必在内存存储所有key及索引,可以根据已有key进行推测。eg:根据handbag和handsome,区间内查找handiword对应的value
- 整体流程:
- 写入时,在内存维护一个类似平衡树的数据结构(红黑树/AVL)
- 当内存表超过阈值,作为SSTables写入磁盘
- 后台周期性的执行段合并和压缩过程,丢弃已经被覆盖或者删除的值
- 在磁盘单独保留日志,先写日志,再写内存表,崩溃后恢复内存表
- 查询多个段文件可能会比较慢,可以使用布隆过滤器提前判断
- LevelDB和RocksDB使用分层压缩(根据key范围分裂成更小的SSTables,旧数据移动到单独的层级),HBase使用大小分级(较新和较小的SSTables合并大较久和较大的SSTables),Cassandra同时支持两种方案
3.2 B-trees
- 与SSTable相似:保留按key排序的kv对,实现高效kv查找,以及区间查询
- 将数据库分解成固定大小的页(通常4KB),更接近底层硬件(硬盘)
- 一个页包含的子页引用,称为分支因子
- 分支因子为500的4KB页的四级树,存储高达256TB
- 具有n个键的btree,深度为O(logn)
- 缺点:
- 写入时直接覆盖页,对ssd不友好
- 如果需要覆盖多个页,比如插入导致页溢出,会进行分裂,如果写入部分页后崩溃,可能会出现索引损坏,以及孤儿页
- WAL(Write Ahead Log)支持崩溃恢复,写入时先写WAL,再修改页
- 多线程访问B-tree,需要使用锁存器(轻量级锁)保护树的数据结构
- 改进:
- 保存键的缩略信息,而不是完整的键,节省页空间(b+树)
- 使用写时复制技术,替代覆盖页+WAL的方案,同时对并发控制也有帮助(如LMDB)
- 尝试对树进行布局,使相邻叶子节点在磁盘相邻,数据量变大之后,这个过程会变得困难
3.3 对比B-tree和LSM-tree
LSM-tree优点:
- LSM-tree能够承受比B-tree更高的写入吞吐量,部分因为有时写放大(由于反复压缩和SSTables和合并,一次数据写入,会导致多次磁盘写,对ssd影响较大),部分因为以顺序方式写入紧凑的SSTables文件,而不必重写树的多个页
- LSM-tree不是面向页,并且定期重写SSTables以消除碎片化,故具有较低的存储开销,尤其是在使用分层压缩时;由于碎片,B-tree当页被分裂获当一行的内容不能适合现有页,页中的空间可能会无法使用。
- 部分ssd的内部会使用日志结构化算法,将随机写入转换为顺序写入,一定程度上能减轻写放大的危害
LSM-tree缺点:
- 硬盘并发的读写资源有限,有时压缩过程会干扰正在进行的读写操作
- 高写入吞吐量时,磁盘的有限写入带宽,需要在初始写入(记录并刷新内存表到硬盘)和后台运行的压缩线程之间共享
- B-tree每个键都唯一对应索引某个位置,对事务语义的实现有帮助,可以直接将锁定义到树中;而LSM-tree一个键则可能存在多个副本,对事务支持不好
3.4 其他索引结构
二级索引
- 基于kv索引来构建,对于高效的联结操作至关重要(我理解就是对外键建立索引)
- 键不唯一,两种方式解决
- 使索引每个值成为匹配行标识符的列表
- 追加行标识符来使键变得唯一
在索引中存储值
- 索引中的值可以是实际行(聚集索引),也可以是指向行的引用(非聚集索引,存储行的位置被称为堆文件)
- 当更新值不更改键时,堆文件实现会非常高效
- 覆盖和聚集索引可以加快读取速度,但是需要额外存储,会增加写入的开销
多列索引
- 将几个字段组合成一个键
- 建立方法:
- 可以使用空格填充曲线将二维位置转换为单个数字,然后使用B-tree索引(联合索引)
- 使用专门的空间索引,如R树(HyperDex也使用了二级索引这种技术)
全文搜索和模糊索引
- 支持对单词的同义词查询
- Lecene支持在某个编辑距离内搜索文本
- Lecene对词典使用类似SSTables的结构,需要小内存结构来告诉查询,为了找到一个键,需要排序文件的那个偏移量。在LevelDB中,这个内存索引是一些键的稀疏集合(未存储所有键),在Lecene中,这个内存索引是建中字符序列的有限状态自动机,类似于字典树
在内存中保存所有内容
- 用于缓存,数据丢失可接受
- 提供了一些基于磁盘索引难以实现的数据模型,比如redis的几种数据类型
3.5 事务处理与分析处理
OLTP
- Online Transaction Processing,在线事务处理
- 读数据基于键,每次查询返回很少数据量
- 写数据随机访问,低延迟写入
- 使用场景为终端用户,通过网络应用程序
- 规模为GB-TB
OLAP
-
- Online Analytic Processing,在线分析处理
- 读数据特征,对大量数据进行汇总
- 写数据批量导入(ETL)或者事件流
- 使用场景为分析师,为决策提供支持
- 规模为TB-PB
数据仓库
- 包含公司所有OLTP系统的只读副本
- 在不影响OLTP的前提下尽情使用
- 可以针对分析访问模式进行优化
星型与雪花分析模式
星型模式:
- 当表关系可视化时,事实表位于中间,被一系列维度表包围,表之间连接像星星的光芒
雪花模式: - 维度进一步细分为子空间
列式存储
面向列存储:
- 将每一列所有的值存储在一起
- 每一行在所有列文件中的偏移量相同(重要)
- 查询时只需要读取和解析在该查询中使用的列,节省大量工作
列压缩:
- 通常,列中不同值的数量小于行数
- 使用单独位图表示列值出现的情况
- 排序之后进行压缩,a个1,b个0...可以极大减少空间占用
- Cassandra和HBase(集成自BigTable)有一个列族的概念,将一行所有列和行主键一起保存,且不进行列压缩,仍然是面向行存储的
列存储的排序
- 帮助进一步压缩列
- 对前几列压缩会取得不错的收益
- 为了防止写入更加困难,借鉴了LSM-tree,写入首先进入内存存储区,添加到已排序结构中,然后准备写入磁盘。当积累了足够写入时,他们与磁盘上的列文件合并,并批量写入新文件(Vertica做了这个事儿)
聚合:数据立方体与物化视图
- 物化聚合(COUNT/SUM/AVG等操作),重复查询浪费性能
- 物化视图:查询结果的十几副本,被写入到磁盘
- 虚拟试图:编写查询的快捷方式(mysql视图,是一个虚表)
- 缺点:数据立方体缺乏像查询原始数据的灵活性
- 仅当数据立方体可以对特性查询显著提升性能时,才会采用多为数据聚合