一、Mnesia与Erlang的分布式基因

Erlang天生就是为分布式而生的语言,而Mnesia作为它的原生数据库,完美继承了这种基因。想象一下,你正在开发一个电信级系统,需要处理数百万并发连接,还要保证数据的高可用——这时候Mnesia的"无主节点"设计就能大显身手。

举个实际例子,我们创建一个分布式表:

%% 启动两个Erlang节点(模拟分布式环境)
%% 终端1执行:erl -sname node1 -setcookie mysecret
%% 终端2执行:erl -sname node2 -setcookie mysecret

%% 在node1上执行以下代码:
-module(dist_setup).
-export([init/0]).

init() ->
    %% 创建分布式schema
    mnesia:create_schema([node1@host, node2@host]),
    
    %% 启动Mnesia
    mnesia:start(),
    
    %% 定义表结构(带分布式副本)
    mnesia:create_table(user, [
        {disc_copies, [node1@host, node2@host]},  % 磁盘副本
        {attributes, [id, name, phone]},          % 表字段
        {type, set}                                % 集合类型
    ]),
    
    %% 插入测试数据
    Fun = fun() ->
        mnesia:write({user, 1, "张三", "13800138000"}),
        mnesia:write({user, 2, "李四", "13900139000"})
    end,
    mnesia:transaction(Fun).

这个示例展示了Mnesia的核心能力:

  1. 自动处理节点间数据同步
  2. 支持内存表和磁盘表的混合存储
  3. 事务操作与普通Erlang代码无缝集成

二、事务优化的三大狠招

2.1 脏操作的艺术

在需要高性能但允许短暂不一致的场景,可以绕过事务锁:

%% 高性能计数器场景
update_counter(UserId) ->
    %% 脏读(比事务快10倍以上)
    case mnesia:dirty_read(user, UserId) of
        [{user, Id, Name, Phone}] ->
            NewPhone = increment_phone(Phone),
            %% 脏写
            mnesia:dirty_write({user, Id, Name, NewPhone});
        [] -> 
            {error, not_found}
    end.

注意:这就像在高速公路上飙车,爽是爽,但记得做好异常处理和安全带(数据校验)。

2.2 细粒度锁控制

Mnesia允许手动选择锁粒度,比如这个订单支付场景:

pay_order(OrderId, UserId) ->
    {atomic, Result} = mnesia:transaction(
        fun() ->
            %% 对用户记录加读锁
            case mnesia:read(user, UserId, read) of
                [_User] -> ok;
                _ -> mnesia:abort(user_not_exist)
            end,
            
            %% 对订单记录加写锁
            case mnesia:lock({order, OrderId}, write) of
                {order, Id, _, unpaid} ->
                    mnesia:write({order, Id, paid, datetime:now()});
                _ -> 
                    mnesia:abort(invalid_order_status)
            end
        end
    ),
    Result.

2.3 表分片策略

当单表数据超过5GB时,就该考虑分片了。比如按用户ID范围分片:

%% 在初始化时创建分片表
init_shards() ->
    lists:foreach(
        fun(ShardId) ->
            TableName = list_to_atom("user_shard_" ++ integer_to_list(ShardId)),
            mnesia:create_table(TableName, [
                {disc_copies, [node()]},
                {attributes, [id, name, phone]},
                {type, ordered_set}  % 有序集合便于范围查询
            ])
        end,
        lists:seq(1, 8)  % 创建8个分片
    ).

%% 分片路由算法
get_shard(UserId) ->
    Hash = erlang:phash2(UserId),
    list_to_atom("user_shard_" ++ integer_to_list(Hash rem 8 + 1)).

三、实战:电商库存系统设计

让我们用Mnesia实现一个防超卖的库存服务:

-module(inventory).
-export([init/0, reserve/2, commit/1, rollback/1]).

-record(stock, {
    item_id,     % 商品ID
    total,       % 总库存
    reserved,    % 预占库存
    version      % 乐观锁版本号
}).

init() ->
    mnesia:create_table(stock, [
        {attributes, record_info(fields, stock)},
        {type, set},
        {ram_copies, [node()]}  % 内存表更快速
    ]).

%% 预占库存(带版本检查)
reserve(ItemId, Amount) ->
    mnesia:transaction(
        fun() ->
            case mnesia:read(stock, ItemId) of
                [Stock = #stock{total=T, reserved=R, version=V}] 
                  when T - R >= Amount ->
                    New = Stock#stock{
                        reserved = R + Amount,
                        version = V + 1
                    },
                    mnesia:write(New),
                    {ok, V + 1};
                _ ->
                    mnesia:abort(out_of_stock)
            end
        end
    ).

%% 提交实际扣减
commit(ItemId) ->
    mnesia:transaction(
        fun() ->
            [Stock = #stock{reserved=R}] = mnesia:read(stock, ItemId),
            mnesia:write(Stock#stock{total=Stock#stock.total - R, reserved=0})
        end
    ).

%% 回滚预占
rollback(ItemId) ->
    mnesia:transaction(
        fun() ->
            [Stock = #stock{reserved=R}] = mnesia:read(stock, ItemId),
            mnesia:write(Stock#stock{reserved=0})
        end
    ).

这个设计亮点:

  1. 使用乐观锁避免长事务
  2. 预占与实际扣减分离
  3. 内存表操作微秒级响应

四、避坑指南与性能调优

4.1 那些年我踩过的坑

坑1:网状事务死锁

%% 错误示范:A事务锁顺序 用户→订单,B事务锁顺序 订单→用户
deadlock_example() ->
    spawn(fun() -> 
        mnesia:transaction(fun() ->
            mnesia:write({user, 1, "locked"}),
            timer:sleep(1000),  % 故意制造死锁窗口
            mnesia:write({order, 100, "locked"})
        end)
    end),
    spawn(fun() -> 
        mnesia:transaction(fun() ->
            mnesia:write({order, 100, "locked"}),
            mnesia:write({user, 1, "locked"})
        end)
    end).

解决方案

  • 统一锁获取顺序
  • 设置事务超时:mnesia:transaction(fun() -> ... end, 5000)

4.2 性能调优参数

app.config中加入这些魔法参数:

[
    {mnesia, [
        {dump_log_write_threshold, 10000},  % 提高日志写入批处理量
        {dc_dump_limit, 40},                % 增加分布式提交并发
        {max_wait_for_decision, 60000},     % 延长死锁等待时间
        {no_table_loaders, 8}               % 并行加载表的工作进程数
    ]}
].

五、何时该用/不该用Mnesia

黄金场景

  • 需要嵌入式数据库的实时系统(如游戏服务器)
  • Erlang集群内的状态共享(如会话管理)
  • 高并发短事务(<10ms)需求

劝退场景

  • 需要复杂SQL查询(老老实实用PostgreSQL)
  • 单机海量数据存储(考虑LevelDB等LSM树存储)
  • 非Erlang生态集成(协议转换成本太高)

最后说句掏心窝的:技术选型就像选鞋子,合脚的才是最好的。Mnesia在Erlang生态里是"自家人",用好了能让你体验什么叫"如臂使指",但强求它跨界表演就可能变成"削足适履"了。