Skip to content
云风 edited this page Oct 28, 2015 · 23 revisions

当你把业务拆分到多个服务中去后,数据如何共享,可能是最易面临的问题。

最简单粗暴的方法是通过消息传递数据。如果 A 服务需要 B 服务中的数据,可以由 B 服务发送一个消息,将数据打包携带过去。如果是一份数据,很多地方都需要获得它,那么用一个服务装下这组数据,提供一组查询接口即可。DataCenter 模块对此做了简单的封装。

如果你仅仅需要一组只读的结构信息分享给很多服务(比如一些配置数据),你可以把数据写到一个 lua 文件中,让不同的服务加载它。Cluster 的配置文件就是这样做的。注意:默认 skynet 使用自带的修改版 lua ,会缓存 lua 源文件。当一个 lua 文件通过 loadfile 加载后,磁盘上的修改不会影响下一次加载。所以你需要直接用 io.open 打开文件,再用 load 加载内存中的 string 。

另一个更好的方法是使用 sharedata 模块。

ShareData

sharedata 正是为了大量共享结构化数据却不常更新它们,这种需求而设计出来的。sharedata 只支持在同一节点内(同一进程下)共享数据,如果需要跨节点,需要自行同步处理。

local sharedata = require "sharedata"

可以引入这个模块。

  • sharedata.new(name, value) 在当前节点内创建一个共享数据对象。
  • value 可以是一张 lua table ,但不可以有环。且 key 必须是字符串和正整数。
  • value 还可以是一段 lua 文本代码,而 sharedata 模块将解析这段代码,把它封装到一个沙盒中运行,最终取得它返回的 table。如果它不返回 table ,则采用它的沙盒全局环境。
  • 如果 value 是一个以 @ 开头的字符串,这个字符串会被解释为一个文件名。sharedata 模块将加载该文件名指定的文件。
  • sharedata.update(name, value) 更新当前节点的共享数据对象。
  • sharedata.delete(name) 删除当前节点的共享数据对象。
  • sharedata.query(name) 获取当前节点的共享数据对象。

一旦 query 到一个共享数据对象,你可以像普通 lua 表那样读取其中的数据,但禁止改写。把其中的分支赋值给 local 变量是安全的,但如果你把最终的叶节点的值取出来后,就不可能被数据源的 update 操作更新了。所以,一般你需要持有至少一级表,每次用它来索引其下的数据。

注意: 你不应该在同一服务中 query 相同的对象多次(通常这样做也没有意义), 那会造成在本地生成多份代理对象以及监控协程。如果有必要,你应该 cache 住 query 的对象反复使用。此外,query 到一个对象后,除非该对象被 delete ,暂没有任何手段可以清除本地服务中的 proxy 对象。

一旦有人调用 sharedata.update ,所有持有这个对象的服务都会自动去数据源头更新数据。但由于这是一个并行的过程,更新并不保证立刻生效。但使用共享数据的读取方一定能在同一个时间片(单次 skynet 消息处理过程)访问到同一版本的共享数据。

sharedata 是基于共享内存工作的,且访问共享对象内的数据并不会阻塞当前的服务。所以可以保证不错的性能,并节省大量的内存。

sharedata 的缺点是更新一次的成本非常大,所以不适合做服务间的数据交换。你可以考虑它的替代品:stm 模块。

STM

STM (Software transactional memory) 模块同样基于共享内存,所以也只能用于同一个 skynet 节点内。它是一个试验性模块,不一定比消息传递的方式更好。只是提供一个新思路来进行同一节点内的服务间数据交换。

因为它不经过 skynet 的消息投递,信息传递的时效性比消息投递要好一些。但由于依旧需要在不同的 lua vm 间交换数据,序列化(使用 skynet.pack 和 skynet.unpack)必不可少。因为绕过了消息投递,它还可以用于广播。多个读取者可以同时去读一个写入者更新的数据。

stm 是以一个 C 编写的 lua 模块形式提供的。

local stm = require "stm"

可以引入这个模块。由于 api 多是操作 C 指针,所以调用其中的 api 上需要小心(否则会有内存泄露)。

  • stm.new(pointer, size) 可以生成一个共享对象,生成者可以改写这个对象。pointer/size 是一个 C 指针以及长度。skynet.pack 可以正确生成它们。它返回一个 stmobj ,是一个 userdata ,lua 的 gc 会正确的回收它引用的内存。
  • stm.copy(stmobj) 可以从一个共享对象中生成一份读拷贝。它返回一个 stmcopy ,是一个 lightuserdata 。通常一定要把这个 lightuserdata 传到需要读取这份数据的服务。随意丢弃这个指针会导致内存泄露。注:一个拷贝只能供一个读取者使用,你不可以把这个指针传递给多个服务。如果你有多个读取者,为每个人调用 copy 生成一份读拷贝。
  • stm.newcopy(stmcopy) 把一个 C 指针转换为一份读拷贝。只有经过转换,stm 才能正确的管理它的生命期。

持有 stmobj ,则是这个共享对象的写入者。你可以用 stmobj(pointer, size) 的方式更新其中携带的信息。(这个 userdata 重载了 call 方法)。

持有 stmcopy ,则是这个共享对象的读取者。stmcpy 是用 stm.copy 生成的那个指针,传递给 stm.newcopy 构造出来的。你可以用 stmcopy(function (pointer, size [,ud]) ... end [,ud]) 的方式读出其中的数据。如果数据没有更新,将返回 false ;否则,将更新过的数据指针 ponter 以及长度,传递给传入的反序列化函数,并返回 true 以及反序列化函数的结果。

test/teststm.lua 是一个简单的使用范例。

ShareMap

sharemap 是对 stm 的简单应用。你可以用 sharemap 创建一个对象负责读写一张预定义的数据结构(使用 sproto 描述结果)。然后构造出读对象传递给其它服务。

当读写方修改了数据内容后,可以通过调用 commit 将修改后的副本同步给所有读取方。而读取方则需要主动调用 update 获得最新副本。

test/testsm.lua 是一个简单的使用范例。

注意:如果需要同步的数据结构比较大,这种方式的成本也会增加。因为每次 commit 都会全量序列化整个结构。

Clone this wiki locally