为什么这么规划(Why’s THE Design)是一系列关于核算机范畴中程序规划决议计划的文章,咱们在这个系列的每一篇文章中都会提出一个详细的问题并从不同的角度讨论这种规划的优缺点、对详细完结形成的影响。假如你有想要了解的问题,可以在文章下面留言。

当咱们想要持久化地存储数据时,运用联系型数据库往往都是最保险的挑选,这不只因为今日的联系型数据库种类十分丰富并且安稳,还因为不同社区对联系型数据库的支撑都十分完备。咱们在前面的文章中曾经剖析过 为什么 MySQL 的自增主键不单调也不连续,这篇文章咱们来剖析联系型数据库中另一个重要的概念 — 外键(Foreign Key)。

在联系型数据库中,外键也被称为联系键,它是联系型数据库中提供联系表之间衔接的多个列1,这一组数据列是当时联系表中的外键,也有必要是另一个联系表中的候选键(Candidate Key),咱们可以经过候选键在当时表中找到唯一的元素2。在通常状况下,咱们都会运用联系表中的主键作为其他表中的外键,这样才干够满足联系型数据库对外键的束缚。

为什么数据库不应该使用外键

图 1 – 联系型数据库与外键

外键不只仅是数据库表中的一个整数,它还提供了额定的共同性确保。因为数据库往往是整个体系的真理之源(Source of Truth),所以确保数据的共同性和正确性十分重要,联系型数据库尽管提供了外键、触发器等特性确保共同性,可是在今日的生产环境中却很少被运用。

引证完整性(Referential Integrity)是数据的属性,假如数据拥有该属性,那么数据中一切的引证都是合法的,在联系型数据库的上下文中,这就意味着联系型数据库中引证另一个表中的值有必要存在3。

ALTER TABLE posts
ADD CONSTRAINT FOREIGN KEY (author_id)
REFERENCES authors(id);

上述 SQL 句子可以向联系表中增加外键束缚,该 SQL 句子的履行前提是 posts 表中存在 author_id 字段。从 SQL 句子中的 CONSTRAINT 要害字咱们也能估测出外键不是一种数据类型,它是不同联系表之间的束缚。

为什么数据库不应该使用外键

图 2 – 无状况服务与数据库

不运用外键的原因其实很简单,MySQL、PostgreSQL 等联系型数据库很难水平扩容,可是无状况的服务往往都可以很简单地扩容。因为外键等特性需求数据库履行额定的作业,而这些操作会占用数据库的核算资源,所以咱们可以将大部分的需求都迁移到无状况的服务中完结以降低数据库的作业负载。

依据更新和删去时的行为不同,咱们可以将外键分红 RESTRICT、CASCADE 和 SET NULL 等几种4,当咱们为联系表中的字段增加外键束缚时,需求指定外键的类型,最常见的也便是 RESTRICT 和 CASCADE 两种,其间 RESTRICT 为外键的默许类型,不同类型的外键会带来不同的额定开支,而这些额定开支便是咱们不运用外键的理由:

  • 运用 RESTRICT 会在更新或许删去记载时对外键对应的记载是否存在进行共同性查看;
  • 运用 CASCADE 会在更新或许删去记载时触发级联更新或许删去操作;

注意:MySQL 中的 NO ACTION 和 RESTRICT 具有相同的语义5。

接下来咱们会详细介绍联系型数据库如何处理上述两种不同类型的外键,而咱们应该如安在运用中模仿这些功用。

共同性查看

当咱们运用默许的外键类型 RESTRICT 时,在创立、修正或许删去记载时都会查看引证的合法性。想要在 MySQL 等数据库中触发外键的共同性查看其实十分简单,假定咱们的数据库中包含 posts(id, author_id, content) 和 authors(id, name)两张表,在履行如下所示的操作时都会触发数据库对外键的查看:

  • 向 posts 表中刺进数据时,查看 author_id 是否在 authors 表中存在;
  • 修正 posts 表中的数据时,查看 author_id 是否在 authors 表中存在;
  • 删去 authors 表中的数据时,查看 posts 中是否存在引证当时记载的外键;

作为专门用于办理数据的体系,数据库与运用服务相比可以更好地确保完整性,而上述的这些操作都是引进外键带来的额定作业,不过这也是数据库确保数据完整性的必要价值。上述的这些剖析都是理论上的定性剖析,咱们其实可以简单的定量剖析一下引进外键对功用的影响。

在这儿咱们在数据库中一起创立 authors、posts 和 foreign_key_posts 三种表,如下所示,其间 posts 和 foreign_key_posts 两个表中的列完全相同,仅仅 foreign_key_posts 表为 author_id 字段增加了 RESTRICT 类型的外键束缚:

为什么数据库不应该使用外键

图 3 – 外键功用测验联系图

咱们先在 authors 表中刺进一条记载,随后分别在 posts 和 foreign_key_posts中刺进多条新数据列引证该条记载,前者不会查看外键的合法性,而后者会做额定的查看。你可以在 这儿 找到作者用来测验外键额定开支的 Go 言语代码6,经过屡次基准测验,咱们可以得到如下所示的成果:

BenchmarkBaseline-8     	    3770	    309503 ns/op
BenchmarkForeignKey-8   	    3331	    317162 ns/op
BenchmarkBaseline-8     	    3192	    315506 ns/op
BenchmarkForeignKey-8   	    3381	    315577 ns/op
BenchmarkBaseline-8     	    3298	    312761 ns/op
BenchmarkForeignKey-8   	    3829	    345342 ns/op
BenchmarkBaseline-8     	    3753	    291642 ns/op
BenchmarkForeignKey-8   	    3948	    325239 ns/op

作者履行了 4 次外键的基准测验,尽管 4 次测验的成果不是特别安稳,可是运用外键的用例在每次测验中都显着弱于不运用外键的用例,外键带来的额定开支分别为 ~2.47%、~0.02%、~10.41% 和 ~11.52%。这儿的基准测验仅仅一个比较简单的定量剖析,可是咱们也可以从成果中看到大概的趋势 — 外键的完整性查看确实会带来额定的功用开支,而这些开支在高并发的服务中需求慎重考虑。

想要在运用程序中模仿数据库外键的功用其实比较简单,咱们只需求遵从以下的几个准则:

  • 向表中刺进数据或许修正表中的数据时,都应该履行额定的 SELECT 句子确保它引证的数据在数据库中存在;
  • 在删去数据之前需求履行额定的 SELECT 句子查看是否存在当时记载的引证;

需求注意的是为了确保共同性,咱们需求在业务中履行上述的查询和修正句子,这样才干完整模仿外键的功用;当咱们向 posts 表中刺进或许修正数据时,需求的处理相对比较简单,咱们只需求履行有限的 SELECT 句子并依照如下所示的模式履行对应的操作就可以了:

BEGIN
SELECT * FROM authors WHERE id = <post.author_id> FOR UPDATE;
-- INSERT INTO posts ... / UPDATE posts ...
END

可是假如咱们要删去 authors 表中的数据,就需求查询一切引证 authors 数据的表;假如有 10 个表都有指向 authors 表的外键,咱们就需求在 10 个表中查询是否存在对应的记载,这个进程相对比较费事,不过也是为了完结完整性的必要价值,不过这种模仿外键办法其实远比运用外键更消耗资源,它不只需求查询相关数据,还要经过网络发送更多的数据包。

级联操作

当咱们在联系型数据库中创立外键束缚时,假如运用如下所示的 SQL 句子指定更新或许删去记载时运用 CASCADE 行为,那么在客户端更新或许删去数据时就会触发级联操作:

ALTER TABLE posts
ADD CONSTRAINT FOREIGN KEY (author_id)
REFERENCES authors(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
  • 当客户端更新 authors 表中记载的主键时,数据库会一起更新 posts 表中一切引证该记载的外键;
  • 当客户端删去 authors 表中的记载时,数据库会删去一切与 authors 表相关的记载;

不过无论是履行更新仍是删去操作,数据库都可以确保各个联系表之间引证的共同性和合法性不会呈现引证到不存在记载的状况,与 RESTRICT 行为一样,一切外键的更新和删去行为都可以经过履行额定的查看和操作确保数据的共同。

为什么数据库不应该使用外键

图 4 – 杂乱的级联操作

尽管级联删去的起点也是确保数据的完整性,可是在规划联系表之间的不同联系时,咱们也需求注意级联删去引起的数据大规模删去的问题。如上图所示,当客户端想要在数据库中删去 authos 表中的数据时,假如咱们一起在 authors 和 posts中指定了级联删去的行为,那么数据库会一起删去一切相关的 posts 记载以及与 posts 表相关的 comments 数据。

这种涉及多级的级联删去行为在数据量较小的数据库中不会导致问题,可是在数据量较大的数据库中删去要害数据或许会引起雪崩,一条记载的删去或许会被扩大到几十倍甚至上百倍,这些对磁盘的随机读写会带来巨大的开支,是咱们想要尽或许防止的状况。假如咱们可以较好地规划各个表之间的联系并且慎用 CASCADE 行为,这对于确保数据库中数据的合法性有着很重要的意义,运用该特功用够防止数据库中呈现过期的、不合法的数据,可是在运用时也要合理预估或许形成的最坏状况。

手动完结数据库的级联删去操作是可行的,假如咱们在一个业务中依照顺序删去一切的数据,确实可以确保数据的共同性,可是这与外键的级联删去功用没有太大的差异,反而会有更差的表现。假如咱们可以接受在一个时刻窗口内的数据不共同,就可以将一个大号的删去使命拆成多个子使命分批履行,降低对数据库影响的峰值。

DELETE FROM posts WHERE author_id = 1 LIMIT 100;
DELETE FROM posts WHERE author_id = 1 LIMIT 100;
...
DELETE FROM authors WHERE id = 1;

与数据库外键的 CASCADE 相比,这种方法会带来更大的额定开支,仅仅咱们能降低对数据库功用的瞬时影响。

总结

外键提供的几种在更新和删去时的不同行为都可以协助咱们确保数据库中数据的共同性和引证合法性,可是外键的运用也需求数据库承当额定的开支,在大多数服务都可以水平扩容的今日,高并发场景中运用外键确实会影响服务的吞吐量上限。在数据库之外手动完结外键的功用是或许的,可是却会带来许多保护上的成本或许需求咱们在数据共同性上做出一些退让。咱们可以从可用性、共同性几个方面剖析运用外键、模仿外键以及不运用外键的差异:

  • 不运用外键牺牲了数据库中数据的共同性,可是却可以削减数据库的负载;
  • 模仿外键将一部分作业移到了数据库之外,咱们或许需求抛弃一部分共同性以获得更高的可用性,可是为了这部分可用性,咱们会付出更多的研制与保护成本,也增加了与数据库之间的网络通信次数;
  • 运用外键确保了数据库中数据的共同性,也将悉数的核算使命悉数交给了数据库;

在大多数不需求高并发或许对共同性有较强要求的体系中,咱们可以直接运用数据库提供的外键协助咱们对数据进行校验,可是在对共同性要求不高的、杂乱的场景或许大规模的团队中,不运用外键也确实可认为数据库减负,而大团队也有更多的时刻和精力去规划其他的方案,例如:分布式的联系型数据库。

当咱们考虑应不应该在数据库中运用外键时,需求关注的中心咱们的数据库承当这部分核算使命后会不会影响体系的可用性,在运用时也不应该一刀切的决议用或许不用外键,应该依据详细的场景做决议计划,咱们在这儿介绍了两个运用外键时或许遇到的问题:

  • RESTRICT 外键会在更新和删去联系表中的数据时对外键束缚的合法性进行查看,确保外键不会引证到不存在的记载;
  • CASCADE 外键会在更新和删去联系表中的数据时触发对相关记载的更新和删去,在数据量较大的数据库中或许会有数量级的扩大作用;

咱们在许多时候其实并不能挑选是否运用外键,大多数公司的 DBA 都会对数据库体系的运用有比较明确的规则,可是咱们要清楚做出运用外键和不运用外键这一选择的原因。到最后,咱们仍是来看一些比较开放的相关问题,有兴趣的读者可以细心思考一下下面的问题:

  • 数据库中还有哪些特性是咱们在生产环境中不会运用的?为什么?
  • 分布式的联系型数据库与 MySQL 等传统数据库有哪些差异?