强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 外部数据库

特性ETSMnesiaPostgreSQL
持久化
分布式
事务
复杂查询Match SpecQLC / MatchSQL
延迟极低中等
数据量受限于内存中等

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 注意事项

⚠️ 常见陷阱

  1. Mnesia 不适合大数据量(百万级以下为宜)
  2. 表结构修改需要特殊处理
  3. 分布式事务有性能开销
  4. disc_only_copies 性能很差
  5. 网络分区时可能出现数据不一致

💡 最佳实践

  1. 小型分布式数据使用 Mnesia
  2. 大型数据使用外部数据库(PostgreSQL, Redis)
  3. 高频读写使用 dirty 操作
  4. 需要一致性时使用事务
  5. 定期清理过期数据

14.9 扩展阅读


上一章:13 - ETS 表 下一章:15 - IO 与网络