不需要显示用锁,插入和查询时间不仅快而且控制为常量,这就是Erlang的ETS Table.

为什么而设计?

Erlang中可以用List表达集合数据,但是如果数据量特别大的话在List中访问元素就会变慢了;这种主要是由于List的绝大部分操作都是基于遍历完成的.

Erlang的设计目标是软实时(参考:http://en.wikipedia.org/wiki/Real-time_computing),在大量数据中检索的时间不仅要快而且要求是常量.为了解决快速查

询的问题,Erlang提供的机制就是ETS(Erlang Term Storage)和DETS(Disk Erlang Term Storage).本文只关注ETS.

ETS基础

  1. ETS查询时间是常量,例外是如果使用ordered_set查询时间与logN成正比(N为存储的数据量)
  2. ETS 存储数据的格式是Tuple,下面的测试代码中我们可以看到细节
  3. ETS Table由进程创建,进程销毁ETS Table也随着销毁,在使用Shell做ETS实验的时候要注意一下,Table的拥有关系可以give_away 转交给其它进程
  4. 一个Erlang节点的ETS表的数量是有限制的,默认是1400个表,在启动erlang节点之前修改 ERL_MAX_ETS_TABLES参数可以修改这个限制ejabberd社区站点上总结的性能调优中提到了这一点,点击这里查看: 

    http://www.ejabberd.im/tuning

  5. ETS表不在GC的管理范围内,除非拥有它的进程死掉它才会终止;可以通过delete删除数据 
  6. 目前版本,insert和lookup操作都会导致对象副本的创建,insert和lookup时间对于set bag duplicate_bag都是常量值与表大小无关.
  7. 并发控制:所有针对一个对象的更新都被保证是原子的、隔离的:修改要么全部成功要么失败。也没有其它的中间结果被其它的进程使用。有些方法可以在处理多个对象的时候保证这种原子性和隔离性。
    在数据库术语中隔离级别被称作序列化,就好像所有隔离的操作一个接一个严格按照顺序执行。
  8. 在遍历过程中,可以使用safe_fixtable来保证遍历过程中不出现错误,所有数据项只被访问一遍.用到逐一遍历的场景就很少,使用safe_fixtable的情景就更少。不过这个机制是非常有用的,
    还记得在.net中版本中很麻烦的一件事情就是遍历在线玩家用户列表.由于玩家登录退出的变化,这里的异常几乎是不可避免的.select match内部实现的时候都会使用safe_fixtable

查看ETS Table

Erlang提供了一个可视化的ETS查看工具 The Table Visualizer,启动tv:start(),界面比较简单.值得一提的是,这个工具可以跨节点查看ETS信息,在File菜单里面有一个nodes选项,

打开会给出和当前节点互相连通的节点列表,点击节点会显示这个节点上的ETS Table信息.

在没有可视化工具的时候我们如何查看ETS的信息?而且这还是比较常见的情况,在文本模式操作服务器的情况下,Table Visualizer根本没法使用.下面的命令可以达到同样的效果:

ets:all() %列出所有的ETS Table

ets:i() %给出一个ETS Table的清单 包含表的类型,数据量,使用内存,所有者信息

ets:i(zen_ets) % 输出zen_ets表的数据,个人感觉这个非常方便比tv还要简单快捷,如果表数据量很大,它还提供了一个分页显示的功能

ets:info(zen_ets) %单独查看一个ETS Table的详细信息也可以使用这个方法,如果怀疑这个表被锁了可以使用ets:info(zen_ets,fixed)查看,ets:info(zen_ets,safe_fixed) 可以

获得更多的信息,这样比较容易定位是哪个模块出了问题.

ets:member(Tab, Key) -> true | false %看表里面是否存在键值为Key的数据项.

创建 删除ETS Table插入数据

上面已经提到了ETS存储数据的格式是Tuples,我们动手写一些测试代码看一下ETS的常规操作:

%快速创建一个ETS Table 并填充数据

T = ets:new(x,[ordered_set]).

[ ets:insert(T,{N}) || N <- lists:seq(1,10) ].

TableID = ets:new(temp_table , []), %Create New ETS Table

ets:insert(TableID,{1,2} ),                  % insert one Item to Table

Result= ets:lookup(TableID ,1),

io:format("ets:lookup(TableID ,1) Result: ~p ~n  " ,[ Result  ]),

ets:insert(TableID,{1,3} ),

Result2 = ets:lookup(TableID, 1 ),

io:format("ets:lookup(TableID ,1) Result2: ~p ~n  ", [ Result2 ]),

ets:delete(TableID),

BagTableID =  ets:new(temp_table, [bag]),

ets:insert(BagTableID,{1,2} ),

ets:insert(BagTableID,{1,3} ),

ets:insert(BagTableID,{1,4} ),

%Note that the time order of object insertions is preserved;

%The first object inserted with the given key will be first in the resulting list, and so on.

Result3 = ets:lookup(BagTableID, 1 ),

io:format("ets:lookup(BagTableID ,1) Result3: ~p ~n  ", [ Result3 ])

%创建ETS表 注意参数named_table,我们可以通过countries原子来标识这个ETS Table

ets:new(countries, [bag,named_table]),

%插入几条数据

ets:insert(countries,{yves,france,cook}),

ets:insert(countries,{sean,ireland,bartender}),

ets:insert(countries,{marco,italy,cook}),

ets:insert(countries,{chris,ireland,tester}).

我不明白为什么宁愿相信别人的话,也不愿意自己动手写一段测试代码看看;别人说对了还好,如果说错了呢?快速的构建一个测试模型,去验证自己的想法,这种能力会在一次次实验中不断强化;比如有人问到"ETS INSERT是每次都新加一个还是会更新一个",在Erlang Shell中就可以验证它(下面默认创建的是Set类型):
Eshell V5.9  (abort with ^G)
1> ets:new(test,[named_table]).
test
2> [ets:insert(test,{Item}) || Item <-[1,2,3,4,5,6]].
[true,true,true,true,true,true]
3> [ets:insert(test,{Item}) || Item <-[1,2,3,4,5,6]].
[true,true,true,true,true,true]
4> ets:i(test).
<1 > {5}
<2 > {3}
<3 > {2}
<4 > {1}
<5 > {4}
<6 > {6}
EOT (q)uit (p)Digits (k)ill /Regexp -->q ok

分页从ETS中提取数据

有时候匹配的数据量很大,如果一次性把所有的数据都取出来,处理会非常慢;一个处理方法就是分批次处理,这也就要求我们能够分多次

从ETS Table中取数据.这和做网页分页很像.ets类库中提供了一系列方法来实现这个功能这里我们以match为例:

match(Tab, Pattern, Limit) -> {[Match],Continuation} | '$end_of_table'

参数Limit就是每一次查询的数量限制,如果实际匹配的数据量超过了Limit就会返回{[Match],Continuation}的结果,Match代表查询的结果集,可以推测

Continuation包含分页的信息,如果继续取下一页的结果集使用下面的方法:

match(Continuation) -> {[Match],Continuation} | '$end_of_table'

我们通过demo看一下分页查询的结果,特别是Continuation的数据结构,首先我们先填充一些测试数据:

  

  

我们每页10条数据,执行4次,代码如下:

{M,C}=ets:match(zen_ets,'$1',10). %第一页

{M2,C2} = ets:match(C).               %第二页

{M3,C3} = ets:match(C2).             %第三页

{M4,C4} = ets:match(C3).            %没有数据了看异常是什么?

展开下面的代码查看调用结果:

执行结果

类似的还有:

match_object(Tab, Pattern, Limit) -> {[Match],Continuation} | '$end_of_table'

match_object(Continuation) -> {[Match],Continuation} | '$end_of_table'

select(Tab, MatchSpec, Limit) -> {[Match],Continuation} | '$end_of_table'

select(Continuation) -> {[Match],Continuation} | '$end_of_table'

只获取匹配数据的数量: select_count(Tab, MatchSpec) -> NumMatched

ETS 使用Match specifications 查询

match方法进行匹配最简单, '$数字'代表占位符,'_'代表通配符;'$数字'这种表示方式,数字的大小代表什么?

从下面的代码示例中可以看出数字控制的是输出结果顺序,数字相对大小代表相对位置顺序;

%'_' 通配符
A= ets:match(countries, {'$1','_','_' } ) ,
io:format(" ets:match(countries, {'$1','_','_' } ) Result : ~p ~n " ,[ A ]), 
B= ets:match(countries , {'$1', '$0' ,'_' } ),
io:format(" ets:match(countries , {'$1', '$0' ,'_' } ), Result : ~p ~n " ,[ B ]),
C= ets:match(countries , {'$11', '$9' ,'_' } ),
io:format(" C= ets:match(countries , {'$11', '$9' ,'_' } ), Result : ~p ~n " ,[ C ]),
D= ets:match(countries , {'$11', '$99' ,'_' } ),
io:format(" ets:match(countries , {'$11', '$99' ,'_' } ), Result : ~p ~n " ,[ D ]),
E= ets:match(countries , {'$101', '$9' ,'_' } ),
io:format("ets:match(countries , {'$101', '$9' ,'_' } ), Result : ~p ~n " ,[ E ]),
F= ets:match(countries,{'$2',ireland,'_'}),
G= ets:match(countries,{'_',ireland,'_'}), % [[],[]] 如果没有数字占位符 是没有结果输出的 只是空列表
H= ets:match(countries,{'$2',cook,'_'}),
I= ets:match(countries,{'$0','$1',cook}),
J= ets:match(countries,{'$0','$0',cook}),

  

如果是需要所有字段,提取整个数据项,那就直接使用match_object,

K= ets:match_object(countries,{'_',ireland,'_'}),

io:format(" ets:match_object(countries,{'_',ireland,'_'}),  Result : ~p ~n " ,[ K ]),

L= ets:match(countries ,'$1' ),

io:format(" ets:match(countries ,'$1' ),  Result: ~p ~n " ,[ L ]),

Result=ets:match_delete(countries,{'_','_',cook}),

io:format("ets:match_delete(countries,{'_','_',cook}),   Result : ~p ~n " ,[ Result ]),

上面的例子countries这个结构很简单,但是如果是一个字段稍多写的结构呢?很容易出现类似ets:match(zen_ets, {'$1','_','_','_','_','_' } )  .这样的代码,不仅可读性差,而且一旦字段顺序发生

变化,这里就容易出错.解决方法在[Erlang 0006] Erlang中的record与宏  一文中已经提到过,使用record可以规避掉tuple字段增减,顺序的问题.

例如:    ets:match_delete(zen_ets, #t{age=24,iabn=1,_='_'}),

有时候我们需要表达更为复杂的匹配条件,这就需要使用Match specifications了,ms的解析依赖ms_transform模块,所以首先我们在模块头添加

include_lib("stdlib/include/ms_transform.hrl").增加对ms_transform.hrl头文件的引用.Match specifications的详细说明参见这里: http://www.erlang.org/doc/apps/erts/match_spec.html

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

MSResult = ets:select(countries, MS ),

io:format("ets:fun2ms(fun({ Name,Country , Position }  ) when Position /=cook -> [Country,Name ] end   ), MSResult:~p~n " , [MSResult ]),

MS2 =ets:fun2ms(fun(Data ={Name, Country ,Position } )  when Position /=cook -> Data end   ),

MSResult2 = ets:select(countries , MS2),

io:format("ets:fun2ms(fun(Data ={Name, Country ,Position } )  when Position /=cook -> Data end   ),   Result : ~p ~n " ,[ MSResult2 ]),

%当我们使用的是Tuple的时候这里必须使用完全匹配

MS3  = ets:fun2ms(fun(Data ={Name, Country ,Position } )  when Position /=cook -> Data end   ),

MSResult2 = ets:select(countries , MS3),

在实战操作中,我们遇到这样一个问题,下面的MS MS2是等效的么?  ets:fun2ms(fun(#t{id =ID , name =Name, _='_'  } ) when ID >30 -> Name  end ),亮点是红色标记的部分.可以运行一下下面的

代码看,两者是生成的ms是一样的.

MS = ets:fun2ms(fun(#t{id =ID , name =Name  } ) when ID >30 -> Name  end ),

io:format(" ets:fun2ms(fun(#t{id =ID , name =Name  } ) when ID >30 -> Name  end ),  MS:  ~p ~n   " , [ MS ]),

MS2 = ets:fun2ms(fun(#t{id =ID , name =Name, _='_'  } ) when ID >30 -> Name  end ),

io:format(" ets:fun2ms(fun(#t{id =ID , name =Name, _='_'  } ) when ID >30 -> Name  end ), MS2: ~p ~n    " ,[  MS2 ]),

io:format("MS==MS2 ? Result : ~p ~n  " , [ MS==MS2 ]),

MSResult = ets:select(zen_ets , MS ),

在使用MS的过程中,还有一个特殊的情况,如果要返回完整的record应该怎么写呢?仔细阅读ETS文档,可以看到这么一句:The return value is constructed using the "match variables" bound in

the MatchHead or using the special match variables '$_' (the whole matching object) and '$$' (all match variables in a list), so that the following ets:match/2 expression:

再翻看http://www.erlang.org/doc/apps/erts/match_spec.html,可以看到下面的说明:

ExprMatchVariable ::= MatchVariable (bound in the MatchHead) | '$_' | '$$'

也就是说只要这样'$_'就可以了,试验了一下MS3 = ets:fun2ms(fun(T=#t{id =ID , name =Name, _='_'  } ) when ID >30 -> T  end )生成的ms是:

,MS3: [{{t, '$1',  '_','$2', '_',  '_'},      [{'>', '$1', 30}],['$_']}]

拓展阅读:

2003年的论文 <<Erlang ETS Table的实现与性能研究>>

A Study of Erlang ETS Table Implementation and Performance. [点此下载]

Scott Lystig Fritchie. 
Second ACM SIGPLAN Erlang Workshop. 
Uppsala, Sweden, August 29, 2003.

详见:ligaoren博园

Erlang ETS Table的更多相关文章

  1. Erlang ets -- something about cache continue

    上一次说到了实现一个简单cache 的基本思路和想法, http://www.cnblogs.com/--00/p/erlang_ets_something_about_cache.html 在文末, ...

  2. Erlang ets -- something about cache

    都说用ets 写一个cache 太简单, 那就简单的搞一个吧, 具体代码就不贴了, 就说说简要的需求和怎么做(说设计有点虚的慌). 需求场景 >> 查询系统,对于主存储而言,一次写入多次查 ...

  3. erlang ets表

    一.表遍历 通过ets:first/1获取表的第一个关键字,表中下一个关键字用ets:next/2得到,直到ets:next/2返回'$end_of_table' 当多几个进程并发访问ets表时,可以 ...

  4. [Erlang 0126] 我们读过的Erlang论文

    我在Erlang Resources 豆瓣小站上发起了一个征集活动 [链接] ,"[征集] 我们读过的Erlang论文",希望大家来参加.发起这样一个活动的目的是因为Erlang相 ...

  5. Erlang--etc结构解析

    Erlang中可以用List表达集合数据,但是如果数据量特别大的话在List中访问元素就会变慢了;这种主要是由于List的绝大部分操作都是基于遍历完成的. Erlang的设计目标是软实时(参考:htt ...

  6. Erlang模块ets翻译

    概要: 内置的存储 描述: 这个模块是Erlang内置存储BIFs的接口.这些提供了在Erlang运行时系统中存储大量数据的能力,并且能够对数据进行持续的访问时间.(在ordered_set的情况下, ...

  7. [Erlang 0127] Term sharing in Erlang/OTP 上篇

    之前,在 [Erlang 0126] 我们读过的Erlang论文 提到过下面这篇论文: On Preserving Term Sharing in the Erlang Virtual Machine ...

  8. Erlang 虚拟机内的内存管理(Lukas Larsson演讲听写稿)

    Erlang核心开发者Lukas Larsson在2014年3月份Erlang Factory上的一个演讲详细介绍了Erlang内存体系的原理以及调优案例: http://www.erlang-fac ...

  9. Erlang使用相关笔记

    #从源码编译安装Erlang 1. wget http://www.erlang.org/download/otp_src_r16b.tar.gz -p /usr/local/src 2. tar z ...

随机推荐

  1. PriorityBlockingQueue 原理分析

    PriorityBlockingQueue是一个支持优先级的无界阻塞队列,直到系统资源耗尽.默认情况下元素采用自然顺序升序排列.也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始 ...

  2. 1333:【例2-2】Blah数集

    1333:[例2-2]Blah数集 注意是数组,答案数组中不能有重复数字 q数组是存储答案的 代码: #include<iostream> #include<cstdio> # ...

  3. Python自然语言处理笔记【一】文本分类之监督式分类

    一.分类问题 分类是为了给那些已经给定的输入选择正确的标签. 在基本的分类任务中,每个输入都被认为与其他的输入是隔离的.每个类别的标签集是预先定义好的(只有把类别划分好了,才能给输入划分类别). 分类 ...

  4. Docker Kubernetes 容器扩容与缩容

    Docker Kubernetes 容器扩容与缩容 环境: 系统:Centos 7.4 x64 Docker版本:18.09.0 Kubernetes版本:v1.8 管理节点:192.168.1.79 ...

  5. PHP快速排序(递归)

    日常的排序算法中,快速排序是其中一种.实现起来相对简单. 假设有一个数组,有若干(N)个元素(数字且无序),需要对其进行从小到大的排序. 快速排序的思路是怎么样的呢? 取一个中间值,然后,用其他数组元 ...

  6. shell的交互式和非交互式登录

    工作中经常碰见环境变量加载问题,归根结底就是配置文件的加载问题. 一般会有四种模式:交互式登陆.非交互式登陆.交互式非登陆.非交互非登陆. 交互式和非交互式对环境变量的加载: +----------- ...

  7. SSM登录跳转到登录页,登录页不能加载js和样式

    SSM登录跳转到登录页,登录页不能加载js和样式选用jsppage添加根路径. <% String rootPath = request.getContextPath(); %> < ...

  8. $O(n+log(mod))$求乘法逆元的方法

    题目 LOJ #152. 乘法逆元 2 题解 一个奇技淫巧qwq.可以离线求乘法逆元,效率\(O(n+log(mod))\). 考虑处理出\(s_n\)表示\(\prod_{i=1}^na_i\).以 ...

  9. 20175312 2018-2019-2 《Java程序设计》第1周学习总结

    20175312 2018-2019-2 <Java程序设计>第1周学习总结 教材学习内容总结 已依照教材要求完成了第一章的学习,我总结的话,主要的学习量还是在安装相关软件上.其他的,比如 ...

  10. Learning-Python【4】:Python流程控制与循环

    一.if...else分支 1.什么是if判断 判断一个条件如果成立则如何,不成立则如何 2.为何要有if判断 让计算机能像人一样具有判断能力 语法1:if...else if 判断条件: 代码块1 ...