🗃存储结构参考
版本支持:v2.16.1+
# SQL 四张表
DDL随liteflow-rule-db-sql模块一起提供,在模块内的src/main/resources/sql/ddl-mysql.sql。表名前缀可以配,默认是lf_,字段名则是固定的。
lf_chain — 主键 (application_name, chain_id)
| 列 | 类型 | 说明 |
|---|---|---|
application_name | VARCHAR(64) | 应用隔离维度 |
chain_id | VARCHAR(128) | chain 标识 |
namespace | VARCHAR(64) NULL | 命名空间 |
el_data | TEXT | EL 表达式 |
route_data | TEXT NULL | 路由 EL(route chain 用) |
version | BIGINT | 每次发布 +1 |
content_md5 | CHAR(32) | el_data的MD5,注意不含route_data |
enable | TINYINT | 1 启用 / 0 停用 |
gmt_create / gmt_modified | DATETIME | 创建/修改时间 |
lf_script — 主键 (application_name, node_id)
| 列 | 类型 | 说明 |
|---|---|---|
application_name | VARCHAR(64) | 应用隔离维度 |
node_id | VARCHAR(128) | 脚本节点标识 |
script_name | VARCHAR(128) NULL | 节点名 |
script_type | VARCHAR(32) | 脚本类型,对齐NodeTypeEnum,可选script、boolean_script、switch_script、for_script。WHILE和ITERATOR属于非脚本节点类型,没有*_script变体 |
script_language | VARCHAR(32) NULL | groovy、js、python之类,为空则用全局默认 |
script_data | TEXT | 脚本源码 |
content_md5 | CHAR(32) | script_data 的 MD5 |
version / enable / gmt_create / gmt_modified | 同上 |
lf_change_log — 主键 seq AUTO_INCREMENT,索引 (application_name, seq)
| 列 | 说明 |
|---|---|
seq | 整张表全局单调递增的变更序号。不同应用共享同一个序号空间,所以跨应用看到跳号是正常的。 |
application_name | 应用隔离维度 |
target_type | CHAIN / SCRIPT |
target_id | chainId / nodeId |
op | UPSERT / DELETE |
version | 变更后的版本号 |
gmt_create | 创建时间 |
lf_change_lock — 单行发布顺序锁
| 列 | 说明 |
|---|---|
lock_id | 主键,必须存在,而且只用值1这一行。Publisher在事务内执行SELECT ... FOR UPDATE,持锁到提交或回滚。 |
这把锁的作用是让change log序号的分配顺序和发布事务的提交顺序对齐,免得并发事务先拿到较小的seq却后提交,导致轮询节点越过尚未提交的变更。它是整套表的全局锁,不按application_name拆分。
change_log的清理
change_log是可以定期清理的,建议保留7天。但要注意SQL和PostgreSQL用的是跨应用共享的全局序号,运行时不会去查MIN(seq)来判断每一个缺口。水位前进了却读不到本应用记录时会请求全量对账;而如果裁剪之后仍然能读到更晚的本应用记录,那么缺失的变化可能要等下一次周期对账才补齐。
所以清理不会破坏最终的正确性,但可能让个别节点失去seq轮询这条快速收敛路径。清理窗口应该明显大于节点的最长离线时间,同时保留周期对账。
# 已有SQL部署的升级
从没有change_lock的旧版本升级时,请务必在恢复发布流量之前执行下面的迁移。示例用的是默认前缀lf_,如果你自定义了table-prefix,记得同步替换表名。
CREATE TABLE IF NOT EXISTS `lf_change_lock` (
`lock_id` TINYINT NOT NULL,
PRIMARY KEY (`lock_id`)
) DEFAULT CHARACTER SET utf8mb4;
INSERT IGNORE INTO `lf_change_lock` (`lock_id`) VALUES (1);
表建了但lock_id = 1这行缺失,发布同样会失败。所以不要删除或者修改这行数据。
# PostgreSQL四张表
PostgreSQL用的是chain、script、change_log、change_lock四张表,逻辑字段和SQL后端一致,只是DDL里用了BIGSERIAL、BOOLEAN和TIMESTAMPTZ。完整DDL在模块内的src/main/resources/postgresql/ddl.sql。
已有的PostgreSQL部署同样要在恢复发布流量之前执行下面的迁移:
CREATE TABLE IF NOT EXISTS lf_change_lock (
lock_id SMALLINT PRIMARY KEY
);
INSERT INTO lf_change_lock (lock_id) VALUES (1)
ON CONFLICT (lock_id) DO NOTHING;
# MongoDB Collection
默认用下面四个Collection:
| Collection | 内容 |
|---|---|
lf_chain | chain的元数据和正文,_id由applicationName加chainId组成 |
lf_script | script的元数据和正文 |
lf_sequence | 每个applicationName独立的连续seq |
lf_change_log | seq、目标类型、目标id、操作和业务版本 |
Manifest查询只投影元数据字段,不读EL和脚本正文,并通过快照事务保证元数据和sequence基线是一致的。Publisher则在一个MongoDB多文档事务里同时更新内容、sequence和change log。
# Redis键结构
键前缀可配,默认是lf,下面的{app}就是application-name。如果配了key-hash-tag,键布局会变成{prefix}:{hashTag}:{app}:...。这些键都没有设置过期时间,因为规则的权威源不该被自动清理,删除请走removeChain和removeScript。
| 键 | 类型 | 内容 |
|---|---|---|
{prefix}:{app}:chain:{chainId} | HASH | 字段:el / route / namespace / version / md5 / enable |
{prefix}:{app}:script:{nodeId} | HASH | 字段:script / name / type / language / version / md5 / enable |
{prefix}:{app}:chain-ids | SET | 所有chain的id集合,只存id,不存版本 |
{prefix}:{app}:script-ids | SET | 所有script的id集合 |
{prefix}:{app}:seq | STRING | 全局变更序号,用INCR自增 |
{prefix}:{app}:changelog | ZSET | score = seq,member = JSON{seq, targetType, targetId, op, version} |
那两个*-ids的SET是有意这么设计的:清单和对账先readAll拿到全部id集合,再用一个pipeline(RBatch)批量读取每个chain和script的元数据HASH,一次往返就把id和版本拿齐了,不用逐个去扫内容键。
changelog需要定期裁剪
changelog这个ZSet每次发布都会ZADD一条,框架不会自动收缩,所以长期高频发布会一直占内存。建议定期清理一下,比如只保留最近7天或者最近N条:
ZREMRANGEBYSCORE lf:{app}:changelog 0 {要清理到的seq}
裁剪不影响正确性。节点发现自己的位点低于ZSet里最小的seq,也就是断档了,会自动触发一次全量对账。
# ZooKeeper路径结构
所有规则节点挂在{root}/{app}/...下,root默认是/liteflow。每个chain和script都拆成两个znode,meta存轻量指纹供清单遍历,content存全文供按需拉取。
| 路径 | 内容 |
|---|---|
{root}/{app}/chains/meta/{chainId} | chain元数据,version、md5、enable等 |
{root}/{app}/chains/content/{chainId} | chain的EL全文,带版本 |
{root}/{app}/scripts/meta/{nodeId} | script元数据 |
{root}/{app}/scripts/content/{nodeId} | script源码全文 |
清单只遍历meta子节点,拿到id和版本,content节点要等懒加载时才读。变更序号用的是zxid,也就是节点的mzxid,所以zk后端没有独立的changelog或seq节点,watch直接监听meta节点的增删改,每个事件自带zxid作为序号。
zk的znode数量随规则数线性增长,规则量大的时候注意一下zk的znode和quota限制。zk这边没有类似SQL change_log那种需要清理的日志结构。
# Etcd键结构
布局和zk是同构的,只是把znode换成了etcd的KV,用前缀key而不是层级节点:
| key前缀 | 内容 |
|---|---|
{root}/{app}/chains/meta/{chainId} | chain元数据 |
{root}/{app}/chains/content/{chainId} | chain的EL全文 |
{root}/{app}/scripts/meta/{nodeId} | script元数据 |
{root}/{app}/scripts/content/{nodeId} | script源码全文 |
变更序号用etcd的KV revision,watch按revision区间订阅meta前缀。etcd会定期compact历史revision,所以一旦节点位点落后到已经被compact的revision,watch会报revision compacted错误,这时会自动降级成一次全量对账,之后再从最新revision续上watch。和zk一样,etcd后端也没有独立的changelog。
# Nacos Catalog
Nacos后端按applicationName存一条JSON Catalog:
dataId = {data-id-prefix}.{application-name}.catalog.json
group = {group}
Catalog的逻辑结构是下面这样,数组里保存的是完整的chain和script记录:
{
"schemaVersion": 1,
"sequence": 2,
"chains": [
{
"chainId": "orderChain",
"el": "THEN(a,b)",
"route": null,
"namespace": null,
"version": 1,
"md5": "...",
"enable": true
}
],
"scripts": [],
"lastChange": {
"seq": 2,
"targetType": "CHAIN",
"targetId": "orderChain",
"op": "UPSERT",
"version": 1
}
}
sequence是应用级的连续序号,lastChange.seq必须和它相等。读取端会严格校验schema、字段类型、重复id、正文MD5、业务版本,以及lastChange和最终记录是否一致。Catalog损坏、序号倒退、内容变了但序号没变,这些都会当成存储错误,不会静默接受。
执行端的常驻快照只保存sequence和Catalog的MD5,不会把chains和scripts的正文长期留在Provider里。但Nacos的读取粒度终究是整份配置,所以Manifest对账和每次缓存未命中的正文读取,都要传输、校验并短暂解码整个Catalog。



