orm远程访问对象的实现思路

前言

skynet_fly的微服务架构中,数据通常由一个节点的ORM服务管理(例如玩家数据由game节点的orm_agent负责)。但其他节点(如大厅、匹配服务)也经常需要读取这些数据。传统方案有两种:

  1. 按需RPC查询 —— 每次需要时远程调用获取数据,延迟高
  2. 全量推送 —— 把数据推给所有需要的节点,浪费带宽

orm_frpc_client走的是第三条路:按需订阅 + 增量同步。订阅某个主键的数据后,首次全量同步,之后通过watch_client自动接收增量变更(add/change/del),本地维护一份实时镜像。这样既减少了RPC调用,又避免了不必要的推送。

实现思路

整体架构

1
2
3
4
5
6
7
8
9
10
11
12
远程节点 (orm_agent)              本地节点 (业务模块)
┌─────────────────────┐ ┌─────────────────────────┐
│ orm_entity │ │ orm_frpc_client │
│ ┌───────────────┐ │ │ ┌───────────────────┐ │
│ │ pubsyn(增量) │──┼── frpc ──┼──│ watch_client(监听)│ │
│ │ │ │ │ │ │ │
│ └───────────────┘ │ │ │ _data_map (镜像) │ │
│ │ │ │ │ │
watch_first_syn()──┼── rpc ───┼──│ 首次全量同步 │ │
call_orm() ───┼── rpc ───┼──│ 远程方法调用 │ │
│ │ │ └───────────────────┘ │
└─────────────────────┘ └─────────────────────────┘

数据结构设计

ORM支持多主键(composite key),比如一个player_item表的主键是(player_id, item_id)orm_frpc_client需要处理单主键和多主键两种情况:

单主键

1
2
-- data_map[main_key] = one_data (直接存储整条记录)
data_map[10001] = {player_id = 10001, name = "test", level = 5}

多主键(嵌套map)

1
2
3
4
5
6
-- data_map[main_key][sub_key1][sub_key2]... = one_data
-- 例如 keylist = {"player_id", "item_id"}
data_map[10001] = {
[1001] = {player_id = 10001, item_id = 1001, count = 5},
[1002] = {player_id = 10001, item_id = 1002, count = 3},
}

第一个key作为main_key进行订阅,该key下的所有数据自动同步。通过嵌套table结构,支持任意多级复合主键。

增量同步处理

增量同步通过watch_client接收三种命令(定义在ORM_SYN_CMD):

ADD —— 新增数据

1
2
3
if syn_cmd == ORM_SYN_CMD.ADD then
add_map_value(main_map, data, keylen, data_map, main_key, keylist, add_cb)
end

根据keylen插入到正确的嵌套层级。

CHANGE —— 修改数据

1
2
3
if syn_cmd == ORM_SYN_CMD.CHANGE then
change_ma_value(main_map, data, keylen, keylist, change_cb)
end

找到对应记录后,使用table_util.merge将变更字段合并进去(只传递变更字段,非全量)。

DEL —— 删除数据

1
2
3
if syn_cmd == ORM_SYN_CMD.DEL then
del_map_value(main_map, data, data_map, main_key, del_cb)
end

删除记录后,从下往上回溯清理空的父级table,避免内存泄漏:

1
2
3
4
5
6
7
8
9
10
-- 从下往上检查并清理空的父级table
for i = #path, 1, -1 do
local parent_map = path[i].map
local parent_key = path[i].key
if not next(parent_map[parent_key]) then
parent_map[parent_key] = nil
else
break
end
end

断线重连处理

当远程节点断线后重连,本地镜像可能已过期。orm_frpc_client通过frpc_client:watch_up监听连接恢复事件:

1
2
3
4
5
6
7
8
9
10
11
12
local function cluster_up(svr_name, svr_id)
-- 遍历所有该节点相关的orm_frpc_client实例
for i = 1, #weak_list do
local t = weak_list[i]
-- 对每个已watch的main_key重新全量同步
for main_key in pairs(watched) do
local ret = t._cli:call_by_alias("watch_first_syn", main_key)
-- 重建本地镜像
init_main_data(keylist, data_map, data, main_key)
end
end
end

重连后不会重复触发add_cb,仅静默替换本地镜像数据。

GC安全

使用__gc元方法,确保对象被回收时自动取消所有watch订阅:

1
2
3
4
5
6
7
__gc = function(self)
if not self._watched then return end
for main_key in pairs(self._watched) do
local push_key = "_orm_" .. self._orm_entity_instance_name .. "_" .. main_key
watch_client.unwatch_byid(self._svr_name, self._svr_id, push_key, "orm_frpc_client")
end
end

同时使用weak_table存储实例引用(g_weak_map),避免全局引用阻止GC。

单例模式

提供instance方法,同一(svr_name, svr_id, orm_entity_instance_name)组合只创建一个实例:

1
2
3
4
function M:instance(svr_name, svr_id, orm_entity_instance_name)
-- 三级map缓存实例
return g_instance_map[svr_name][svr_id][orm_entity_instance_name]
end

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local orm_frpc_client = require "skynet-fly.client.orm_frpc_client"

-- 获取远程orm访问对象(单例)
local player_orm = orm_frpc_client:instance("game", 1, "player")

-- 监听玩家 10001 的数据变更
player_orm:watch(10001,
function(one_data) -- add_cb
log.info("新增数据:", one_data)
end,
function(one_data, change_value) -- change_cb
log.info("数据变更:", one_data, change_value)
end,
function(one_data) -- del_cb
log.info("数据删除:", one_data)
end
)

-- 读取本地镜像
local data = player_orm:get_data(10001)
log.info("当前数据:", data)

-- 远程调用orm方法
local result = player_orm:call_orm("get_by_key", 10001)

-- 取消监听
player_orm:unwatch(10001)

注意点

  1. 尽量手动unwatch —— 虽然__gc会自动清理,但GC时机不确定。如果watch回调的upvalue引用了self,可能形成环引用导致无法GC。
  2. main_key粒度 —— watch是以第一个主键为粒度的,会同步该key下所有子数据。如果子数据量很大,要考虑是否合适。
  3. 重连后数据是全量替换 —— 重连后不会触发add_cb/del_cb,而是静默全量刷新本地镜像,业务逻辑不需要额外处理重连。
  4. change只传变更字段 —— change_cb的第二个参数change_value只包含变更的字段,不是完整记录。完整记录看第一个参数。
  5. 删除后自动清理空表 —— 多级key结构中删除末端数据后,会自动回收空的中间层table。

API文档

skynetfly源码地址


orm远程访问对象的实现思路
https://huahua132.github.io/2026/05/15/skynet_fly_ss/orm_frpc_client/
作者
huahua132
发布于
2026年5月15日
许可协议