ets和Dets

需要实际的系统都需要在有限的时间存储和检索大量的数据。

在程序中主要使用的是一个数据组合类型是一些项的聚合。Erlang的列表提供了实现聚合的一种方法,但是如果列表中的项超过一定数量,存取元素过程就会变慢。平均来说,我们需要校验聚合中50%的元素来确定一个给定元素的存在,而需要遍历所有的元素来确定一个给定值还存在。

为处理快速检索,Erlang使用两种机制,一个是Erlang项元存储(Erlang Term Storage,ETS)机制和Erlang磁盘项元存储(Erlang Disk Term Storage,Dets)机制,分别提供内存高效和磁盘高效的大量数据聚合的存储和检索。Erlang同时也提供了一个完整的数据库应用程序Mnesia。

ETS表

1.ETS表存储元组,通过元组中的关键字字段存储元素,这些表是通过散列表和二叉树实现的,不同的表现形式提供不同类型的聚合。

2.集合和袋是两种不同的集合,它们都包含元素,但不同点是集合每个元素只包含一次,袋可以包含某个元素多次。

3.Erlang中有四种不同的ETS表:

集合(Set):在集合中每个关键字只出现一次,所以使用这种表作为索引的例子,意味着每个词只对应表中的一个元素。

有序集合(Ordered Set):一个有序集合与集合具有同样的属性,但其元素按关键字的字典顺序存储,比如说,记录"refactoring"会位于记录"reply"之前的位置。

袋(Bag):袋允许不同的记录对应同一个关键字,就像{"refactoring",4}和{"refactoring",34}是允许的,元素必须间隔开,在索引的例子中,这意味着只有一个项元对应于特定行上的一个特定单词。

复袋(Duplicate Bag):一个复袋允许包含重复的元素,也可以有重复的关键字,所以,表中可能包含项元{"refactoring",4}两次或多次。

4.在Erlang语言中可以表达为对关键字的处理:集合和有序集合只包含每个关键字一次,而袋和复袋对于关键字的重复给以两种不同的变化。

实现和平衡

1.对于这些集合的实现,除了对有序集合中的元素访问时间相对于聚合大小有对数级别变化外,其他的检索时间都是恒定的。两种情况的表现都要远好于基于列表形式的线性访问时间。

2.集合、袋和复袋都被存储为散列表形式,其中存储元组的位置是通过函数(hash function,散列函数)把元组的关键字映射到存储这个值的内存地址来确定。假设我们的散列表对10个项元分配了空间,那么散列函数会返回10个独一无二的内存位置,每个项元一个。如果需要插入一个附加项元,此表会重新计算散列值(重新组织),为更多的项元创建空间并且返回更多互不相同的内存空间作为关键字的散列结果。这给我们一个从关键字到对应元组、常量的存取时间,但当我们需要写入一个项元和重新计算散列表时,这个时间将会变化。

3.一个有序集合存储在一个AVL平衡二叉树中,其顺序由关键字字段内在的顺序确定。这意味着分支的长度决定存取的时间复杂度,它是存储在二叉树中聚合大小的对数。有序集允许集合按照键的顺序来遍历,然而其它表现形式只是简单按照存储顺序来遍历。在集合中,顺序依赖于散列函数的实现细节,同时依赖于它们在内存中的存储顺序。

4.选择使用哪种形式的表取决于特定的应用。如果要写出一个复杂的索引,需要按字母(关键字)顺序遍历索引,但是要从在线索引中查找单个词汇,只需要存取某个特定的元组,这样一个无序的集合就足够了。

5.Erlang发行版中ETS表的实现非常灵活,允许关键字字段是任何类型,包括复杂数据结构。

创建表

1.ets模块包含了一系列创建、操作和删除ETS表的函数。通过调用ets:new/2创建一个表,第一个参数是表的名字,第二个参数由可选项的列表组成。函数调用ets:new(myTable,opts)返回表的标识符来用作表的引用。

2.在默认设置中,如果传入一个空选项表给ets:new/2函数,则创建一个集合,其关键字在第一个位置,并提供表中值的受保护访问允许所有进程读取表,但只有表的所有者可以写入。其它选项包括:

set,ordered_set,bag,duplicate_bag:如果选项表中包括这些中的任意一个,则创建响应类别的ETS表。

{keypos,Pos}:创建一个表,其关键字在位置Pos上。

public,protected,private:所有进程可读写一个公共表,仅表的所有者可读写一个私有表。

named_table:静态注册表的名字,并且可以将其在ETS操作中用作表的引用。

3.通过传入表的标识符使用ets:info/1函数获取表的信息。

4.如果表是通过named_table创建的,那么既可以通过名字又可以通过标识符对表进行存取。

5.警告:即使每个表都是通过ets:new/2函数创建的,且表的名字通过表的信息可以查到,也不可以将表的名字用于表的存取,除非把named_table选项设置为enabed。如果选项没有enabled,试图通过这种方式存取表将导致坏参数运行错误信息。

6.当不再有程序引用表的时候,该表使用的存储空间是不会被自动回收的(也就是说,它们无法进行"垃圾回收")。因此,需要通过调用ets:deleted(TabId)来手动删除这个表。然而,表与创建它的进程相连,如果该进程终止了,那么该表会被自动删除。

7.警告:因为ETS表和创建它的表相连,所以在终端测试时需要特别小心,如果引发了运行时错误,那么终端进程将崩溃并重启,结果是将会失去所有的ETS表和与它们相关的数据。如果这种情况发生了,那么可以通过终端命令f()清除所有与表引用有关的变量并重新启动。

处理表元素

1.可以使用ets:insert/2函数插入元素到表格,并且通过ets:lookup/2函数根据关键字来存取它们:

ets:insert(TabId,{alison,sweden}).

ets:lookup(TabId,alison).

在这个例子中,TabId是一个集合,插入以alison为关键字的第二个元素会覆盖第一个元素。当处理集合时一个通常的错误做法是,在插入一个更新前先删除一个元素。这个删除操作是多余的,因为插入会自动覆盖旧的项元。

ets:insert(TabId,{alison,italy}).

ets:lookup(TabId,alison).

如果删除了现有表,并重建这个表为袋,将会看到一些不同的变化:

ets:delete(TabId).

TabId2=ets:new(myTable,[bag]).

ets:insert(TabId2,{alison,sweden}).

ets:insert(TabId2,{alison,italy}).

ets:lookup(TabId2,alison)结果为[{alison,sweden},{alison,italy}]

保留袋中元素的插入顺序,在结果中元素的顺序即时它们加入袋中的顺序,最先进入的最先出来。

因为这个ETS表与复袋相比更像是一个袋,所以第二次插入同一个元组没有任何效果。

ets:insert(TabId2,{alison,italy}).

ets:lookup(TabId2,alison)结果为[{alison,sweden},{alison,italy}]

 表的遍历

1.通过ets:first/1函数得到表中的第一个关键字

First = ets:first(indexTable)

2.通过ets:next/2函数得到表中的下一个关键字

ets:next(indexTable,First)

3.现在关键字的顺序是由关键字的散列值的顺序决定的,既不取决于关键字自己的顺序,又不取决于值插入表中的顺序。在本质上,顺序是任意的,但对我们来说唯一可靠的是,能够通过使用first和next一步一步地遍历表来访问所有的关键字。

4.ETS对并发更新只提供非常有限的支持,如果正在使用est:first/1和ets:next/2函数遍历集合或袋类型的ETS表,而同时又有其它进程试图写入或删除其中的元素,这将会发生什么?

如果其它进程正在写入一个新元素,而这将导致表的重新散列,完全把所有记录重新排序,这将会发生什么?在最好的情况下,next/2会引发一个运行时错误,在最坏的情况下,这种行为将是未定义的,将返回任何元素或'$end_of_table'基元,或者引发badarg错误。

如果知道在使用ets:first/1和ets:next/2函数遍历ets表时,其它的进程执行破坏性的调用,可以使用ets:safe_fixtable/2函数。它保证在遍历的时候,每个元素只访问一次。如果一个新的元素在遍历开始后加入(由遍历表的进程加入或者另外一个进程),那么将无法保证这个元素可以访问到。然而,所有其它的元素只遍历一次。通过把Flag设置为true,一个进程调用ets:safe_fixtable(TableRef,Flag)锁定一个表。通过把Flag设置为false或者在进程终止时,Flag被释放。如果几个进程锁定同一张表,直到所有进程释放了这个锁定或者全部终止,锁定才被解除。

如果锁定了一个表,不要忘记释放它,因为只要表处于锁定状态中,被删除的对象就不会从表中移除。这不仅会导致没有释放被删除对象使用的内存,还会导致表的运算性能的下降。

5.ets:last/1给出有序集合的最后一个元素。对于其它类型的表,ets:last/1将返回它的第一个元素。如果Last是表中的最后一条记录,那么调用next会返回'$end_of_table'。

提取表的信息:匹配

1.通过ets:match/2来从表中提取数据:

表的元素是几元元组,所以与这个表匹配的模式也是几元元组,此模式包含3中元素:

'_'是通配符,它会匹配任何一个在这个位置上的值;

'$0'和'$1'是变量,它会匹配任何一个在这个位置上的值;

一个值。

2.匹配结果是给出结果的列表,它包含表中每一个成功的匹配。通配符和变量匹配的区别是,如果值与变量匹配,则会在结果中显示,而通配符匹配则不会。值对变量的匹配会在列表中以这些变量的升序给出。

3.可以用match_object函数来返回全部匹配模式的元组。

4.可以通过match_delete函数来删除匹配的对象。

5.警告:使用匹配操作要非常小心,因为它们会改变系统的实时行为,这是因为所有的匹配操作都是作为内置函数来实现的,而内置函数是原子性操作的,因此,对于一个巨大表的一个匹配操作会阻碍其它进程的执行,直到完成对整个表的遍历。

为了避免这个问题,最好使用first和next,虽然它可能需要更多的时间,但它不会破坏系统的实时性。

提取表的信息:筛选

1.一个匹配规约是一个Erlang的项元,它描述了一个小程序。它是一个模式的概括,允许以下的形式:

在匹配变量进行求值的保护元。

返回的表达式而不仅仅是一个简单的绑定的表。

例如:

ets:select(countries,[{{'$1','$2','$3'},[{'/=','$3',cook}],[['$2','$1']]}]).

其中{'$1','$2','$3'}是一个模式。

[{'/=','$3',cook}],这是一个保护元表达式的列表,用前缀形式写成。这里的单一保护元检查条件$3/=cook是否满足,只有保护元判断为true,匹配才会成功。

[['$2','$1']]这是返回表达式。

2.Erlang实现以闭包来描述的匹配规约,函数ets:fun2ms/1把fun作为参数,描述了我们想要在ETS表中执行的比较(或)模式匹配,还有我们想要通过select返回的返回值。幸运的是,fun2ms返回一个匹配规约,我们可以把它作为参数调用select函数,免除了我们要理解和书写匹配规约的困难。

例如:

MS=ets:fun2ms(fun({Name,Country,Job}) when Job /= cook->[Country,Name] end).

结果为:[{{'$1','$2','$3'},[{'/=','$3',cook}],[['$2','$1']]}]

ets:select(countries,MS).

结果为:[[ireland,sean],[ireland,chris]].

这允许书写匹配描述时采用通常的顺序来书写变量的命名和子表达式。主要fun必须是一个文字函数,而不可以作为一个变量传递给fun2ms。所谓的文字函数,我们是指在ets:fun2ms/1调用中输入的函数,而不是绑定到一个变量的那个。fun函数自己也需要一个元组作为参数。最后,如果在模块中使用,需要包含一个头文件:

-include_lib("stdlib/include/ms_transform.hrl")。

函数ets:select/2会把所有成功匹配的结果返回表。可以通过函数ets:select/3进行批量匹配,函数的第三个参数限制了匹配的数量。ets:select/3返回匹配并且附加一个延续,通过把它传递给ets:select/1来调用并进一步匹配。

在表上的其他操作

1.ets:tab2file(TableId|TableName,Filename)

tab2file/2把表转存到文件中,返回ok或者{error,Reason}

2.ets:file2tab(FileName)

file2tab把一个转存的表都会到系统中,返回{ok,Tab}或者{error,Reason}

3.ets:tab2list(TableId|TableName)

tab2list返回一个包含表中所有元素的列表

4.ets:i()

这会列出当前进程可见的所有关于ETS表的汇总信息。

5.ets:info(TableId|TableName)

它返回给定表的属性。

记录和ETS表

1.在ETS中,默认关键字位置是元组的第一个元素。在记录中,这个位置保留给了记录的类型,除非明确指定关键字的位置,否则你无法获得想要的行为。通过表达式#RecordType.KeyField获取RecordType里KeyField的位置,可以把{keypos,#RecordType.KeyField}加入到ets:new/2函数调用的选项列表中。

例如:

rd(capital,{name,country,pop}).

结果为capital。

ets:new(countries,[named_table,{keypos,#capital.name}]).

结果为countries。

ets:insert(countries,#capital{name="Budapest",country="Hungary",pop=2400000}).

结果为true。

ets:insert(countries,#capital{name="Pretoria",country="South Africa",pop=2400000}).

结果为true。

ets:insert(countries,#capital{name="Rome",country="Italy",pop=5500000}).

结果为true。

ets:lookup(countries,"Pretoria").

结果为[#capital{name="Pretoria",country="South Africa",pop=2400000}]。

ets:match(countries,#capital{name='$1',country='$2',_='_'}).

结果为[["Rome","Italy"],["Budapest","Hungary"],["Pretoria","South Africa"]]。

ets:match_object(countries,#capital{country="Italy",_='_'}).

结果为[#capital{name="Rome",country="Italy",pop=5500000}]。

MS=ets:fun2ms(fun(#capital{pop=P,name=N}) when P < 5000000->N end).

结果为[{#capital{name='$1',country='_',pop='$2'},[{'<','$2',50000}],['$1']}]。

est:select(countries,MS).

结果为["Budapest","Pretoria"]。

为了保证记录字段的变化不会影响到ETS表上的操作,这些操作中并没有使用这些字段,一个格式为#captial{name='$1',country='Italy',_='_'}的表达式作为一个模式传递到匹配函数。构造_=Expression把所有没有绑定的记录字段替换为Expression。在例子中,我们选取'_',这意味着“所有没有被明确提及的字段“和匹配通配符'_'匹配。

 Dets

1.Dets表为Erlang项元提供了一种有效、基于文件的存储形式。当需要快速存储并保持一致性时,它和ets表一起使用。Dets表与ets表有着类似的函数集合,包括检索、匹配和选择函数。当但函数调用磁盘搜索和读操作时,它与在ETS表上同样的操作相比要慢的多。在R13发布版本中,一个Dets文件的大小不能超过2G。如果需要保存超过2G的数据,那么,需要将它们分割成多个Dets表中。

2.Dets包含set,bag和duplicate bag。可以用dets:open_file(TableName,options)来打开它们,其options参数是关键字元组的一个列表,包含下列这些内容:

{auto_save,Interval}:设置表周期刷新的时间间隔。刷新表意味着即使表非正常关闭,也不需要进行修复,间隔是以毫秒为单位的整数(默认值是180000,即3分钟),如果不希望刷新文件,可以使用基元infinity。

{file,FileName}:用于以FileName来覆盖表的默认名字,并且提供存储Dets文件的位置。

{repair,Bool}:指示表非正常关闭时是否需要修复。如果需要修复,设置Bool为true将引发自动修复,设置为false将返回元组{error,need_repair}。

{type,TableType}:可以为set、bag或duplicate bag。目前Dets不支持有序集合。

以下是用于优化的选项:

{max_no_slots,Number}:此选项将把表分段,用于优化表关于插入操作的时间。默认值为200万个记录,最大值为3200万。

{min_no_slots,Number}:如果估算值正确,将会提高性能。默认值为256项元。

{ram_file,Bool}:如果需要在表中填充大量的元素,此选项可以提高性能。它把元素存储在RAM中,当调入dets:sync(Name)函数或关闭表时,输入这些元素到文件,标签flag默认设为false。

3.在自己的进程结束或调用dets:close(Name)函数后将会关闭Dets表。如果几个进程打开同一个表,这个表会保持打开的状态,直到所有的进程终止或者明确关闭这个表,如果在Erlang运行时系统终止之前没有关闭表,这将导致在下次表打开时自动修复。这可能会是一个很耗时的任务,而时间取决于表的大小。

4.打开一个已经存在的表使用dets:open_file(filePath)会返回{ok,Ref},其中表是通过引用来进行存取的,而不是通过名字,只有在打开表的进程崩溃(或终止)后,表才变得不可用。

5.当需要存储大小可预估的关键数据时,就需要使用ETS和Dets表。如果系统需要事务、查询、复制和一致性等功能,应该使用Mnesia,这个数据库应用程序作为OTP中间件的一部分发布。它把ETS和Dets结合Erlang分布式一起使用,提供一个关系数据模型,它支持原子性的分布式事务、检查点、备份、分段、运行时模式配置改变。

 6.traverse函数使用Dets表名和一个应用到每个元素的fun函数作为参数,例如:

restore_backup()->

Insert = fun(#usr{msisdn=PhoneNo,id=Id}=Usr)->

ets:insert(usrRam,Usr),

ets:insert(usrIndex,{Id,PhoneNo}),

continue

end.

dets:traverse(usrDisk,Insert).

我们传递的fun函数在usrRam和usrIndex都创建了一个记录,并把用户数据库恢复到最初的状态。

7.当遍历表时,我们需要使用safe_fixtable/2锁定表,因为在遍历时,具有破坏性的操作会引起运行时错误,或者更糟,将导致无法预计的行为。使用safe_fixtable/2可以保证只遍历每个元素一次,而不会受到任何在遍历开始后执行的破坏性操作的影响。把遍历操作封装在catch语句中是个好主意,因为需要保证在发生运行时错误时错误事件时释放表。不这样做,如果在其他地方捕获了异常,那将会导致内存泄漏。使用catch,而不是try catch的原因是,我们不关于函数的返回值,只是想确定它没有使进程崩溃。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值