共享数据
目录
- 概述
- [sharedata 完整流程](#sharedata 完整流程)
- [创建 sharedatad 服务](#创建 sharedatad 服务)
- [sharedata.new() 流程](#sharedata.new() 流程)
- [sharedata.query() 流程](#sharedata.query() 流程)
- [sharedata.update() 流程](#sharedata.update() 流程)
- [sharedata 核心实现](#sharedata 核心实现)
- [Lua 层实现](#Lua 层实现)
- [C 层实现](#C 层实现)
- 数据结构
- sharetable
- datacenter
- 对比与选择
- 最佳实践
概述
skynet 提供三种共享数据机制,用于在不同服务之间共享数据:
核心设计目标:
- 内存共享:减少数据复制,降低内存占用
- 高性能访问:提供快速的数据读取能力
- 数据一致性:保证多服务间的数据同步
- 易用性:提供简单的 API 接口
三种机制对比:
| 机制 | 特点 | 适用场景 | 性能 | 更新方式 |
|---|---|---|---|---|
| sharedata | 只读,内存共享,版本管理 | 配置文件、静态数据 | 极高 | 全量更新 |
| sharetable | 只读,虚拟机共享,零复制 | 大表数据、模板数据 | 极高 | 全量更新 |
| datacenter | 可读写,服务间共享 | 动态数据、会话管理 | 中等 | 增量更新 |
模块位置:
- sharedata:
lualib/skynet/sharedata.lua+service/sharedatad.lua+lualib-src/lua-sharedata.c - sharetable:
lualib/skynet/sharetable.lua+lualib-src/lua-sharetable.c - datacenter:
lualib/skynet/datacenter.lua+service/datacenterd.lua
sharedata 完整流程
创建 sharedatad 服务
服务启动流程
require "skynet.sharedata" 时,创建 sharedatad 服务
┌─────────────────────────────────────────────────────────────────────────────┐
│ sharedatad 服务创建流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 服务启动 │
│ skynet.uniqueservice "sharedatad" │
│ ↓ │
│ │
│ 2. 初始化全局数据结构 │
│ local pool = {} -- 共享数据池: name -> {obj, watch} │
│ local pool_count = {} -- 计数器: name -> {n, threshold} │
│ local objmap = {} -- 对象映射: cobj -> v 或 true │
│ ↓ │
│ │
│ 3. 启动垃圾回收协程 │
│ skynet.fork(collectobj) │
│ ↓ │
│ │
│ 4. 注册消息处理函数 │
│ skynet.dispatch("lua", function(session, source, cmd, ...) │
│ local f = CMD[cmd] │
│ local r = f(...) │
│ if r ~= NORET then │
│ skynet.ret(skynet.pack(r)) │
│ end │
│ end) │
│ ↓ │
│ │
│ 5. 服务就绪,等待客户端请求 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
核心数据结构
lua
-- sharedatad.lua 全局变量
-- 共享数据池:存储所有共享数据
-- 结构: pool[name] = {obj = cobj, watch = {...}}
local pool = {}
-- 监控计数器:优化监控队列检查
-- 结构: pool_count[name] = {n = 当前监控数, threshold = 检查阈值}
local pool_count = {}
-- 对象映射:跟踪所有共享对象的生命周期
-- 结构: objmap[cobj] = v (正在使用) 或 true (待回收)
local objmap = {}
-- 垃圾回收计数器
local collect_tick = 10 -- 每 10 分钟执行一次 GC
服务接口
lua
local CMD = {}
-- 创建共享数据
function CMD.new(name, t, ...)
-- 支持三种方式:
-- 1. 直接传入表: new("config", {...})
-- 2. 加载文件: new("config", "@/path/to/config.lua")
-- 3. 执行代码: new("config", "return {...}")
end
-- 查询共享数据
function CMD.query(name)
-- 返回共享对象指针
end
-- 更新共享数据
function CMD.update(name, t, ...)
-- 创建新版本并通知所有监控者
end
-- 删除共享数据
function CMD.delete(name)
-- 标记对象待回收
end
-- 监控数据更新
function CMD.monitor(name, obj)
-- 如果数据有更新,返回新对象;否则挂起协程
end
-- 确认引用
function CMD.confirm(cobj)
-- 减少引用计数
end
sharedata.new() 流程
完整调用流程
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ sharedata.new(name, table) 流程 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端服务 sharedatad 服务 C 模块 │
│ │ │ │ │
│ │ sharedata.new("config", t) │ │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ │ CMD.new("config", t) │ │
│ │ │ ↓ │ │
│ │ │ │ │
│ │ │ 检查是否已存在 │ │
│ │ │ assert(pool[name]==nil)│ │
│ │ │ │ │
│ │ │ │ │
│ │ │ 判断参数类型 │ │
│ │ │ if type(t) == "table" │ │
│ │ │ value = t │ │
│ │ │ elseif type(t) == "string" │
│ │ │ 加载文件或代码 │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ newobj(name, value) │ │
│ │ │ ↓ │ │
│ │ │ │ │
│ │ │ sharedata.host.new(tbl) │ │
│ │ ├────────────────────────►│ │
│ │ │ │ │
│ │ │ │ 创建独立 Lua 状态机 │
│ │ │ │ │
│ │ │ │ L = luaL_newstate() │
│ │ │ │ │
│ │ │ │ 转换表结构 │
│ │ │ │ convtable(L, tbl) │
│ │ │ │ │
│ │ │ │ 返回指针 │
│ │ │ │ │
│ │ │ ◄── cobj ───────────── │ │
│ │ │ │ │
│ │ │ 增加引用计数 │ │
│ │ │ sharedata.host. │ │
│ │ │ incref(cobj) │ │
│ │ ├────────────────────────►│ │
│ │ │ │ │
│ │ │ 创建共享对象记录 │ │
│ │ │ v = {obj=cobj, watch={}}│ │
│ │ │ │ │
│ │ │ objmap[cobj] = v │ │
│ │ │ pool[name] = v │ │
│ │ │ pool_count[name] = │ │
│ │ │ {n=0, threshold=16} │ │
│ │ │ │ │
│ │ ◄── 创建成功 ───────────── │ │ │
│ │ │ │ │
└─────────────────────────────────────────────────────────────────────────────────────┘
sharedata.query() 流程
完整调用流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ sharedata.query(name) 流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端服务 sharedatad 服务 C 模块 │
│ │ │ │ │
│ │ sharedata.query("config") │ │ │
│ │ ↓ │ │ │
│ │ │ │ │
│ │ 检查缓存 │ │ │
│ │ if cache[name] then │ │ │
│ │ return cache[name] │ │ │
│ │ end │ │ │
│ │ │ │ │
│ │ skynet.call(service, "lua", │ │ │
│ │ "query", name) │ │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ │ CMD.query(name) │ │
│ │ │ ↓ │ │
│ │ │ │ │
│ │ │ 获取共享对象 │ │
│ │ │ v = pool[name] │ │
│ │ │ obj = v.obj │ │
│ │ │ │ │
│ │ │ 增加引用计数 │ │
│ │ │ sharedata.host. │ │
│ │ │ incref(obj) │ │
│ │ ├────────────────────────►│ │
│ │ │ │ │
│ │ ◄── cobj ───────────────── │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ 检查缓存 │ │ │
│ │ if cache[name] and │ │ │
│ │ cache[name].__obj==cobj │ │ │
│ │ then │ │ │
│ │ 确认引用 │ │ │
│ │ send(service, "confirm",cobj) │ │
│ │ return cache[name] │ │ │
│ │ end │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ 创建共享对象盒子 │ │ │
│ │ r = sd.box(cobj) │ │ │
│ ├─────────────────────────────────────────────────────► │ │
│ │ │ │ │
│ │ │ │ 创建 ctrl │
│ │ │ │ 结构 │
│ │ │ │ c->root │
│ │ │ │ = cobj │
│ │ ◄── r (userdata) ────────────────────────────────── │ │
│ │ │ │ │
│ │ 确认引用 │ │ │
│ │ send(service, "confirm", cobj) │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ 启动监控协程 │ │ │
│ │ skynet.fork(monitor, │ │ │
│ │ name, r, cobj)│ │ │
│ │ │ │ │
│ │ 缓存对象 │ │ │
│ │ cache[name] = r │ │ │
│ │ │ │ │
│ │ return r │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ 监控协程(异步执行) │ │ │
│ │ while true do │ │ │
│ │ newobj = call(service, │ │ │
│ │ "monitor", │ │ │
│ │ name, cobj)│ │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ │ 检查是否有更新 │ │
│ │ │ if obj != pool[name].obj │
│ │ │ then │ │
│ │ │ incref(newobj) │ │
│ │ ◄── newobj ──────────────── │ return newobj │ │
│ │ │ end │ │
│ │ │ │ │
│ │ │ 加入监控队列 │ │
│ │ │ table.insert(v.watch, │ │
│ │ │ skynet.response())│ │
│ │ │ │ │
│ │ (协程挂起,等待更新通知) │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ if newobj == nil then │ │ │
│ │ break -- 数据已删除 │ │ │
│ │ end │ │ │
│ │ │ │ │
│ │ 更新本地对象 │ │ │
│ │ sd.update(obj, newobj) │ │ │
│ ├─────────────────────────────────────────────────────► │ │
│ │ │ │ 更新引用 │
│ │ │ │ c->root │
│ │ │ │ = newobj │
│ │ ◄── 更新完成 ────────────────────────────────────── │ │
│ │ │ │ │
│ │ 确认更新 │ │ │
│ │ send(service, "confirm", newobj) │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ end -- 继续监控 │ │ │
│ │ │ │ │
└─────────────────────────────────────────────────────────────────────────────┘
sharedata.update() 流程
完整调用流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ sharedata.update(name, new_table) 流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端服务 sharedatad 服务 其他服务 │
│ │ │ │ │
│ │ sharedata.update("config", │ │ │
│ │ new_config) │ │ │
│ ├─────────────────────────────►│ │ │
│ │ │ │ │
│ │ │ CMD.update(name, t) │ │
│ │ │ ↓ │ │
│ │ │ │ │
│ │ │ 获取旧对象 │ │
│ │ │ v = pool[name] │ │
│ │ │ oldcobj = v.obj │ │
│ │ │ watch = v.watch │ │
│ │ │ │ │
│ │ │ 标记旧对象待回收 │ │
│ │ │ objmap[oldcobj] = true│ │
│ │ │ sharedata.host. │ │
│ │ │ decref(oldcobj) │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ 创建新对象 │ │
│ │ │ CMD.new(name, t) │ │
│ │ │ newobj = pool[name].obj│ │
│ │ │ │ │
│ │ │ │ │
│ │ │ 标记旧对象为 dirty │ │
│ │ │ sharedata.host. │ │
│ │ │ markdirty(oldcobj) │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ 通知所有监控者 │ │
│ │ │ for _, response in │ │
│ │ │ pairs(watch) do │ │
│ │ │ incref(newobj) │ │
│ │ │ response(true, newobj) │
│ │ │ end │ │
│ │ │ │ │
│ │ │ ┌───────────────┤ │
│ │ │ │ 唤醒监控协程 │ │
│ │ │ │ newobj = │ │
│ │ │ │ call(...) │ │
│ │ │ │ 返回 newobj │ │
│ │ │ │ │ │
│ │ │ │ sd.update(obj,│ │
│ │ │ │ newobj) │ │
│ │ │ │ │ │
│ │ │ │ send("confirm"│ │
│ │ │ │ , newobj) │ │
│ │ │ └───────────────┤ │
│ │ │ │ │
│ │ │ 触发垃圾回收 │ │
│ │ │ collect1min() │ │
│ │ │ │ │
│ │ ◄── 更新成功 ───────────── │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ 1分钟后,GC 协程执行 │ │
│ │ │ collectgarbage() │ │
│ │ │ for obj, v in pairs(objmap) do │
│ │ │ if v == true then │ │
│ │ │ if getref(obj)<=0 then │
│ │ │ delete(obj)│ │
│ │ │ end │ │
│ │ │ end │ │
│ │ │ end │ │
│ │ │ │ │
└─────────────────────────────────────────────────────────────────────────────┘
cobj 和共享对象盒子详解
核心概念
什么是 cobj?
cobj 是一个 C 指针,指向 struct table 结构,它代表了一个在独立 Lua 状态机中创建的共享数据对象。
c
// cobj 的类型
struct table * cobj;
// cobj 指向的结构
struct table {
int sizearray; /* 数组部分大小 */
int sizehash; /* 哈希部分大小 */
uint8_t *arraytype; /* 数组部分类型数组 */
union value * array; /* 数组部分值数组 */
struct node * hash; /* 哈希部分节点数组 */
lua_State * L; /* 关联的独立 Lua 状态机 */
};
cobj 的特点:
- 真正的共享内存:所有服务访问同一份数据,零复制
- 独立 Lua 状态机:存储在独立的状态机中,避免污染主状态机
- 只读访问:数据一旦创建,不可修改(除非整体更新)
- 引用计数管理:自动跟踪使用情况,智能垃圾回收
什么是共享对象盒子?
共享对象盒子是一个 Lua userdata,包装了 cobj 指针,提供了 Lua 层的访问接口。
c
// 共享对象盒子的类型
struct ctrl {
struct table * root; /* 当前使用的表指针 (cobj) */
struct table * update; /* 新表指针(更新时使用) */
};
共享对象盒子的作用:
- 提供 Lua 访问接口 :通过元表实现
__index、__pairs等 - 跟踪对象引用:记录哪些服务在使用这个对象
- 支持更新机制:当数据更新时,可以无缝切换到新版本
- 实现只读访问:防止客户端修改共享数据
为什么需要这样的设计?
问题 1:如何实现真正的内存共享?
传统方式的问题:
lua
-- ❌ 错误方式:直接传递 Lua 表
local config = {version = "1.0"}
skynet.call(service, "lua", "get_config")
-- 问题:每次都要序列化/反序列化,内存复制开销大
sharedata 的解决方案:
lua
-- ✅ 正确方式:共享内存
local cobj = sharedata.host.new(tbl) -- 创建 cobj
local box = sd.box(cobj) -- 创建盒子
-- 所有服务访问同一个 cobj,零复制
内存布局:
lua
local config = {
version = "1.0",
server = {
host = "127.0.0.1",
port = 8888,
},
}
┌─────────────────────────────────────────────────────────────────────────────┐
│ 内存布局示意图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ sharedatad 服务(独立 Lua 状态机) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Lua State (独立状态机) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 字符串表 │ │ │
│ │ │ [1] = "version" │ │ │
│ │ │ [2] = "server" │ │ │
│ │ │ [3] = "host" │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 共享内存区域(struct table) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ struct table * cobj (指针地址: 0x12345678) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 数组部分 │ │ │
│ │ │ [0]: type = NIL │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 哈希部分 │ │ │
│ │ │ ["version"]: type = STRING, value = 1 │ │ │
│ │ │ ["server"]: type = TABLE, value = cobj2 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 客户端服务 A 客户端服务 B │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ctrl (userdata) │ │ ctrl (userdata) │ │
│ │ root = 0x12345678 │ │ root = 0x12345678 │ │
│ │ update = NULL │ │ update = NULL │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ 都指向同一个 cobj │
│ (0x12345678) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
问题 2:如何跟踪对象的使用情况?
引用计数机制:
c
// state 结构中的引用计数
struct state {
int dirty; /* 更新标记 */
ATOM_INT ref; /* 原子引用计数 */
struct table * root; /* 根表指针 */
};
引用计数流程:
创建 cobj
↓
state.ref = 0
服务 A 查询
↓
创建 ctrl (box)
↓
state.ref++ (ref = 1)
服务 B 查询
↓
创建 ctrl (box)
↓
state.ref++ (ref = 2)
服务 A 退出
↓
ctrl 垃圾回收
↓
state.ref-- (ref = 1)
更新数据
↓
创建新 cobj2
↓
服务 A 的 ctrl 更新: root = cobj2
↓
旧 cobj.ref-- (ref = 0)
↓
垃圾回收旧 cobj
问题 3:如何实现无缝更新?
盒子机制的优势:
lua
-- 客户端服务
local config = sharedata.query("config") -- 获取盒子
-- 访问数据(通过盒子)
print(config.version) -- 访问旧版本
-- sharedatad 服务更新数据
sharedata.update("config", new_config)
-- 监控协程收到通知
↓
sd.update(config, new_cobj) -- 更新盒子的 root 指针
↓
config.root = new_cobj
-- 客户端继续使用同一个盒子
print(config.version) -- 自动访问新版本!
关键点:
- 客户端持有的是盒子(ctrl),不是 cobj
- 盒子的 root 指针可以动态更新
- 客户端无感知,自动使用新版本
详细数据流转
创建 cobj 的完整过程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 创建 cobj 流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1: 客户端调用 │
│ sharedata.new("config", tbl) │
│ │
│ 步骤 2: sharedatad 服务处理 │
│ CMD.new("config", tbl) │
│ ↓ │
│ newobj(name, tbl) │
│ ↓ │
│ sharedata.host.new(tbl) ─────► C 层: lnewconf() │
│ │
│ 步骤 3: C 层创建 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 1. 创建独立 Lua 状态机 │ │
│ │ ctx.L = luaL_newstate() │ │
│ │ │ │
│ │ 2. 分配 table 结构 │ │
│ │ tbl = malloc(sizeof(struct table)) │ │
│ │ tbl->L = ctx.L // 关联状态机 │ │
│ │ │ │
│ │ 3. 转换 Lua 表为内部格式 │ │
│ │ convtable(L, tbl) │ │
│ │ - 分配数组部分: arraytype[], array[] │ │
│ │ - 分配哈希部分: hash[] │ │
│ │ - 填充数据值 │ │
│ │ - 字符串存储到独立状态机 │ │
│ │ │ │
│ │ 4. 创建 state 结构 │ │
│ │ s = lua_newuserdatauv(L, sizeof(*s), 1) │ │
│ │ s->dirty = 0 │ │
│ │ s->ref = 0 // 引用计数初始化为 0 │ │
│ │ s->root = tbl // 指向 table │ │
│ │ │ │
│ │ 5. 返回 cobj │ │
│ │ lua_pushlightuserdata(L, tbl) // 返回指针 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 步骤 4: sharedatad 记录 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ local cobj = sharedata.host.new(tbl) │ │
│ │ │ │
│ │ // 增加引用计数(初始引用) │ │
│ │ sharedata.host.incref(cobj) // ref = 1 │ │
│ │ │ │
│ │ // 创建共享对象记录 │ │
│ │ local v = { │ │
│ │ obj = cobj, // 保存 cobj 指针 │ │
│ │ watch = {} // 监控队列 │ │
│ │ } │ │
│ │ │ │
│ │ // 记录到映射表 │ │
│ │ objmap[cobj] = v │ │
│ │ pool["config"] = v │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 结果: cobj 创建完成,存储在独立 Lua 状态机中 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
cobj 的内存布局:
c
// cobj 指向的完整结构
struct table * cobj = 0x12345678;
// 内存布局
地址 0x12345678: struct table {
sizearray = 2,
sizehash = 1,
// 数组部分
arraytype = 0xAAAA0000 [
[0] = VALUETYPE_NIL,
[1] = VALUETYPE_TABLE
],
array = 0xAAAA1000 [
[0] = {...},
[1] = {.tbl = 0x12345690} // 嵌套表指针
],
// 哈希部分
hash = 0xBBBB0000 [
[0] = {
key = 1, // 字符串索引
keytype = KEYTYPE_STRING,
keyhash = 0x87654321,
valuetype = VALUETYPE_STRING,
v = {.string = 1}, // 字符串索引
next = -1, // 无冲突
nocolliding = 1
}
],
// 关联的独立 Lua 状态机
L = 0xCCCC0000 // 独立状态机,存储字符串等
}
创建共享对象盒子的完整过程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 创建共享对象盒子流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1: 客户端查询 │
│ sharedata.query("config") │
│ │
│ 步骤 2: 获取 cobj │
│ local cobj = skynet.call(service, "lua", "query", "config") │
│ ↓ │
│ sharedatad: CMD.query("config") │
│ ↓ │
│ sharedata.host.incref(cobj) // 增加引用计数 │
│ ↓ │
│ return cobj // 返回 lightuserdata │
│ │
│ 步骤 3: 创建盒子 │
│ local r = sd.box(cobj) ─────► C 层: lboxconf() │
│ │
│ 步骤 4: C 层创建 ctrl │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 1. 获取 state 结构 │ │
│ │ struct state * s = lua_touserdata(cobj->L, 1) │ │
│ │ │ │
│ │ 2. 增加引用计数 │ │
│ │ ATOM_FINC(&s->ref) // ref++ │ │
│ │ │ │
│ │ 3. 创建 ctrl (userdata) │ │
│ │ struct ctrl * c = lua_newuserdatauv(L, sizeof(*c), 1) │ │
│ │ c->root = cobj // 指向共享数据 │ │
│ │ c->update = NULL // 无更新 │ │
│ │ │ │
│ │ 4. 设置元表 │ │
│ │ luaL_newmetatable(L, "confctrl") │ │
│ │ - __index: 读取访问 │ │
│ │ - __pairs: 遍历支持 │ │
│ │ - __len: 获取长度 │ │
│ │ - __gc: 垃圾回收 │ │
│ │ lua_setmetatable(L, -2) │ │
│ │ │ │
│ │ 5. 返回 ctrl (userdata) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 步骤 5: 启动监控协程 │
│ skynet.fork(monitor, "config", r, cobj) │
│ │
│ 结果: 盒子创建完成,可以访问共享数据 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
ctrl 的内存布局:
lua
-- ctrl 是一个 Lua userdata
local ctrl = {
-- C 层数据(不可见)
root = 0x12345678, -- 指向 cobj
update = NULL, -- 指向新 cobj(更新时使用)
-- 元表
__index = function(self, key)
-- 从 cobj 中查找 key
local cobj = self.root
return lookup_from_cobj(cobj, key)
end,
__pairs = function(self)
-- 遍历 cobj
return pairs_iterator, self, nil
end,
__gc = function(self)
-- 减少引用计数
local cobj = self.root
decref(cobj)
end
}
访问共享数据的完整流程
读取字段
lua
-- 客户端代码
local config = sharedata.query("config")
local version = config.version
底层流程:
┌─────────────────────────────────────────────────────────────────────────────┐
│ config.version 访问流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Lua 层访问 │
│ config.version │
│ ↓ │
│ 触发 __index 元方法 │
│ │
│ 2. __index 实现 (C 层) │
│ static int lindex(lua_State *L) { │
│ // 获取 ctrl 结构 │
│ struct ctrl * c = lua_touserdata(L, 1); │
│ struct table * cobj = c->root; │
│ │
│ // 获取键 "version" │
│ const char * key = lua_tostring(L, 2); │
│ size_t sz = strlen(key); │
│ │
│ // 计算哈希值 │
│ uint32_t keyhash = calchash(key, sz); │
│ │
│ // 在 cobj 的哈希表中查找 │
│ struct node * n = lookup_key(cobj, keyhash, ...); │
│ │
│ // 获取值 │
│ if (n->valuetype == VALUETYPE_STRING) { │
│ // 从独立 Lua 状态机获取字符串 │
│ const char * str = lua_tolstring(cobj->L, n->v.string, &sz); │
│ lua_pushlstring(L, str, sz); │
│ } │
│ │
│ return 1; // 返回值 │
│ } │
│ │
│ 3. 返回值到 Lua 层 │
│ version = "1.0" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
访问嵌套表
lua
-- 客户端代码
local config = sharedata.query("config")
local server = config.server -- 获取嵌套表
local host = server.host -- 访问嵌套表的字段
底层流程:
config.server
↓
__index("server")
↓
lookup_key(cobj, "server")
↓
找到节点: n->valuetype = VALUETYPE_TABLE
↓
返回 n->v.tbl (嵌套表的 cobj 指针)
↓
返回 lightuserdata (0x12345690)
↓
Lua 层收到 lightuserdata
↓
不能直接使用!需要创建新的盒子
↓
实际上,__index 会自动创建子盒子
↓
返回子盒子(包含嵌套表指针)
↓
server.host
↓
访问子盒子的 __index
↓
lookup_key(嵌套表cobj, "host")
↓
返回 "127.0.0.1"
引用计数管理
引用计数的生命周期
┌─────────────────────────────────────────────────────────────────────────────┐
│ 引用计数生命周期 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 创建 cobj │
│ sharedata.new("config", tbl) │
│ ↓ │
│ cobj 创建,state.ref = 0 │
│ ↓ │
│ sharedatad 持有引用:incref(cobj) │
│ ↓ │
│ state.ref = 1 ─────────────────────────────────────────┐ │
│ │ │
│ 服务 A 查询 │ │
│ sharedata.query("config") │ │
│ ↓ │ │
│ 创建 ctrl_A: incref(cobj) │ │
│ ↓ │ │
│ state.ref = 2 ◄────────────────────────────────────────┘ │
│ │
│ 服务 B 查询 │
│ sharedata.query("config") │
│ ↓ │
│ 创建 ctrl_B: incref(cobj) │
│ ↓ │
│ state.ref = 3 │
│ │
│ 更新数据 │
│ sharedata.update("config", new_tbl) │
│ ↓ │
│ 创建新 cobj2,state2.ref = 1 │
│ ↓ │
│ 标记旧 cobj 为 dirty │
│ ↓ │
│ 通知监控者 │
│ ↓ │
│ 服务 A 的监控协程收到通知 │
│ ↓ │
│ sd.update(ctrl_A, cobj2) │
│ ↓ │
│ ctrl_A->root = cobj2 │
│ incref(cobj2) // state2.ref = 2 │
│ decref(cobj) // state.ref = 2 │
│ ↓ │
│ 服务 B 的监控协程收到通知 │
│ ↓ │
│ sd.update(ctrl_B, cobj2) │
│ ↓ │
│ ctrl_B->root = cobj2 │
│ incref(cobj2) // state2.ref = 3 │
│ decref(cobj) // state.ref = 1 │
│ │
│ 服务 A 退出 │
│ ↓ │
│ ctrl_A 垃圾回收 │
│ ↓ │
│ __gc: decref(cobj2) // state2.ref = 2 │
│ │
│ 服务 B 退出 │
│ ↓ │
│ ctrl_B 垃圾回收 │
│ ↓ │
│ __gc: decref(cobj2) // state2.ref = 1 │
│ │
│ 垃圾回收(1分钟后) │
│ ↓ │
│ collectgarbage() │
│ ↓ │
│ 检查旧 cobj: state.ref = 1 (sharedatad 持有) │
│ ↓ │
│ decref(cobj) // state.ref = 0 │
│ ↓ │
│ state.ref = 0,删除 cobj │
│ ↓ │
│ sharedata.host.delete(cobj) │
│ ↓ │
│ lua_close(cobj->L) // 关闭独立状态机 │
│ delete_tbl(cobj) // 释放内存 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
cobj 和盒子的关系
关系图
┌─────────────────────────────────────────────────────────────────────────────┐
│ cobj 和盒子的关系 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ sharedatad 服务 │
│ ┌───────────────┐ │
│ │ pool["config"]│ │
│ │ .obj = cobj │ │
│ │ .watch = [] │ │
│ └───────┬───────┘ │
│ │ │
│ │ 持有 │
│ ▼ │
│ ┌─────────────┐ │
│ │ cobj │ ◄──────────────────┐ │
│ │ (struct │ │ │
│ │ table*) │ │ 真正的共享数据 │
│ │ │ │ (独立 Lua 状态机) │
│ │ ref = 3 │ │ │
│ └──────┬──────┘ │ │
│ │ │ │
│ ┌─────────────────┼─────────────────┐ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ │ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ ctrl_A │ │ ctrl_B │ │ ctrl_C │ │ │
│ │ (box) │ │ (box) │ │ (box) │ │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │ │
│ │root=cobj│ │root=cobj│ │root=cobj│ │ │
│ │update= │ │update= │ │update= │ │ │
│ │ NULL │ │ NULL │ │ NULL │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │ │
│ ▲ ▲ ▲ │ │
│ │ │ │ │ │
│ 服务 A 持有 服务 B 持有 服务 C 持有 │ │
│ │ │
│ 通过 ctrl 访问 cobj(所有服务共享同一份数据)─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
职责划分
| 组件 | 职责 | 位置 |
|---|---|---|
| cobj | 存储实际的共享数据 | 独立 Lua 状态机 |
| state | 管理引用计数和更新状态 | 独立 Lua 状态机 |
| ctrl | 提供 Lua 访问接口 | 客户端服务 |
| pool | 管理所有共享数据 | sharedatad 服务 |
| objmap | 跟踪对象生命周期 | sharedatad 服务 |
设计优势
1. 真正的内存共享
lua
-- 传统方式:每个服务都有一份拷贝
local config1 = load_config() -- 服务 A 的拷贝
local config2 = load_config() -- 服务 B 的拷贝
-- 内存占用:N * sizeof(config)
-- ShareData:所有服务共享一份数据
local config1 = sharedata.query("config") -- 服务 A 获取盒子
local config2 = sharedata.query("config") -- 服务 B 获取盒子
-- 内存占用:sizeof(config) + N * sizeof(ctrl)
// ctrl 很小,只有两个指针
2. 自动更新通知
lua
-- 客户端无需主动轮询
local config = sharedata.query("config")
-- 监控协程自动运行
-- 收到更新通知时,自动更新盒子的 root 指针
-- 客户端继续使用同一个盒子,自动访问新数据
print(config.version) -- 自动访问最新版本
3. 引用计数自动管理
lua
-- 客户端无需手动管理引用计数
local config = sharedata.query("config")
-- 使用完毕后,盒子被垃圾回收
-- __gc 元方法自动减少引用计数
config = nil
collectgarbage() -- 触发 __gc,自动 decref
4. 线程安全
c
// 使用原子操作保证线程安全
ATOM_FINC(&s->ref); // 原子递增
ATOM_FDEC(&s->ref); // 原子递减
ATOM_LOAD(&s->ref); // 原子读取
总结
cobj 的作用
- 存储共享数据:在独立 Lua 状态机中存储实际的共享数据
- 零复制共享:所有服务访问同一份数据,减少内存占用
- 类型安全:C 层严格管理数据类型,保证数据一致性
- 生命周期管理:通过引用计数自动管理生命周期
共享对象盒子的作用
- 提供 Lua 接口:将 cobj 包装为 Lua userdata,提供元表访问
- 跟踪引用:每个盒子代表一个服务的引用
- 支持更新:盒子的 root 指针可以动态更新
- 自动垃圾回收:通过 __gc 元方法自动管理引用计数
两者的关系
cobj (数据) ←─── 盒子 (接口)
│ │
│ │
▼ ▼
存储实际数据 提供 Lua 访问
独立状态机 客户端服务
引用计数 跟踪引用
可更新 无感知更新
核心优势
- 内存效率:零复制共享,大幅减少内存占用
- 性能优越:直接访问内存,无序列化开销
- 自动管理:引用计数自动管理,无内存泄漏
- 线程安全:原子操作保证并发安全
- 无缝更新:客户端无感知的数据更新
sharedata 核心实现
Lua 层实现
参见 sharedata.lua 文件的详细注释。项目地址
C 层实现
参见 lua-sharedata.c 文件的详细注释。项目地址
数据结构
核心数据结构
c
/* 值类型 */
#define VALUETYPE_NIL 0 /* nil 值 */
#define VALUETYPE_REAL 1 /* 浮点数 */
#define VALUETYPE_STRING 2 /* 字符串 */
#define VALUETYPE_BOOLEAN 3 /* 布尔值 */
#define VALUETYPE_TABLE 4 /* 表 */
#define VALUETYPE_INTEGER 5 /* 整数 */
/* 键类型 */
#define KEYTYPE_INTEGER 0 /* 整数键 */
#define KEYTYPE_STRING 1 /* 字符串键 */
/**
* 值联合体
* 用于存储不同类型的值,节省内存空间
*/
union value {
lua_Number n; /* 浮点数值 */
lua_Integer d; /* 整数值 */
struct table * tbl; /* 嵌套表指针 */
int string; /* 字符串索引 */
int boolean; /* 布尔值 */
};
/**
* 哈希节点
* 存储哈希表中的键值对
*/
struct node {
union value v; /* 值 */
int key; /* 键:整数或字符串索引 */
int next; /* 冲突链下一个节点索引 */
uint32_t keyhash; /* 键的哈希值 */
uint8_t keytype; /* 键类型 */
uint8_t valuetype; /* 值类型 */
uint8_t nocolliding; /* 是否无哈希冲突 */
};
/**
* 状态结构
* 管理共享数据的状态信息
*/
struct state {
int dirty; /* 更新标记 */
ATOM_INT ref; /* 原子引用计数 */
struct table * root; /* 根表指针 */
};
/**
* 数据表结构
* 存储共享数据的完整结构
*/
struct table {
int sizearray; /* 数组部分大小 */
int sizehash; /* 哈希部分大小 */
uint8_t *arraytype; /* 数组部分类型数组 */
union value * array; /* 数组部分值数组 */
struct node * hash; /* 哈希部分节点数组 */
lua_State * L; /* 关联的独立 Lua 状态机 */
};
/**
* 控制结构
* 用于客户端服务跟踪共享数据
*/
struct ctrl {
struct table * root; /* 当前使用的表指针 */
struct table * update; /* 新表指针 */
};
sharetable
此处不展开
datacenter
此处不展开
对比与选择
详细对比
| 特性 | sharedata | sharetable | datacenter |
|---|---|---|---|
| 数据类型 | 只读表 | 只读表 | 任意类型 |
| 访问方式 | 共享内存 | 虚拟机共享 | 服务调用 |
| 更新方式 | 全量更新 | 全量更新 | 增量更新 |
| 性能 | 极高 | 极高 | 中等 |
| 内存占用 | 低 | 最低 | 中等 |
| 适用场景 | 配置文件 | 大表数据 | 动态数据 |
| 并发安全 | 是 | 是 | 是 |
| 等待机制 | 否 | 否 | 是 |
| 更新通知 | 自动推送 | 手动触发 | 无 |
最佳实践
参考 共享数据项目级应用.md
总结
sharedata 核心流程
- 创建服务 :
skynet.uniqueservice "sharedatad" - 创建数据 :
sharedata.new(name, table)-> C 层创建独立 Lua 状态机 -> 转换表结构 - 查询数据 :
sharedata.query(name)-> 返回共享对象盒子 -> 启动监控协程 - 更新数据 :
sharedata.update(name, new_table)-> 创建新版本 -> 通知所有监控者
关键设计
- 独立 Lua 状态机:每个共享数据在独立的状态机中,避免污染
- 引用计数:自动跟踪对象引用,智能垃圾回收
- 监控机制:自动通知客户端数据更新
- 弱引用缓存:客户端缓存,减少重复查询
参考文件:
skynet/lualib/skynet/sharedata.lua- sharedata 客户端skynet/service/sharedatad.lua- sharedata 服务skynet/lualib-src/lua-sharedata.c- sharedata C 层- https://gitee.com/jiucows/skynet_study - 项目地址