[Erlang23]怎么有效的遍历ETS表?


 
最近处理的Bug,记录下:
出现的问题:
不稳定出现gen_server:call/3 的timeout;
直接原因:是call的timeout时间为10s,但遍历ets表处理时间大于10s[居然会有进程处理一个请求大于10s,真是奇迹,下面再解释原因].
 
1.最原始的代码处理:
 
%%遍历表并回写数据
%%do_update_data/2里面做了ets:instert/2更新的操作和其它处理
handle_call({update_data,Request}, _From, State) ->
  [begin do_update_data(Item,Request)end|| Item<-ets:tab2list(?TABLE_NAME)],
  {reply, ok, State};
一看,居然有ets:tab2list/1这种取数据的操作,当表数据记录少时,不会出问题,但是当数量达到14w时,这样一次性取数据就会把内存骤然加大【一直想不通为什么ETS设计者要把这种函数导出来...】。
 
2.改进后代码处理:
handle_call({update_data,Func}, _From, State) ->
  ets:safe_fixtable(?TABLE_NAME, true),
  update_data(ets:first(?TABLE_NAME), Func),
  ets:safe_fixtable(?TABLE_NAME, false),
  {reply, ok, State}.

update_data('$end_of_table',_) ->
  ok;
update_data({key, Value1, Value2, _, _, _}=Idx,Func)
  when Value1 == "test"; value1 == '_' ->
  case ets:lookup(?TABLE_NAME, Idx) of
    [Ele] -> Func(Ele);
    [] -> ok
  end,
  update_data(ets:next(?TABLE_NAME, Idx),Func);
update_data(Idx,Func) ->
  update_data(ets:next(?TABLE_NAME, Idx), Func).

锁定一个类型是 set,bag 或 duplicate_bag 的表,使其可以安全遍历表里的数据;

在一个进程里调用 ets:safe_fixtable(Tab, true) 可以锁定一个表,直到在进程里调用 ets:safe_fixtable(Tab, false) 才会解锁,或进程崩溃。

如果同时有几个进程锁定一个表,那么表会一直保持锁定状态,直到所有进程都释放它(或崩溃)。有一个引用计数器记录着每个进程的操作,有 N 个持续的锁定操作必须有 N 个释放操作,表才会真正被释放。

当一个表被锁定,一序列的 ets:first/1 和 ets:next/2 的调用都会保证成功执行,并且表里的每一个对象数据只返回一次,即使在遍历的过程中,对象数据被删除或插入。在遍历过程中插入到表里的新数据可能由 ets:next/2 返回(这取决有键的内部顺序)。
 
所以不用担心你在遍历同时又做了ets:insert/2操作手,遍历还是不是有效的【绝对有效】.
 
3.那么还有什么不对么?
  对于gen_server进程还除了处理这种请求外还有其它事,如果一个请求时间处理过长,其它的请求就会连锁timeout.
再改进:
handle_call({update_data,Func}, _From, State) ->
  ets:safe_fixtable(?TABLE_NAME, true),
  update_data(ets:first(?TABLE_NAME), Func,0),
  ets:safe_fixtable(?TABLE_NAME, false),
  {reply, ok, State}.

update_data('$end_of_table',_,_) ->
  ok;
update_data({key, Value1, Value2, _, _, _}=Idx,Func,Counter)
  when Value1 == "test"; value1 == '_' ->
  case ets:lookup(?TABLE_NAME, Idx) of
    [Ele] -> Func(Ele);
    [] -> ok
  end,
  NewCounter =
    if Counter >=50 ->
      timer:sleep(10),1;
      true ->
        Counter+1
    end,
  update_data(ets:next(?TABLE_NAME, Idx),Func,NewCounter);
update_data(Idx,Func,Counter) ->
  update_data(ets:next(?TABLE_NAME, Idx), Func,Counter).
加了一个参数counter,每处理50个就会sleep 10ms,这样sleep时间一到时,就会所有的消息队列重新有均等的机会来竞争;
 
4. 上面这个方案有一个缺点:会把这个遍历ETS表的操作时间再增加Length*10ms的时间。明显很不好。
我们再换换:
 
 
这样就可以把读写分开,并且把每一条记录单独处理,完全解决了被请求进程处理时间过长的问题,
代码改动太多,就不贴了,基本思路就是新造一个gen_server用来把这个非常耗时的请求分成众多小的请求
 
也许你会想:为什么不直接在请求进程里面分成小请求,就不用新起进程了,
如果你的请求进程做的事不多,也可以用这样做,但是在实际中请求进程也会处理大量的请求,所以为了不把请求进程负担加重,
最好的方法还是新起进程来做特定的事。
 
 
通过这个Bug,我明显感觉到了写代码心态很重要:
其实这个Bug最直接的修复方式就是把Timeout时间变成infinity就可以解决Bug,
但是通过分析原因,我们可以把最根本的设计问题解决掉,这种感觉真是太棒啦!!!
 
最后:大家以后千万不要在项目中使用ets:tab2list/1来做遍历啦
 

 
坐车坐过站,就是这样子的。。。
posted @ 2014-09-24 15:02  写着写着就懂了  阅读(4736)  评论(8编辑  收藏  举报