14、Elixir 中使用 ETS 和 Mnesia 存储结构化数据

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}])
  1. 插入数据 :使用 :ets.insert 函数将数据插入到表中。
:ets.insert(cache_table, {:key1, "value1"})
  1. 查询数据 :使用 :ets.lookup 函数根据键查询数据。
result = :ets.lookup(cache_table, :key1)
8.2 复杂分布式数据存储场景

如果应用需要处理复杂的多节点数据存储和事务处理,Mnesia 是更好的选择。操作步骤如下:
1. 启动 Mnesia :首先创建数据库模式,然后启动 Mnesia。

:mnesia.create_schema([node()])
:mnesia.start()
  1. 创建表 :使用 :mnesia.create_table 函数创建表,并指定表的属性和存储模式。
:mnesia.create_table(OrderTable, [
  {:attributes, [:order_id, :user_id, :amount, :status]},
  {:record_name, :order},
  {:disc_copies, [node()]}
])
  1. 执行事务操作 :将数据插入、读取或删除操作封装在事务中。
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 开发中更加得心应手地处理结构化数据存储问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值