7 个关键的数据库概念,人人都应该了解
2024.09.06划重点:
数据库存储的是事务而不是状态
选择到合适的数据库很难
迁移到合适的数据库难上加难
NoSQL是SQL的补充,不会取代 SQL
数据库的扩展很难
索引就像魔法,直到你搞清楚其中的奥秘
事务对数据库有着巨大影响
关于数据库需要了解的有很多。它们是承担关键任务的复杂应用,有时需要专业的学科专家来维护,但这并不意味着数据库就得是某种神奇的黑匣子。数据库是我们应用的支柱,对数据库的工作方式了解得越多,就越能更好地使用它们,针对它们编写应用,以及在不可避免地出现问题的时候把问题解决掉。
所以,我们就一起深入探讨一下(可能)应该了解的有关数据库的七件事吧。
注意:除非另有说明,否则我一般讨论的是PostgreSQL或 MySQL 这样的关系数据库,而不是NoSQL数据库。
1. 数据库存储事务——而不是状态
数据库,或者说它们的内部架构,并不是很直观。你可能认为数据库只是一两个数据文件,再加上一些管理这些
数据文件的连接以及修改数据文件的代码。从某种程度上来说你是对的。但实际上,从本质上来讲,数据库只是一个日志文件。这个日志文件存放的是提交到数据库的事务列表,而且是按提交顺序保存的。
其他的一切(你的表、行、schema等的状态)都是来自于记录进这份日志里面,累积起来的变更。每一个数据库引擎存储这份日志的方式各不相同,但一部分伪日志大概会像下面这样:
2021-06-07 12:24:35.044513-4000 INSERT INTO 'People' "jeff" "wilson"
2021-06-07 12:25:33.232098-4000 INSERT INTO 'People' "mike" "lou"
2021-06-07 12:25:37.140013-4000 INSERT INTO 'People' "pat" "cat"
2021-06-07 12:26:11.030002-4000 INSERT INTO 'People' "mark" "wilson"
如果数据文件不知道怎么回事被损坏的话,数据库就可以根据这份日志重建所有的表、数据库、schemas以及其他所有内容。但是这里有个非常重要的警告:一旦事务被提交或回滚,就会被从日志当中删除掉。日志存在的目的是在事务完成之前把变更暂存——而不是作为一种备份机制。小一点或者无关紧要的问题可以通过日志恢复,但任何更严重的问题都需要靠某种外部备份机制来恢复。
在PostgreSQL以及其他一些关系数据库里面,这种日志叫做预写日志 (WAL)。管理这种WAL 及它的各种功能,是对这些数据库进行性能调优的重要部分,这也是PostgreSQL管理复制的方式。任何写入 WAL 的事务也会广播给复制方,好让复制一方把事务添加到自己的 WAL里面。
如果你想了解数据库是怎么工作的,并希望在出现问题的时候对数据库进行故障排除,那么了解这种机制就是关键。
2. 选择合适的数据库很难
我见过很多关于什么是“最好”或“最差”数据库的教条式的抨击,但事实上,最适合你的应用的数据库才是最好的数据库。没有那种一体万用的数据库,就像没有一体万用的编程语言或操作系统一样。
在开始一个新项目的时候,选择合适的数据库也许是你将要做出的最重要的决定之一。那么你应该怎么去选择用哪个数据库呢?我在那篇面向开发人员介绍的数据库文章中列出了需要考虑的五件事,不过在这里我们不妨再快速回顾一下。
什么样的数据要存储到数据库里面?
你存的是日志文件还是用户账号?
要存的数据有多复杂?
数据可以很轻松地规范化吗?
数据的统一程度如何?
你的数据是否大致遵循相同的模式,还是完全不同或严重嵌套的?
数据多久需要读取或写入一次?
你的应用读得多还是写得多,还是两者兼而有之?
是否有环境或商业方面的考虑?
我们是不是跟供应商签订过什么协议?我需要供应商支持吗?
通过回答这些问题,可以帮助把选择范围缩小到几个候选者。一旦选出这些候选者之后,测试应该可以告诉你哪一个才最适合你的应用。
3. 迁移到合适的数据库更加困难
有时你别无选择,数据库已经给你选好了。不管你是在项目启动后才加入该项目,还是政治风向用某种方式强迫你做出那种选择,但项目用错了数据库进行工作都会令人沮丧。
但同样地,如果有机会的话,迁移数据库的进程甚至会更令人沮丧。开弓没有回头箭,一旦你沿着一条路径走下去,到了有机会想改变路径就很困难了。你不仅需要想办法把数据从一个数据库复制到另一个数据库,同时学习一个全新的系统,而且根据你的数据库代码跟应用其余部分的紧耦合程度,你可能还需要考虑进行大规模的重写。更换数据库不是一件轻描淡写的任务,必须经过大量的考虑、讨论、测试以及计划。有很多方式可以让事情变得非常糟糕。这就是第二条如此重要的原因:一旦你做出了选择,那个选择就很难撤销了。
4. NoSQL不会取代 SQL,会是后者的补充
关于用SQL 还是NoSQL数据库之争会无休止地持续下去。我算是弄明白了。但他们吵来吵去往往忽视了一个事实,那就是NoSQL数据库不会取代 SQL 数据库。NoSQL是SQL的补充。
有些事情NoSQL数据库做得比较好,另外一些事情SQL 数据库做得比较好。Prometheus 非常擅长存储时间序列数据,比如指标,但你不会为此使用 MySQL。用MySQL做这个技术上可行吗?可行,但MySQL不是为此而设计的,你不会得到最好的性能或开发人员体验。另一方面,出于同样的原因,你也不希望用Redis来存储高度相关的数据,比方说用户帐户或金融交易。当然,你可以用代码搞定问题,但是当你可以用合适的工具来完成工作时,为什么还要增加复杂性,自找麻烦呢?
在某些领域不可避免会有一些重合的地方。有一些优秀的数据库在技术上属于NoSQL,但在存储关系数据方面做得也很好(参见:Couchbase ),但还有其他一些外部因素会影响你用这种而不是另一种。在选择数据库时,客户端语言支持、操作工具、云支持等都是需要考虑的因素。
5. 扩展很难
在尝试扩展时,数据库会提出独特的挑战。由于它们存储状态,因此本质上属于有状态的应用,所以你很难找到一种能用一致、安全且足够快且对应用透明的方式,跨多个数据库实例去复制这一状态。这就是为什么扩展数据库(尤其是关系数据库)最常见的方法是垂直扩展的原因。
说到这里我们稍微暂停一下,先花点时间谈谈两种扩展:垂直和水平。要正确理解这两种方式,需要单独去写一整篇文章(其实这不是一个坏主意……),但现在,我们可以用一种简单的方式来拆解一下。我认为有一个类比可以最好地解释这一点:假设你有容量为一升的一壶水,但水壶现在出现了一个小洞,所以你需要把水转移到另一个容器上。可是你手头只有一个不到一升的小一点的容器。你可以用两种方法转移水:
用大量比较小的容器(如水杯),把一升水分配到这些容器里面。
使用少量大一点的容器把将水分配到几个较大的容器里面。
使用很多的小容器是水平扩展,工作负载(或者说水)会分布到很多小容器里面。使用几个大容器是垂直扩展,负载会分布到少数大型容器上。如果是水平扩展,当你需要增加容量时,你可以添加更多容器。如果是垂直扩展,你增加原有的那几个容器。(编者注:水平扩展是指给资源池添加更多的机器,而垂直扩展指的是提高已存在的机器的能力,如CPU,内存等......)
对于数据库来说,通常的做法是增加 CPU 和 RAM 容量垂直扩展数据库实例。这避免了必须跨越大量实例复制状态的问题,但仍能让数据库承担额外的负载。
部分数据库——尤其是NoSQL数据库——可以进行大规模的水平扩展,但往往需要在一致性方面做出一些取舍。这些数据库往往最终还是能够保持一致,也就是数据最终会在整个集群中保持一致,但复制可能对访问这一数据的任何应用都不透明。也就是说,读取数据库节点 A的应用会得到一个版本的数据,而读取节点 D的应用得到的是另一个版本,直到集群更新完所有节点上的数据。虽然这听起来像是事务的破坏者,但如果你在设计和开发应用的时候考虑到了这一点,那么就不会成为问题。
6. 索引就像魔法,直到你搞清楚其中的奥秘
索引可以说是数据库最重要的东西之一了,但往往也是最容易被忽视的东西之一。索引,简单来说,就是你查找数据库的数据时候的目录。你不需要扫描一整列来找到某个值,索引可以告诉你这个值在什么位置,然后数据库引擎马上就可以跳到它那里。
如果你看到这里脱口而出,“嘿,这不就是哈希表吗?”,呃,你说的也没错。索引基本上就是针对表的特定列设计的哈希表。大多数关系数据库都会自动给主键创建索引,但你可以根据需要给任意数量的列建立索引。
但请不要这么做。
虽然索引可以显著加快读取速度——尤其是大型数据集的读取速度——但却要以数据写入表放缓作为牺牲。每次更新表的时候,表的索引也需要更新,这会为每一个写入事务增加额外的时间。因为索引就是干这个的。索引不像书的目录只是指针列表那样,而是包含了以不同顺序存储必要的列的第二个副本。这意味着索引要占用相应比例的磁盘空间,并在更新的时候需要 I/O处理。如果你只需要处理单表的几个索引,那这种权衡取舍往往可以忽略不计,但如果索引不止几个的话,事务就会导致写入速度受损,因为跟每一个事务相关的索引都需要更新一遍。
这就是为什么确定在什么时候什么地方使用索引的策略很重要的原因所在。决定要不要给某一列建立索引的最好办法是仔细看看你的应用,查看最常用的查询类型是什么,并以此为基础做出决定。此外,通过利用应用性能监控或数据库监控等工具,也可以跟踪各种数据库查询的事务执行时间,从而帮助发现可能通过添加索引而改进性能的那些查询。
7. 事务
每次跟数据库交互时,它都会利用所谓的事务这个概念。在数据库的世界里,事务就是一个原子的工作单位——一个独立的、离散的操作,它的结果只有这两个:要么提交,要么回滚。
这听起来可能没什么大不了的,但它对数据库的性能和操作有着非常实际的影响——对于那些符合 ACID (atomicity原子性、Consistency一致性、Isolation隔离性、Durability持久性)的数据库来说尤其如此。事务提交的顺序以及这些事务所处理的数据量可能会产生巨大影响,这具体要取决于数据库配置的事务隔离规模怎样。我的一位朋友曾经讲过一个关于事务阻塞的经历的小故事:
“很多年以前,有个人对‘数据库跑得慢且经常会超时’感到很恼火,后来才发现,原来是有一位团队成员被安排到哥斯达黎加工作,那里的互联网连接速度很慢,而他们安装在笔记本电脑的客户端应用会执行一个SELECT *的操作,但这条查询语句读取的表是每个人都要用到的,所以每次这条语句运行的时候,为了把全部结果都发往哥斯达黎加,数据库都会在发送结果的期间把整个表都锁定,因为就算是select也是一个事务。”
这是因为当客户端从服务器端接收数据时,数据库服务器必须保证数据的一致性,因此在运行查询并将数据返回给客户端的这段时间内数据库是没法处理其他事务的。一旦连接很慢、要处理的数据很多,或者这两者兼有的情况下,可能就会给使用数据库的其他人制造严重的瓶颈。
其中的部分限制可以通过调整请求来克服,比方说只读取需要的数据、利用靠近使用数据库的用户或应用的只读副本、调整事务的设置来满足需要。