📤发布API与写入规范
版本支持:v2.16.1+
# 推荐:统一发布API
七个后端共用一套发布接口com.yomahub.liteflow.publisher.RulePublisher,通过RulePublisherFactory.create(config),按你传入的后端配置实例化出来。
这是推荐的写入方式,尤其适合独立的管理后台,只依赖一个插件jar,不用拉起FlowExecutor,也不依赖全局的LiteflowConfig。
// 以 Redis 为例;其他后端换成对应的 XxxPublisherConfig
try (RulePublisher publisher = RulePublisherFactory.create(
RedisPublisherConfig.builder()
.address("redis://127.0.0.1:6379")
.applicationName("your-app")
.build())) {
PublishResult r = publisher.publishChain(PublishChainRequest.builder()
.chainId("orderChain")
.el("THEN(a, b)")
.route("AND(a)") // 可选:路由EL
.namespace("ns1") // 可选:命名空间
.build());
r.getVersion(); // 新版本号
r.getSequence(); // 变更序号
}
getSequence()的含义随后端不同:SQL、PostgreSQL、MongoDB、Redis、Nacos这几个是seq,zk是zxid,etcd是revision。
# 请求与返回对象
三个请求类型都是不可变的builder对象:
| 类型 | 字段 |
|---|---|
PublishChainRequest | chainId、el、route(可空)、namespace(可空)、expectedVersion(可空) |
PublishScriptRequest | nodeId、script、name(可空)、type(script、boolean_script、switch_script、for_script之一)、language(可空)、expectedVersion(可空) |
RemoveRuleRequest | targetId、expectedVersion(可空),removeChain和removeScript共用 |
PublishResult(返回) | targetId、targetType、operation(UPSERT或DELETE)、version、sequence |
发布时md5由框架自己算,version由存储层自增。
发布脚本只负责保存源码和语言标识,所以每个执行应用都必须显式引入对应的liteflow-script-*插件,否则这个脚本首次加载时会失败。
# 乐观锁expectedVersion
这是并发安全发布的关键,三种取值:
| 取值 | 语义 |
|---|---|
不设(null,默认) | UPSERT。已存在则version = version + 1,不存在则插入version = 1。已有行的并发更新在行锁、Lua或事务下原子自增,本身就是安全的。 |
0 | 意思是必须新建。目标已经存在的话抛VersionConflictException。并发首发同一个id时用这个,第一个成功,其余都被明确拒绝。 |
N(大于0) | CAS。只有目标当前版本恰好是N时才更新到N+1,否则抛VersionConflictException。适合读出来改完再存、要确保中间没人改过的场景,管理后台的编辑表单就是典型。 |
VersionConflictException和配置、校验类的错误各有独立的异常类型,都在com.yomahub.liteflow.publisher.exception.*下,方便上层区分是该重试冲突还是参数写错了。
# 生命周期
RulePublisher实现了AutoCloseable。SQL和PostgreSQL后端是每次操作借一次连接,而Redis、MongoDB、zk、etcd、Nacos可能持有客户端连接,所以用完一定要close(),推荐直接用try-with-resources。
从外部传进来的DataSource、MongoClient、ConfigService或其他客户端仍归调用方所有,Publisher不会去关它们。
# 各后端的原子性保证
| 后端 | 保证 |
|---|---|
| SQL | 单事务内先锁定change_lock.lock_id = 1,再完成UPSERT内容行和INSERT change_log,要回滚就一起回滚。 |
| PostgreSQL | 同SQL,另外用RETURNING seq返回提交序号。 |
| MongoDB | 在多文档事务里完成内容CAS、sequence自增和change_log插入。 |
| Redis | 一段Lua脚本在Redis单线程内原子完成HSET内容、SADD索引、INCR seq、ZADD changelog四步,要么全成要么全不成。 |
| zk | 一个multi-op事务内原子写meta和content两个znode。 |
| etcd | 一个Txn内原子写meta和content两个key。 |
| Nacos | 读取当前Catalog,再以它的MD5为条件执行CAS,原子替换正文、业务版本、sequence和lastChange。并发CAS失败会重新读取后重试,最多8次。 |
# SQL兼容门面(新代码不推荐用)
SQL模块里还保留着com.yomahub.liteflow.repository.sql.SqlRulePublisher。它的无参构造从全局LiteflowConfig取连接配置,但不支持route、namespace和expectedVersion,也没有统一Publisher那套基于change_lock的发布顺序协议。
所以它不能作为多节点或者并发发布场景的生产写入入口。2.16.1的新代码和管理后台请用上面的RulePublisherFactory加SqlPublisherConfig。
下面这段代码只是给你识别和迁移旧调用用的,不建议新写:
SqlRulePublisher publisher = new SqlRulePublisher();
long v = publisher.publishChain("orderChain", "THEN(a, b)");
publisher.publishScript(scriptRecord); // 传 ScriptRecord
publisher.removeChain("orderChain");
publisher.removeScript("s1");
迁移的时候把连接参数放进SqlPublisherConfig,再把ScriptRecord转成PublishScriptRequest就行。
# 停用(enable=0)
v1的Publisher没有提供enableChain和enableScript这两个API,留作后续。如果只是想临时停用而不删除,可以直接写存储把enable置成0:
- SQL:
UPDATE lf_chain SET enable=0 WHERE application_name=? AND chain_id=? - PostgreSQL:
UPDATE lf_chain SET enable=FALSE WHERE application_name=? AND chain_id=? - MongoDB:把对应文档的
enable改成false,并且递增version - Redis:
HSET {prefix}:{app}:chain:{id} enable 0 - zk和etcd:把对应meta节点里的enable标志置0,编码格式见各后端的
*RecordCodec - Nacos:当前的Catalog协议不接受
enable=false的记录,所以不支持直改停用,请用removeChain和removeScript
注意
直接改enable不会产生变更日志和通知,各节点要等下一个对账周期才能感知,默认最多60秒。zk和etcd如果改了meta节点的内容会触发watch,那就是秒级感知。另外,已经在缓存里的编译产物在感知之前还会继续执行。
想让停用立即生效,请用removeChain,删除会走变更通知,秒级收敛;或者停用之后再按下面的规范补一条变更日志。
# 绕过API直接写存储的规范
如果你已经有自己的管理后台,不想引Java客户端,那直接写存储也可以,但必须把Publisher的事务和原子语义完整复制过去,缺一步都会导致节点收敛失败。
SQL直写要在一个事务里完成下面这些:
- 事务开始后先执行
SELECT lock_id FROM lf_change_lock WHERE lock_id = 1 FOR UPDATE,持锁到提交或回滚。自定义了表前缀记得同步替换表名,另外不要按application_name去拆锁。 - UPSERT
lf_chain或lf_script行,version = version + 1。这里要靠行锁下的原子自增,不要先SELECT出来再在Java里加一,并发发布会丢更新。同时重算并写入content_md5,chain是MD5(el_data)且不含route_data,script是MD5(script_data)。算法必须和Publisher一致,否则会产生虚假的对账diff。 INSERT INTO lf_change_log (application_name, target_type, target_id, op, version) VALUES (...)。- 提交事务,要回滚就一起回滚。
- 删除的场景:拿到同一把顺序锁之后,DELETE内容行并INSERT一条
op=DELETE的change_log,同样在一个事务里完成。
PostgreSQL直写遵循同一套事务协议。MongoDB直写则必须在一个多文档事务里完成正文CAS、lf_sequence自增和lf_change_log插入。
Redis直写必须用一段Lua脚本完成HSET内容、SADD索引、INCR seq、ZADD changelog这四步,可以参考模块内的lua/publish-chain.lua。不要用普通命令拼,拼出来的话多个命令之间存在竞态,可能让别的客户端读到内容已更新但seq还没推的中间态。
zk和etcd直写必须在一个事务里(zk用multi-op,etcd用Txn)同时写meta和content,保证两者版本一致。
Nacos不支持直改Catalog,请只用RulePublisher。
# content_md5这道双保险
对账的时候先比version,相同的话再比content_md5。除了Nacos,比的都是content_md5列或字段里存着的值,引擎不会去拉取内容重算哈希。所以这道双保险防的是version判据失灵、但指纹仍然可信的情况:
- 备份恢复或者跨环境导表,版本号恰好撞车了(都是v7)但内容不同,这时md5不一样,对账能纠正过来。
- 写方更新了内容和md5,但version没加上去(工具有bug,或者并发丢了更新),同样md5不一样,对账能纠正。
它防不住只改内容、version和content_md5都不动的裸改
两个元数据都没变,对账每一轮都会判定为无变化,于是你的改动永远不会生效。这是手改库最常见的坑。
临时运维时手动改一条规则,最小的正确姿势是这样:
UPDATE lf_chain
SET el_data = 'THEN(a, c, b, s1)',
version = version + 1, -- 必须:对账感知变更的主判据
content_md5 = MD5(el_data) -- 建议:保持指纹与内容一致
WHERE application_name = 'your-app' AND chain_id = 'chain1';
只做这一步的话,最迟reconcile-seconds(默认60秒)会生效。想在seq轮询周期内(SQL默认3秒)就生效,那还得按上面的规范补一条change_log。lf_script同理,只是指纹算的是MD5(script_data)。
停用一条chain只要enable = 0就够了,它会从manifest里消失,对账按DELETE处理,不用动version。
这些是兜底手段,不是鼓励你绕过规范,规范路径才是快路径。
Nacos是个例外,它每次读取都会对Catalog内的正文重新计算MD5并校验,指纹不匹配会直接拒绝整份Catalog。
# 发布校验与依赖顺序
Publisher保证的是单个目标的存储原子性和版本并发控制,它不负责解析或者编译业务规则:
- 它会校验必填字段、长度、后端键名这些存储约束,但不校验EL语法,也不会去确认Java组件、子chain或脚本节点是否存在。
- 一次API调用只原子发布一个chain或一个script。目前没有把多条相互依赖的规则作为一个bundle同时切换的事务API,所以多次调用之间始终存在最终一致性窗口。
- 新增或升级依赖时,按先脚本和叶子子chain、再引用它们的父chain这个顺序发布;删除时反过来,先把父chain对依赖的引用去掉,再删脚本或子chain。
- 发布前建议在隔离环境用和生产相同的Java组件、脚本插件跑一次冷加载测试。管理后台收到
PublishResult只表示存储写入成功了,不代表所有执行节点都编译成功。 - 回滚应该把已验证的旧正文作为一个新版本重新发布,不要把存储里的
version直接改小。



