1. Mnesia数据库被组织为一个表的集合, 每个表又实例(Erlang record)构成, 表也有一些属性,
如位置(location)和持久性(persistence)等.
2. 通过一个基本的例子介绍Mnesia数据库的使用:
<1> 有如下的数据模型:
三个实体:
雇员employee,项目project,部门department
三个实体的关系式:
a. 一个部门由一个雇员管理,因此有管理者manager的关系
b. 一个雇员在一个部门工作,因此有在此部门in_department的关系
c. 每个雇员都在做一些项目,因此有在项目中in_project的关系
<2> 分析数据模型,创建表格
根据上面的数据模型,定义了六张表:
这种设计体现了Mnesia的数据模型的组织方式:
a. 数据按照表的结合来组织
b. 不同记录之间的关系通过描述实际关系的附加表来建模
c. 每个表包含了数据的实例.
-record(employee, {id, name, salary, phone})
-record(project, {id, name, number})
-record(department, {id, name})
-record(manager, {employee_id, department_id})
-record(in_department, {employee_id, department_id})
-record(in_project, {employee_id, project_id})
注意:
对于存在一对一关系的表,可以使用{type, set}来限定;
对于存在一对多关系的表,可以使用{type, bag}来限定。
在使用上述记录创建table的时候, manager和in_project应该使用{type, bag}类型,
因为同一个雇员可能管理多个部门, 同一个雇员可能同时在多个项目中.
in_department使用默认的{type, set}因为一个员工只能属于一个部门.
使用下面代码来创建表格:
erl -mnesia dir '"/home/woomsgadmin/tmp/mnesia.company"'
mnesia:create_schema([node()]).
mnesia:start().
company:init() %% 创建六张表格
mnesia:info(). %% 查看结果
-module(company).
-include_lib("/usr/local/lib/erlang/lib/stdlib-1.16.2/include/qlc.hrl").
-include("company.hrl").
-export([init/0]).
init() ->
mnesia:create_table(employee,
[{attributes, record_info(fields, employee)}]),
mnesia:create_table(project,
[{attributes, record_info(fields, project)}]),
mnesia:create_table(department,
[{attributes, record_info(fields, department)}]),
mnesia:create_table(manager,
[{type, bag}, %% 反映一对多的关系
{attributes, record_info(fields, manager)}]),
mnesia:create_table(in_department,
[{attributes, record_info(fields, in_department)}]),
mnesia:create_table(in_project,
[{type, bag}, %% 反映一对多的关系
{attributes, record_info(fields, in_project)}]).
<3> 如何插入一条员工(employee)记录?
需要做以下三步操作:
a. 在employee表中插入一条记录
b. 在in_department表中插入一条记录
c. 在in_project表中插入0条或者多条记录
使用下面的代码来插入一条员工记录:
注意Mnesia的下面两个特性:
a. 事务F要么完全成功,要么完全失败
b. 操作同样的记录代码可以在不同的process中执行而不会相互干扰.
insert_employee(Employee, DepartmentId, ProjectIdList) ->
EId = Employee#employee.id,
F = fun() ->
mnesia:write(Employee),
In_Department = #in_department{employee_id=EId, department_id=DepartmentId},
mnesia:write(In_Department),
handle_projects(EId, ProjectIdList)
end,
mnesia:transaction(F).
handle_projects(EId, [ProjectId|Tail]) ->
In_Project = #in_project{employee_id=EId, project_id=ProjectId},
mnesia:write(In_Project),
handle_projects(EId, Tail);
handle_projects(_, []) ->
ok.
测试:
rr("company.hrl"). %% 在Erlang Shell中引用*.hrl
[department,employee,in_department,in_project,manager,project]
Employee = #employee{id=1, name="liqiang", salary=29, phone=1234567}.
#employee{id = 1,name = "liqiang",salary = 29,
phone = 1234567}
company:insert_employee(Employee, 100, [1001,1002,1003,1004]). %% 插入数据
{atomic,ok}
mnesia:dirty_read({employee, 1}).
[#employee{id = 1,name = "liqiang",salary = 29, phone = 1234567}] %% 在employee表中插入了一条记录
mnesia:dirty_read({in_department, 1}).
[#in_department{employee_id = 1,department_id = 100}] %% 在in_department表中插入了一条记录
mnesia:dirty_read({in_project, 1}).
[#in_project{employee_id = 1,project_id = 1001}, %% 在in_project表中插入了四条记录
#in_project{employee_id = 1,project_id = 1002},
#in_project{employee_id = 1,project_id = 1003},
#in_project{employee_id = 1,project_id = 1004}]
<4> Oid (Object identifier)
由一个二元组组成{Tab, Key}, 第一个元素是表名,第二个元素是Key,我们可以用这个Oid来查询数据.
例如:
Oid = {employee, 1}, %% {employee, id}
mnesia:dirty_read(Oid).
<5> 员工记录的读取:
Mnesia读取数据有有三种方式, 这三种当中第一种是最快的, 第三种语法最好,但是开销最大.
a. 直接使用mnesia:read({Tab, Key})
b. 使用mnesia的模式匹配语句
c. 使用QLC
(用三种方式分别实现根据员工的ID涨工资的功能)
需要的查询语句是: SELECT * FROM employee WHERE id = EmployeeId
a-impl:
代码:
raise_salary(EmployeeId, Raise) ->
F = fun() ->
[Employee] = mnesia:read({employee, EmployeeId}), %% 根据Oid读取数据
Salary = Employee#employee.salary + Raise,
NewEmployee = Employee#employee{salary=Salary},
mnesia:write(NewEmployee)
end,
mnesia:transaction(F).
b-impl:
代码:
'_'匹配任意的Erlang数据结构
'$<number>'可以作为Erlang变量使用
'$_'整个记录
raise_salary(EmployeeId, Raise) ->
F = fun() ->
MatchHead = #employee{id = EmployeeId, _='_'},
Guard = [],
Result = ['$_'], %% 返回整个记录
[Employee] = mnesia:select(employee, [{MatchHead, Guard, Result}]),
Salary = Employee#employee.salary + Raise,
NewEmployee = Employee#employee{salary=Salary},
mnesia:write(NewEmployee)
end,
mnesia:transaction(F).
c-impl:
QLC的使用分为三步:
Query = qlc:q(XXXX),
F = fun() ->
qlc:e(Query)
end,
mnesia:transaction(F).
代码:
raise_salary(EmployeeId, Raise) ->
Query = qlc:q([X || X <- mnesia:table(employee), %% 第一步
X#employee.id == EmployeeId]),
F = fun() ->
[Employee] = qlc:e(Query), %% 第二步
Salary = Employee#employee.salary + Raise,
NewEmployee = Employee#employee{salary=Salary},
mnesia:write(NewEmployee)
end,
mnesia:transaction(F). %% 第三步
测试: a-impl, b-impl, c-impl:
假设employee表中已经有这样一条记录
#employee{id = 1,name = "liqiang",salary = 50,phone = 1234567}
company:raise_salary(1, 40).
{atomic,ok}
mnesia:dirty_read({employee, 1}).
[#employee{id = 1,name = "liqiang",salary = 90,phone = 1234567}] %% 查看salary的变化
3. Schema - 系统配置表
<1>Mnesia系统的配置在模式(schema)里描述。模式是一种特殊的表,它包含了诸如表名、每个
表的存储类型(例如,表应该存储到RAM、硬盘或者可能是两者以及表的位置)等信息。
不像数据表,模式表里包含的信息只能通过与模式相关的函数来访问和修改。
Mnesia提供多种方法来定义数据库模式,可以移动、删除表或者重新配置表的布局。
这些方法的一个重要特性是当表在重配置的过程中可以被访问。
例如,可以在移动一个表的同时执行写操作。该特性对需要连续服务的应用非常好
<2>测试mnesia:move_table_copy(Tab, From, To).
mnesia:add_table_copy(Tab, Node, Type)
mnesia:del_table_copy(Tab, Node)
a. 第一个函数是将Tab的副本从From节点移动到To节点, 表的{type}属性被保留,
移动之后From节点上将不存在这个表的副本.
b. 第二个函数是在Node节点上创建Tab的备份, 并且可以指定新的{type}类型,
这里的type必须是ram_copies, disc_copies, disc_only_copies.
c. 第三个函数在节点Node上删除表的副本,如果表的最后一个副本被删除,则表被删除.
结论:
a. 如果表在本地没有副本,是一个remote类型的表也是可以访问的.
测试mnesia:move_table_copy(Tab, From, To):
启动两个Erlang Shell:
erl -sname node1 -setcookie testcookie -mnesia dir '"c:/home/mnesia/node1"'
erl -sname node2 -setcookie testcookie -mnesia dir '"c:/home/mnesia/node2"'
在任意一个Shell中执行:
mnesia:create_schema([node() | nodes()]).
在两个节点上都执行:
mnesia:start()
在node2上创建一个表格user, 并插入数据:
mnesia:create_table(user, [{disc_copies, [node()]},{attributes, [id, username, age]}]).
mnesia:dirty_write({user, 1, liqiang, 23}).
然后在两个节点上都执行dirty_ready/1, 发现虽然我们只在node2上创建了user表, 但是两个节点上都能查询到数据:
mnesia:dirty_read({user, 1}).
[{user,1,liqiang,23}]
我们在node2上执行mnesia:info()
---> Active tables <--- %% node2上当前两张表
user : with 1 records occupying 296 words of mem
schema : with 2 records occupying 508 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node2" is used.
use fallback at restart = false
running db nodes = ['node1@liqiang-tfs','node2@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = []
ram_copies = []
disc_copies = [schema,user] %% node2上当前两张表
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
[{'node2@liqiang-tfs',disc_copies}] = [user]
我们在node1上执行mnesia:info()
---> Active tables <--- %% node1上当前一张表
schema : with 2 records occupying 508 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node1" is used.
use fallback at restart = false
running db nodes = ['node2@liqiang-tfs','node1@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = [user] %% node1上当前有一张remote表在node2上
ram_copies = []
disc_copies = [schema] %% node1上当前一张表
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
[{'node2@liqiang-tfs',disc_copies}] = [user]
我们把user表从node2移动到node1, 在调用mnesia:info()查看结果
在node2上执行:
mnesia:move_table_copy(user, node(), 'node1@liqiang-tfs').
我们在node2上执行mnesia:info()
---> Active tables <--- %% 移动之后, node2上当前一张表
schema : with 2 records occupying 515 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node2" is used.
use fallback at restart = false
running db nodes = ['node1@liqiang-tfs','node2@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = [user] %% 因为我们移动了user表,所以node2上当前有一张remote表在node1上
ram_copies = []
disc_copies = [schema] %% 移动之后, node2上当前一张表
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies}] = [user]
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
我们在node1上执行mnesia:info()
---> Active tables <--- %% 移动之后, node1上当前两张表
user : with 1 records occupying 296 words of mem
schema : with 2 records occupying 515 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node1" is used.
use fallback at restart = false
running db nodes = ['node2@liqiang-tfs','node1@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = []
ram_copies = []
disc_copies = [schema,user] %% 移动之后, node1上当前两张表
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies}] = [user]
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
测试mnesia:add_table_copy(Tab, Node, Type)
在node2上创建表user的一个ram_copies的副本:
mnesia:add_table_copy(user, node(), ram_copies).
我们在node2上执行mnesia:info()
---> Active tables <---
user : with 1 records occupying 296 words of mem
schema : with 2 records occupying 517 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node2" is used.
use fallback at restart = false
running db nodes = ['node1@liqiang-tfs','node2@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = []
ram_copies = [user] %% 我们最新在node2上创建的表user的ram副本
disc_copies = [schema]
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',ram_copies}] = [user]
我们在node1上执行mnesia:info()
---> Active tables <---
user : with 1 records occupying 296 words of mem
schema : with 2 records occupying 517 words of mem
===> System info in version "4.4.11", debug level = none <===
opt_disc. Directory "c:/home/mnesia/node1" is used.
use fallback at restart = false
running db nodes = ['node2@liqiang-tfs','node1@liqiang-tfs']
stopped db nodes = []
master node tables = []
remote = []
ram_copies = []
disc_copies = [schema,user]
disc_only_copies = []
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',disc_copies}] = [schema]
[{'node1@liqiang-tfs',disc_copies},{'node2@liqiang-tfs',ram_copies}] = [user]
<3> Mnesia表的加载:
如果Mnesia推断另一个节点(远程)的拷贝比本地节点的拷贝更新时,初始化时在节点上复制
表可能会导致问题,初始化进程无法处理。在这种情况下,对mnesia:wait_for_tables/2的调用将暂
停调用进程,直到远程节点从其本地磁盘初始化表后通过网络将表复制到本地节点上。这个过程可能相当耗时.
<4> 特殊的本地表: {local_content, true}
当应用需要一个其内容对每个节点来说在本地都是唯一的表时,可使用local_content表。
表名对所有Mnesia节点可见,但是内容对每个节点都是唯一的。这种类型的表只能在本地进行存取.
遗留的问题?
如果添加一个新启动的节点到db nodes的集合中?
在Erlang FAQ - erlang_faq_20091125.txt中已经解决了这个问题. ///我暂时找不到这个文件,找到的可以给我提个醒
4. 事务的细节
<1> 原子性,事务要么完全成功,要么完全失败,如果事务成功,将在所有节点上复制这个动作,失败
将不对任何节点产生影响, 原子性的另外一部分,要么事务在所有的节点上有效,要么没有一个节点有效。
<2> 隔离性,保证了事物在不同的节点或者process中并发执行的时候相互不影响, 通过事务实行访问数据库
可以认为自己对数据库有唯一的访问权限.
<3> Mnesia使用"等待-死亡"wait-die的策略来解决死锁:
当某个事务尝试加锁的时候,如果Mnesia怀疑可能出现死锁,强制该事务释放所有的锁并休眠一段时间,
所以包含在事务中的Fun函数可能被求值一次或者多次, Mnesia会保证每个事务最终会被运行,最后的结果是
Mnesia释放了死锁,也释放了活锁.
我们使用mnesia:read/1这样的函数在事务中读取数据的时候,Mnesia会动态的加锁和解锁.
<4> mnesia:read/1和mnesia:wread/1的区别?
这两个函数都必须在事务中运行,区别是wread在执行的时候会获取一个写锁而不是像read获取一个读锁,
在这种情况下:
如果我们获取一条记录,然后修改这条记录,在写入这条记录,那么立即获取一个写锁会更有效。
如果我们先调用read, 然后在调用write, 那么执行写操作的时候必须把读锁升级为写锁.
<5> Mnesia四种常用的锁:
读锁,写锁,读表锁,写表锁
读表锁: 如果事务要扫描整张表来获取记录,对表里的一条条记录加锁效率也很低也很耗内存,因此,可以
对这张表设置一个读表锁.
写表锁: 要写大量的记录到表里,可以设置写表锁.
Mnesia锁的策略是在读取一条记录的时候,锁住该条记录;在写一条记录的时候锁住记录的所有副本
写锁一般会要求在所有存放表的副本并且是活动的节点上设置,读锁只设置一个节点.
<6> Mnesia一种特殊的锁: Sticky粘锁
如何理解?
<7> Mnesia表锁:
表锁是针对单条记录的普通锁的补充.
应用场景:
如果在一个事务中需要对某个表的大量记录进行读写操作, 那么我们在这个事务开始的时候对这个表加表锁
来阻塞其它并发进程对这个表的访问会更有效率。
mnesia:read_lock_table(Tab)
mnesia:write_lock_table(Tab)
或者:
mnesia:lock({table, Tab}, read)
mnesia:lock({table, Tab}, write)
<8> 脏操作:
脏操作的优势是速度,但是失去了事务的原子性和隔离性。在某种程度上可以保证一致性,脏操作
不会返回混乱的记录,因此每一个单独的读写操作都是以原子的方式执行的,Mnesia也能保证如果
执行对脏操作的写操作,这个表的所有副本都会被更新.
<9> 如何来通过key来遍历表呢?
APIs:
mnesia:dirty_first(Tab) 返回Tab中第一个Key, 如果表中没有记录,返回'$end_of_table'
mnesia:dirty_next(Tab, Key) 返回Tab中的下一个Key, 如果表中没有下一个key,返回'$end_of_table'
mnesia:dirty_last(Tab) 和mnesia:dirty_first(Tab)工作方式一样,只有当type是ordered_set的时候
返回Erlang排序中的最后一项,其它类型的和mnesia:dirty_first(Tab)完全一样.
mnesia:dirty_prev(Tab, Key) 和mnesia:dirty_next(Tab, Key)工作方式一样,只有当type是ordered_set的时候
返回Erlang排序中的前一项,其它类型的和mnesia:dirty_next(Tab, Key)完全一样
下面的代码可以遍历类型type=set的tables, 显示tables的所有内容:
display_table(Tab) ->
case mnesia:dirty_first(Tab) of
'$end_of_table' ->
ok;
Key ->
[Data] = mnesia:dirty_read({Tab, Key}), %% 如果是bag类型的table,匹配会出错!
io:format("~w~n", [Data]),
display_table(Tab, Key)
end.
display_table(Tab, Key) ->
case mnesia:dirty_next(Tab, Key) of
'$end_of_table' ->
ok;
NewKey ->
[Data] = mnesia:dirty_read({Tab, NewKey}),
io:format("~w~n", [Data]),
display_table(Tab, NewKey)
end.
5. activity?
<1> 区别异步事务和同步事务:
我们使用的mnesia:transaction(Fun [, Args])是异步事务, mnesia:sync_transaction(Fun [, Args])是同步事务.
我的理解是:
mnesia:sync_transaction调用返回前,同步事务一直等待,直到全部激活的副本提交事务(写到磁盘), mnesia:transaction
不能保证这一点.
同步事务对需要发消息给远端进程前确实节点更新已经被执行的应用,以及组合了事务写和脏读的时候特别有用。
同步事务对于频繁的执行大量更新操作草成其它节点的Mnesia过载overload也很有用:
分析:
同步操作是一种放慢写操作的方式,因为函数在记录被提交到事务(写到transaction log)之前是不会返回的;而异步操作
可能非常快速的提交事务(写道transaction log),这个速度超出了dumped的速度,会引起overload的错误.
也就是如果你在用disc_copies的表,在异步事务中一次写入很多数据,就可能遇到下面的错误:
=ERROR REPORT==== 10-Dec-2008::18:07:19 ===
Mnesia(node@host): ** WARNING ** Mnesia is overloaded: {dump_log, write_threshold}
<2> 同步脏操作和异步脏操作:
mnesia:dirty(Fun [, Args])是异步脏操作, mnesia:sync_dirty(Fun [, Args])是同步脏操作
异步脏操作的更新是异步的,即在一个节点上等待操作被执行而不管其它节点.
同步脏操作调用者会的呢古代全部的激活副本更新完成.
同步脏操作对需要发消息给远端进程前确实节点更新已经被执行的应用,或者防止其它节点的Mnesia过载的应用有用.
<3> mnesia:dirty_*类的函数总是以异步脏(async_dirty)的语法执行而不管作业存取上下文
是如何请求的。其甚至可以不需要封装到任何作业存取上下文中即可调用.