都说用ets 写一个cache 太简单, 那就简单的搞一个吧, 具体代码就不贴了, 就说说简要的需求和怎么做(说设计有点虚的慌).
需求场景
>> 查询系统,对于主存储而言,一次写入多次查询
所以,cache 需要能实现:
UserA 在查询 RecordA 时, UserB 也需要查询RecordA, 就让UserB waiting, 待UserA 查询完成之后, 共享RecordA 的查询结果.
>> 限制单个ets 表的内存使用量,先进先出
那就需要个queue,求 queue length 的频率较大,考虑下RabbitMQ 的 lqueue
>> 限制单个Record 的内存使用量, 如果小于limit,就保留Record,反之,不保留
>> 辅助性的一些feature (reset memory limit, clean all cache, get cache informations, delete single cache ...)
Query 状态
既然UserA 在查询RecordA 时,若UserB 也需要查询,就让UserB等待.就需要保存查询的状态, cache 的结构:
{QueryTerms, QueryStatus, WaitingUser, QueryResult}
QueryTerms 即查询条件
QueryStatus 是查询状态, 正在处理查询为handling, 查询已经处理完毕为handled
WaitingUser 是等在查询的user, 若QueryStatus 为 handling, 就将 ‘erlang:self()‘ append 到WaitingUser, 若QueryStatus 为handled, QueryResult 即为需要的查询结果
QueryResult 查询结果
FIFO queue
cache 不能无休无止的消耗内存, 需要加一个memory total limit, 当超过limit 后, cache 就FIFO .
这样的话, gen_server 进程除了维持ets table 外, 还需要维护queue , 然后refresh queue len 和 memory .
refresh memory 的简单代码:
1 handle_info({refresh_mem}, #state{queue_mem = UNQueueMem, 2 queue = Queue, 3 etstable = EtsTable} = State) -> 4 QueueMem = UNQueueMem * 1024 * 1024 / 8, 5 case catch ets:info(EtsTable, memory) of 6 Mem when erlang:is_integer(Mem) -> 7 if 8 Mem > QueueMem -> 9 case lqueue:is_empty(Queue) of 10 true -> 11 {noreply, State, ?HIBERNATE_TIMEOUT}; 12 _ -> 13 {{value, OldQueryTerms}, NewQueue} = lqueue:out(Queue), 14 delete_old_ets(EtsTable, OldQueryTerms), 15 erlang:send(erlang:self(), {refresh_mem}), 16 {noreply, State#state{queue = NewQueue}, ?HIBERNATE_TIMEOUT} 17 end; 18 true -> 19 {noreply, State, ?HIBERNATE_TIMEOUT} 20 end 21 ; 22 _ -> 23 {noreply, State, ?HIBERNATE_TIMEOUT} 24 end;
L1 处的 queue_mem 为 total memory limit
若超过 total memory limit 且queue 不为空, 就 queue out 并在ets table 中将Record 删除.
single cache limit
既然要作单条Record 内存使用量的限制, 就需要知道single Record 的内存占用量, 最简单的办法是:
ets:info(T, memory) ---> ets:insert(T, R) ---> ets:info(T, memory)
然后计算前后memory 的差值.
在"单进程写入/删除, 多进程读"的模式下,此方式不会出现什么问题.
多进程读写
"单进程(gen_server 进程)写入/删除,多进程读" 的方式应该是比较合理的模式,但是这种方式的弊端也显而易见:效率低,在重负载的单进程的压力增加,进程message queue 堆积,进而出现问题.(即便是能做好隔离,同样会对系统产生影响)
那多进程读写的方式呢?
多进程读写,然后将refresh memory的工作交给gen_server 进程. 这种方式,对于大多数功能,是没有问题的(得益于ets 的特性),但是对single cache limit feature 的实现,就会出现很大的影响.single cache limit 需要对ets 做三次操作:
ets:info(T, memory) ---> ets:insert(T, R) ---> ets:info(T, memory)
多进程读写的话,就很难避免在这三次操作中,穿插 delete/insert 操作, 就很难保证正确性.
这个时候, 就需要safe_fixtable 操作.在网上关于safe_fixtable 的资料比较少, 在此收集一些:
1, 坚强blog (http://www.cnblogs.com/me-sa/archive/2011/08/11/erlang0007.html)
在遍历过程中,可以使用safe_fixtable来保证遍历过程中不出现错误,所有数据项只被访问一遍.用到逐一遍历的场景就很少,使用safe_fixtable的情景就更少。不过这个机制是非常有用的,还记得在.net中版本中很麻烦的一件事情就是遍历在线玩家用户列表.由于玩家登录退出的变化,这里的异常几乎是不可避免的.select match内部实现的时候都会使用safe_fixtable
2, google group 的讨论(https://groups.google.com/forum/#!topic/erlang-china/OnwM5uPVjmI)
其他功能
其他的feature 就没什么好说的了, 堆码而已.