DiceDB 多线程分片架构设计解析:Store 抽象、Shard 管理与跨分片命令协调(2024-08-19 架构讨论纪要)
2026/9/16 6:52:35 网站建设 项目流程

DiceDB 多线程分片架构设计解析:Store 抽象、Shard 管理与跨分片命令协调(2024-08-19 架构讨论纪要)

【免费下载链接】dicedbOpen-source, low-latency key/value engine built on Valkey with query subscriptions and hierarchical storage tiers.项目地址: https://gitcode.com/GitHub_Trending/dic/dicedb

导读:本文基于仓库 docs/src/content/updates/2024-08-19.md 这一份架构设计讨论纪要展开,它是 DiceDB 从单线程迈向多线程、从单一 Store 迈向多分片架构的关键设计节点。文中记录了关注点分离(Separation of Concerns)、IOLayer 协调策略、"Store 保持简单"原则以及一整套分片化改造任务清单。读完本文,你将理解 DiceDB 的分片模型、跨分片命令(MGET/MSET)与 QWATCH 的协调方案,以及这些设计在当前仓库源码中的落地形态。

一、文档背景:一次决定架构走向的设计讨论

2024-08-19.md并不是一篇完整的发布日志,而是一次架构会议的设计纪要(以 "Discussed" 开头)。它讨论的核心问题是:当 DiceDB 从单 Store 演进为多 Shard(分片)+ 多线程架构时,命令执行、数据存储与订阅通知三层之间应该如何切分职责

结合仓库现状看,这次讨论的直接产物是internal/shardinternal/shardmanagerinternal/shardthread三个包的出现,以及internal/store中 Store 抽象的重构。因此这篇纪要可视为理解 DiceDB 当前多线程分片架构的"设计蓝图"。

二、核心议题一:Keep Store Dumb —— 让存储层保持简单

纪要中提出了一条贯穿始终的原则:

Keep store dumb. All ops on store as atomic as possible. Add complexity to IOLayer / Coordinator in future if optimisations are needed in future.

即:Store 只做最原子的数据操作,所有跨分片的编排复杂度都上移给 IOLayer / Coordinator。这是典型的分层设计思想——先保证正确性,再谈优化。

从当前源码可以看到这一原则的落地:

  • internal/store/store.go 中的Store结构体职责非常收敛:持有数据表store、过期表expires、键计数numKeys、命令监听通道cmdWatchChan和淘汰策略evictionStrategy,并通过ShardID标识自己属于哪个分片;
  • Store 对外暴露的操作接口基本限定在Put(store.go)、PutAllGet(store.go)、GetAll(store.go)、Del(store.go)等原子读写方法,与纪要中"Limit Store operations to Put / Get / Delete & Scan"的任务方向一致;
  • 数据表底层采用sync.Map封装的RegMap(store.go),避免在热点路径上引入显式Mutex,这与纪要中"Removing all Mutex from Store"的目标相符。

也就是说,"Store 保持简单"意味着:像 MGET/MSET 这类跨分片命令,绝不能在单个 Store 内完成全部逻辑,而必须由上层协调

三、核心议题二:Shard 与 ShardManager —— 分片模型如何落地

纪要讨论的核心前提是数据会被拆分到多个分片。当前仓库中分片模型由三层构成:

组件文件职责
Shardinternal/shard/main.go极简结构体,仅包含ID与一个ShardThread引用
ShardThreadinternal/shardthread/main.go每个分片独立的执行线程,持有自己的Store与 cron 周期
ShardManagerinternal/shardmanager/main.go管理全部Shard,负责生命周期与 key 路由

关键实现细节:

  • key 路由ShardManager.GetShardForKey使用xxhash.Sum64String(key) % shardCount将任意 key 稳定映射到某个分片(shardmanager/main.go)。这决定了跨分片命令的本质难点:一个命令涉及多个 key,而这些 key 可能落在不同分片上
  • 独立 Store:每个ShardThread通过dstore.NewStore(nil, evictionStrategy, id)创建自己专属的Store(shardthread/main.go),分片之间数据天然隔离;
  • 后台任务ShardThread.Startconfig.ShardCronFrequency为周期运行 cron 任务(如DeleteExpiredKeys清理过期键),并响应 context 取消做清理(shardthread/main.go);
  • 生命周期管理ShardManager.Run监听 SIGINT/SIGTERM 与父 context,统一启停所有分片 goroutine(shardmanager/main.go)。

这套结构与纪要中"Skeleton for multithreaded mode"的任务直接对应——每个分片一个线程、一份 Store,为后续多线程执行打下骨架。

四、核心议题三:跨分片命令与事务 —— 需要 Coordinator 抽象层

纪要明确指出了一个设计缺口:

Transactional commands like MGET/MSET - We need an abstraction layer outside shards that can manage the transaction across the shards.

即 MGET、MSET 这类命令的 key 分散在多个分片,需要分片之外的抽象层(Coordinator)来跨分片管理事务与一致性。纪要还给出了后续任务:"Extract Coordinator from IO Tasks"。

从当前源码可以看到相关的过渡性设计。在 internal/eval/execute.go 中有一段关键注释:

dealing with store object is not recommended for all commands. These operations are specialised for the commands which requires transferring data across multiple shards. e.g. COPY, RENAME, PFMERGE

也就是说,部分跨分片命令通过StoreObjectEval(在 Store 层面取对象、评估、修改、写回)来处理,这正是"Coordinator 抽象层"尚未完全成型前,为跨分片操作保留的特殊通道。

另外在命令执行入口 internal/cmd/cmds.go 中,Cmd.Execute通过CommandRegistry查找CommandMeta并调用c.Meta.Execute(c, sm)——注意它接收的是*shardmanager.ShardManager而非单个 Store,说明命令层天然被设计为"面向多分片"执行,与纪要中"抽象层位于分片之外"的思路一致。

五、核心议题四:QWATCH 的扇出 —— 模式跨分片时的订阅协调

纪要对 QWATCH 的讨论非常具体:

Qwatch: each io thread launches a qwatch command, this fans out to every shard. Each shard now maintains records for which io threads are listening to which queries.

moar thoughts - the watchlist can be maintained at the said coordinator level.

问题的本质是:QWATCH 监听的是 key 模式(pattern)而非单个 key,而符合模式的 key 可能分布在所有分片上。纪要给出的两种方案:

  1. 扇出(Fanout)方案:每个 IO 线程发起 QWATCH 命令后,把查询扇出到每个分片,每个分片记录"哪些 IO 线程在监听哪些查询";
  2. Coordinator 集中维护方案:watchlist 统一维护在 Coordinator 层。

从当前仓库的 internal/server/ironhawk/watch_manager.go 看,订阅关系目前由三层映射维护:

  • keyFPMapkey → 命令指纹集合,记录某个 key 被哪些查询订阅;
  • fpClientMap命令指纹 → 客户端 ID 集合,记录某个查询被哪些客户端订阅;
  • fpCmdMap命令指纹 → 原始命令,当数据变化时需要重放该命令获取最新结果。

命令指纹由 internal/cmd/cmds.go 中的farm.Fingerprint64([]byte(c.String()))计算——指纹化的意义正是让"同一查询"在不同位置可被唯一识别与去重。而NotifyWatchers(watch_manager.go)在数据变更后重新执行订阅命令并推送结果,实现了"查询结果实时刷新"。

关于 QWATCH 命令本身的语法与行为,可参见 docs/src/content/docs/QWATCH.md,它支持SELECT $key, $value WHERE ... ORDER BY ... LIMIT n的 DSQL 语法。

六、核心议题五:IOLayer 的协调策略与一致性

纪要对 IOLayer 提出了一个待定问题:

IOLayer - Should it wait for all shards to complete the tasks, proceed only after all shards have responded.

IOLayer 是否应该等待所有分片完成任务后再响应客户端。这是一个典型的"强一致 vs 低延迟"权衡:

  • 等待全部 shard 响应:能保证结果一致(尤其对 MGET/MSET 这类跨分片命令),但延迟取决于最慢的分片;
  • 部分响应即返回:延迟更低,但可能返回不完整或中间状态的数据。

纪要同时记下了"Fanout commands that require consistency"作为后续任务,说明需要一致性的扇出命令(如跨分片事务)必须协调好聚合时机

从 internal/server/ironhawk/iothread.go 可以看到 IOLayer 的雏形:IOThread.Start循环接收wire.Command,封装为cmd.Cmd后调用_c.Execute(shardManager)执行,再依据命令是否以WATCH/UNWATCH结尾,交由WatchManager处理或直接回包。这个"IO 线程执行 → 分片路由 → 订阅管理"的链路,正是纪要中 IOLayer 与 Coordinator 职责分工的早期实现。

七、后续任务清单(纪要原文要点)

2024-08-19.md最后列出的 next step 任务,完整记录了那次讨论确定的演进方向:

任务负责人(纪要)对应现状/源码线索
Remove locks and provide atomic operationsStore 底层使用sync.Map(store.go)
Address review comments and merge Store abstraction refactorYashinternal/store已重构为独立包
Watch to move out of store / implement a scan operator in storeJyotinderWatch 逻辑位于 internal/server/ironhawk/watch_manager.go,已从 Store 移出
Limit Store operations to Put / Get / Delete & ScanPratikStore 当前核心方法即Put/Get/GetAll/Del(store.go)
Move from unsafe pointer to empty struct/interface, use genericsAshwin KulkarniStore 数据表为泛型接口common.ITable[string, *object.Obj]
Removing all Mutex from Store after shards and channels createdsoumyaRegMap基于sync.Map,无显式 Mutex
Skeleton for multithreaded modeYashinternal/shardthread+internal/shardmanager已落地
Extract Coordinator from IO TasksGaurav跨分片命令暂通过StoreObjectEval通道处理(execute.go)
Qwatch fanout & per-shard listening recordsJyotinderWatchManager 三层映射(watch_manager.go)
Fanout commands that require consistencysoumya一致性聚合策略仍在演进

这些任务中,多线程分片骨架、Store 抽象、Watch 移出 Store三项在现有代码中已有清晰落地,其余(Coordinator 提取、一致性扇出)属于持续演进中的设计方向。

八、实践启示:如何基于此设计使用 DiceDB

对于使用 DiceDB 的开发者,这份纪要的价值在于理解其架构行为边界:

  1. 单 key 操作(SET/GET/DEL):由GetShardForKey哈希路由到唯一分片,天然原子、无跨分片开销;
  2. 多 key 命令(MGET/MSET):涉及多个分片,属于纪要讨论的"需要协调层"的命令类别,使用时应关注其跨分片语义与一致性保证;
  3. QWATCH 模式订阅:监听的是跨分片的 key 模式,由 WatchManager 统一维护订阅关系并在数据变更时重放查询(参见 QWATCH.md 的 DSQL 语法与实时排行榜示例);
  4. 过期清理:由每个ShardThread的 cron 任务独立执行(shardthread/main.go),因此不同分片的过期删除节奏是并行、独立的。

九、总结

2024-08-19.md虽然篇幅简短,却浓缩了 DiceDB 架构演进中最关键的一次职责切分决策:Store 保持原子与简单、分片按哈希隔离、跨分片命令交由上层协调、QWATCH 以扇出方式跨分片监听。透过当前仓库的internal/shardinternal/shardmanagerinternal/shardthreadinternal/storeinternal/server/ironhawk等实现,可以看到这份纪要并非纸上谈兵——大部分设计已经转化为可运行的代码骨架,为后续的多线程执行、Coordinator 抽象与一致性扇出提供了清晰的演进起点。

【免费下载链接】dicedbOpen-source, low-latency key/value engine built on Valkey with query subscriptions and hierarchical storage tiers.项目地址: https://gitcode.com/GitHub_Trending/dic/dicedb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询