认识一下 Datomic:不可变且函数式的数据库。
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
“将两段数据结合起来,你会得到数据。但将两台机器结合起来,你会遇到麻烦。”——里奇·希基在《函数式数据库》一书中如是说。
函数式编程的概念,尤其是关于不可变性的概念,已经越来越多地出现在我们的日常生活中,因此,没有什么比了解一个以数据不可变性为理念的数据库更合适的了,它以一种与我们习惯的完全不同的方式带来了对事实的控制。
在本文中,我们将了解 Datomic,它之所以得名,正是因为它以与传统格式略有不同的格式呈现数据,力求将数据不可变性更接近数据库级别,并采用专注于与分布式系统良好配合的功能性方法。
目录
什么是 Datomic?
2012 年初,Relevance 团队(后来与 Metadata 合并组成 Cognitec)与 Rich Hickey 一起推出了 Datomic,他们从 2010 年开始着手开发 Datomic,其主要动机是将分配给数据库服务器的大部分能力转移到应用程序服务器,以便程序员在应用程序逻辑中拥有更大的数据编程能力。
Datomic Cloud于2018年初发布,使用了亚马逊的组件:
- DynamoDB、EFS、EBS 和 S3 作为存储服务;
- 使用 CloudFormation 进行部署;
- AWS CloudWatch 用于日志记录、监控和指标管理。
Cognitect(之前负责开发 Datomic 的公司)于 2020 年被 Nubank 收购,Nubank 于 2023 年 4 月宣布 Datomic 二进制文件已公开提供并可免费使用(这意味着其 Pro 版本现在可以免费使用)。
Datomic是用Clojure编写的,它的工作方式与我们通常使用的数据库略有不同,它用于管理数据,而不是存储数据。我们将详细介绍它的架构,但简而言之,这意味着Datomic可以使用多种其他数据存储服务(甚至是其他数据库)来存储事务,从而实现良好的组合。
概念
Datomic 的核心理念是数据整体上是不可变的。为了帮助您更好地理解其工作原理,我们可以用以下这个有趣的类比:
- 想象一下你经常使用的数据库,例如 PostgreSQL 或 MySQL;
- 现在假设你有两个表,一个是产品表,还有一个是这些产品的日志表,存储对原始产品表所做的每一次修改;
- 当您更新一个项目时,该项目在产品表中的数据会被修改,但我们会将其先前的值添加到日志表中,以突出显示所做的操作(在本例中是对其值的更新);
- 对于 Datomic 来说,只会有一个产品“表”(在我们的例子中是schema),外加一个额外的列,指示该项是否为真。因此,当我们更新产品值时,会添加一个新行来表示新值,但是,旧行的 check 列值现在设置为 false,毕竟,它当前不再为真。
这意味着过去的数据仍然存在,但有一种表示方法可以表明产品的价值是否有效。这些线被称为事实,所以当提到“事实”这个词时,请记住这个类比。
需要强调的一点是:记住,无论产品的价值发生了多大的变化,它仍然是一个事实——虽然不再适用于当前情况,但它仍然是曾经发生过的事实,毕竟,该产品过去确实具有这种价值。
建筑学
为了更好地了解 Datomic 内部的一切运作方式,我们首先需要更好地了解它的架构是如何运作的。
我该如何存储数据?
如前所述,Datomic 的特点并非像我们通常理解的那样以数据库的方式存储数据,它主要用于“数据交易和管理”。您可以将 Datomic 以多种方式组合使用,例如:
- SQL 数据库(例如 PostgreSQL 和 MySQL);
- DynamoDB(如果您选择使用 Datomic Cloud,索引将存储在 S3 中,事务日志将存储在 DynamoDB 中,缓存将存储在 EFS 中);
- Cassandra 和 Cassandra2;
- 开发模式,数据存储在内存中。工作原理非常简单:根据您的选择,将在数据库中创建一个表来存储所有数据,Datomic 将负责管理这些数据。
同龄人
应用程序与 Datomic 的所有交互都将通过 Peer 进行,Peer 负责执行简单的缓存控制、组装和执行查询、获取索引数据以及发送提交。Peer 主要有两种使用方式:
- 对等库:一个添加到您的依赖项中的库,它将始终与您的应用程序一起工作,毕竟,您将通过它对数据库执行任何类型的操作;
- 对等服务器:主要与 Datomic 的云格式一起使用,您的应用程序现在只有一个客户端库,负责与对等服务器通信,该服务器将对 Datomic 执行直接操作,以及对等库。
节点可以长时间无忧地使用给定的数据库值。这些值是不可变的,能够提供稳定一致的数据视图,满足节点的长期需求,相当于数据库的“快照”,从而以更高效的方式返回数据,避免实时执行大量查询而造成过载。这与关系型数据库截然不同,关系型数据库需要使用生命周期短的数据快速完成工作。您可以在官方文档中查看更多关于节点工作原理的描述。
需要强调的一点是:如果您使用 Peer 库,应用程序的每个节点、每个服务都会有一个 Peer 进程与之并行运行。这意味着在分布式系统中,这些 Peer 进程将负责向数据库发送提交操作。然而,在分布式系统中,控制数据一致性至关重要。毕竟,如果多个服务并行发送查询,如何确保数据的真实性并正确通过竞态条件验证呢?为了更好地理解这一点,我们先来了解一下 Transactor 是什么。
交易者
事务处理器 (Transactor)负责处理接收到的提交,并将数据存储到数据库中。基于 Datomic 的应用程序架构的特点是:多个 Peer 节点同时工作并发送提交(毕竟,我们会有多个服务),但只有一个事务处理器,从而保证数据的完全一致性。这意味着,无论事务处理器每秒接收多少个提交,它们都会被排队,以确保数据的完全一致性。
这种架构格式的主要观点源于这样一种认识:处理并发性,允许多个数据并行存储在数据库中,尤其是在分布式系统中,可能会对存储数据的一致性产生负面影响,这是一个严重的问题。
现在,我们可以更详细地看一下这张图表,它展示了整个架构的运行方式:
如上图所示,事务器 (Transactor) 负责所有需要与数据存储服务直接通信的活动,此外还负责控制数据索引格式、操作 memcached 集群以及响应提交操作。因此,我们可以说 Datomic 处理的是ACID事务,ACID 是定义事务的四个关键属性的首字母缩写:原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability)。
存储服务
“对等节点从存储服务读取数据。存储服务返回的数据永不改变,因此对等节点会进行大量的缓存。每个对等节点的缓存都代表数据库中所有数据的部分副本。对等节点缓存采用最近最少使用 (LRU) 策略来丢弃数据,从而可以处理无法完全加载到内存中的数据库。一旦对等节点的工作集被缓存,读取操作几乎不会产生网络流量。”——摘自Datomic Pro 文档。
您可以根据自己的喜好配置存储服务,您只需创建一个属性文件(名为transactor.properties)来表示您的事务处理程序将如何创建和管理。
例如,如果您使用 PostgreSQL,则需要配置要使用的驱动程序、连接 URL、用户名和密码,还可以配置 `<username>`、`<username>`、`<username>` 甚至 `<username>` 的值memory-index-threshold,memory-index-max最终object-cache-max在read-concurrencyPostgreSQL中write-concurrency创建一个名为 `<table_name>` 的表datomic_kvs:
CREATE TABLE datomic_kvs (
id text NOT NULL,
rev integer,
map text,
val bytea,
CONSTRAINT pk_id PRIMARY KEY (id)
) WITH (OIDS=FALSE);
数据结构
我们已经讨论了 Datomic 的整体架构,现在让我们更好地了解 Datomic 数据结构的基础,从Datoms开始。
数据
“数据原子是不可变的原子事实,它表示实体、属性、值和事务之间关系的添加或撤销。”
所以,数据原子(datom)本质上是日志中的一个简单的事实,表示关系的数据变化。我们可以将数据原子表示为一个五元组:
- 实体 ID (E)
- 属性(A)
- 属性 (V) 的值
- 交易 ID (Tx)
- 一个布尔值(Op),指示数据原子是被添加还是被移除。
以上内容均来自Datomic Cloud 文档。请看下面的示例:
| E | 42 |
|---|---|
| 一个 | :user/favorite-color |
| V | :蓝色的 |
| 德克萨斯州 | 1234 |
| 操作 | 真的 |
实体
“Datomic 实体提供了一种惰性的、关联式的视图,可以访问从 Datomic 实体 ID 可以访问的所有信息。”
我们可以将实体可视化为一个表格:
| E | 一个 | V | 德克萨斯州 | 操作 |
|---|---|---|---|---|
| 42 | :user/favorite-color | :蓝色的 | 1234 | 真的 |
| 42 | :user/名字 | 约翰 | 1234 | 真的 |
| 42 | :用户名/姓氏 | “Doe” | 1234 | 真的 |
| 42 | :user/favorite-color | :绿色的 | 4567 | 真的 |
| 42 | :user/favorite-color | :蓝色的 | 4567 | 错误的 |
交易ID可以被视为代表该数据的时间点。在上面的示例中,我们有1234和4567。请看1234……在第一行中,:user/favorite-colour属性 的值为:blue,其中op为true。但是,在未来的某个时间点,4567属性 的值为 时,该属性的 被op设置为 false :blue(此时:green的值为 true)。
对我们来说,我们没有手动更改过这些值Op。Datomic 会在我们更新值时自动完成此操作。这意味着:Datomic 会自动管理我们的数据并设置或更新值,我们能够准确地知道值更改的:user/favorite-color时间点。:user/favorite-color
模式
正如文档所述:属性的定义采用与应用程序数据相同的数据模型。也就是说,属性本身由具有关联属性的实体定义。
要定义一个新属性,我们需要定义:
:db/ident数据库中唯一的名称:db/cardinality指定实体的该属性可以具有一个值还是一组值。:db/valueType该类型允许属性值:db/doc(可选)属性的描述/文档
你看,所有这些:db/ident,:db/cardinality等等,都只是相互指向的简单实体。它们在初始阶段由 Datomic 自动生成。这意味着:它们有一个默认值entity id。
交易是如何运作的?
“在 Datomic 中,每一笔交易都是一个独立的实体,因此很容易添加有关交易添加原因(或添加者、来源等)的信息。”
我们有两种交易方式:add或retraction。每笔交易都会返回交易 ID 以及交易前后的数据库状态。交易形式可以是:
[:db/add entity-id attribute value]
[:db/retract entity-id attribute value]
正如我们之前所见:每个事务都以排队的方式进行。
如果事务成功完成,数据将被提交到数据库,并且我们将获得一个事务报告,该报告以映射的形式返回,包含以下键:
| 钥匙 | 用法 |
|---|---|
| :db-before | 事务处理前的数据库值 |
| :db-after | 交易后的数据库值 |
| :tx-数据 | 交易产生的数据 |
| :tempids | 将临时 ID 映射到已分配的 ID |
数据库值就像数据库的“快照”,正如我们之前所看到的。
让我们来看一个例子,了解它的:db/add工作原理。请看下面的例子:
;; We have this schema
{:internal/id ...
:internal/value 0
:internal/key " "}
;; Making a simple transaction
[[:db/add id :internal/value 1]]
;; This will update the value...
;; But, we can perform multiple
;; transactions, look:
[[:db/add id :internal/value 1]
[:db/add id :internal/key "another"]]
;; It will work fine.
;; But, when we perform something like:
[[:db/add id :internal/value 1]
[:db/add id :internal/value 2]]
;; We will have a conflict
当同一个实体的同一个属性发生更改时,就会出现冲突。这是合理的,因为同一个事实不能在同一时间段内多次更新。
一个很棒的事实:如果我们执行多个事务,它们会并行处理(采用多进程处理)。这很安全,因为正如我们之前看到的,同一个属性不能在同一个事务中被更新。
结论
本文及本系列文章的第一篇旨在介绍 Datomic,并展示其各种功能和优势。值得强调的是,Datomic 官方文档非常出色,因此,如需进行更深入的研究,请务必参考官方文档!当然,如果您想开始使用 Datomic,可以参考官方文档中的“入门指南”;如果您需要包含所用代码的仓库,我已经在我的 GitHub 上创建了一个仓库(别忘了点个赞哦)!
文章来源:https://dev.to/guto/meet-datomic-the-immutable-and-functions-database-10af