You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15 KiB

30 | 如何权衡关系数据库与NoSQL数据库

你好,我是陶辉。

到了第4部分课程的最后一讲我们来结合前面介绍过的知识点看看面对NoSQL、关系数据库时该如何选择。

在分布式系统中我们会同时使用多种数据库。比如你可能会在Redis中存放用户Session会话将业务数据拆解为由行、列构成的二维表存储在MySQL中将需要全文检索的数据放在ElasticSearch中将知识图谱放在Neo4j图数据库中将数据量、访问量很大的数据放在Cassandra列式数据库或者MongoDB文档型数据库中等等。

选择数据库时我们的依据可能是访问速度比如基于哈希表的Redis查询复杂度只有O(1)也可能从事务的支持程度上选择了关系数据库甚至从应用层的开发效率上还给它添加了Hibernate等ORM框架也可能从处理数据的体量上选择了NoSQL数据库。可是除了各种实现层面上的差异外各类NoSQL与关系数据库之间有没有最本质的区别在实际工程中我们可否从此入手确定大方向再从细微处选择不同的实现

在我看来答案就在于“关系”这两个字这也是我权衡数据库时最先考虑的前提。接下来我们就沿着关系数据库的特性看看NoSQL数据库究竟做了哪些改变我们又该如何选择它们。

关系数据库的优点

关系数据库对业务层开发效率的提升有很大帮助。下面我们先基于一个简单的例子,看看关系数据库有何优点。疫情期间新增了一批能够测量体温的考勤机,通过关系数据库我们新建了用户、考勤机、考勤记录三张表,如下图所示:

在关系数据库中表中的每行数据由多个从属于列的单一值比如数字、字符串构成。虽然表中可以存放任意行数据但列却是预先定义且不变的因此我们很容易通过行、列交汇处的单一值进行关联操作进而完成各类业务目的不同的查询。比如业务开发者可以通过下面这行SQL语句找到体温超过37度的员工上报其姓名、测量时间以及所在地理位置

select user.name, record.time, machine.location from user, record, machine where user.id = record.user_id and machine.id = record.machine_id and record.temporature > 37;

运营人员则可以通过下面这行SQL语句找出各类考勤机的使用频率

select count(*), machine.id from machine, record where machine.id = record.machine_id group py machine.id;

因此,关系数据库**可以通过预定义的关系,由数据库自身完成复杂的逻辑计算,为不同的场景提供数据服务。**由于不同的数据间具有了关系,关系数据库还提供了“Transaction事务用于保证相关数据间的一致性这大大释放了应用开发者的生产力。所谓“事务”会同时具有ACID4个特性

Atomicity原子性指多个SQL语句组成了一个逻辑单位执行时要么全部成功要么全部失败。

Consistency一致性,指数据库只能从一个一致性状态转换到另一个一致性状态。即使数据库发生了重启,仍然得维持一致性。

Isolation隔离性由于数据库可以支持多个连接并发操作因此并发的事务间必须互相隔离才有意义。SQL标准定义了以下4种隔离级别

  • READ UNCOMMITTED未提交读它表示在事务A还未提交时并发执行的事务B已经可以看到事务A改变的数据。这种隔离级别会带来很多问题因此很少使用。
  • READ COMMITTED提交读它表示当事务A未提交时事务B看不到事务A改变的任何数据这是PostgreSQL数据库的默认隔离级别。
  • REPEATABLE READ可重复读指在READ COMMITTED的基础上解决了脏读问题。所谓脏读是指在一个事务内多次读取到同一数据时结果可能不一致。这是MySQL数据库的默认隔离级别。
  • SERIALIZABLE可串行化它通过对每一行数据加锁使得所有事务串行执行虽然隔离性最好但也大大降低了数据库的并发性所以很少使用。

Durability持久性,指一旦事务提交,事务所做的修改必须永久性地保存到数据库中。

可见事务的ACID特性简化了本应由应用层完成的流程这也是关系数据库与NoSQL数据库之间最大的差别。除事务外关系数据库还在以下4点上降低了应用层的开发成本

  • 无论是商业版的Oracle还是开源的MySQL、PostgreSQL只要是关系数据库就拥有同样的数据模型,因此它们可以通过SQL 语言为应用层提供标准化、几乎没有差异的访问接口;
  • 生产级的数据库对持久化都有良好的支持,全面的冷备、热备方案提供了很高的可用性;
  • 通过索引、缓存等特性,当行数在亿级以下时,关系数据库的性能并不低;
  • 关系数据库支持还不错的并发度,一般可以服务于上千个并发连接。

所以应用层会将许多计算任务放在关系数据库中在此基础上还诞生了MVC等将数据层从业务中剥离、以关系数据库为中心的架构。

关系数据库的问题

虽然基于单一值的关系映射提供了事务等许多功能但同时也引入了3个问题。

首先,内存中的数据结构非常多样,难以直接映射到行列交汇处的单一值上。不过,这个问题可以通过ORMObject-relational mapping**框架解决。**比如Python中的Django ORM框架可以将上述3张表映射为内存中的3个类

from django.db import models

class User(models.Model):
    name = models.CharField(max_length=20)

class Machine(models.Model):
    location = models.CharField(max_length=100)
    
class Record(models.Model):
    time = models.DateTimeField()
    temporature = models.FloatField()
    user = models.ForeignKey(User)
    machine= models.ForeignKey(Machine)

ORM框架会为每张表生成id字段而Record表将User和Machine表中的id字段作为外键ForeignKey互相关联在一起。于是这3个类就映射了数据库中的那3张表而内存中的对象即类的实例则映射为每张表中的一行数据。在ORM框架下找到体温大于37度员工的那串长SQL可以转化为OOP中的函数调用如下所示

#gte表示大于等于
records = Record.objects.filter(temporature__gte = 37)
for r in records:
  print(r.user.name, r.machine.location, r.time)

相比起SQL语句映射后的OO编程要简单许多。

其次为了实现关系映射每张表中的字段都得预先定义好一旦在产品迭代过程中数据模型发生了变化便需要同步完成以下3件事

  • 修改表结构;
  • 修改应用层操作数据的代码;
  • 根据新的规则转换、迁移已有数据。

在ORM中我们可以把这3步放在一个migration迁移脚本中完成。当然如果数据迁移成本高、时间长可以设计更复杂的灰度迁移方案。

**最后是关系数据库固有的可伸缩性问题这是各类NoSQL数据库不断诞生的主要原因。**在[第21讲]我们介绍过沿AKF X轴扩展的复制型主从结构然而单点主库无法解决数据持续增长引入的性能问题。

沿AKF Z轴扩展数据库虽然能够降低数据规模但分库分表后单一值关系引申出的ACID事务不能基于高时延、会抖动的网络传输强行实现否则会导致性能大幅下降这样的可用性是分布式系统无法接受的。

因此在单机上设计出的关系数据库难以处理PB级的大数据。而NoSQL数据库放弃了单一值数据模型非常适合部署在成千上万个节点的分布式环境中。

NoSQL数据库是如何解决上述问题的

虽然所有的NoSQL数据库都无法实现标准的SQL语言接口但NoSQL绝不是“No SQL拒绝SQL语言”的意思。当然NoSQL也不是“Not Only SQL不只是SQL语言”的意思否则Oracle也能算NoSQL数据库了。实际上没有必要纠结NoSQL的字面含义NoSQL数据库只是放弃了与分布式环境相悖的ACID事务提供了另一种聚合数据模型从而拥有可伸缩性的非关系数据库。

NoSQL数据库可以分为以下4类

Key/Value数据库,通常基于哈希表实现(参见[第3讲]性能非常好。其中Value的类型通常由应用层代码决定当然Redis这样的Key/Value数据库还可以将Value定义为列表、哈希等复合结构。

文档型数据库在Key/Value数据库中由于没有预定义的值结构所以只能针对Key执行查询这大大限制了使用场景。文档型数据库将Value扩展为XML、JSON比如MongoDB等数据结构于是允许使用者在文档型数据库的内部解析复合型的Value结构再通过其中的单一值进行查询这就兼具了部分关系数据库的功能。

列式数据库,比如[第22讲] 介绍过的Cassandra。列式数据库基于Key来映射行再通过列名进行二级映射同时它基于列来安排存储的拓扑结构这样当仅读写大量行中某个列时操作的数据节点、磁盘非常集中磁盘IO、网络IO都会少很多。列式数据库的应用场景非常有针对性比如博客文章标签的行数很多但在做数据分析时往往只读取标签列这就很适合使用列式数据库。再比如通过倒排索引实现了全文检索的ElasticSearch就适合使用列式存储存放Doc Values这样做排序、聚合时非常高效。

图数据库在社交关系、知识图谱等场景中携带各种属性的边可以表示节点间的关系由于节点的关系数量多而且非常容易变化所以关系数据库的实现成本很高而图数据库既没有固定的数据模型遍历关系的速度也非常快很适合处理这类问题。当然我们日常见到的主要是前3类NoSQL数据库。

相对于关系数据库NoSQL在性能和易用性上都有明显的优点。

首先我们来看可用性及性能这是NoSQL数据库快速发展的核心原因

  • NoSQL数据库的可伸缩性都非常好。虽然许多文档型、列式数据库都提供了类SQL语言接口但这只是为了降低用户的学习成本它们对跨节点事务的支持极其有限。因此这些NoSQL数据库可以放开手脚基于Key/Value模型沿AKF Z轴将系统扩展到上万个节点。
  • 在数据基于Key分片后很容易通过[第28讲] 介绍过的MapReduce思想提高系统的计算能力。比如MongoDB很自然的就在查询接口中提供了MapReduce 函数。
  • 通过冗余备份NoSQL可以提供优秀的容灾能力。比如Redis、Cassandra等数据库都可以基于[第22讲] 介绍过的NWR算法灵活地调整CAP权重。
  • 如果每个Key中Value存放的复合数据已经能满足全部业务需求那么NoSQL的单机查询速度也会优于关系数据库。

其次再来看易用性这主要体现在我们可以低成本地变更Value结构。虽然NoSQL数据库支持复合型Value结构但并不限定结构类型。比如文档型数据库中同一个表中的两行数据其值可以是完全不同的JSON结构同样的列式数据库中两行数据也可以拥有不同的列数。因此当数据结构改变时只需要修改应用层操作数据的代码并不需要像关系数据库那样同时修改表结构以及迁移数据。

那么到底该如何选择关系数据库与NoSQL数据库呢其实沿着“单一值关系”这一线索我们已经找到了各自适用的场景。

如果多个业务数据间互相关联我们需要从多个不同的角度分析、计算并保持住相关数据的一致性那么关系数据库最为适合。一旦数据行数到了亿级别以上就需要放弃单一值结构将单行数据聚合为复合结构放在可以自由伸缩的NoSQL数据库中。此时我们无法寄希望于NoSQL数据库提供ACID事务只能基于二段式提交等算法在应用层代码中自行实现事务。

小结

这一讲我们介绍了关系数据库与NoSQL数据库各自的特点及其适用场景。

关系数据库通过行、列交汇处的单一值实现了多种数据间的关联。通过统一的SQL接口用户可以在数据库中实现复杂的计算任务。为了维持关联数据间的一致性关系数据库提供了拥有ACID特性的事务提升了应用层的开发效率。

虽然单一值无法映射内存中的复合数据结构但通过ORM框架关系数据库可以将表映射为面向对象编程中的类将每行数据映射为对象继续降低开发成本。然而关系数据库是为单机设计的一旦将事务延伸到分布式系统中执行成本就会高到影响基本的可用性。因此关系数据库的可伸缩性是很差的。

NoSQL数据库基于Key/Value数据模型可以提供几乎无限的可伸缩性。同时将Value值进一步设计为复合结构后既可以增加查询方式的多样性也可以通过MapReduce提升系统的计算能力。实际上关系数据库与每一类NoSQL数据库都有明显的优缺点我们可以从数据模型、访问方式、数据容量上观察它们结合具体的应用场景权衡取舍。

思考题

最后留给你一道讨论题。你在选择NoSQL与关系数据库时是如何考虑的欢迎你在留言区与大家一起探讨。

感谢阅读,如果你觉得这节课让你有所收获,也欢迎你把今天的内容分享给身边的朋友。