- 日常开发中常用的存储方案选型很多都是 “拿来主义” 的,凭借着经验、习惯选用,但对它们的细节特性或约束少有研究。
- 除了手边会用的存储方案,也应该关注市面上更合适的存储方案。
- 一定的技术预研和储备能够帮助未来更好的技术方案设计。
故写了这篇文章,抛出我的观察和思考,希望日后可以将一些更先进 (合适) 的技术引入公司业务,助力业务发展。
存储选型的考虑要素
存储选型的目的还是为了我们的使用场景和用户服务,因此在选型前需要回答一些业务指标 & 技术指标方面的问题,以便于我们清楚存储选型的应用环境。
- 用户量:用户量预估多少?几百几万还是几亿?
- 数据量:数据量预估多少?日均增量能有多少?
- 读写偏好:数据是读多一些还是写多一些?
- 数据场景:强事务型还是分析型需求?
- 运行性能要求:并发量是多少?高峰、平均、低谷分别预估是多少?
存储引擎分类及特性
数据库的分类方式非常多样,因参考维度不同而存在较大差异,下面是常见的一些分类。
先拿我们最熟悉的关系数据库来说,它的优点非常多,我们选用关系数据库的理由可简单概括为以下几点:
- 容易理解
可由二维表结构来逻辑表达,相对网状、层次等其他模型更加容易被理解。严格遵循数据格式与长度规范,数据以行为单位,一行数据表示一个实体信息,每一行数据的属性都是相同的。
- 事务特性
支持 ACID 特性,可以维护数据之间的一致性,这是使用关系数据库非常重要的一个理由。
- 操作方便
通用的 SQL 语言使得操作关系型数据库非常方便,支持 join 等复杂查询,Sql + 二维关系是关系型数据库最无可比拟的优点,这种易用性非常贴近开发者。
- 数据稳定
数据持久化到磁盘,没有丢失数据风险。
- 服务稳定
最常用的关系型数据库产品 MySql、Oracle 服务器性能卓越,服务稳定,通常很少出现宕机异常。
然而,在享受关系数据库带来的便利的同时,我们也不得不面临很多麻烦的问题:
- 高并发下数据库瓶颈明显
数据按行存储,即使只针对某一列进行运算,也会将整行数据从存储设备中读入内存,导致 IO 较高。写入更新频繁的情况下,数据库往往会出现 CPU 飙高、Sql 执行慢、客户端报数据库连接池不够等异常情况,且性能瓶颈通过加 CPU、换固态硬盘、继续买服务器加数据库做分库等方式处理 ROI 不高,受限于其本身的特点,可能花了很多钱都未必能达到想要的效果。因此例如万人秒杀这种场景,我们绝对不可能通过数据库直接去扣减库存,需要做好流量漏斗。
为维护数据一致性付出的代价大
数据一致性是关系型数据库的核心,但是同样为了维护数据一致性的代价也非常大。SQL 标准为事务定义了不同的隔离级别,从低到高依次是读未提交、读已提交、可重复度、串行化,事务隔离级别越低,可能导致的并发异常越多,但是能提供的并发能力越强。那么为了保证事务一致性,数据库就需要提供并发控制与故障恢复两种技术,前者用于减少并发异常,后者可以在系统异常的时候保证事务与数据库状态不会被破坏。对于并发控制,其核心思想就是加锁,无论是乐观锁还是悲观锁,只要提供的隔离级别越高,那么读写性能必然会受影响。
- 为维护索引付出的代价大
为了提供丰富的查询能力,通常热点表都会有多个二级索引,一旦有了二级索引,数据的新增必然伴随着所有二级索引的新增,数据的更新也必然伴随着所有二级索引的更新,这不可避免地降低了关系型数据库的读写能力,且索引越多读写能力越差。除了数据文件不可避免地占空间外,索引占的空间其实也并不少。
- 水平扩展后带来的种种问题难处理
随着业务规模扩大,一种方式是对数据库做分库,做了分库之后,数据迁移(1 个库的数据按照一定规则打到 2 个库中)、跨库 join、分布式事务处理都是需要考虑的问题,尤其是分布式事务处理,业界当前都没有特别好的解决方案。
- 全文搜索功能弱
例如 like “% 新年快乐 %”,只能搜索到 “新年快乐,爱大家”,无法搜索到 “新年真是太快乐了,爱大家” 这样的文本,即不具备分词能力,且 like 查询在 “% 新年快乐” 这样的搜索条件下,无法命中索引,将会导致查询效率大大降低。
- 表结构扩展不方便
由于数据库存储的是结构化数据,因此表结构 schema 是固定的,扩展不方便,如果需要修改表结构,需要执行 DDL(data definition language)语句修改,修改期间会导致锁表,部分服务不可用。
如上文所分析的,关系型数据库优点明显,缺点同样不能忽视,因此通常在企业规模不断扩大的情况下,不会一味指望通过增强数据库的能力来解决数据存储问题,而是会引入其他存储,也就是我们说的 NoSql。
NoSql 的全称为 Not Only SQL,泛指非关系型数据库,是对关系型数据库的一种补充,特别注意补充这两个字,这意味着 NoSql 与关系型数据库并不是对立关系,二者各有优劣,取长补短,在合适的场景下选择合适的存储引擎才是正确的做法。
下面看一下常用的 NoSql 及他们的代表产品,并对每种 NoSql 的优缺点和适用场景做一下分析,便于熟悉每种 NoSql 的特点,方便技术选型。
1、KV 型 NoSql(代表——Redis)
KV 型 NoSql 顾名思义就是以键值对形式存储的非关系型数据库,是最常见的一种 NoSql。Redis、MemCache 是其中的代表,Redis 又是 KV 型 NoSql 中应用最广泛的 NoSql,KV 型数据库以 Redis 为例,最大的优点总结下来主要有两点:
- 数据基于内存,读写效率高。
- KV 型数据,时间复杂度为 O(1),查询速度快。
所以说,KV 型 NoSql 最大的优点就是高性能,利用 Redis 自带的 BenchMark 做基准测试,TPS 可达到 10 万的级别,性能非常强劲。同样的 Redis 也有所有 KV 型 NoSql 都有的比较明显的缺点:
- 内存是有限的,无法支持海量数据存储。
- 只能根据 K 查 V,无法根据 V 查 K。
- 查询方式单一,只有 KV 的方式,不支持条件查询,多条件查询唯一的做法就是数据冗余,但这会浪费很多存储空间。
- 由于 KV 型 NoSql 的存储是基于内存的,会有丢失数据的风险(有持久化存储方案)。
综上所述,KV 型 NoSql 最合适的场景就是缓存的场景:
- 读远多于写。
- 没有持久化的需求,可以容忍数据丢失。
针对那些读远多于写的数据,引入一层缓存,每次读从缓存中读取,缓存中读取不到,再去数据库中取,取完之后再写入到缓存,对数据做好失效机制通常就没有大问题了。通常来说,缓存是性能优化的第一选择也是见效最明显的方案。
2、搜索型 NoSql(代表——ElasticSearch)
传统关系型数据库主要通过索引来达到快速查询的目的,但是在全文搜索的场景下,索引是无能为力的,like 查询无法满足所有模糊匹配需求,使用限制太大且使用不当容易引起慢查询问题,搜索型 NoSql 的诞生正是为了解决关系型数据库全文搜索能力较弱的问题,ElasticSearch 是搜索型 NoSql 的代表产品。
全文搜索的原理是倒排索引,我们看一下什么是倒排索引,它是关键字 –> 文档的映射,举例来说,现在这里有四个短句:
- "Tom is Tom"
- "Tom is my friend"
- "Thank you, Betty"
- "Tom is Betty's husband"
搜索引擎会根据一定的分词规则将一句话切成多个关键字,并以关键字的维度维护关键字在每个文本中的出现次数。这样下次搜索“Tom”关键字的时候,由于 Tom 这个词语在“Tom is Tom”、“Tom is my friend”、“Tom is Betty’s husband” 三句话中都出现过,因此这三条记录都会被检索出来,而且由于”Tom is Tom” 这句话中”Tom” 出现了 2 次,因此这条记录对”Tom” 这个单词的匹配度最高,最先展示。这就是搜索引擎倒排索引的基本原理,假设某个关键字在某个文档中出现,那么倒排索引中有两部分内容:
- 文档 ID
- 该关键字在该文档中出现的位置情况
相对应的,我们搜索”Betty Tom” 这两个词语也是一样,搜索引擎将”Betty Tom” 切分为”Tom”、”Betty” 两个单词,根据开发者指定的满足率,比如满足率 = 50%,那么只要记录中出现了两个单词之一的记录都会被检索出来,再按照匹配度进行展示。
搜索型 NoSql 以 ElasticSearch 为例,它的优点为:
1)支持分词场景、全文搜索,这是区别于关系型数据库最大特点。
2)数据写文件无丢失风险,在集群环境下可以方便横向扩展,可承载 PB 级别的数据。
3)支持条件查询,支持聚合操作,类似关系型数据库的 Group By,但是功能更加强大,适合做数据分析。
4)高可用,自动发现新的或者失败的节点,重组和重新平衡数据,确保数据是安全和可访问的。
同样,ElasticSearch 也有比较明显的缺点:
1)性能全靠内存来顶,也是使用的时候最需要注意的点,非常吃内存,大数据量下 64G + SSD 基本就是标配,相同的配置多一倍内存,一个月差不多就要多花好多钱。至于 ElasticSearch 内存主要用在以下几个地方:
- Indexing Buffer----ElasticSearch 基于 Luence,Lucene 的倒排索引是先在内存里生成,然后定期以 Segment File 的方式刷磁盘的,每个 Segment File 实际就是一个完整的倒排索引。
- 各类缓存 ----Filter Cache、Field Cache、Indexing Cache 等,用于提升查询分析性能,例如 Filter Cache 用于缓存使用过的 Filter 的结果集。
- Segment Memory---- 倒排索引前面说过是基于关键字的,Lucene 在 4.0 后会将所有关键字以 FST 这种数据结构的方式将所有关键字在启动的时候全量加载到内存,加快查询速度,官方建议至少留系统一半内存给 Lucene。
- Cluter State Buffer----ElasticSearch 被设计为每个 Node 都可以响应用户请求,因此每个 Node 的内存中都包含有一份集群状态的拷贝,一个规模很大的集群这个状态信息可能会非常大。
2)数据结构灵活性不高,字段一旦建立就没法修改类型了,假如建立的数据表某个字段没有加全文索引,想加上,那么只能把整个表删了再重建。
3)读写之间有延迟,写入的数据差不多 1s 样子会被读取到(数据写入时需要维护很多索引)。
因此,搜索型 NoSql 最适用的场景就是有条件搜索尤其是全文搜索的场景,作为关系型数据库的一种替代方案,通常搜索型 NoSql 也会作为一层前置缓存,来对关系型数据库进行保护。
此外,搜索型数据库还有一种非常重要的应用场景。我们可以想,一旦对数据库做了分库分表后,原来可以在单表中做的聚合操作、统计操作是否统统失效?例如我把订单表分 16 个库,1024 张表,那么订单数据就散落在 1024 张表中,我想要统计昨天浙江省单笔成交金额最高的订单是哪笔如何做?这就是搜索型 NoSql 的另一大作用了,我们可以把分表之后的数据统一打在搜索型 NoSql 中,利用搜索型 NoSql 的搜索与聚合能力完成对全量数据的查询。
3、列式 NoSql(代表——HBase)
列式 NoSql 和关系型数据库一样都有主键的概念,区别在于关系型数据库是按照行组织的数据,数据字段即使没有值同样占空间,列式存储完全是另一种方式,它是按列进行数据组织的,好处在于:
- 查询时只有指定的列会被读取,不会读取所有列。
- 存储上节约空间,空值不会被存储,一列中有时候会有很多重复数据(尤其是枚举数据,性别、状态等字段),这类数据可压缩。
- 列数据被组织到一起,一次磁盘 IO 可以将一列数据一次性读取到内存中。
大数据时代最具代表性的技术之一 HBase 就是列式 NoSQL 的产品实现,其优点主要是:
- 海量数据存储,PB 级别数据随便存,底层基于 HDFS(Hadoop 文件系统),数据持久化。
- 读写性能好,只要没有滥用造成数据热点,读写基本没任何问题。
- 横向扩展在关系型数据库及非关系型数据库中都是最方便的之一,只需要添加新机器就可以实现数据容量的线性增长,且可用在廉价服务器上,节省成本。
- 可存储结构化或者半结构化的数据。
- 本身没有单点故障,可用性高。
- 列数理论上无限制,HBase 本身只对列族数量有要求,建议 1~3 个。
缺点主要表现在:
- HBase 是 Hadoop 生态的一部分,因此它本身是一款比较重的产品,依赖很多 Hadoop 组件,数据规模不大没必要用,运维还是有点复杂的。
- 不支持分页查询,因为统计不了数据总数。
- KV 式存储,条件查询很弱,HBase 在 Scan 扫描一批数据的情况下还是提供了前缀匹配这种 API 的,条件查询除非定义多个 RowKey 做数据冗余。
因此 HBase 比较适用于 KV 型存储且未来无法预估数据增长量的场景,另外 HBase 使用还是需要一定的经验,主要体现在 RowKey 的设计上。
4、文档型 NoSql(代表——MongoDB)
文档型 NoSql 指的是将半结构化数据存储为文档的一种 NoSql,文档型 NoSql 通常以 JSON 或者 XML 格式存储数据,因此文档型 NoSql 是没有 Schema 的,由于没有 Schema 的特性,我们可以随意地存储与读取数据,因此文档型 NoSql 的出现是解决关系型数据库表结构扩展不方便的问题的。
MongoDB 是文档型 NoSql 的代表产品,同时也是所有 NoSql 产品中的明星产品之一,它的很多概念与关系数据库类似,因此,对于 MongDB,我们只需要理解成一个 Free-Schema 的关系型数据库就好了,其优点主要是:
- 没有预定义的字段,扩展字段容易。
- 相较于关系型数据库,读写性能优越,命中二级索引的查询不会比关系型数据库慢,对于非索引字段的查询则是全面胜出。
缺点在于:
- 不支持事务操作,虽然 Mongodb4.0 之后宣称支持事务,但是效果待观测。
- 多表之间的关联查询不支持(虽然有嵌入文档的方式),join 查询还是需要多次操作。
- 空间占用较大,这个是 MongDB 的设计问题,空间预分配机制 + 删除数据后空间不释放,只有用 db.repairDatabase () 去修复才能释放。
- 目前没发现 MongoDB 有关系型数据库例如 MySql 的 Navicat 这种成熟的运维工具。
总而言之,MongDB 的使用场景很大程度上可以对标关系型数据库,但是比较适合处理那些没有 join、没有强一致性要求且表 Schema 会常变化的数据。
通过以上讨论分析我们心中已经有了一个基本的选型框架指导,实际上在数据库选型时回答自己两个核心问题就好了:
- 什么时候选用关系型数据库,什么时候选用非关系型数据库。
- 选用非关系型数据库的话,使用哪种非关系型数据库。
NoSQL 数据库都是通过牺牲了 ACID 特性来获取更高性能的,假设表数据有很强的事务特性需求,那么这类数据是不适合放在非关系型数据库。此外,选用 NoSQL 数据库时也要根据公司技术栈框架、业务特性、运维成本等多方面考虑是否采纳。
总结
关系型数据库和 NoSQL 数据库的选型,往往需要考虑几个指标:
- 数据量
- 并发量
- 实时性
- 一致性要求
- 读写分布和类型
- 安全性
- 运维成本
常见软件系统数据库选型参考如下:
- 中后台管理型系统 - 如运营系统,数据量少,并发量小,首选关系型数据库。
- 大流量系统 - 如电商单品页,后台考虑选关系型数据库,前台考虑选内存型数据库。
- 日志型系统 - 原始数据考虑选列式数据库,日志搜索考虑选搜索引擎。
- 搜索型系统 - 例如站内搜索,非通用搜索,如商品搜索,后台考虑选关系型数据库,前台考虑选搜索引擎。
- 事务型系统 - 如库存,交易,记账,考虑选关系型数据库 + K-V 数据库(作为缓存)+ 分布式事务。
- 离线计算 - 如大量数据分析,考虑选列式数据库或关系型数据库。
- 实时计算 - 如实时监控,可以考虑选内存型数据库或者列式数据库。
设计实践中,要基于需求、业务驱动架构,无论选用 RDB/NoSQL, 一定是以需求为导向,最终数据存储方案必然是各种权衡的综合性设计。