[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来做遍历啦
坐车坐过站,就是这样子的。。。
写下来是好习惯: Notes