Erlang/OTP 完全指南 / 14 - Mnesia 数据库
第 14 章:Mnesia — Erlang 内置分布式数据库
Mnesia 是 Erlang/OTP 内置的分布式、软实时数据库,支持事务和多种表类型,特别适合电信和分布式系统。
14.1 Mnesia 简介
14.1.1 特性
| 特性 | 说明 |
|---|---|
| 分布式 | 数据可跨节点复制 |
| 事务 | ACID 事务支持 |
| 软实时 | 低延迟读写 |
| 混合存储 | 内存 + 磁盘 |
| Schema 动态 | 运行时创建/修改表 |
| 与 Erlang 深度集成 | 存储任意 Erlang term |
14.1.2 vs ETS vs 外部数据库
| 特性 | ETS | Mnesia | PostgreSQL |
|---|---|---|---|
| 持久化 | ❌ | ✅ | ✅ |
| 分布式 | ❌ | ✅ | ❌ |
| 事务 | ❌ | ✅ | ✅ |
| 复杂查询 | Match Spec | QLC / Match | SQL |
| 延迟 | 极低 | 低 | 中等 |
| 数据量 | 受限于内存 | 中等 | 大 |
14.2 基本操作
14.2.1 初始化
%% 启动 Mnesia
application:start(mnesia).
%% 创建 Schema(数据目录)
mnesia:create_schema([node()]). %% 在当前节点创建
%% 启动后创建表
mnesia:create_table(person, [
{attributes, record_info(fields, person)},
{disc_copies, [node()]}, %% 磁盘副本
{type, set} %% 表类型
]).
14.2.2 定义表结构
%% 使用 record 定义表结构
-record(person, {
id, %% 主键(默认第一个字段)
name,
age,
city
}).
%% 创建表
mnesia:create_table(person, [
{attributes, record_info(fields, person)},
{type, set},
{ram_copies, [node()]} %% 仅内存
]).
14.2.3 表类型
| 类型 | 说明 |
|---|---|
set | 键唯一,无序 |
ordered_set | 键唯一,有序 |
bag | 允许多个相同键 |
duplicate_bag | 允许完全相同的行 |
14.2.4 存储类型
| 类型 | 说明 |
|---|---|
ram_copies | 仅内存,最快,重启丢失 |
disc_copies | 内存 + 磁盘,性能好 |
disc_only_copies | 仅磁盘,最慢 |
14.3 读写操作
14.3.1 事务中的读写
%% 写入
mnesia:transaction(fun() ->
mnesia:write(#person{id = 1, name = "Alice", age = 25, city = "Beijing"})
end).
%% 读取
mnesia:transaction(fun() ->
mnesia:read(person, 1)
end).
%% {atomic, [{person, 1, "Alice", 25, "Beijing"}]}
%% 删除
mnesia:transaction(fun() ->
mnesia:delete({person, 1})
end).
%% 更新(先读再写)
mnesia:transaction(fun() ->
[P] = mnesia:read(person, 1),
mnesia:write(P#person{age = 26})
end).
14.3.2 dirty 操作(无事务,更快)
%% dirty 操作不经过事务管理,性能更好
%% 但不保证一致性
mnesia:dirty_write(#person{id = 2, name = "Bob", age = 30, city = "Shanghai"}).
mnesia:dirty_read(person, 2).
mnesia:dirty_delete({person, 2}).
%% dirty 更新计数器
mnesia:dirty_update_counter(counter_table, my_counter, 1).
14.4 事务
14.4.1 事务特性
%% 事务保证 ACID
%% Atomicity: 原子性(全部成功或全部回滚)
%% Consistency: 一致性
%% Isolation: 隔离性
%% Durability: 持久性(disc_copies 表)
mnesia:transaction(fun() ->
%% 在事务中可以执行多个操作
mnesia:write(#person{id = 1, name = "Alice", age = 25}),
mnesia:write(#person{id = 2, name = "Bob", age = 30}),
%% 如果中间任何操作失败,所有操作都会回滚
%% 例如:
case some_condition() of
true -> ok;
false -> mnesia:abort(rollback) %% 中止事务
end,
ok
end).
14.4.2 事务返回值
%% 成功
{atomic, Result} = mnesia:transaction(fun() ->
mnesia:read(person, 1)
end).
%% 失败
{aborted, Reason} = mnesia:transaction(fun() ->
mnesia:write(#person{id = 1, name = "Alice", age = 25}),
mnesia:abort(some_reason)
end).
14.4.3 超时
%% 默认超时 4 秒,可自定义
mnesia:transaction(fun() ->
mnesia:read(person, 1)
end, 10000). %% 10 秒超时
14.5 查询(QLC)
%% 使用 QLC(Query List Comprehension)查询
-include_lib("stdlib/include/qlc.hrl").
%% 查询所有年龄 > 20 的人
mnesia:transaction(fun() ->
Q = qlc:q([P || P = #person{age = Age} <- mnesia:table(person), Age > 20]),
qlc:e(Q)
end).
%% 查询特定城市的人
mnesia:transaction(fun() ->
Q = qlc:q([{Name, Age} || #person{name = Name, age = Age, city = "Beijing"}
<- mnesia:table(person)]),
qlc:e(Q)
end).
%% 排序
mnesia:transaction(fun() ->
Q = qlc:q([P || P <- mnesia:table(person)],
[{order_by, fun(#person{age = A}) -> A end}]),
qlc:e(Q)
end).
14.6 分布式 Mnesia
14.6.1 复制表到其他节点
%% 在 Node2 上加入集群
mnesia:change_config(extra_db_nodes, ['node2@host']).
%% 复制表到 Node2
mnesia:add_table_copy(person, 'node2@host', ram_copies).
%% 改变存储类型
mnesia:change_table_copy_type(person, 'node2@host', disc_copies).
14.6.2 分布式事务
%% 事务自动在所有节点上执行
mnesia:transaction(fun() ->
mnesia:write(#person{id = 1, name = "Alice"})
end).
%% 如果表有多个副本,事务会在所有副本上提交
14.7 实战:会话存储
%% session_store.erl
-module(session_store).
-export([init/0, create/3, get/1, update/2, delete/1]).
-record(session, {
id, %% session ID
user_id, %% 用户 ID
data, %% 会话数据 (Map)
expires_at %% 过期时间
}).
init() ->
mnesia:create_table(session, [
{attributes, record_info(fields, session)},
{type, set},
{ram_copies, [node()]},
{index, [user_id]}
]).
create(SessionId, UserId, Data) ->
ExpiresAt = erlang:system_time(second) + 3600, %% 1 小时后过期
Session = #session{
id = SessionId,
user_id = UserId,
data = Data,
expires_at = ExpiresAt
},
mnesia:dirty_write(Session).
get(SessionId) ->
case mnesia:dirty_read(session, SessionId) of
[#session{expires_at = Expires} = Session] ->
Now = erlang:system_time(second),
if
Now < Expires -> {ok, Session};
true ->
delete(SessionId),
{error, expired}
end;
[] ->
{error, not_found}
end.
update(SessionId, NewData) ->
case mnesia:dirty_read(session, SessionId) of
[Session] ->
mnesia:dirty_write(Session#session{data = NewData}),
ok;
[] ->
{error, not_found}
end.
delete(SessionId) ->
mnesia:dirty_delete({session, SessionId}).
14.8 注意事项
⚠️ 常见陷阱
- Mnesia 不适合大数据量(百万级以下为宜)
- 表结构修改需要特殊处理
- 分布式事务有性能开销
disc_only_copies性能很差- 网络分区时可能出现数据不一致
💡 最佳实践
- 小型分布式数据使用 Mnesia
- 大型数据使用外部数据库(PostgreSQL, Redis)
- 高频读写使用
dirty操作 - 需要一致性时使用事务
- 定期清理过期数据
14.9 扩展阅读
上一章:13 - ETS 表 下一章:15 - IO 与网络