Lua 中 Cache 冷数据的落地

栏目: Lua · 发布时间: 4年前

内容简介:今天有同学跟我讨论了一下最近发现的一个 bug ,我觉得挺有意思的。需求是这样的:我们的系统中,有一些数据是从外存(数据库)加载进来的,由于性能考虑,并不需要每次修改这些数据就写回外存。希望在数据变冷后,定期落地即可。

今天有同学跟我讨论了一下最近发现的一个 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的区别是什么?    (阅读:6104)
  2. Linux操作系统中内存buffer和cache的区别    (阅读:4683)
  3. 学习:一个并发的Cache    (阅读:4342)
  4. 关于 Linux 的文件系统cache    (阅读:4162)
  5. Nginx与Lua    (阅读:4065)
  6. Twitter架构图(cache篇)    (阅读:3742)
  7. 详解MyISAM Key Cache(前篇)    (阅读:3602)
  8. [squid] 过期时间在 60 秒内 squid 不 Cache 的问题    (阅读:3442)
  9. Lua GC 的源码剖析 (2)    (阅读:3344)
  10. 7个示例科普CPU Cache    (阅读:3346)

QQ技术交流群:445447336,欢迎加入!

扫一扫订阅我的微信号:IT技术博客大学习


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构与算法分析(C++版)(第3版)

数据结构与算法分析(C++版)(第3版)

Clifford A. Shaffer / 张铭、刘晓丹、等译 / 电子工业出版社 / 2013 / 59.00元

本书采用当前流行的面向对象的C++程序设计语言来描述数据结构和算法, 因为C++语言是程序员最广泛使用的语言。因此, 程序员可以把本书中的许多算法直接应用于将来的实际项目中。尽管数据结构和算法在设计本质上还是很底层的东西, 并不像大型软件工程项目开发那样, 对面向对象方法具有直接的依赖性, 因此有人会认为并不需要采用高层次的面向对象技术来描述底层算法。 但是采用C++语言能更好地体现抽象数据类型的......一起来看看 《数据结构与算法分析(C++版)(第3版)》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具