Elixir 中使用 ETS 和 Mnesia 存储结构化数据
1. 行星数据概述
首先,我们有一组行星相关的数据,记录了不同行星的引力、直径以及与太阳的距离,如下表所示:
| Planemo | Gravity (m/s²) | Diameter (km) | Distance from Sun (10⁶ km) |
| ---- | ---- | ---- | ---- |
| earth | 9.8 | 12756 | 149.6 |
| moon | 1.6 | 3475 | 149.6 |
| mars | 3.7 | 6787 | 227.9 |
| ceres | 0.27 | 950 | 413.7 |
| jupiter | 23.1 | 142796 | 778.3 |
| saturn | 9.0 | 120660 | 1427.0 |
| uranus | 8.7 | 51118 | 2871.0 |
| neptune | 11.0 | 30200 | 4497.1 |
| pluto | 0.6 | 2300 | 5913.0 |
| haumea | 0.44 | 1150 | 6484.0 |
| makemake | 0.5 | 1500 | 6850.0 |
| eris | 0.8 | 2400 | 10210.0 |
2. 使用 ETS 存储数据
2.1 创建和填充表
在 Elixir 中可以使用
:ets
模块来操作 ETS(Erlang Term Storage)。使用
:ets.new/2
函数创建表,第一个参数是表名,第二个参数是选项列表。其中,
:named_table
和
:keypos
是两个重要选项。
-
:named_table
:指定表名是否可被其他进程访问。若不指定,表名仅在数据库内部可见;指定后,其他进程只要知道表名就能访问。
-
:keypos
:指定记录中的哪个值作为键。默认情况下,ETS 将元组的第一个值作为键,但对于记录来说,这种方式可能不合适。
行星记录的格式如下:
defmodule Planemo do
require Record
Record.defrecord :planemo, [name: :nil, gravity: 0, diameter: 0,
distance_from_sun: 0]
end
设置 ETS 表的示例代码如下:
planemo_table = :ets.new(:planemos,[ :named_table, {:keypos,
Planemo.planemo(:name) + 1} ])
上述代码创建了名为
:planemos
的表,使用
:named_table
使其对其他进程可见,默认访问级别为
:protected
,即当前进程可写,其他进程只读。同时,指定
:name
字段作为键。
创建表后,可以使用
:ets.info/1
函数查看表的详细信息。示例代码如下:
defmodule PlanemoStorage do
require Planemo
def setup do
planemo_table = :ets.new(:planemos,[:named_table,
{:keypos, Planemo.planemo(:name) + 1}])
:ets.info planemo_table
end
end
在 IEx 中运行上述代码,会得到一个空 ETS 表的详细信息:
iex -S mix
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [smp:4:4] [async-threads:10]
[hipe] [kernel-poll:false]
Compiling 2 files (.ex)
Generated planemo_storage app
Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> PlanemoStorage.setup
[read_concurrency: false, write_concurrency: false, compressed: false,
memory: 299, owner: #PID<0.57.0>, heir: :none, name: :planemos, size: 0,
node: :nonode@nohost, named_table: true, type: :set, keypos: 2,
protection: :protected]
需要注意的是,不能创建同名的 ETS 表,若重复调用
PlanemoStorage.setup/0
会报错。为避免此问题,可以使用
:ets.delete/1
删除表,或者在创建表前检查
:ets.info/1
是否返回
:undefined
。
2.2 向表中插入数据
使用
:ets.insert/2
函数向表中插入数据。示例代码如下:
defmodule PlanemoStorage do
require Planemo
def setup do
planemo_table = :ets.new(:planemos,[:named_table,
{:keypos, Planemo.planemo(:name) + 1}])
:ets.insert :planemos, Planemo.planemo(name: :mercury, gravity: 3.7,
diameter: 4878, distance_from_sun: 57.9)
:ets.insert :planemos, Planemo.planemo(name: :venus, gravity: 8.9,
diameter: 12104, distance_from_sun: 108.2)
# 其他行星数据插入...
:ets.insert :planemos, Planemo.planemo(name: :eris, gravity: 0.8,
diameter: 2400, distance_from_sun: 10210.0)
:ets.info planemo_table
end
end
再次使用
:ets.info/1
查看表信息,会发现表中有 14 条记录。可以使用
:ets.tab2list/1
函数查看表中的所有记录:
iex(7)> :ets.tab2list :planemos
[{:planemo, :neptune, 11.0, 30200, 4497.1},
{:planemo, :jupiter, 23.1, 142796, 778.3},
# 其他行星记录...
{:planemo, :eris, 0.8, 2400, 10210.0}]
也可以使用 Erlang 的
:observer.start()
启动表可视化工具,更直观地查看表内容。
2.3 简单查询
使用
:ets.lookup/2
函数根据键查找记录,返回值始终是一个列表。若确定只有一个返回值,可以使用
hd/1
函数获取列表的头部元素。示例如下:
iex(8)> :ets.lookup(:planemos, :eris)
[{:planemo, :eris, 0.8, 2400, 10210.0}]
iex(9)> hd(:ets.lookup(:planemos, :eris))
{:planemo, :eris, 0.8, 2400, 10210.0}
iex(10)> result = hd(:ets.lookup(:planemos, :eris))
{:planemo, :eris, 0.8, 2400, 10210.0}
iex(11)> require Planemo
Planemo
iex(12)> Planemo.planemo(result, :gravity)
0.8
2.4 覆盖值
可以使用
:ets.insert/2
函数覆盖已有键的值。示例如下:
iex(13)> :ets.insert(:planemos, Planemo.planemo(name: :mercury,
...(13)> gravity: 3.9, diameter: 4878, distance_from_sun: 57.9))
true
iex(14)> :ets.lookup(:planemos, :mercury)
[{:planemo, :mercury, 3.9, 4878, 57.9}]
但要注意,不要随意用 ETS 表内容替换变量,也不要将所有表都设为公共的,以免引入难以调试的错误。
2.5 ETS 表与进程
结合 ETS 表和
drop
模块可以创建更强大的下落速度计算器。示例代码如下:
defmodule Drop do
require Planemo
def drop do
setup
handle_drops
end
def handle_drops do
receive do
{from, planemo, distance} ->
send(from, {planemo, distance, fall_velocity(planemo, distance)})
handle_drops
end
end
def fall_velocity(planemo, distance) when distance >= 0 do
p = hd(:ets.lookup(:planemos, planemo))
:math.sqrt(2 * Planemo.planemo(p, :gravity) * distance)
end
def setup do
:ets.new(:planemos, [:named_table,
{:keypos, Planemo.planemo(:name) + 1}])
info = [
{:mercury, 3.7, 4878, 57.9},
{:venus, 8.9, 12104, 108.2},
# 其他行星数据...
{:eris, 0.8, 2400, 10210.0}]
insert_into_table(info)
end
def insert_into_table([]) do # stop recursion
:undefined
end
def insert_into_table([{name, gravity, diameter, distance} | tail]) do
:ets.insert(:planemos, Planemo.new(name: name, gravity: gravity,
diameter: diameter, distance_from_sun: distance))
insert_into_table(tail)
end
end
上述代码中,
drop/0
函数调用
setup
初始化表,
handle_drops
处理消息,
fall_velocity
根据行星名从 ETS 表中查找引力常数并计算下落速度。
setup
函数创建行星信息列表,递归调用
insert_into_table/1
插入数据。
在 IEx 中结合
mph_drop
模块可以计算不同行星上的下落速度:
iex(1)> c("drop.ex")
[Drop]
iex(2)> c("mph_drop.ex")
[MphDrop]
iex(3)> pid1 = spawn(MphDrop, :mph_drop, [])
#PID<0.47.0>
iex(4)> send(pid1, {:earth, 20})
On earth, a fall of 20 meters yields a velocity of 44.289078952755766 mph.
{:earth,20}
iex(5)> send(pid1, {:eris, 20})
On eris, a fall of 20 meters yields a velocity of 12.65402255793022 mph.
{:eris,20}
iex(6)> send(pid1, {:makemake, 20})
On makemake, a fall of 20 meters yields a velocity of 10.003883211552367 mph.
{:makemake,20}
3. ETS 的更多功能
-
复杂查询
:可以使用 Erlang 的匹配规范和
:ets.fun2ms创建更复杂的查询,如:ets.match和:ets.select。 -
删除操作
:使用
:ets.delete可以删除行和表。 -
表遍历
:
{:ets.first, :ets.next, :ets.last}函数可以递归遍历表。 - DETS :磁盘存储的 ETS,数据不会随控制进程停止而消失,但速度较慢,有 2GB 限制。
4. 存储记录到 Mnesia
Mnesia 是一个随 Erlang 提供的数据库管理系统,也可在 Elixir 中使用。它基于 ETS 和 DETS,但提供了更多功能。
当满足以下条件时,可考虑从 ETS(和 DETS)表迁移到 Mnesia 数据库:
- 需要在多个节点上存储和访问数据。
- 不想考虑数据存储在内存还是磁盘,或者两者都要。
- 需要在出现问题时回滚事务。
- 希望有更友好的语法来查找和连接数据。
- 管理层更喜欢“数据库”这个说法。
4.1 启动 Mnesia
若要将数据存储在磁盘上,需要先使用
:mnesia.create_schema/1
函数创建数据库:
iex(1)> :mnesia.create_schema([node()])
:ok
默认情况下,Mnesia 将模式数据存储在启动时的目录中,会创建一个名为
Mnesia.nonode@nohost
的目录。
启动 Mnesia 使用
:mnesia.start()
:
iex(2)> :mnesia.start()
:ok
启动后会在
Mnesia.nonode@nohost
目录中创建
schema.DAT
文件。也可以使用
:mnesia.stop/0
停止 Mnesia。
4.2 创建表
Mnesia 的表也是记录的集合,提供
:set
、
:orderered_set
和
:bag
选项,但不提供
:duplicate_bag
。Mnesia 需要更多关于数据的信息,通常定义记录并使用记录的字段名作为 Mnesia 表的字段名。
设置 Mnesia 表的示例代码如下:
defmodule Drop do
require Planemo
def drop do
setup
handle_drops
end
def handle_drops do
receive do
{from, planemo, distance} ->
send(from, {planemo, distance, fall_velocity(planemo, distance)})
handle_drops
end
end
def fall_velocity(planemo, distance) when distance >= 0 do
{:atomic, [p | _]} = :mnesia.transaction(fn() ->
:mnesia.read(PlanemoTable, planemo) end)
:math.sqrt(2 * Planemo.planemo(p, :gravity) * distance)
end
def setup do
:mnesia.create_schema([node()])
:mnesia.start()
:mnesia.create_table(PlanemoTable, [{:attributes,
[:name, :gravity, :diameter, :distance_from_sun]},
{:record_name, :planemo}])
f = fn ->
:mnesia.write(PlanemoTable, Planemo.planemo(name: :mercury, gravity: 3.7,
diameter: 4878, distance_from_sun: 57.9), :write)
:mnesia.write(PlanemoTable, Planemo.planemo(name: :venus, gravity: 8.9,
diameter: 12104, distance_from_sun: 108.2), :write)
# 其他行星数据写入...
:mnesia.write(PlanemoTable, Planemo.planemo(name: :eris, gravity: 0.8,
diameter: 2400, distance_from_sun: 10210.0), :write)
end
:mnesia.transaction(f)
end
end
在
setup
函数中,
{:mnesia.create_table}
明确指定表的属性,
{:mnesia_write}
调用需要三个参数:表名、记录和数据库锁类型。所有写入操作都包含在一个函数中,并通过
{:mnesia.transaction}
作为事务执行。
使用
{:mnesia.table_info}
函数可以查看表的详细信息:
iex(1)> c("drop.ex")
[Drop]
iex(2)> Drop.setup
{:atomic,:ok}
iex(3)> :mnesia.table_info(PlanemoTable, :all)
[access_mode: :read_write,
active_replicas: [:"nonode@nohost"],
all_nodes: [:"nonode@nohost"],
arity: 5,
attributes: [:name,:gravity,:diameter,:distance_from_sun],
...
ram_copies: [:"nonode@nohost"],
record_name: :planemo,
record_validation: {:planemo,5,:set},
type: :set,
size: 14,
...]
默认情况下,Mnesia 将表存储在当前节点的 RAM 中,速度快,但节点崩溃时数据会丢失。可以指定
{:disc_copies}
将数据库副本存储在磁盘上,同时使用 RAM 提高速度;也可以指定
{:disc_only_copies}
,但速度较慢。
综上所述,ETS 和 Mnesia 都可用于在 Elixir 中存储结构化数据,具体选择取决于应用的需求。ETS 适用于简单的键值存储和快速访问,Mnesia 则更适合复杂的多节点数据存储和事务处理。
Elixir 中使用 ETS 和 Mnesia 存储结构化数据(续)
5. Mnesia 的事务处理
在 Mnesia 中,所有重要的操作(如写入、读取和删除)都应该在事务中进行,特别是当数据库在多个节点间共享时。这是因为 Mnesia 的主要操作方法(如
:mnesia.write
、
:mnesia.read
和
:mnesia.delete
)只能在事务内工作。
事务处理的核心机制是将操作封装在一个匿名函数中,然后通过
:mnesia.transaction
来执行。例如在之前创建行星数据表的代码中:
f = fn ->
:mnesia.write(PlanemoTable, Planemo.planemo(name: :mercury, gravity: 3.7,
diameter: 4878, distance_from_sun: 57.9), :write)
# 其他行星数据写入...
:mnesia.write(PlanemoTable, Planemo.planemo(name: :eris, gravity: 0.8,
diameter: 2400, distance_from_sun: 10210.0), :write)
end
:mnesia.transaction(f)
Mnesia 会在有其他活动阻塞事务时重新启动事务,所以传递给
:mnesia.transaction
的函数可能会被多次执行。因此,在这个函数中不要包含会产生副作用的调用,也不要在事务内捕获 Mnesia 函数的异常。如果函数调用了
:mnesia.abort/1
(可能是因为执行条件不满足),事务将被回滚,返回的元组将以
aborted
开头,而不是
atomic
。
另外,当需要在事务中混合更多类型的任务时,可以探索更灵活的
:mnesia.activity/2
函数。
6. Mnesia 表的不同存储模式
Mnesia 提供了多种存储模式,不同的模式适用于不同的场景,以下是对这些模式的详细介绍:
| 存储模式 | 特点 | 适用场景 |
| ---- | ---- | ---- |
|
ram_copies
| 数据仅存储在当前节点的内存中,读写速度快,但节点崩溃时数据会丢失。 | 对读写速度要求极高,且数据丢失影响较小的场景,如缓存。 |
|
disc_copies
| 在磁盘上保留数据库的副本,同时使用内存进行快速访问。 | 需要保证数据持久化,同时又希望有较好读写性能的场景。 |
|
disc_only_copies
| 数据仅存储在磁盘上,速度较慢。 | 数据量较大,对内存使用有严格限制的场景。 |
通过合理组合这些存储模式和多个节点,可以创建出快速且具有弹性的系统。例如,在一个分布式系统中,可以将重要的数据表设置为
disc_copies
模式,以确保数据的持久化和可用性;对于一些临时的、可重建的数据表,可以使用
ram_copies
模式来提高性能。
7. ETS 与 Mnesia 的对比总结
ETS 和 Mnesia 虽然都可以用于在 Elixir 中存储结构化数据,但它们在功能和适用场景上有明显的区别。以下是一个对比表格:
| 特性 | ETS | Mnesia |
| ---- | ---- | ---- |
| 数据存储位置 | 主要在内存,DETS 可在磁盘 | 可在内存、磁盘或两者结合 |
| 多节点支持 | 无直接支持 | 支持多节点数据存储和访问 |
| 事务处理 | 无 | 支持事务回滚 |
| 查询语法 | 简单,基本的键值查询 | 更灵活,有更友好的语法用于复杂查询 |
| 数据持久化 | DETS 有一定持久化能力,但有 2GB 限制 | 可配置多种持久化模式,数据更安全 |
| 性能 | 读写速度快 | 事务处理时性能有一定开销 |
根据这些对比,我们可以总结出它们的适用场景:
-
ETS
:适用于简单的键值存储,对读写速度要求极高,且数据不需要持久化或多节点共享的场景。例如,在一个 Web 应用中,用于缓存一些频繁访问的静态数据,如配置信息、热门文章列表等。
-
Mnesia
:适合复杂的多节点数据存储和事务处理场景。比如,在一个分布式电商系统中,用于存储订单信息、用户账户信息等,需要保证数据的一致性和完整性,并且支持事务操作来处理并发的订单处理和支付操作。
8. 实际应用建议
在实际开发中,我们可以根据具体的需求来选择合适的存储方案。以下是一些具体的建议和操作步骤:
8.1 简单缓存场景
如果只是需要一个简单的缓存来提高数据访问速度,可以选择 ETS。操作步骤如下:
1.
创建 ETS 表
:使用
:ets.new
函数创建一个命名表,并指定合适的键位置。
cache_table = :ets.new(:data_cache, [:named_table, {:keypos, 1}])
-
插入数据
:使用
:ets.insert函数将数据插入到表中。
:ets.insert(cache_table, {:key1, "value1"})
-
查询数据
:使用
:ets.lookup函数根据键查询数据。
result = :ets.lookup(cache_table, :key1)
8.2 复杂分布式数据存储场景
如果应用需要处理复杂的多节点数据存储和事务处理,Mnesia 是更好的选择。操作步骤如下:
1.
启动 Mnesia
:首先创建数据库模式,然后启动 Mnesia。
:mnesia.create_schema([node()])
:mnesia.start()
-
创建表
:使用
:mnesia.create_table函数创建表,并指定表的属性和存储模式。
:mnesia.create_table(OrderTable, [
{:attributes, [:order_id, :user_id, :amount, :status]},
{:record_name, :order},
{:disc_copies, [node()]}
])
- 执行事务操作 :将数据插入、读取或删除操作封装在事务中。
f = fn ->
:mnesia.write(OrderTable, {:order, 1, 101, 200.0, :pending}, :write)
end
:mnesia.transaction(f)
9. 总结与展望
通过对 ETS 和 Mnesia 的学习,我们了解到它们在 Elixir 生态系统中为存储结构化数据提供了强大的支持。ETS 以其简单高效的特点,为快速数据访问提供了解决方案;而 Mnesia 则通过丰富的功能,满足了复杂分布式系统对数据一致性和事务处理的需求。
在未来的开发中,随着分布式系统和大数据应用的不断发展,Mnesia 的多节点支持和事务处理能力将发挥更大的作用。同时,ETS 作为轻量级的内存存储,也将在缓存和临时数据存储方面继续发挥重要作用。开发者可以根据具体的业务需求,灵活选择和组合这两种存储方案,构建出高效、稳定的应用系统。
希望通过本文的介绍,能帮助开发者更好地理解和使用 ETS 和 Mnesia,在 Elixir 开发中更加得心应手地处理结构化数据存储问题。
超级会员免费看
1860

被折叠的 条评论
为什么被折叠?



