你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:
- 了解大厂经验
- 拥有和大厂相匹配的技术等
希望看什么,评论或者私信告诉我!
一、 前言
"为什么 StarRocks 主键表可以毫秒级 UPSERT,还能保证点查性能不崩,数据不丢?"
社区里 90% 的调优踩坑,归根结底是没把 dump 和 apply 这两件事分开。
今天用"一张图 + 五条时间线"把完整读写链路拆到代码级,让你 10 分钟变成"主键表懂王"。
二、 鸟瞰图:把 4 个核心组件先印在脑子里
sql
┌────────────┐ 1 write ┌──────────────┐ 2 dump ┌──────────────┐
│ Flink/Java │ ────────► │ Memory │ ────────► │ Disk │
│ Clients │ INSERT/ │ DeltaBuffer │ Mem→Disk │ Delta RS * │
└────────────┘ UPDATE/ │ (跳表+索引) │ │ (尚未合并) │
DELETE └──────────────┘ └──────────────┘
│
│ 3 apply
▼
┌────────────┐ 4 read ┌──────────────┐ ┌──────────────┐
│ SQL │ ◄──────── │ Index │ ◄──────── │ Base RS │
│ SELECT │ │ PrimaryKey │ │ (已合并) │
└────────────┘ │ + DelVec │ └──────────────┘
└──────────────┘
一句话注解:
写只碰 DeltaBuffer → dump 只落盘不合并 → apply 才真正把 Delta 合并进 Base → 读永远三路归并。
把这张图截屏当桌面,后面所有概念都能一一映射回来。
2 五条时间线:写 → dump → apply → 读 → 后台维护
2.1 写入:内存里完成 UPSERT,不到 1 ms
| 步骤 | 动作 | 并发保证 |
|---|---|---|
| ① Write Ahead Log | 追加写 _wal/xxx.log |
崩溃后可重放 |
| ② DeltaBuffer 插入 | 跳表(skiplist)按 pk 排序,同 key 覆盖 | 行级锁仅怼跳表,无 IO |
| ③ 更新 PK Index | 内存 B+ 树直接覆盖 RowLocation | 无锁 CAS |
| ④ 返回客户端 | 纯内存,延迟 P99 < 1 ms | --- |
关键点:
- 跳表只保留 最新版本,天然 UPSERT。
- Base 数据文件零写入,因此没有随机写放大。
2.2 dump:内存 → 磁盘,只写不合并
触发三件套:
- 时间间隔:
pk_dump_interval_seconds(默认 1 h)。 - 内存水位:
update_memory_limit_percent30%。 - 文件个数:Delta Rowset ≥ 1000。
内部三步:
- 把跳表顺序扫一遍 → 按主键排好序的列存块(含 DELETE 标记)。
- 写临时文件
.dat/.idx→ 原子 rename 成DeltaRowsetID。 - 释放整块 DeltaBuffer,内存瞬间掉回 0。
结果:
- 查询需要 Union 所有 Delta RS + 残留内存。
- 不会去重 Base,所以 dump 越快,文件越碎,读放大越高。
2.3 apply:磁盘 Delta + Base → 新 Base,真正合并
StarRocks 里叫 update compaction 。
触发阈值:
- 总 Delta 大小 ≥ 256 MB(
update_compaction_size_threshold)。 - Delta 文件数 ≥ 1000。
- 手动
ADMIN COMPACT TABLE tbl;
四步完成:
- 选集:挑一个 Base + 若干 Delta(主键区间对齐)。
- 多路归并:顺序扫描,同 key 只保留最新一条;遇到 DELETE 就丢弃。
- 写新 Base :全新
.dat/.idx/.col/.del四件套。 - 原子切换:元数据版本号 +1,老文件引用计数减 1,GC 线程 5 min 后物理删除。
副作用:
- CPU 密集 + 读放大(要读旧 Base),但 查询无阻塞(快照隔离)。
2.4 读取:点查 / 范围扫都靠"三路归并"
可见性顺序(从新到旧):
内存 DeltaBuffer → 磁盘 Delta RS → Base RS
点查流程:
- PK Index 先定位 <RowSetId, RowId>。
- 按类型取行:
- DeltaBuffer → 直接返回。
- Delta RS → 读主键索引页解析。
- Base RS → 先查 DelVec,bit=1 表示已删,否则返回。
范围扫:对三路建迭代器 → 小顶堆归并 → 输出最新版本。
性能关键:
- PK Index 全内存,点查 O(logN) 毫秒级。
- Delta RS 越多,堆归并层数越高 → 范围查询 CPU 飙升;因此需要 apply。
2.5 后台维护:DelVec / GC / PIndex 重建
- DelVec
每份 Base 一份位图,apply 时把删除标记合并进新 Base,旧 DelVec 直接扔。 - GC
引用计数为 0 后延迟 5 min 删除,保证正在跑的查询快照不踩空。 - PK Index 重建
老版本:BE 重启要扫 全量 Base + Delta 重建 B+ 树,10 亿行≈5 min。
3.1+:支持 持久化 pindex 快照 ,重启增量恢复,秒级。
三、 dump vs apply 对照表:别再混淆!
| 误区 | 真相 |
|---|---|
| dump 就是合并 | ❌ 只落盘,不去重 Base |
| dump 越频繁越好 | ❌ 文件更碎,读放大,IOPS 爆炸 |
| apply 会锁表 | ❌ 快照隔离,读写无锁 |
| DelVec 会无限膨胀 | ❌ 每次 apply 生成新 Base,旧位图丢弃 |
| 主键表写放大严重 | ❌ 写只进内存,零随机写 Base |
四、 一张时序图再串一遍(文字版)
ini
时间轴 ──►
| 写入 ─┐ ┌─ dump ─┐ ┌─ apply ─┐
| │ DeltaBuffer │ 新 Delta RS │ 新 Base RS
| key=1 │ v1 → v2 → v3 │ v3 落盘 │ v3 进 Base
| key=2 │ delete │ delete 标记落盘 │ 丢弃 key=2
| 查询 │ 看 v3 │ 看 v3 │ 仍看 v3(无锁切换)
五、 总结
StarRocks 主键表 = 内存 DeltaBuffer + 磁盘 Delta/Base + 内存 PKIndex + DelVec 四件套:
写只进内存,dump 不落基线,apply 真正合并,读三路归并,全程无锁。
把 dump(落盘) vs apply(合并) 这条分界线刻进脑子,
以后任何"UPSERT 延迟高 / 点查毛刺 / 磁盘 IOPS 爆炸"的报警,
你都能 30 秒内说出根因和参数。