技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 算法 --> Lua 中 Cache 冷数据的落地

Lua 中 Cache 冷数据的落地

浏览:650次  出处信息

   今天有同学跟我讨论了一下最近发现的一个 bug ,我觉得挺有意思的。

   需求是这样的:

   我们的系统中,有一些数据是从外存(数据库)加载进来的,由于性能考虑,并不需要每次修改这些数据就写回外存。希望在数据变冷后,定期落地即可。

   典型的场景是一个 cache 模块,cache 的是一些玩家的业务数据,可以通过 uuid 从数据库索引到。一旦业务需要访问玩家数据,cache 模块会从数据库加载对应数据,然后把数据表交出去。当业务再次需要这些数据的时候,cache 模块一旦发现数据存在于 cache 中,就直接交给玩家。

   cache 模块还希望在数据很久没有被业务访问时,将这些数据写回数据库。

   我们的系统是基于 lua 构建的,数据 cache 模块和修改这些数据的逻辑在同一个 vm 里。难点在于,修改数据的业务逻辑是可以长期持有数据的,cache 模块需要正确感知这点。

   先来看看最朴素的实现方法:

   cache 模块其实就是一张 uuid : 数据  表。加载数据的时候,检查 cache 中是否存在,如果没有就把数据写到 cache 表中。然后刷新一下数据的时间戳,表示最后访问的时间。

   数据落地流程是:从队列中取出一个超时(最后一次访问过于久远的)待落地数据,擦除 cache 对应项,标记 uuid 为锁定状态(阻止加载流程在落地过程重新加载),落地,完成后解锁(并唤醒潜在的加载需求)。

   这个方法可以实现在使用数据的过程中,如果有新的访问需求,是不需要从数据库加载,并贡献内存中的同一份数据对象的。

   但是这个方法是有漏洞的,因为访问时间久远,并不意味着没有人持有它。而落地前的锁只能阻止加载的冲突,不能阻止持有数据的人在落地过程中改写数据。


   为了解决这个问题,我们之前采取了一个改进方案。

   使用 lua 的弱表来管理 cache 。在没有人引用数据后,弱表中对应项会消失,此时才是数据落地的最佳时机。因为不会有改写者干扰这个流程,仅仅锁住新的加载会引起的冲突即可。注:如果需要定期落地,只需要定期把数据复制出去落地即可。

   直接给数据块加上 __gc 方法,在 gc 流程中做数据落地是不可行的。因为不提倡在 __gc 方法中做过于复杂的工作。所以我们只是在 __gc 中把对象重新放回一张叫 save 的待处理表,即让这个数据表“复活”了。所谓复活,指在之前的 gc mark 流程,它已不被 vm 里除 __gc 方法外的任何地方引用,但是在 sweep 阶段,又被重新塞会 vm 中,并不真的被 sweep 掉。关于这个用法,lua 实现的很好。

   之后,落地流程可以慢慢的逐个处理 save 表。这个过程中,如果有业务需要访问数据,那么它可以同时检查 cache 表和 save 表里是否有数据,如果存在于 save 表中,则移回 cache 表。

   ps. 在此基础上,还可以做一个优化:在 cache 表的数据不多的时候,不要设置成弱表,防止不用的数据太早被清理。只有表大到一定程度才设置成弱表;或者使用两张表做 cache ,一张弱表一张强表,定期按时间戳把冷数据移去弱表部分。


   方案看起来不错。

   当数据正被引用时,它总是存在于 cache 表中,不同地方的多次访问会取到同一个引用。

   只有当数据没有任何业务引用时,它才会从 cache 表中移走,有另一个落地流程会逐步处理这些不被人引用的数据。这可以防止在落地流程中,有业务对数据修改。

   在待落地处理的数据尚未处理时,如果有新的访问需求,那么会抢在落地前拿回来,整个系统中每份数据的引用还是一致的。

   锁只需要加在数据落地和数据加载上,防止数据落地的过程中,同时加载数据。


   但今天有同学报告了这个方案的 bug 。

   问题出在 gc 把未引用的数据从 cache 这张弱表里抹掉的操作,和被抹掉的数据的 __gc 方法将其加回 save 表这两者并不在一个原子操作内。

   也就是说系统会处于某种第三状态。一个 uuid 对应的数据并不在 cache 表中,也不在 save 表中,从业务逻辑上看,这组数据从 vm 中消失了。

   当一组数据处于第三态时,如果此刻发生了访问请求,那么就会触发加载流程,从数据库加载一个老版本(新版本尚未落地)。

   怎么解决呢?

   直接的方法是,当一组数据加载时,我们把 uuid 记录在一个独立集合中;只有在它真正被落地/丢弃处理后,才从这个集合抹掉。

   这样数据无论处于三种状态中的什么状态,我们都可以阻止已经处于系统中的数据再次从外存加载一个旧版本。

   但一旦处于第三态的数据被请求,我们似乎没有什么好的方法把它从第三态拉回来。因为它的的确确从 vm 中(暂时)消失了。能做的只有等。说到等,这和等数据从数据库加载似乎没有本质区别。我们只能在有等待操作期间,不断的调用 gc 的 step ,督促 gc 过程进行下去,直到(也肯定能等到)数据从第三态出来,进入 save 表。(lua 的 gc 默认行为是只有新的内存申请发生,才可能发生下一步的行动)


   有没有别的方案?

   利用元表给数据访问加一个间接层能从另一个角度解决这个问题。

   如果我们给需要 cache 数据表加上一层代理,让代理的 __index__newindex 都指向真正的数据表。那么业务用起来就是完全一样的。

   cache 表里放的仅仅是 uuid : 代理对象。

   另外,将真正的数据表全部放在一张额外的 all 表中,并不让业务层直接接触这张表。

   业务层不再引用某个 uuid 对应的数据时,cache 中消失的其实是代理对象,而不是真正的数据表。真正的数据表依然存在于 all 表中。

   落地流程要处理的其实是 all 和 cache 的差集。它可以定期把差集求出来,放入 save 表中去处理。注:这里依旧生成一张 save 表,而不是直接对差集处理,是因为求差集时刻在变化,而我们无法一次将所有的差集里的数据全部落地。


   ps. 以上的方案都隐含着另一个问题没有解决:如果业务私自保留了数据表中的部分子表的引用,cache 模块是无法感知的。不过这点比较容易通过约束业务的使用方法来回避。

建议继续学习:

  1. Buffer和cache的区别是什么?    (阅读:6849)
  2. 谈冷热数据    (阅读:5853)
  3. Linux操作系统中内存buffer和cache的区别    (阅读:5328)
  4. 学习:一个并发的Cache    (阅读:5043)
  5. 关于Linux的文件系统cache    (阅读:4848)
  6. Twitter架构图(cache篇)    (阅读:4796)
  7. Nginx与Lua    (阅读:4761)
  8. 详解MyISAM Key Cache(前篇)    (阅读:4089)
  9. 7个示例科普CPU Cache    (阅读:4208)
  10. [squid] 过期时间在 60 秒内 squid 不 Cache 的问题    (阅读:4029)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1