目录
一、seekdb 是什么
二、整体分层架构
三、三种部署形态
四、源码顶层目录
五、Observer 接入与调度
六、SQL 引擎
七、存储引擎
八、事务子系统
九、LogService(PALF 复制日志)
十、RootServer 集群大脑
十一、share 公共服务
十二、Change Stream 异步索引流水线 ★
十三、向量索引(两级 HNSW / IVF)★
十四、全文检索(FTS)
十五、Hybrid Search 混合检索
十六、FORK / MERGE COW 沙箱 ★
十七、Plugin 框架
十八、PL / ObJIT
十九、典型数据流
二十、组件能力总表
附 A · LSM-Tree 存储引擎深入
附 B · MVCC 实现深入
附 C · 向量索引层架构深入
附 D · HNSW 算法与工程实现
附 E · Change Stream 正确性与背压
附 F · 写路径全链路时序
附 G · 常见问题回答
一、seekdb 是什么
seekdb 是 OceanBase 团队推出的、面向 AI Agent 状态存储 (state store)
的多模态数据库:同时承载 向量 / 全文 / 关系 三类数据,对外完全兼容
MySQL 协议 ,可作为嵌入式库 、单机 Server 或分布式集群 运行。
它继承自 OceanBase 的 SQL 优化器与 LSM-Tree 存储引擎,并针对 Agent 的
「流式写 + 毫秒级检索 + 并发查询」工作负载,重写了索引构建路径,并提供
Kernel-level Copy-on-Write 沙箱。
三态部署一致性 :嵌入式 / Server / 分布式共享同一份 oceanbase_static 内核库(见 src/observer/embed/CMakeLists.txt )。
多租户单进程 :src/observer/omt 把同一个 observer 进程切分为多个独立租户。
Paxos 复制日志 :src/logservice/palf (Paxos-based Append-only Log File)。
LSM-Tree + MVCC 存储 :src/storage/memtable + src/storage/blocksstable 。
异步索引流水线 :src/share/change_stream/ 解耦 DML 与索引构建,消除 P99 抖动。
两级 HNSW 向量索引 :src/share/vector_index/ + storage/ddl/ob_hnsw_embedmgr.* 。
COW Fork 沙箱 :src/rootserver/fork_table/ + share/ob_fork_table_util.* 。
性能定位 :在 VectorDBBench Streaming 场景下,seekdb 在持续写入 500 行/秒的同时取得
1,523 QPS、并发 P99 21.7 ms(Milvus 的 10.7×;P99 抖动比 1.1× vs ES/Milvus 的 ~10×)。
本质原因是「写路径不碰索引」+「查询永远只扫两层索引」。
二、整体分层架构
从客户端协议到底层物理存储,seekdb 由六个逻辑层组成。每一层既在嵌入模式下作为函数调用栈存在,也能在 Server 模式下被网络与 RPC 框架横向切分到多节点:
flowchart TB
subgraph CLIENT["客户端 / SDK 层"]
PY["Python SDK pyseekdb"]
MY["MySQL 客户端 JDBC / mysql-cli / SQLAlchemy"]
EMB["嵌入式 C API seekdb_embed.h"]
JNI["Android JNI seekdb_jni"]
end
subgraph PROTO["接入 / 协议层 · src/observer"]
MP["MySQL 协议 obmp_query / obmp_stmt_*"]
NET["网络框架 ob_srv_network_frame"]
DLV["请求派发 ob_srv_deliver"]
OMT["多租户调度 OMT observer/omt/ob_multi_tenant"]
end
subgraph SQL["SQL 引擎 · src/sql"]
PARSE["Parser"]
RESOLVE["Resolver"]
OPT["Optimizer"]
REWRITE["Rewrite"]
CG["Code Generator"]
EXEC["Executor + DTL"]
PC["Plan Cache"]
end
subgraph TX_DAS["数据访问 / 事务 · src/sql/das + storage/tx"]
DAS["DAS Data Access Service"]
TRX["ObTransService MVCC + 2PC"]
GTS["GTS Timestamp Service"]
end
subgraph STORAGE["存储引擎 · src/storage"]
LS["LS 日志流 storage/ls"]
MT["Memtable storage/memtable"]
SST["Block SSTable storage/blocksstable"]
COMP["Compaction storage/compaction"]
DDL["DDL / Direct Load storage/ddl"]
FTS_S["FTS storage/fts"]
end
subgraph PLAT["平台 / 集群服务"]
PALF["LogService PALF src/logservice"]
RS["RootServer src/rootserver"]
SHARE["share 公共服务 change_stream / vector_index / hybrid_search"]
PLUG["Plugin / PL / ObJIT"]
end
PY --> EMB
JNI --> EMB
EMB -. in-process .-> SQL
MY --> MP
MP --> NET --> DLV --> OMT --> SQL
PARSE --> RESOLVE --> REWRITE --> OPT --> CG --> EXEC
EXEC --> DAS
DAS --> TRX
DAS --> MT
DAS --> SST
DAS --> FTS_S
MT --> SST
COMP --> SST
MT --> LS
LS --> PALF
EXEC --> SHARE
DDL --> SHARE
RS --> LS
RS --> SHARE
PLUG --> SQL
读路径与写路径的关键差异 :写入只经过 Memtable → CLOG/PALF 即返回;
索引构建(向量 HNSW、FTS、物化视图增量)由 Change Stream 异步在背后消费 redo
完成。读路径则在 DAS 中并行下推到 Memtable + SSTable + Delta 索引 + Snapshot 索引
并做行级融合。
三、三种部署形态
flowchart LR
subgraph EMBED["Embedded 模式 · in-process"]
direction TB
APP1["App 进程 Python / Java / Go / C / Android"]
LIB["libseekdb_embed oceanbase_static"]
APP1 -.链接.-> LIB
LIB --> KER1["完整 OB 内核 不走网络"]
end
subgraph SERVER["单机 Server 模式"]
direction TB
CLI1["MySQL Clients"]
OBS1["observer 进程 :2881 MySQL · :2886 RPC"]
KER2["Sys Tenant + User Tenant"]
CLI1 -- MySQL 协议 --> OBS1 --> KER2
end
subgraph CLUSTER["OceanBase 分布式集群"]
direction TB
PROXY["OBProxy / Sharding-JDBC"]
O1["observer #1 RS/Leader"]
O2["observer #2"]
O3["observer #3"]
PROXY --> O1
PROXY --> O2
PROXY --> O3
O1 <-- Paxos --> O2
O2 <-- Paxos --> O3
O1 <-- Paxos --> O3
end
3.1 嵌入模式(Embedded)
入口位于 src/observer/embed/{c,python,android} 。
C API (seekdb.h ):seekdb_open / seekdb_connect / seekdb_execute / seekdb_prepare / seekdb_step,仿 SQLite 风格 (SEEKDB_OK / SEEKDB_ROW / SEEKDB_DONE)。
Python 绑定 :ob_embed_impl.cpp ,pybind11;亦可通过 SEEKDB_NO_PYTHON 编出纯 C 库供 Go/Android 复用。
Android :android/seekdb_jni.cpp 通过 JNI 桥接 C 库;client/embedded_client.c 是 NDK 交叉编译的 CLI。
无网络栈、无 RPC,函数调用直达 SQL → Storage。
3.2 单机 Server 模式
启动入口:src/observer/main.cpp → ob_server.cpp::ObServer 。
对外暴露 MySQL Wire Protocol(端口 2881)+ 内部 RPC(端口 2886)。
单 observer 进程内 多租户共存 ,sys/user 租户隔离 CPU、内存、IO。
3.3 OceanBase 分布式模式
多 observer 节点组成 Zone/Region。
每个 partition(即 LS / tablet)有三副本,使用 PALF 做 Paxos 复制。
RootServer 服务在 sys 租户内运行,负责调度、Schema、负载均衡。
四、源码顶层目录
路径 角色 关键内容
src/observer/ 接入层 + 进程入口 MySQL 协议、OMT 多租户、嵌入式 API、main.cpp
src/sql/ SQL 引擎 parser/resolver/optimizer/code_generator/executor/das/dtl/plan_cache
src/storage/ 存储引擎 LSM-Tree (memtable+blocksstable)、tx、ls、compaction、ddl、fts、向量列存、mview
src/logservice/ 复制日志服务 PALF Paxos、apply/replay/archive/restore/cdc/logfetcher
src/rootserver/ 集群大脑(仅 sys 租户) DDL service、freeze、balancer、fork_table、mview、direct_load
src/share/ 跨模块公共服务 change_stream、vector_index、hybrid_search、cache、ai_service、catalog、schema
src/pl/ 存储过程 (PL/SQL & MySQL Routines) resolver/compile/code_generator/package/pl_cache/sys_package
src/plugin/ 插件机制 动态加载分词器、UDF、Storage Engine 等
src/objit/ 表达式 JIT 基于 LLVM 的 expression / PL 字节码 JIT
src/diagnose/ 诊断 / Lua 脚本 调试探针、问题诊断脚本
deps/ 第三方 oblib、easy、libobcdc 等
unittest/ mittest/ test/ 测试体系 mysqltest、单元测试、模块集成测试
tools/ 工具集 deploy 脚本、obadmin、ob_admin、benchmark 等
五、Observer 接入与调度层
Observer 是整个进程的大门 。它负责接收客户端请求、按租户调度执行、并把请求转换成 SQL 引擎可识别的格式。代码位于 src/observer/ 。
flowchart TB
subgraph IN["网络入口"]
NF["ObSrvNetworkFrame libeasy/easy_io 线程"]
DLV["ObSrvDeliver 按租户/优先级派发"]
end
subgraph PROTO["MySQL 协议 · observer/mysql"]
CONN["obmp_connect 登录握手"]
QRY["obmp_query 文本协议"]
PREP["obmp_stmt_prepare/execute 二进制协议"]
PIPE["obmp_packet_sender 结果集回包"]
end
subgraph OMT["OMT 多租户 · observer/omt"]
MTNT["ObMultiTenant"]
TNT["ObTenant 每租户独立线程池/资源"]
WK["ObThWorker 工作线程"]
RQ["ObRetryQueue 重试队列"]
end
subgraph CORE["核心服务句柄"]
SRV["ObServer 单例"]
SVC["ObService RPC 处理"]
SCHU["ObServerSchemaUpdater"]
LIVE["ObSrvLiveSession"]
end
NF --> DLV --> OMT
OMT --> CONN
OMT --> QRY
OMT --> PREP
QRY --> SQL_ENG[("SQL 引擎")]
PREP --> SQL_ENG
PIPE --> NF
SRV --- SVC
SRV --- MTNT
SRV --- SCHU
5.1 关键文件
文件 职责
main.cpp / ob_server.cpp 进程入口、Server 生命周期、全局单例初始化
ob_srv_network_frame.cpp 基于 libeasy 的网络框架,监听 MySQL/RPC 端口
ob_srv_deliver.cpp 请求按租户、按优先级派发到 OMT 队列
ob_srv_xlator.cpp RPC 包到内部 processor 的路由
mysql/obmp_*.cpp MySQL 协议命令的逐条 processor(query/prepare/execute/init_db/auth 等)
mysql/ob_query_driver.cpp 查询统一驱动:同步/异步、流式/缓冲
omt/ob_multi_tenant.cpp 多租户管理器,租户的添加/卸载/资源分配
omt/ob_tenant.cpp 单租户对象(自有线程池、自适应工人池)
omt/ob_th_worker.cpp worker 线程,绑定到租户上下文执行 SQL
embed/c/seekdb_embed.cpp 嵌入式 C 入口;本地直接驱动 SQL Engine
embed/python/ob_embed_impl.cpp Python pybind11 绑定
virtual_table/ 所有 __all_virtual_*、oceanbase.gv$* 视图实现
table_load/ Direct Load API 入口(旁路写)
5.2 多租户 (OMT) 设计
每个租户拥有自有 worker pool + retry queue + 内存隔离上下文;越界请求被排队或拒绝。
用户租户之外存在 sys tenant (id=1),运行 RootServer、调度内部 SQL。
线程通过 MTL_SWITCH(tenant_id) 切换到目标租户上下文执行,所有模块对应的 MTL(Module*) 单例都按租户隔离。
六、SQL 引擎
SQL 引擎位于 src/sql/ ,是 OceanBase 历经多年沉淀的核心,seekdb 在其上扩展了向量、FTS、混合检索算子与 FORK/MERGE 语义。
flowchart LR
REQ["SQL 文本"] --> PARSER
subgraph PIPELINE["执行流水线"]
direction LR
PARSER["Parser parser/ flex+bison"] --> RESOLVER["Resolver resolver/"]
RESOLVER --> REWRITE["Rewrite rewrite/"]
REWRITE --> OPTIM["Optimizer optimizer/"]
OPTIM --> CG["Code Generator code_generator/"]
CG --> PLAN["Physical Plan"]
PLAN --> EXEC["Executor engine/executor"]
end
PLAN -- 缓存 --> PC[("Plan Cache plan_cache/")]
PC --> EXEC
EXEC --> DAS["DAS Layer das/"]
EXEC --> DTL["DTL 数据传输 dtl/ 分布式"]
DAS --> STORAGE[("Storage")]
EXEC --> PRIV["Privilege Check privilege_check/"]
EXEC --> SESSION["Session 管理 session/"]
EXEC --> MONITOR["Monitor / Trace monitor/"]
6.1 子模块一览
子目录 能力
parser/ Lex/Yacc 词法语法,输出 ParseNode AST
resolver/ 语义分析、列绑定、权限基础校验;按 DML/DDL/DCL/TCL 分子目录
rewrite/ 查询改写(视图展开、子查询消除、谓词下推、Outer-Join 重排)
optimizer/ 基于成本的优化器 (CBO);支持并行、分布式 plan、向量/FTS hint
code_generator/ 把逻辑 plan 翻译成物理算子(ObOpSpec)
engine/ 40+ 算子实现:aggregate / basic / cmd / dml / expand / expr / join / 等
executor/ 物理 plan 执行驱动;分布式调度
plan_cache/ 语句指纹 → Plan 复用
das/ Data Access Service:把 SQL 算子下推到存储;含 ob_text_retrieval_op 、ob_vector_index_lookup_op
dtl/ Data Transfer Layer:分布式算子间数据流
monitor/ 执行 trace、慢日志、real-time stat
printer/ 反解析 SQL(schema 重建、explain 输出)
privilege_check/ 权限检查
6.2 SQL 执行链路
sequenceDiagram
participant C as Client
participant MP as obmp_query
participant SQL as ObSql
participant RS as Resolver
participant OPTI as Optimizer
participant EX as Executor
participant DAS as DAS Layer
participant ST as Storage
C->>MP: COM_QUERY "SELECT ..."
MP->>SQL: stmt_query()
SQL->>SQL: Parse → ParseNode
SQL->>RS: resolve
RS->>SQL: stmt(逻辑 AST)
SQL->>OPTI: rewrite + plan
OPTI-->>SQL: Physical Plan
SQL->>EX: open(plan)
EX->>DAS: scan / dml ops
DAS->>ST: 行级读写(含向量/FTS lookup)
ST-->>DAS: rows
DAS-->>EX: rows
EX-->>MP: result set
MP-->>C: MySQL packets
6.3 向量 / 全文相关的 DAS 算子
das/ob_vector_index_lookup_op.cpp — 向量索引 ANN 查找(HNSW / IVF),结果按距离回表。
das/ob_text_retrieval_op.cpp — 倒排表扫描,支持 MATCH AGAINST。
das/ob_das_vec_define.cpp — 向量 DAS 任务结构。
das/ob_das_domain_utils.cpp — 文本/向量等"领域索引"通用入口。
七、存储引擎
位于 src/storage/ ,是经典的 LSM-Tree + MVCC 架构。一个表被分成多个 tablet ,多个 tablet 隶属于一个 LS (Log Stream) ,LS 是复制和事务的最小单位。
flowchart TB
subgraph LSMAP["LS · Log Stream"]
direction TB
LSMETA["ObLS ls_meta + state"]
LSDDL["ObLSDDLLogHandler"]
LSFR["ObFreezer 转储"]
LSREPL["ObLSReservedSnapshotMgr"]
end
subgraph TBLT["Tablet · 行存/列存"]
direction TB
META["meta_mem tablet 元信息"]
MT["Memtable memtable/"]
SST["SSTable blocksstable/"]
LOB["LOB lob/"]
MV["Mview mview/"]
DDLKV["DDL KV ddl/ob_tablet_ddl_kv"]
HNSW["HNSW EmbedMgr ddl/ob_hnsw_embedmgr"]
end
subgraph BSL["blocksstable 物理层"]
BLKMGR["ObBlockManager 本地宏块管理"]
BLKSTORE["blockstore/ 分层存储 / S3"]
BF["BloomFilter Cache"]
ENC["encoding / cs_encoding 编码压缩"]
IDX["index_block 多级索引块"]
end
subgraph COMP["compaction/ 合并"]
direction TB
MNF["Minor Freeze memtable→mini sstable"]
MJF["Major Merge 合并基线"]
FLT["filter / 谓词下推"]
end
subgraph CKPT["checkpoint/"]
CKE["ObCheckpointExecutor"]
DCK["ObDataCheckpoint"]
FCK["ObFreezeCheckpoint"]
end
LSMETA --> TBLT
MT --> SST
COMP --> SST
SST --> BSL
HNSW -. 列存 .-> BSL
LSFR --> COMP
CKE --> CKPT
CKPT --> LSMETA
7.1 关键概念
对象 说明
LS (Log Stream)复制和事务最小单元;含若干 tablet;一个 LS 对应一组 PALF 日志
Tablet 表/分区/索引在一个 LS 内的物理实例;行存或列存
Memtable 内存增量;行存 B+Tree / Hash;MVCC 版本链
Mini/Minor SSTable memtable 转储产物;按时间窗口合并
Major SSTable 基线数据;定期全量合并
HEAP / 行存 / 列存 HEAP = 无主键堆表;普通索引组织表用 IOT;分析列用 column_store
7.2 模块清单
子目录 职责
access/ 统一的 scan / get / multi_get / multi_scan 接口
blocksstable/ SSTable 宏块/微块格式、读写器、行/列编码、bloom filter
blockstore/ 本地/远端 (S3、对象存储) 块存储抽象
checkpoint/ 多维度检查点(数据 / 冻结)
column_store/ 列存 (CG: Column Group) 实现,分析型查询用
compaction/ Mini/Minor/Major Merge 调度,及谓词/重排
concurrency_control/ 表锁、行锁、ROW LOCK
ddl/ DDL 增量与回放、Direct Load、Tablet split/fork、HNSW EmbedMgr
direct_load/ 旁路写 (skip memtable) 加速大批量装载
fts/ 全文检索内嵌的分词器、词典、参数化 helper
high_availability/ 副本迁移、Rebuild、Restore
lob/ 大对象 (LOB) 列存储
ls/ LS 对象、状态机、handler 集合
memtable/ Memtable 实现 + MVCC + Multi-source Data
meta_mem/ tablet/ls 元信息内存管理
meta_store/ 持久化元信息
multi_data_source/ 事务多源数据(DDL / TableLock / TXBuffer)
mview/ 物化视图刷新
tx/ 分布式事务、GTS、2PC、版本号
八、事务子系统
位于 src/storage/tx/ 。seekdb 完整继承 OceanBase 的 MVCC + 2PC + Paxos 事务设计。
flowchart LR
subgraph TX["ObTransService"]
BEG["Begin TX"] --> WR["写入 memtable 挂载到 tx_ctx"]
WR --> CMT{"Commit?"}
CMT -- 单分区 --> ONE["ObOnePhaseCommitter"]
CMT -- 多分区 --> TWO["2PC Prepare→Commit"]
ONE --> CLOG["CLOG 落日志"]
TWO --> CLOG
CLOG --> PALF[("PALF Paxos")]
PALF --> CB["end_trans_cb"]
end
GTS["ObTimestampService / GTS 全局时间戳"] --> TX
GTSL["ObGtsSource / Local Cache"] --> GTS
DLK["deadlock_adapter 分布式死锁"]
TX --- DLK
GTS (Global Timestamp Service):通过 ob_timestamp_service.cpp / ob_gts_source.cpp 提供全局单调递增时间戳作为 MVCC 版本。
MVCC :行多版本由 memtable 链维护;读用 snapshot version 进行可见性判断。
分布式事务 :跨 LS 写入走 2PC;ob_one_phase_committer 优化单 LS 提交。
多源数据 (MDS) :DDL、TableLock、auto-inc 元数据等附挂在事务里,与数据一起落 CLOG。
九、LogService(PALF 复制日志)
位于 src/logservice/ ,是分布式一致性、灾备、CDC 的统一底座。
flowchart TB
subgraph PALF["PALF · Paxos Append-only Log File"]
direction TB
LH["LogHandler 对外 API"]
LE["LogEngine 日志读写"]
LCM["LogConfigMgr 配置变更"]
LSW["LogSlidingWindow 同步窗口"]
LELE["election/ Paxos 选举"]
LCAC["LogCache"]
LBM["LogBlockMgr 本地块"]
LFR["fetch_log_engine 追赶/补日志"]
end
subgraph SVC["围绕 PALF 的服务"]
APPLY["applyservice commit→memtable callback"]
REPLAY["replayservice followers 重放"]
ARC["archiveservice 归档到对象存储"]
REST["restoreservice 从归档恢复"]
CDC["cdcservice / logfetcher 对外抽 CDC 流"]
DD["data_dictionary schema 字典"]
LC["leader_coordinator 主选举协调"]
RC["rcservice 角色/容灾"]
end
LH --> LE --> LBM
LH --> LSW
LH --> LCM --> LELE
APPLY --> LH
REPLAY --> LH
CDC --> LFR
ARC --> LE
REST --> LFR
9.1 子目录
子目录 职责
palf/ Paxos 实现,提供强一致 append-only 日志
applyservice/ Leader 把日志 commit 后回调 memtable,完成本地可见
replayservice/ Follower 顺序重放 redo
archiveservice/ 把 redo 归档到 NAS / S3 / OSS
restoreservice/ 物理备份恢复
cdcservice/ logfetcher/ logrouteservice/ OBCDC:把 redo 翻译成行级变更流
data_dictionary/ 嵌入到日志流里的 schema 字典快照
leader_coordinator/ 多 LS 选主 / 切主调度
rcservice/ 角色变更(主备切换、停机维护)
seekdb 的关键复用 :Change Stream(异步索引)也走 PALF 的 logfetcher 接口订阅
redo,因此对外的 CDC 接口与内部异步索引共享同一份日志读取基础设施。
十、RootServer(集群大脑)
仅 sys 租户启动,位于 src/rootserver/ 。负责 schema、调度、备份、DDL 串行化、Fork/Merge、合并冻结等。
flowchart TB
RS["ObRootService"] --> DDL["ObDDLService 所有 DDL 入口"]
DDL --> DDLT["ddl_task/ DDL 任务追踪与重做"]
RS --> FRZ["freeze/ major freeze 调度"]
RS --> LB["ObLoadBalancer partition 负载均衡"]
RS --> SCH["ObServerLocalityCache"]
RS --> FORK["fork_table/ ObForkDatabase/TableService"]
RS --> MV["mview/ 物化视图调度"]
RS --> DI["direct_load/ 大批量装载调度"]
RS --> BACK["backup/ 备份调度"]
DDL --> IDX["ObIndexBuilder 建索引 含 vector/FTS"]
DDL --> LOB["ObLobMetaBuilder ObLobPieceBuilder"]
DDL --> CCL["ob_ccl_ddl_service 限流规则"]
DDL --> AI["ob_ai_model_ddl_service AI 模型注册"]
DDL service :CREATE/ALTER/DROP/INDEX 等全部串行化执行;产生 schema 版本,通过 schema service 广播。
fork_table :核心实现 FORK DATABASE / FORK TABLE,下面专门展开。
mview :物化视图刷新调度,对接 Change Stream。
ai_model :注册外部 LLM/Embedding 模型;与 share/ai_service/ 协同。
ccl (Concurrency Control):限流规则 DDL。
十一、share 公共服务
src/share/ 是跨 SQL、Storage、RS 的横切层,集中存放公共服务、跨节点 RPC、缓存、AI 集成。
子目录 角色
schema/ inner_table/ 所有 __all_xxx 内部表、schema getter/cache
cache/ 统一 KV cache(schema cache、row cache、block cache)
location_cache/ tablet/LS → server 的位置缓存与 RPC 探测
io/ 统一 IO 框架,分级 IO 调度
change_stream/ ★ 异步索引流水线(Fetcher / Dispatcher / Worker / Plugin)
vector_index/ ★ HNSW / IVF 向量索引引擎适配层(接 VSAG)
hybrid_search/ ★ JSON 风格混合查询入口(Pinecone-like API)
ai_service/ 外部 LLM/Embedding 模型代理(HTTP 调用 OpenAI 等)
catalog/ 外部数据源 catalog(Iceberg/Hive/外表)
aggregate/ datum/ 向量化执行所需的 datum、聚合算子原语
ash/ index_usage/ longops_mgr/ ASH 活动会话、索引使用统计、长任务监控
backup/ 备份策略、备份目录
compaction/ 合并 schedule 配置 / 异常追踪
deadlock/ 分布式死锁检测器
diagnosis/ 诊断信息收集
external_table/ 外表(S3/CSV)读取
allocator/ 租户内存子分配器
client_feedback/ 给 OBProxy 的反馈信息(路由优化)
config/ 所有配置项注册中心
十二、Change Stream 异步索引流水线 ★ Agent 性能关键
位于 src/share/change_stream/ 。这是 seekdb 区别于 OceanBase 的核心新增模块,
也是「写路径不碰索引」、「查询永远只扫两层索引」得以成立的根因。
12.1 概念
普通 LSM 数据库写入时同步构建索引(向量/全文/物化视图),引起 P99 抖动;seekdb 让事务只写 redo ,
索引由独立流水线从 PALF 异步消费 redo 构建。
flowchart LR
subgraph WRITE["写路径"]
DML["INSERT/UPDATE"] --> MT["Memtable + Tx"]
MT --> CLOG["PALF Redo Log"]
CLOG --> RET["Commit OK 立即返回"]
end
subgraph CS["Change Stream 异步流水线 · src/share/change_stream"]
direction LR
FET["ObCSFetcher 单线程,按 LSN 顺序消费 PALF"]
FET -- "已 commit 事务" --> RING["Ring Buffer 每事务一条 ObCSTxInfo"]
RING --> DIS["ObCSDispatcher 切片到 worker"]
DIS --> SUB["ObCSExecSubTask 按 heap_pk 切片"]
SUB --> EX["ObCSExecutor LinkQueue 线程池"]
EX --> PLUG["ObCSPlugin 实例池 每 batch 创建"]
PLUG --> AIDX["Async Index Plugin ob_cs_plugin_async_index"]
AIDX --> DELTA["Delta HNSW / FTS 内存增量索引"]
end
CLOG --> FET
DELTA --> Q[("查询路径合并")]
12.2 关键组件
类 线程模型 职责
ObChangeStreamMgr 租户级 MTL 单例 持有 Fetcher / Dispatcher / Worker 生命周期
ObCSFetcher 单线程 从 PALF 顺序拉 redo,按 tx_id 组装;commit 时推入 ring buffer。事务级零拷贝,rollback to savepoint 用 [to_seq, from_seq) 区间裁剪
ObCSDispatcher 单线程消费 把单事务拆成多个 ObCSExecSubTask(按 heap_pk 切片,邻近行落到同一 worker)
ObCSWorker / ObCSExecutor 多线程 LinkQueue 池 并发执行子任务;自适应线程数
ObCSExecCtx per-batch 共享事务句柄、refresh_scn、Plugin 实例集;任务结束统一回收
ObCSPlugin per-batch 工厂创建 插件式接口:向量异步索引、未来可挂 FTS、Mview Refresh、自定义流
ObCSPluginAsyncIndex — 把 redo 行写入 delta HNSW / IVF buckets
12.3 行可见性 / Rollback 支持
每条 redo 携带 stmt_seq_no,与 tx 的 rollback 区间集合 [to_seq, from_seq) 比较;命中区间的行直接丢弃,避免把已回滚的写入构建到索引。
Fetcher 的 min_dep_lsn / refresh_scn 周期性写入全局表,使查询路径知道「索引追到了什么时间点」。
12.4 关键文件
ob_change_stream_mgr.{h,cpp} 租户级管理器,MTL 注入点
ob_change_stream_fetcher.{h,cpp} PALF 消费者,事务组装
ob_change_stream_dispatcher.{h,cpp} 事务切片、可见性
ob_change_stream_worker.{h,cpp} LinkQueue 工人池
ob_change_stream_plugin.{h,cpp} Plugin 注册表
ob_cs_plugin_async_index.{h,cpp} 异步索引插件实现
十三、向量索引(两级 HNSW / IVF) ★
实现位于 src/share/vector_index/ + src/storage/ddl/ob_hnsw_embedmgr.* + sql/das/ob_vector_index_lookup_op.cpp 。底层算法库通过 VSAG (OceanBase 开源的向量索引内核)实现。
13.1 两级索引架构
flowchart LR
subgraph WR["写 / 异步构建"]
DML2["INSERT/UPSERT"] --> RDLOG["Redo"]
RDLOG --> CS2["Change Stream"]
CS2 --> DELTA2["Delta HNSW in-memory,可立即查询"]
end
subgraph SP["周期性 Snapshot"]
DELTA2 -- rotate --> SNAP["Snapshot HNSW 持久化到 SSTable"]
DELTA2 -. 保留新增 .-> DELTA2
end
subgraph QRY["查询路径"]
Q["ANN Query"] --> DASOP["ObVectorIndexLookupOp"]
DASOP --> SDELTA["search delta HNSW"]
DASOP --> SSNAP["search snapshot HNSW"]
SDELTA --> MERGE["结果归并 + scalar/FTS 过滤"]
SSNAP --> MERGE
MERGE --> ROW["回表取完整行"]
end
为什么 P99 抖动只有 1.1×?
• 索引数量固定为 2 ,并发查询永远只对两个索引各做一次 ANN,不会随写入累积膨胀的段数发散;
• Delta HNSW 内存常驻,并提供细粒度读锁,新写入毫秒级可见;
• Snapshot HNSW 通过 SSTable 落盘,重启可加载,不必重建。
13.2 关键类
类 / 文件 职责
ObPluginVectorIndexService 租户级向量索引服务总入口(vector_index/)
ObPluginVectorIndexMgr tablet → adapter 注册表
ObPluginVectorIndexAdaptor 单 tablet 上 delta+snapshot 双 HNSW 的封装;FilterInterface 注入 bitmap 谓词下推
ObVectorQueryAdaptorResultContext 查询请求 context(topK、ef、过滤位图)
ObPluginVectorIndexScheduler delta→snapshot 切换、合并的后台调度器
ObVectorIndexIvfCacheMgr / ObIvfAsyncTaskExecutor IVF 系列索引的缓存与异步构建
ObVsagMemContext / ObVsagSearchAlloc VSAG 内存子分配器
ObVectorEmbeddingHandler 向量字段类型 / 编码 / 持久化 handler
ObHnswEmbedMgr 位于 storage/ddl/,把 HNSW 段写成 SSTable
ObHybridVectorRefreshTask vector + 标量混合刷新任务
ob_vector_index_lookup_op.cpp SQL 执行端的 ANN lookup 算子
13.3 支持的索引参数
类型 常用参数
HNSW M (default 16)、ef_construction、ef_search、distance (l2/ip/cosine)、lib=vsag
IVF_FLAT / IVF_SQ8 / IVF_PQ nlist、nprobe、训练样本数;通过 ob_vector_kmeans_ctx 训练
BQ / HNSW_BQ BinaryQuant 量化变体
十四、全文检索(FTS)
分词器与查询入口分布在 src/storage/fts/ 与 share/ob_fts_index_builder_util.* 。
flowchart TB
subgraph DDL_FTS["建索引 · ob_fts_index_builder_util"]
CRT["CREATE FULLTEXT INDEX ... WITH PARSER xxx"] --> BLD["FTS Builder"]
BLD --> TBLS["doc_id 倒排表 + doc_word 表 + fts_index 表"]
end
subgraph WR_FTS["写路径"]
INS["INSERT 文档"] --> MT2["Memtable"]
MT2 --> CSF["Change Stream 异步分词+倒排"]
CSF --> INVTBL[("倒排表 SSTable")]
end
subgraph QR_FTS["查询路径"]
MATCH["MATCH(content) AGAINST(...)"] --> TRET["ob_text_retrieval_op"]
TRET --> PARSE2["FT Parser ik/ngram/beng/whitespace"]
PARSE2 --> SCAN["倒排扫描+评分 BM25"]
SCAN --> RET2["doc_id 列表"]
end
14.1 内置分词器
Parser 说明
ik 中文 IK 分词(细粒度+智能模式),位于 storage/fts/ik/
ngram / ngram2 n-gram 通用切分
beng 英文基础分词(小写化、停用词)
whitespace 纯空白切分(标记型场景)
plugin parsers 通过 plugin 框架挂入第三方分词
14.2 关键文件
ob_fts_plugin_helper.* 分词器插件统一接入(ObFTParser / ObFTParseHelper)
ob_fts_struct.* 分词、文档结构定义
ob_fts_stop_word.* 停用词管理
share/ob_fts_index_builder_util.* FTS 三个内部表的构建/重建
sql/das/ob_text_retrieval_op.cpp SQL 执行端倒排扫描算子
十五、Hybrid Search 混合检索
src/share/hybrid_search/ 提供了 Pinecone 风格的 JSON 查询入口,把 向量 + 全文 + 标量 翻译成一条 SQL Plan。
flowchart LR
REQ2["JSON Hybrid Query"] --> PARSE3["ob_query_parse 解析"]
PARSE3 --> RQT["ObQueryRequest 结构体"]
RQT --> TRX2["ob_query_translator 翻译为 SQL"]
TRX2 --> SQL2[("SQL Plan")]
SQL2 --> EXEC2["ob_hybrid_search_executor"]
EXEC2 --> VEC["Vector Lookup"]
EXEC2 --> FTSO["FTS Retrieval"]
EXEC2 --> SCA["Scalar Filter / Sort"]
VEC --> MRG["执行计划内归并"]
FTSO --> MRG
SCA --> MRG
MRG --> OUT["结果集"]
关键文件:ob_hybrid_search_executor.cpp 、ob_query_translator.cpp 、ob_query_parse.cpp 、ob_query_request.cpp 、ob_request_base.cpp 。
十六、FORK / MERGE COW 沙箱 ★ Agent 探索安全网
实现位于 src/rootserver/fork_table/ + share/ob_fork_table_util.* + storage/ddl/ob_tablet_fork_task.* 。这是 seekdb 区别于普通 DB 的另一个核心新特性。
16.1 概念
「FORK」并不真正复制数据;它在 schema 层为目标库/表创建新的 ID,并把所有 tablet 标记为
共享基线 + 写时复制 (Copy-on-Write) 模式。Agent 在沙箱里读写时:
读 = 旧 tablet 数据 + 沙箱自有增量;
写 = 只落到沙箱独有的 memtable / SSTable;
主库完全不受影响。
flowchart LR
subgraph BEFORE["Step 1 · FORK 之前"]
direction TB
MAIN1["main DB · agent_state"]
T1["tablet T1 SSTable基线"]
MAIN1 --> T1
end
subgraph FORKED["Step 2 · FORK DATABASE"]
direction TB
MAIN2["main DB"]
SB["sandbox_42"]
SHTAB["共享基线 SSTable 引用计数"]
MAIN2 --> SHTAB
SB --> SHTAB
end
subgraph WRITE["Step 3 · sandbox 写入"]
direction TB
MAIN3["main"]
SB3["sandbox_42"]
SHB["共享基线"]
SBN["sandbox 私有 memtable/SSTable"]
MAIN3 --> SHB
SB3 --> SHB
SB3 --> SBN
end
subgraph MERGE["Step 4 · MERGE / DROP"]
direction TB
MM["MERGE TABLE STRATEGY FAIL/THEIRS/OURS"]
DD["DROP DATABASE 直接丢弃"]
MM --> MAIN4["已更新 main"]
DD --> MAIN5["main 保持不变"]
end
16.2 执行流程
sequenceDiagram
participant U as User SQL
participant RS as RootServer / ObDDLService
participant FK as fork_table service
participant SCH as Schema Service
participant LS as Storage / LS
U->>RS: FORK DATABASE src TO dst
RS->>FK: ObForkDatabaseService
FK->>SCH: 复制 schema:新 db + 新 table_id + tablet_id
FK->>LS: ObTabletForkTask 标记 tablet 为 COW 共享基线
LS->>LS: 引用计数 +1(不拷贝数据)
FK-->>U: OK(秒级返回)
Note over U,LS: sandbox 上的写入只生成新 memtable/SSTable
U->>RS: MERGE TABLE ... STRATEGY THEIRS
RS->>FK: 合并任务,按 strategy 解决冲突
FK->>LS: 把 sandbox 增量打回 main tablet
16.3 关键文件
rootserver/fork_table/ob_fork_database_service.cpp FORK DATABASE 入口
rootserver/fork_table/ob_fork_table_service.cpp FORK TABLE 入口
rootserver/fork_table/ob_fork_table_task.cpp DDL Task:跨节点协调
rootserver/fork_table/ob_fork_table_info_builder.cpp 构建 fork 后的 schema/tablet 映射
rootserver/fork_table/ob_fork_table_helper.cpp 跨模块辅助:MERGE 策略 (FAIL/THEIRS/OURS)
share/ob_fork_table_util.cpp 共享工具:可见性、版本判定
storage/ddl/ob_tablet_fork_task.cpp tablet 层的 COW 落地
storage/ddl/ob_table_fork_info.cpp tablet fork 元信息
MERGE STRATEGY 含义 :
• FAIL :行冲突即报错,回滚整个 merge;
• THEIRS :以 sandbox 内容为准覆盖主库;
• OURS :以主库内容为准,sandbox 冲突行丢弃。
十七、Plugin 框架
src/plugin/ 提供动态加载机制(FT Parser、UDF、Storage engine 等)。
flowchart LR
CFG["INSTALL PLUGIN xxx plugin 表注册"] --> MGR["ObPluginMgr"]
MGR --> DLOPEN["ObPluginDlHandle dlopen 动态库"]
DLOPEN --> ENT["ObPluginEntryHandle 读符号 entry"]
ENT --> ADP["Adaptor plugin/adaptor"]
ADP --> IF["Interface plugin/interface"]
IF --> CONS["内核消费者 FTS / UDF / Storage"]
plugin/sys/ 插件加载/卸载/管理器、handle 抽象
plugin/interface/ 各类插件的 C++ 接口契约
plugin/adaptor/ 把插件 API 适配给内核
plugin/export/ include/ 对外头文件、ABI
十八、PL 引擎 & ObJIT
PL 引擎
位于 src/pl/ 。实现 MySQL/Oracle 兼容的存储过程、函数、Package、Trigger。
parser/ — PL 词法/语法
ob_pl_resolver.* — 语义分析
ob_pl_compile.* / ob_pl_code_generator.* — 编译为 PL 指令
pl_cache/ — Routine/Package 缓存
ob_pl_package_*.* — Package 状态、依赖追踪
sys_package/ — 内置系统包(DBMS_*、UTL_*)
pl_recompile/ — 失效后重编译
diagnosis/ — PL 诊断
ObJIT
位于 src/objit/ ,基于 LLVM 的 JIT 框架,把表达式、PL 字节码即时编译成本地指令,加速热点计算。
独立 CMake 子项目,可单独打 rpm
提供给 SQL Expression、PL VM 调用
支持表达式 fold、循环 unroll
诊断
src/diagnose/ 提供 Lua 脚本环境,运行时可注入采样、追踪逻辑。
十九、典型数据流
19.1 流式写 + 立即可查(Agent Memory)
sequenceDiagram
participant App as Agent App
participant MP as obmp_query / pyseekdb
participant SQL as SQL Engine
participant TX as TX + Memtable
participant PALF as PALF
participant CS as Change Stream
participant VEC as Vector Index Adaptor
App->>MP: INSERT INTO memory embedding,content VALUES ...
MP->>SQL: parse → plan
SQL->>TX: write → memtable
TX->>PALF: append redo
PALF-->>TX: majority commit
TX-->>App: OK(不等索引)
PALF->>CS: ObCSFetcher 拉取 redo
CS->>VEC: AsyncIndex Plugin 写 Delta HNSW
Note over App,VEC: 通常 ~ms 后即对 ANN 查询可见
App->>MP: SELECT ... ORDER BY l2_distance(...) APPROXIMATE
MP->>SQL: plan with vector_index_lookup
SQL->>VEC: query delta + snapshot HNSW
VEC-->>SQL: top-K
SQL-->>App: rows
19.2 混合检索一条 SQL 多模态下推
flowchart LR
Q3["SELECT id, title, l2_distance(emb, ?) dist FROM docs WHERE MATCH(content) AGAINST(quarterly report) AND author_id = 42 AND created_at > 2026-01-01 ORDER BY dist APPROXIMATE LIMIT 10"]
Q3 --> OPT3["Optimizer 生成多模态计划"]
OPT3 --> VEC3["Vector ANN"]
OPT3 --> FTS3["FTS 倒排"]
OPT3 --> SCAL["标量索引/范围"]
VEC3 --> JOIN3["执行端内归并 (无客户端拼接)"]
FTS3 --> JOIN3
SCAL --> JOIN3
JOIN3 --> TOPK["Top-K + 回表"]
19.3 FORK / MERGE 沙箱生命周期
sequenceDiagram
participant U as Agent
participant RS as RootServer
participant LS as Storage_tablets
U->>RS: FORK DATABASE agent_state TO sandbox_42
RS->>LS: 创建共享 tablet 引用,标记 COW
RS-->>U: OK(秒级)
U->>U: USE sandbox_42; 大量 INSERT / UPDATE
U->>RS: MERGE TABLE sandbox_42.memory INTO agent_state.memory STRATEGY THEIRS
RS->>LS: 把 sandbox 私有增量打回 main
RS-->>U: OK
Note over U,RS: 若失败:DROP DATABASE sandbox_42 即丢弃所有变更
二十、组件能力总表
组件 所在路径 核心能力 关键类 / 入口
Observer (协议接入) src/observer/ MySQL 协议、嵌入 API、Worker ObServer, obmp_query, ObSrvDeliver
OMT 多租户 observer/omt/ 租户隔离、线程池 ObMultiTenant, ObTenant, ObThWorker
SQL Engine src/sql/ Parse/Plan/Exec/Cache ObSql, ObResultSet, ObDASRef
DAS sql/das/ 跨存储/节点的统一数据访问 ObDataAccessService, ObDasScanOp
Storage src/storage/ LSM-Tree + MVCC ObLS, ObTablet, ObMemtable, ObSSTable
事务 / GTS storage/tx/ 2PC、MVCC、GTS ObTransService, ObTimestampService
LogService PALF src/logservice/ Paxos 强一致复制日志 LogHandler, LogEngine, election/
RootServer src/rootserver/ DDL、负载、Schema ObRootService, ObDDLService
Change Stream share/change_stream/ 异步索引流水线 ★ ObChangeStreamMgr, ObCSFetcher, ObCSDispatcher
Vector Index share/vector_index/ 两级 HNSW / IVF ★ ObPluginVectorIndexService, ObPluginVectorIndexAdaptor
FTS storage/fts/ 多语种全文检索 ObFTParser, IK/ngram/beng
Hybrid Search share/hybrid_search/ JSON 多模态查询入口 ObHybridSearchExecutor
FORK / MERGE rootserver/fork_table/ COW 沙箱 ★ ObForkDatabaseService, ObTabletForkTask
Direct Load storage/direct_load/ 旁路写大批量数据 ObDirectLoadMgr
Mview storage/mview/ rootserver/mview/ 物化视图刷新 ObMviewService
Plugin src/plugin/ 动态加载分词器/UDF ObPluginMgr
AI Service share/ai_service/ 外部 LLM/Embedding 代理 ObAIServiceExecutor
PL Engine src/pl/ 存储过程 / 包 ObPL, ObPLPackage
ObJIT src/objit/ LLVM 表达式 JIT ObJit
嵌入式 API observer/embed/ C/Python/JNI in-process API seekdb.h, ob_embed_impl.cpp
一句话总结 seekdb 架构 :在 OceanBase 的 LSM-Tree + Paxos + 多租户 SQL
内核之上,新增了三件「为 Agent 而生」的关键能力——Change Stream 异步索引流水线 、
两级 HNSW 向量索引 、FORK/MERGE COW 沙箱 ;
再通过 嵌入式 C/Python API 让它能像 SQLite 一样 in-process 运行,
也能在 Server / 分布式模式下水平扩展。
附 A · LSM-Tree 存储引擎深入
A.1 一行数据的一生
flowchart LR
subgraph IN["写入路径"]
direction TB
DML["INSERT/UPDATE/DELETE"] --> CTX["ObStoreCtx 事务上下文 + snapshot_version"]
CTX --> KV["ObMTKVBuilder 构造 key/value"]
KV --> QE["ObQueryEngine 双索引 Hash + KeyBTree"]
QE --> ROW["ObMvccRow 版本链头节点"]
ROW --> NODE["ObMvccTransNode 新版本挂链头"]
NODE --> CLOG["redo: ObMemtableMutator"]
end
subgraph FREEZE["转储 / 冻结"]
direction TB
ACT["Active Memtable"] -->|freeze| FROZ["Frozen Memtable 只读"]
FROZ -->|dump dag| MINI["Mini SSTable"]
end
subgraph COMPACT["合并 Compaction"]
direction TB
MINI2["多个 Mini"] -->|Minor Merge| MINOR["Minor SSTable 仍多版本"]
MINOR -->|每日 Major Merge| MAJOR["Major SSTable 基线 单版本"]
end
IN --> FREEZE --> COMPACT
A.2 LSM 三层物理结构
层级 位置 形态 生命周期
Active Memtable 内存 Hash 点查 + KeyBTree 范围 + 多版本链 写入直到 freeze 触发
Frozen Memtable 内存 同上但只读,等待 dump 转储后销毁
Mini SSTable 磁盘 Macro 2MB → Micro 16KB,行存或列存 等待 Minor Merge
Minor SSTable 磁盘 多版本合并,保留快照内活跃版本 下一次 Major Merge 前
Major SSTable (基线) 磁盘 单版本,每行保留 snapshot 可见的最新 下一轮 Major Merge
A.3 与 RocksDB 的差异
维度 RocksDB seekdb / OB
层级 L0..L6 七层 Leveled 仅 3 类:Memtable / Minor 增量 / Major 基线
写放大 10×+,每层都重写 "基线 + 累积增量",写放大显著更低
合并触发 后台异步,时机不可控 每日全集群同步 Major Freeze ,所有分区在同一全局快照点合并
多版本标识 seq number SCN (=GTS 时间戳) ,全局单调
读路径 L0..LN 逐层 Bloom 兜底 Memtable + Minor + Major 三段融合;Bloom + Row Cache + Block Cache 三级 cache
分区 无原生 partition tablet 是物理分区单位,跨副本由 PALF Paxos 保证一致
A.4 Macro / Micro Block 物理布局
flowchart TB
subgraph MACRO["Macro Block · 默认 2MB"]
direction TB
HDR["Macro Header checksum / 列描述 / micro index"]
M1["Micro Block 1 16KB"]
M2["Micro Block 2"]
M3["Micro Block N"]
IDXB["Micro Block Index 每微块首键 + offset"]
HDR --> M1 --> M2 --> M3 --> IDXB
end
subgraph SST["SSTable 由多个 Macro 组成 + 多级索引"]
direction LR
L1["L0 root index"] --> L2["L1 mid index"] --> L3["leaf macro"]
end
Micro Block 16KB:解压/解码的最小单位,进 Block Cache。
列编码 (blocksstable/encoding ):DICT、CONST、INTEGER、HEX_STRING、COLUMN_EQUAL、BIT_PACKING…每列独立选最优编码,含 AVX2/Neon 加速。
列存 Column Group :分析查询只读必要列,I/O 极省。
Bloom Filter Cache :每个宏块独立训练,过滤负命中。
多级 Index Block :N 级 B+Tree-like 索引块,定位行只需 O(log N) 次 IO。
A.5 读路径的"行融合"
sequenceDiagram
participant SQL as DAS scan
participant MEM as Active Memtable
participant FRZ as Frozen Memtable
participant MIN as Minor SST
participant MAJ as Major SST baseline
SQL->>MEM: get(key, snapshot_scn)
MEM-->>SQL: row v3
SQL->>FRZ: get(key, snapshot_scn)
FRZ-->>SQL: row v2
SQL->>MIN: get(key, snapshot_scn)
MIN-->>SQL: row v1
SQL->>MAJ: get(key)
MAJ-->>SQL: row v0 baseline
SQL->>SQL: ObRowFuse 按版本从新到旧融合 遇 DELETE 标记立即终止
「DELETE 实际是怎么落的?」
写入只生成一个 type=DELETE 的 ObMvccTransNode(tombstone);读路径融合到 tombstone 即终止;Major Merge 才真正物理回收。
附 B · MVCC 实现深入
B.1 三个核心数据结构
flowchart TB
subgraph MT["ObMemtable"]
direction TB
QE["ObQueryEngine Hash + KeyBTree 双索引"]
QE --> R1["ObMvccRow key=A list_head_ → 链头"]
QE --> R2["ObMvccRow key=B"]
R1 --> N1A["ObMvccTransNode v3 tx=T7 scn=120"]
N1A --> N1B["ObMvccTransNode v2 tx=T5 scn=110"]
N1B --> N1C["ObMvccTransNode v1 tx=T2 scn=100"]
end
TXC["ObMemtableCtx 本事务 callback list"]
TXC -. 反向引用 .-> N1A
TXC -. 关联 .-> R2
对象 角色 关键字段
ObQueryEngine memtable 索引 KeyBTree (range scan) + ObMtHash (point get)
ObMvccRow 一行的版本链头 list_head_、latest_compact_node_、max_trans_version_、latch_(spin lock)
ObMvccTransNode 一个版本 tx_id_、trans_version_、scn_、seq_no_、prev_/next_、type_ = NORMAL/COMPACT/DELETE
ObMemtableCtx 事务级 ctx callback_list_:commit/rollback 时回写所有挂在版本链上的节点
ObMvccAccCtx 访问 ctx snapshot_version_、tx_id_、读写模式
B.2 写入:mvcc_write 的并发控制
sequenceDiagram
participant T as Tx T 写入者
participant QE as QueryEngine
participant R as ObMvccRow
T->>QE: lookup(key)
QE-->>T: row 存在或新建
T->>R: 获取 latch_ (轻量 spin)
T->>R: 检查 WW 冲突 链头 tx_id != self 且仍 ACTIVE
alt 冲突
R-->>T: conflict_tx_id 返回
T->>T: 走 LockWaitMgr 排队等待
else 无冲突
T->>R: new ObMvccTransNode tx_id=T, scn=MAX (未提交)
R->>R: 插入到 list_head_ 前
T->>T: callback 加入 ObMemtableCtx
end
T->>R: 释放 latch_
关键点:写入时 scn = MAX 。这是「未提交版本」的标记,对所有其他事务的读都不可见(因为没有任何快照版本 ≥ MAX)。
B.3 提交:「延迟打版本号」
事务 T 发起 commit;从 GTS 拿到全局严格单调的 commit_version (SCN) 。
写 redo 到 PALF;多数派落盘后认定 commit 成功。
遍历 ObMemtableCtx.callback_list_:把所有该事务挂上去的 TransNode 的 trans_version_ 从 MAX 改写为 commit_version。
从此该版本对所有 snapshot_version ≥ commit_version 的读可见。
B.4 回滚 / Rollback to Savepoint
整事务 rollback :遍历 callback_list_,把每个 TransNode 标记为 ABORTED 或直接从版本链摘除。
Rollback to Savepoint :每条 DML 携带 seq_no_;rollback 给出区间 [to_seq, from_seq),命中区间的节点不可见。Change Stream 也复用同一套区间判定。
B.5 读:可见性判定算法
flowchart TB
RQ["Read with snapshot_scn=S, reader_tx=R"] --> LOOK["QueryEngine lookup key"]
LOOK --> ROW["ObMvccRow"]
ROW --> ITER["从 list_head_ 向后遍历 TransNode"]
ITER --> JUDGE{"判定该 node"}
JUDGE -- "tx_id==R 且 seq_no 未 rollback" --> SEE["可见 (自己刚写的)"]
JUDGE -- "tx_id != R 且 trans_version_ <= S 且 COMMITTED" --> SEE
JUDGE -- "ACTIVE 或 trans_version_ > S" --> SKIP["跳过 → 看下一个"]
SKIP --> ITER
SEE --> RET["返回该版本数据 / 或 tombstone"]
B.6 并发原语
ObRowLatch per-row 自旋锁,纳秒级,仅在写入挂链/链头检查时持有
QSync / Hazard Pointer KeyBTree 用 epoch-based reclaim,读不加锁也安全
ObLockWaitMgr WW 冲突时挂等;不轮询;锁释放唤醒
分布式死锁 share/deadlock/ 基于消息环检测
B.7 GTS:MVCC 的心脏
GTS 服务 (storage/tx/ob_timestamp_service.cpp ):sys 租户的 LS-1 是 GTS leader,提供单调递增的全局 SCN。
本地缓存 :每个 observer 把最近拿到的 GTS 缓存几毫秒,批量分发,避免每事务一次 RPC。
SCN ≈ HLC :物理时间戳 + 逻辑计数器混合,保证「外部一致性」(Lamport happens-before)。
B.8 Compact Node(链路压缩)
当一行的版本链超过 INDEX_TRIGGER_COUNT (=500) 个节点,会异步在链上插入一个 COMPACT 类型 TransNode,把若干历史版本压成一个完整的快照点,缩短后续读取路径。
B.9 ELR · Early Lock Release
OceanBase 的优化:commit 流程内一旦 redo 多数派落盘,立即释放行锁 (甚至先于 callback 改写完所有 TransNode)。ObMvccRow 上的 max_elr_trans_version_ 记录了 ELR 后的最大版本,避免脏读。
「为什么写不阻塞读?」三个理由:(1) 写新增链头 + 旧版本仍可见;(2) 读不申请行锁,只做版本比较;(3) latch_ 只在挂链瞬间持有,纳秒级,不与读冲突(读通过 RCU/Hazard 安全遍历)。
附 C · 向量索引层架构深入
C.1 一个向量列在内核里到底有几张「表」?
看似一条 VECTOR INDEX idx_vec (emb) WITH (TYPE=hnsw),seekdb 实际在内核创建 5 张辅助 tablet :
tablet 角色 形态
data_tablet 原表行存(用户表) 普通行存 SSTable
rowkey_vid rowkey → vid 映射 普通二级索引
vid_rowkey (delta_buffer) vid → 向量 + 行键的增量缓冲 "delta" 部分,对应 Delta HNSW 的真身
vbitmap (vid bitmap) 已删除/活跃 vid 位图 支持过滤推下 + 删除标记
snapshot "快照"索引数据 序列化后的 Snapshot HNSW 图结构,写到 SSTable
这 5 张表绑定在一起,由 ObPluginVectorIndexAdaptor 统一管理,对外只暴露「一个向量索引」概念。
C.2 Adaptor 把 5 张表黏成「两级 HNSW」
flowchart TB
USER["用户表 data_tablet"] -->|insert| RWK["rowkey_vid 分配 vid"]
RWK -->|新 vid + emb| DELTA["vid_rowkey delta_buffer = Delta HNSW (内存图)"]
DELTA -. 周期 rotate .-> SNAP["snapshot = Snapshot HNSW (持久化图)"]
DEL["DELETE 行"] --> BMAP["vbitmap 标 vid 失效"]
subgraph QUERY["查询"]
ANN["ANN(query_vec, K)"] --> SAD["search delta HNSW"]
ANN --> SAS["search snapshot HNSW"]
SAD --> FBM["filter by vbitmap 剔除已删 vid"]
SAS --> FBM
FBM --> MRG["合并 K + 距离重排"]
MRG --> LOOK["按 vid 回 rowkey_vid → 回 data_tablet 取行"]
end
C.3 关键对象
对象 作用
ObPluginVectorIndexService 租户级单例 (MTL);管理本租户全部向量索引的生命周期
ObPluginVectorIndexMgr per-LS 管理器;持有 tablet_id → Adapter 的注册表
ObPluginVectorIndexAdaptor 一个向量索引实例;持有 5 个 tablet_id、delta/snapshot 句柄、写锁、引用计数
ObVectorQueryAdaptorResultContext 一次 ANN 查询的 ctx:topK / ef_search / 过滤 bitmap / 结果数组
ObVsagMemContext VSAG 内存上下文,托管所有 HNSW 节点分配
ObVsagSearchAlloc 查询期独立 allocator,避免污染主分配器
ObHnswBitmapFilter 实现 VSAG 的 FilterInterface,把 SQL 标量谓词转成 bitmap 在 ANN 内部过滤
ObPluginVectorIndexScheduler 后台调度:delta 满 → snapshot 切换、HNSW 重建、合并
ObHnswEmbedMgr 位于 storage/ddl,把 HNSW 序列化字节流写成 SSTable 微块
C.4 写路径:从 INSERT 到 Delta HNSW 出现一个点
sequenceDiagram
participant U as INSERT
participant TX as Tx + Memtable
participant PALF as PALF
participant CS as Change Stream
participant AD as VectorIndexAdaptor
participant DH as Delta HNSW
U->>TX: 写 data_tablet + rowkey_vid (vid 自增)
TX->>PALF: redo append
PALF-->>U: commit OK (返回客户端)
PALF->>CS: ObCSFetcher 拉 redo
CS->>AD: AsyncIndex 插件投递 (vid, emb_vector)
AD->>DH: add_node(vid, vec) VSAG hnsw_index->add()
Note over DH: 该 vid 立刻对 ANN 可见
C.5 Delta → Snapshot 切换 (相当于向量索引的 minor freeze)
触发:delta 节点数超阈值 / 内存超阈值 / 主动调用 REFRESH INDEX。
Scheduler 把当前 delta 「冻结」,新写入流向新的空 delta。
冻结 delta 与现有 snapshot 合并(直接增量插入或全量重建)→ 序列化 → 写 snapshot tablet 的 SSTable。
切换完成后,旧 delta 销毁,引用计数归零的 vsag 内存释放。
查询永远只扫两个索引 :无论写了多少数据,活跃 HNSW 数量恒为 2 (delta + snapshot)。这是 P99 抖动 1.1× 的根因。
C.6 查询路径:Hybrid 过滤如何下推
flowchart LR
PLAN["SQL 计划 WHERE author=42 AND MATCH(...) ORDER BY l2(emb,?)"] --> DAS["ObVectorIndexLookupOp"]
DAS --> SCALAR["1) 先评估标量谓词 得到候选 rowkey 集合"]
SCALAR --> VID["2) 转成 vid bitmap"]
VID --> FILT["3) 注入 ObHnswBitmapFilter"]
FILT --> DELTA2["4) ANN @ delta HNSW (带 filter)"]
FILT --> SNAP2["4) ANN @ snapshot HNSW (带 filter)"]
DELTA2 --> MRG2["5) 合并 top-K"]
SNAP2 --> MRG2
MRG2 --> LOK2["6) 回 rowkey_vid 取 rowkey,回主表取整行"]
关键:filter 是下推到 HNSW 图遍历内部 的,而非 ANN 出 K 后再过滤——后者会因被过滤掉太多而出现「召回不足」需要放大 ef_search 的恶性循环。
C.7 支持的算法族
类型 调用引擎 关键参数
HNSW VSAG hnsw M, ef_construction, ef_search
HNSW_BQ VSAG hnsw + binary quant 同上 + bq 量化层
IVF_FLAT seekdb 自管 nlist, nprobe;用 ObVectorKmeansCtx 训练
IVF_SQ8 同上 + 标量量化 nlist, nprobe
IVF_PQ 同上 + 乘积量化 nlist, m (子空间数), nbits
IVF 系列的桶(centroid)数据走 ObVectorIndexIvfCacheMgr + ObIvfAsyncTaskExecutor 异步训练与刷新。
附 D · HNSW 算法与工程实现
D.1 算法直觉:跳表 + 邻居图
HNSW (Hierarchical Navigable Small World) 把 ANN 看作「在一张图上做最短路径搜索」:
把每个向量当作图的一个节点,节点之间通过邻接边 相连;
由于 small-world 特性,从任何点出发,沿"距离 query 最近的邻居"贪心走,期望 O(log N) 步可到 query 的近邻;
为了避免一开始就被困在局部最优,引入多层 :高层稀疏(节点少、边长),用作"高速公路";底层稠密(节点全、边短),用作"精修"。
D.2 图的层次结构
flowchart TB
subgraph L2["Layer 2 · 最稀疏 (e.g. 1% 节点)"]
E["entry point"]
L2A["o"] --- L2B["o"]
E --- L2A
end
subgraph L1["Layer 1"]
L1A["o"] --- L1B["o"]
L1B --- L1C["o"]
L1A --- L1C
end
subgraph L0["Layer 0 · 全部节点 + 全部邻居 (M_max 条边)"]
A0["o"] --- B0["o"]
B0 --- C0["o"]
A0 --- D0["o"]
D0 --- C0
end
E -. 同一节点不同副本 .-> L1A
L1C -. 同一节点 .-> A0
D.3 关键参数
参数 含义 取值经验
M 每个节点保留的双向邻居上限 (高层) 16(默认),越大召回越高、内存越大
M_max0 底层 Layer 0 的邻居上限 通常 2 × M
ef_construction 建图时的候选集大小 200–512,影响构图质量
ef_search 查询时维护的候选集大小 topK 的 2–10 倍;越大召回越高、延迟越大
mL (= 1 / ln(M)) 新节点最高层的指数分布参数 由 M 决定,无需调
D.4 插入算法(伪流程)
sequenceDiagram
participant U as Insert(vec)
participant H as HNSW Index
U->>H: 随机一个最高层 l = floor(-ln(uniform()) * mL)
H->>H: 从 entry_point 在 layer L..l+1 贪心下行,每层只保留 1 个最近邻
H->>H: 在 layer l..0 上做 ef_construction 的 beam search 拿候选集
H->>H: 在候选中用启发式选 M 个邻居 (避免聚簇)
H->>H: 双向连边;若邻居超 M 上限则收缩
alt l > 当前最高层
H->>H: 更新 entry_point 为新节点
end
D.5 查询算法
从 entry_point 出发,沿 layer L → 1 贪心下行:每层用 ef=1 的 best-first 搜索,找到该层离 query 最近的点。
到达 layer 0,把上一层的最近点作为起点,做 ef_search 大小的 best-first 搜索(维护两个堆:candidates 最小堆 + W 最大堆)。
循环:从 candidates 弹出最近点 c;若 dist(c, q) > W 堆顶 → 提前停止;否则展开 c 的邻居,未访问过的点计算距离,放入 candidates 和 W。
最终返回 W 中的 top-K。
flowchart LR
Q["query vec"] --> EP["entry_point @ Top Layer"]
EP --> G1["greedy descent 逐层只保留 1 近邻"]
G1 --> L0Q["到达 Layer 0"]
L0Q --> BFS["beam search ef_search 大小候选集"]
BFS --> FLT["可选: BitmapFilter 跳过被过滤 vid"]
FLT --> OUT["top-K"]
D.6 seekdb 的工程化要点
问题 解决
动态写入下 entry_point 失效 VSAG 内部用版本号;Adapter 同时维护 delta 与 snapshot,新写入只入 delta,避免破坏 snapshot 图结构
删除 HNSW 不支持删点;用 vbitmap tablet 标记软删除,查询时 FilterInterface 过滤;Major Merge 时随 Snapshot 重建
过滤后召回坍塌 不在 ANN 出 K 后过滤,而是用 ObHnswBitmapFilter 在 best-first 搜索每次展开邻居时 就过滤;并自适应放大 ef_search
持久化 Snapshot HNSW 整图序列化为字节流,按 16KB micro block 切分,写到 snapshot tablet 的 SSTable;重启 mmap 加载
并发查询 整图只读时无锁;只有 add/refresh 期间 Adapter 持写锁,写时其他查询自动落到旧版本
内存隔离 ObVsagMemContext 把 VSAG 内部 alloc 全部托管到租户 MemoryContext,超限被熔断
距离函数 L2 / IP / Cosine 三选一;SIMD (AVX2/Neon) 内联
D.7 复杂度
插入 期望 O(log N) × M 邻居筛选
查询 期望 O(log N + ef_search × M)
内存 每点 ~ M × 4 字节 邻居指针 × 层数;典型 384 维 + M=16 ≈ 1.6KB / 点
「为什么 ef_search 调大就慢、调小召回就低?」
ef_search 是 best-first 的候选集大小:候选越大,越不容易陷入局部最优(召回↑),但每次都要计算距离并维护堆(延迟↑)。本质是图搜索的 beam width 调参。
陷阱题 「HNSW 支持范围 + 向量混合查询吗?」
HNSW 本身不支持。要么前过滤(pre-filter,先标量再 ANN,seekdb 的做法 ),要么后过滤(post-filter,召回多了再裁),要么用 IVF + 倒排(部分场景)。seekdb 把过滤位图传给 FilterInterface,所以是带 filter 的图搜索 。
附 E · Change Stream 正确性与背压
E.1 为什么 Fetcher 是单线程
PALF 是顺序日志。索引构建必须保证同一行的事件按顺序消费 :先 INSERT 后 UPDATE 必须按这个顺序进入 HNSW,否则最终状态错。所以 Fetcher 单线程顺序拉日志,并按 tx_id 聚合,只在事务 commit 时整事务下推。
E.2 同事务多 DML 的顺序
每个 DML 携带 stmt_seq_no + row_seq_no。
Dispatcher 切片到 worker 时,按 heap_pk 哈希 选 worker —— 同一主键永远落到同一 worker,保证同行多事件顺序执行;不同行允许并行(吞吐源)。
E.3 Rollback to Savepoint 的正确性
flowchart LR
TX["事务 T"] --> S1["INSERT a seq=10"]
S1 --> SP["SAVEPOINT sp1"]
SP --> S2["UPDATE b seq=20"]
S2 --> S3["DELETE a seq=30"]
S3 --> RB["ROLLBACK TO sp1 产生 rollback 区间"]
RB --> S4["COMMIT"]
S4 --> CS2["Change Stream 看到所有 redo + rollback 区间"]
CS2 --> SEL["对 seq_no 命中 rollback 区间的行跳过"]
SEL --> ONLY["最终只把 INSERT a 投到 HNSW"]
这保证索引不会出现「曾发生又被回滚」的脏数据。
E.4 背压
Fetcher → Dispatcher 固定容量 ring buffer;满则 Fetcher 暂停拉 PALF(PALF 自带保留期,不丢日志)
Dispatcher → Worker LinkQueue 自适应线程数;Worker 慢则 Dispatcher 减速
Worker → 索引 VSAG 内存超限触发熔断 → 主动 freeze delta → 切 snapshot,腾空内存
E.5 refresh_scn 与可见性
Fetcher 周期性把 refresh_scn 写入全局状态表。
查询路径调用 refresh_index() = 等待 refresh_scn ≥ 用户期望 snapshot。
嵌入式模式下 pyseekdb 的 refresh_index() 就是这个开关。
「索引落后数据多少?」
正常 ~ ms 级;高写入压力下取决于 worker 数与向量维度。但不会无限落后 ——内存到上限就强制 freeze + snapshot 切换,把队列腾空。
附 F · 写路径全链路时序
sequenceDiagram
participant C as Client
participant OB as observer / OMT worker
participant SQL as ObSql Parser-Optimizer
participant DAS as DAS Insert Op
participant TX as TransService
participant MT as Memtable QueryEngine
participant PALF as PALF Paxos
participant CS as ChangeStream
participant VEC as Vector Adaptor
C->>OB: INSERT INTO t VALUES emb
OB->>SQL: parse + plan
SQL->>DAS: das_insert_op
DAS->>TX: trans_begin / join
DAS->>TX: get_store_ctx snapshot_scn
DAS->>MT: mvcc_write key,val
MT->>MT: 找/建 ObMvccRow
MT->>MT: 抢 latch_, 检查 WW 冲突
MT->>MT: 挂 ObMvccTransNode scn=MAX
MT->>TX: callback 入 ObMemtableCtx
TX->>PALF: append redo
PALF->>PALF: 多数派落盘
PALF-->>TX: on_commit scn=X
TX->>MT: 回填 callback 的 trans_version_ = X
TX-->>OB: end_trans_cb
OB-->>C: OK Packet ACK
Note over PALF,VEC: 以下完全异步,不阻塞客户端
PALF->>CS: ObCSFetcher 顺序拉 redo
CS->>CS: 按 tx_id 组装 + 等 commit
CS->>VEC: Dispatcher → Worker → AsyncIndex Plugin
VEC->>VEC: add_node vid,vec → Delta HNSW
F.1 关键耗时分解
阶段 典型耗时 说明
SQL 解析 + plan 10–100 μs 命中 plan cache 可 < 5 μs
mvcc_write 挂链 1–5 μs latch 自旋 + 链头比较
PALF append 多数派 1–5 ms 跨节点 RTT 主导
客户端 ACK 1–6 ms = 上面之和(SLA 关键)
异步索引落地 1–几十 ms 取决于负载与维度,不阻塞客户端
F.2 嵌入式模式下的差异
无网络栈,无 RPC:客户端 = 进程本身。
PALF 仍在跑(写本地磁盘),但只单副本,"多数派" = 自己。append 耗时 ~ 几百 μs。
Change Stream 同样运行,对应用透明。
附 G · 常见问题回答
Q1 · seekdb 怎么做到写入和查询并发还不抖动?
写不碰索引 :DML 只写 memtable + PALF redo;索引由 Change Stream 异步消费。
索引层数恒为 2 :Delta + Snapshot;查询永远只对两个 HNSW 做 ANN,不会随写入累积膨胀。
读不加行锁 :MVCC 用版本链 + SCN 比较解决可见性,避免读写竞争。
资源隔离 :OMT 给每个租户独立 worker pool + memstore 配额。
Q2 · 刚 INSERT 完为什么 APPROXIMATE 查询有可能查不到?
因为向量索引是异步构建的。解法:
显式 refresh_index()(pyseekdb 提供);
SQL 加 hint 等索引追平;
小表场景直接走精确扫描,不走 ANN。
Q3 · FORK DATABASE 怎么做到秒级、零拷贝?
RootServer 在 schema 层创建新 db + 新 table_id + 新 tablet_id(纯元数据,秒级)。
每个新 tablet 标记 COW,引用原 tablet 的 SSTable 宏块,仅引用计数 +1 ,不复制数据。
沙箱写入只写沙箱自己的 memtable / SSTable。读时融合「共享基线 + 沙箱增量」。
MERGE 按 STRATEGY 解冲突(FAIL/THEIRS/OURS)打回主库;DROP 直接释放引用,引用计数归零时回收宏块。
Q4 · DELETE 一个向量,HNSW 里这点为什么不能立刻拿掉?
HNSW 是双向邻居图,删点会让邻居出现悬挂边,破坏 small-world 性质。所以 seekdb 用 tombstone :
DELETE 时写一个 DELETE TransNode + 把 vid 设入 vbitmap ;查询时 FilterInterface 跳过;
只有 Snapshot 重建 / Major Merge 时才物理去除节点。
Q5 · HNSW 的 M 调大有什么后果?
召回↑(每点邻居更多,更难陷入局部最优);
内存↑(线性增长);
构图时间↑(每次插入更新更多邻居);
查询延迟略↑(展开邻居更多次距离计算)。
经验:M=16 通用够用;高维稀疏数据可调到 32–48。
Q6 · Hybrid 查询里标量过滤掉 90%,ANN 还能召回吗?
后过滤 (post-filter) 先 ANN 取 topK,再过滤——容易召回崩溃(K 个候选可能全被过滤掉)
seekdb 做法 (内嵌过滤) 把谓词转 bitmap 注入 HNSW FilterInterface;图搜索每次邻居展开时即过滤;自适应放大 ef_search 直至找到 K 个真正有效结果
因此即便选择率 ≤ 10%,召回率依然能保持。
Q7 · 事务 commit 之后,follower 还没重放完,从 follower 读会怎样?
读请求要求 snapshot_scn 已被本节点重放到(applyservice/replayservice 维护进度)。
若 follower 没追上:(a) 在 follower 上等到追平再读,(b) 路由器把请求重定向到 leader。具体由读策略与 ObProxy 决定。
强一致读总是从 leader / 多数派读。
Q8 · seekdb 比 Milvus / ES 在 Streaming 场景快的本质?
索引段数不发散 :Milvus 每次刷盘产生一个 segment,写久了几百段,并发查询时 N×M 段 × 线程争抢 CPU;seekdb 索引数永远是 2。
写路径与索引解耦 :seekdb 写 commit 不等索引;ES 的 refresh 是同步阻塞。
MVCC 不抢锁 :高并发下读完全无锁;ES 的 segment 锁、Milvus 的 growing segment lock 都会成为瓶颈。
同一存储 + 同一执行器 :标量、向量、全文用同一套 SQL 计划下推到存储,比客户端 N+1 拼接快一个数量级。
Q9 · GTS 挂了集群还能写吗?
GTS leader 故障 → 触发 LS-1 选主,新 leader 接管(PALF 保证);典型 RTO < 8s。
切主期间事务拿不到 commit_version,会等待;不会读到旧版本(SCN 全局单调)。
容灾架构中可启用 standby_timestamp_service 减少 RTO。
Q10 · 一条 SQL 同时含 vector + fulltext + scalar 怎么执行?
Optimizer 生成一个 plan :
标量索引/范围查询先产生候选行集合 → 转 bitmap;
把 bitmap 注入 ANN 的 FilterInterface(前过滤),向量索引出 topK;
同时倒排表按词命中产生 doc_id 集合;
执行器内部按 SQL ORDER BY 合并打分(向量距离 + BM25 + 标量 score),最终 top-K 回主表取整行。
关键:所有过滤都在存储/索引层完成,结果集没有客户端拼接 ,这是性能/正确性优势的核心。
你怎么验证一致性?答:(1) Major Merge 时做全局校验和 (column_checksum) 对比三副本;(2) ddl_checksum 保证 schema 变更后数据不丢;(3) PALF 自带 Paxos 多数派认定 commit;(4) ObLockWaitMgr + 分布式死锁检测器保证并发正确。