写哪算哪,有些地方不正确的,欢迎指正。逻辑混乱,不喜忽喷。
工作中碰到过两类服务,一个是数据密集型的,一个是计算密集型的。计算密集型的是我工作的第一家公司,当时主要是处理信息流(包括图文和视频)的各种特征识别。每一种特征的识别都是一个独立的服务,这些服务没有自己的数据库。所有信息都存储在一个公共数据库里,用的是 hbase,在 hbase 的基础上加了缓存。数据密集型的系统就比较多了,各种工作台基本上都是数据密集型的,以数据为核心,负责接收数据,转换数据,写入数据。它的核心是数据库,关注的是数据库的负载,读写性能,事务的支持等等。如果使用了多个数据库,还需要关注数据的一致性,尤其在强依赖多个数据库的时候,可能还需要额外关注数据同步的延迟。数据一致性/数据同步是一个非常繁琐和复杂的问题。虽然已经有开源的解决方案,如 kafka 的 connector,logstash 等等,各个大厂内部也会有现成的解决方案,但是真正用起来,就是另外一回事了。举两个亲生经历的例子,背景是 mysql 的数据同步到 es:
- 同步会存在延迟,比如 mysql 的数据被大量删除的时候
- 数据覆盖。请求被处理的顺序是未知的,可能先发出的请求被后处理
即使是拿来主义,东西也不是那么好拿的,不是拿过来就可以高枕无忧的。所以在引入多个数据源的时候,不如先想想是否有必要,尤其是在客户私有化部署的时候。
数据怎么存储
先定义个业务逻辑背景吧,比如我们有两类数据,公司数据和员工数据,公司数据包括公司ID,公司名称,信用代码,法人,成立时间,主营业务等等,员工数据包括姓名,员工ID,入职时间,岗位,薪资等等。
有三种方案可以存储这批数据
- 文档型存储。员工只会隶属于某一家公司,所以可以将员工数据和公司数据存储在一起,将员工信息作为公司的一个子属性。
- 关系型存储。这应该是最常见的存储方式了,分别建一个员工表和公司表,员工表里增加一个 公司ID 字段,将两者关联起来,相比文档型,现在已经将员工和公司的数据分开存储了。
- 列式存储。和关系型存储一样,员工和公司的数据是分开存储。但是关系型是按行存储,列式存储是按列存储。
文档型存储非常适合用于一对多的场景,比如像我们这里假设员工只隶属于一个公司那样。它的好处是可以快速获取到一个对象的全部数据,而不用像关系型存储那样需要从多个地方获取。但是考虑下多对多的情况,比如想要记录员工入职过的每一家公司,这时候需要在员工属性中新增一个字段,这个字段有两种方式:
- 完整记录公司的各种数据。但是想想如果公司数据发生变更呢,你要修改所有涉及到的地方,麻烦!
- 记录公司的 ID,这和关系型存储还有什么差别吗?
关系型存储适应的情况比文档型存储多一些,不论是一对一,一对多,多对多都可以通过关系型存储实现,即使是图模型,不论是三元组还是属性图,都可以通过关系性存储实现。可能这也是可以无脑选择关系存储,但是如果选择文档存储就需要深思熟虑一下的原因吧。
列式存储主要使用在大数据/OLAP场景,在这些场景里,可能需要存储几百个字段(比如开头提到过的图文字段的存储),但是检索的时候只需要其中很少的一部分。这个时候按行存储的话,会获取到很多不必要的字段。另外,按列存储也方便数据压缩,可以提供更小的存储空间。这些都是针对查询和存储的优化,但是想想写入的时候会发生什么呢?如果是新增,我们需要往所有列文件中写入数据,而行存储只需要往一个地方写入。如果是更新可能更麻烦一点,需要找对对应的位置,然后新数据覆盖,如果新数据长度更长,可能会引起一连串的写入。有些列式存储会进行排序,此时的情况就又更复杂了。相比行存储,列存储优化了查询和大小,但对写入并不是那么友好。
在传统认知里,OLTP 和 OLAP 是两种场景,无法兼得,但是 HTAP 试图同时兼顾 OLAP 和 OLTP,如果真有一款数据系统能做到,那应该会引起很多追随者吧,成为新的标准。我对 HTAP 并不是很了解,当前只知道 TIDB 是 HTAP 的,之前有幸被同事推荐过另外一个 pg 的插件 hydra,也是号称 HTAP 的,但是它是列存储的,在自己的官网上就写明了,他更新字段的速度比较慢。
数据怎么索引
索引是为了加速查询,针对不同的场景,索引的实现也不一样,比如时序数据库和关系数据库的索引实现肯定不同,内存数据库不需要过多关注持久化的问题,他对索引的实现肯定又不一样(想想 redis 的 hashtable 和 skip table),lucene 支持全文检索,则使用了倒排索引。
我这里想聊的其实是 LSM(Log Structured Merge Tree)。使用这种索引的一个典型数据库是 leveldb,leveldb 是一个键值对数据库,可以快速的根据 key ,以及 key 的范围查询数据。LSM 有几个核心概念如下:
- 数据追加写。不论是新插入数据,更新数据,还是删除数据,都是追加写到文件里。查询的时候以最新一次写入为准
- 段合并。一旦文件过大,就会新建一个文件写入,这些文件称之为段。随着数据库的运行,可能会产生很多段,这时候去查询,可能就需要遍历所有的段文件。因而会有个段合并的后台逻辑,用于将不同的段合并成一个,合并后,每个 key 只保留一条记录
- SSTable (sort string table)。每个段都是按照key有序排列的,所以能非常快速的定位到某个 key,以及查询一个范围内的 key。
B+ 树作为一个非常典型的适合磁盘的索引数据结构,在关系数据库中被广泛使用,对 B+ 树索引了解的人可能会多于知道 LSM 的人。另外,MongoDB 作为一个文档型数据库,使用的也是 B+ 树,所以索引是索引,数据模型是数据模型,他们是两个维度的概念,可以任意组合,这是题外话了。 不过 LSM 的应用也很广泛,除了 leveldb 和 rocksdb,还包括:
- ES,准确的说应该是 Lucene。它的词典的存储就使用了 LSM,至少用到了 LSM 的思想,所以它也有段和段合并的概念。不过 Lucene 作为一个准实时的数据库,这个并不是 LSM 的特性,Lucene 的准实时是因为他的 FST 的数据结构,这个又是题外话了
- 列存储,比如 hbase 和 hydra 等。一些列存储的更新也是通过追加的方式,这比找到对应的位置,进行覆盖更新要快一些。另外,如果对列存储的值进行排序,是不是可以进一步加快查询的效率以及更好的压缩。这不都是 LSM 的思路吗?
行文至此了了结束。