蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— 表结构设计:从TB构造器到MongoDB分片

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】—— 表结构设计:从TB构造器到MongoDB分片

在蓝鲸 CMDB 的存储层设计中,有一套独特的数据表抽象体系。它以 "TB 前缀" 命名函数族,提供类型安全的表对象获取方式;同时支持 "分片表" 机制,将自定义模型实例按业务隔离存储。

本文从源码出发,深度剖析这套设计的全貌。

src/storage/driver/mongodb/table.go     ← TB 构造器函数族(25个)
src/storage/driver/mongodb/monogdb.go   ← Table() 实现 + Client() 全局单例
src/storage/dal/dal.go                  ← DB 接口定义(dal 层抽象)
src/storage/dal/types/types.go          ← Table 接口 + Find 接口定义
src/storage/dal/mongo/local/mongo.go    ← Mongo 实现(Collection/Find 结构体)
src/common/tablenames.go                ← BKTableName 常量 + 分片表函数

TB 构造器 MongoDB 分片 dal 层抽象 Table 接口 蓝鲸 CMDB

学习重点提示

核心概念

  • 理解 TB 函数族的命名规范和类型安全机制
  • 掌握 dal 层三层接口抽象:DB → Table → Find
  • 理解 MongoDB 分片表的命名规则和路由逻辑

源码必读

  • src/storage/driver/mongodb/table.go L25-164(TB 函数族完整实现)
  • src/storage/dal/mongo/local/mongo.go L46-53(L327-333)Collection 结构体
  • src/common/tablenames.go L191-240(分片表命名与判断函数)

一、三层架构总览:从 TB 到 MongoDB 的调用链路

What — 这套调用链路在 CMDB 中扮演什么角色?

蓝鲸 CMDB 的存储层采用了"三层接口抽象 + 一层工厂函数"的设计模式。最上层是 TBxxx() 系列函数(工厂函数),负责根据业务场景返回正确的 types.Table 接口实例;中间层是 dal.DBdal.Table 接口定义;最底层是 Mongo 结构体实现的 MongoDB 驱动逻辑。

Why — 为什么需要这套三层抽象?解决了什么问题?

问题一:硬编码表名容易出错

如果业务代码直接写 "cc_HostBase" 这样的字符串,敲错一个字符就会导致查询到错误的集合。TB 函数族通过函数名约束,将表名信息封装在函数内部,编译期就能发现问题。

问题二:自定义模型需要动态表名

蓝鲸 CMDB 支持用户创建自定义模型(如"服务器""机柜"等)。这些模型的实例不能共享一张表,否则数据会混乱。分片表机制根据模型 ID 动态生成表名,实现物理隔离。

没有这套抽象会发生什么?

  • 没有 TB 函数:表名硬编码,修改表名前缀需要改所有业务代码
  • 没有 dal 层抽象:无法支持 MySQL 等其他存储引擎的切换
  • 没有分片表:自定义模型实例全部存在一张表,查询性能随数据量线性下降

整体调用链路如下:

业务代码
    │
    ▼
TBHostBase()                          ← TB 构造器(table.go L32-34)
    │
    ▼
Table(common.BKTableNameBaseHost)     ← 传入表名(monogdb.go L53-55)
    │
    ▼
Client().Table(name)                  ← 获取 MongoDB 单例(monogdb.go L44-50)
    │
    ▼
&Collection{collName: "cc_HostBase"} ← 返回 Collection 指针(mongo.go L327-333)
我理解源码的意思是说

蓝鲸 CMDB 的存储层设计本质上是一个工厂模式 + 适配器模式的组合应用。TB 函数族是工厂方法,根据传入的标识返回对应 Table 实例;dal 层接口是适配器,屏蔽了底层是 MongoDB 还是 MySQL 的差异。

二、TB 构造器函数族:25 个函数的命名规范

What — TB 函数族是什么?

src/storage/driver/mongodb/table.go 中,定义了大量以 TB 为前缀的函数。注释明确说明:"TB 前缀为 Table 缩写,表示为 collection 对象类型的数据"。这类函数共有 25 个,涵盖了 CMDB 的所有核心数据表。

src/storage/driver/mongodb/table.go 第 25-29 行的注释说明:

/*
FILE:  构建数据库中的表对象
Timed: 2020年11月08日
Description:

TB 前缀: 为Table 缩写,表示为collection 对象类型的数据

注意:差异环境table 对象,可以新开一个文件来存取

*/

2.1 内置模型 TB 函数

内置模型指蓝鲸 CMDB 出厂即有的模型,包括业务、集群、模块、主机等。看源码中内置模型的 TB 函数实现:

// TBHostBase TODO
func TBHostBase() types.Table {
    return Table(common.BKTableNameBaseHost)
}

// TBApplicationBase TODO
func TBApplicationBase() types.Table {
    return Table(common.BKTableNameBaseApp)
}

// TBSetBase TODO
func TBSetBase() types.Table {
    return Table(common.BKTableNameBaseSet)
}

// TBModuleBase TODO
func TBModuleBase() types.Table {
    return Table(common.BKTableNameBaseModule)
}

可以看到,每个函数都极其简洁:调用 Table() 函数并传入对应的表名常量。这些常量定义在 src/common/tablenames.go 中。

2.2 自定义模型 TB 函数

对于自定义模型,TB 函数需要接收模型 ID 和供应商账号作为参数:

// TBModelInstance 模型实例数据表,TBInstance别名函数
func TBModelInstance(objID, supplierAccount string) types.Table {
    return Table(common.GetObjectInstTableName(objID, supplierAccount))
}

// TBInstance 模型实例数据表, TBModelInstance 别名函数
func TBInstance(objID, supplierAccount string) types.Table {
    return TBModelInstance(objID, supplierAccount)
}

这里的 GetObjectInstTableName() 函数会根据传入的参数动态生成分片表名,稍后会详细讲解。

2.3 完整 TB 函数列表

整理 src/storage/driver/mongodb/table.go 全文,25 个 TB 函数映射关系如下:

TB 函数Table 常量集合名(实际)
TBHostBase() BKTableNameBaseHost cc_HostBase
TBApplicationBase() BKTableNameBaseApp cc_ApplicationBase
TBSetBase() BKTableNameBaseSet cc_SetBase
TBModuleBase() BKTableNameBaseModule cc_ModuleBase
TBCloudArea() BKTableNameBasePlat cc_PlatBase
TBProcess() BKTableNameBaseProcess cc_Process
TBAuditLog() BKTableNameAuditLog cc_AuditLog
TBInstanceAsst() 动态计算 cc_InstAsst_xxx_yyy_zzz(分片表)

TB 函数族设计原则

  • 命名规范:TB + 业务实体名称(Host/Application/Module 等)+ Base/Instance/Asst 等后缀
  • 返回值统一:所有函数返回 types.Table 接口,不暴露具体实现
  • 表名集中管理:表名常量统一在 tablenames.go 中定义,便于全局修改
  • 别名兼容:如 TBInstanceTBModelInstance 的别名,兼容历史代码

三、dal 层接口抽象:DB / Table / Find 三层设计

What — dal 层是什么?

dal 是 Data Access Layer 的缩写,即数据访问层。蓝鲸 CMDB 的 dal 层定义了一套标准接口,抽象了所有数据库操作。这套接口包含三个核心接口:DB(数据库连接)、Table(集合操作)、Find(查询操作)。

3.1 DB 接口:数据库连接抽象

src/storage/dal/dal.go 第 29-66 行定义的 DB 接口:

// DB db operation interface
type DB interface {
    // Table collection 操作
    Table(collection string) types.Table

    // NextSequence 获取新序列号(非事务)
    NextSequence(ctx context.Context, sequenceName string) (uint64, error)

    // NextSequences 批量获取新序列号(非事务)
    NextSequences(ctx context.Context, sequenceName string, num int) ([]uint64, error)

    // Ping 健康检查
    Ping() error

    // HasTable 判断是否存在集合
    HasTable(ctx context.Context, name string) (bool, error)
    // ListTables 获取所有的表名
    ListTables(ctx context.Context) ([]string, error)
    // DropTable 移除集合
    DropTable(ctx context.Context, name string) error
    // CreateTable 创建集合
    CreateTable(ctx context.Context, name string) error
    // RenameTable 更新集合名称
    RenameTable(ctx context.Context, prevName, currName string) error

    IsDuplicatedError(error) bool
    IsNotFoundError(error) bool

    Close() error

    // CommitTransaction 提交事务
    CommitTransaction(context.Context, *metadata.TxnCapable) error
    // AbortTransaction 取消事务
    AbortTransaction(context.Context, *metadata.TxnCapable) (bool, error)

    // InitTxnManager TxnID management of initial transaction
    InitTxnManager(r redis.Client) error
}

3.2 Table 接口:集合操作抽象

Table 接口定义了针对单个集合的操作方法,包括查询、插入、更新、删除、索引管理等。看 src/storage/dal/types/types.go 第 38-89 行:

// Table TODO
type Table interface {
    // Find 查询多个并反序列化到 Result
    Find(filter Filter, opts ...*FindOpts) Find
    // AggregateOne 聚合查询
    AggregateOne(ctx context.Context, pipeline interface{}, result interface{}) error
    AggregateAll(ctx context.Context, pipeline interface{}, result interface{}, opts ...*AggregateOpts) error
    // Insert 插入数据, docs 可以为 单个数据 或者 多个数据
    Insert(ctx context.Context, docs interface{}) error
    // Update 更新数据
    Update(ctx context.Context, filter Filter, doc interface{}) error
    // Upsert update or insert data
    Upsert(ctx context.Context, filter Filter, doc interface{}) error
    // UpdateMultiModel data based on operators.
    UpdateMultiModel(ctx context.Context, filter Filter, updateModel ...ModeUpdate) error

    // Delete 删除数据
    Delete(ctx context.Context, filter Filter) error

    // CreateIndex 创建索引
    CreateIndex(ctx context.Context, index Index) error
    // BatchCreateIndexes 批量创建索引
    BatchCreateIndexes(ctx context.Context, index []Index) error

    // DropIndex 移除索引
    DropIndex(ctx context.Context, indexName string) error
    // Indexes 查询索引
    Indexes(ctx context.Context) ([]Index, error)

    // AddColumn 添加字段
    AddColumn(ctx context.Context, column string, value interface{}) error
    // RenameColumn 重命名字段
    RenameColumn(ctx context.Context, filter Filter, oldName, newColumn string) error
    // DropColumn 移除字段
    DropColumn(ctx context.Context, field string) error
    // DropColumns 根据条件移除字段
    DropColumns(ctx context.Context, filter Filter, fields []string) error

    // DropDocsColumn remove a column by the name for doc use filter
    DropDocsColumn(ctx context.Context, field string, filter Filter) error

    // Distinct 查询不同值
    Distinct(ctx context.Context, field string, filter Filter) ([]interface{}, error)

    // DeleteMany delete document, return number of documents that were deleted.
    DeleteMany(ctx context.Context, filter Filter) (uint64, error)
    // UpdateMany update document, return number of documents that were modified.
    UpdateMany(ctx context.Context, filter Filter, doc interface{}) (uint64, error)
}

3.3 Find 接口:查询链式调用抽象

Find 接口采用"链式调用"设计,允许业务代码流畅地组合多个查询条件。看 src/storage/dal/types/types.go 第 91-111 行:

// Find find operation interface
type Find interface {
    // Fields 设置查询字段
    Fields(fields ...string) Find
    // Sort 设置查询排序
    Sort(sort string) Find
    // Start 设置限制查询上标
    Start(start uint64) Find
    // Limit 设置查询数量
    Limit(limit uint64) Find
    // All 查询多个
    All(ctx context.Context, result interface{}) error
    // One 查询单个
    One(ctx context.Context, result interface{}) error
    // Count 统计数量(非事务)
    Count(ctx context.Context) (uint64, error)
    // List 查询多个, start 等于0的时候,返回满足条件的行数
    List(ctx context.Context, result interface{}) (int64, error)

    Option(opts ...*FindOpts)
}

链式调用的典型用法:

// 链式调用示例
filter := util.NewMapBuilder().EleMatch("bk_supplier_account", supplierAccount).Build()
count, err := db.Table(common.BKTableNameBaseHost).
    Find(filter).
    Fields("bk_host_innerip", "bk_host_id").
    Sort("-bk_host_id").
    Start(0).
    Limit(20).
    List(ctx, &hosts)

Why — 为什么采用三层接口设计?

可测试性:业务代码只需要依赖接口,不需要依赖具体实现。通过 mock 实现可以轻松进行单元测试。

可扩展性:如果未来需要支持 PostgreSQL 或其他数据库,只需要实现一套新的适配器,不需要修改业务代码。

职责分离:DB 接口负责连接管理、Table 接口负责集合操作、Find 接口负责查询构建,职责清晰。

没有三层抽象会发生什么?

  • 业务代码直接依赖 MongoDB 驱动,更换数据库需要重写所有业务代码
  • 无法 mock 数据库进行单元测试
  • 查询逻辑分散在业务代码中,难以统一优化

四、MongoDB 分片表:自定义模型的物理隔离策略

What — 什么是分片表?为什么需要分片表?

在蓝鲸 CMDB 中,自定义模型(如用户创建的"服务器""机柜"等模型)的实例数据,存储在分片表中。分片表的命名规则是:cc_ObjectBase_{supplierAccount}_{specifier}_{objectID}。这样每个自定义模型都有独立的物理集合,避免数据全部堆积在一张表里。

4.1 分片表命名规则

src/common/tablenames.go 第 191-207 行的分片表常量定义和函数:

// BKObjectInstShardingTablePrefix is prefix of object instance sharding table.
BKObjectInstShardingTablePrefix = BKTableNameBaseInst + "_"
// "cc_ObjectBase_"

// BKObjectInstAsstShardingTablePrefix is prefix of object instance association sharding table.
BKObjectInstAsstShardingTablePrefix = BKTableNameInstAsst + "_"
// "cc_InstAsst_"

// GetObjectInstTableName return the object instance table name in sharding mode base on
// objID and supplierAccount.
func GetObjectInstTableName(objID, supplierAccount string) string {
    return fmt.Sprintf("%s%s_%s_%s", BKObjectInstShardingTablePrefix,
        supplierAccount, TableSpecifierPublic, objID)
    // 格式: cc_ObjectBase_{supplierAccount}_pub_{objectID}
}

// GetObjectInstAsstTableName return the object instance association table name in sharding mode.
func GetObjectInstAsstTableName(objID, supplierAccount string) string {
    return fmt.Sprintf("%s%s_%s_%s", BKObjectInstAsstShardingTablePrefix,
        supplierAccount, TableSpecifierPublic, objID)
    // 格式: cc_InstAsst_{supplierAccount}_pub_{objectID}
}

4.2 分片表判断函数

分片表命名后,还需要有判断函数来识别一个表名是否是分片表。看 src/common/tablenames.go 第 222-240 行:

// IsObjectShardingTable returns if the target table is an object sharding table
func IsObjectShardingTable(tableName string) bool {
    if IsObjectInstShardingTable(tableName) {
        return true
    }
    return IsObjectInstAsstShardingTable(tableName)
}

// IsObjectInstShardingTable returns if the target table is an object instance sharding table.
func IsObjectInstShardingTable(tableName string) bool {
    return strings.HasPrefix(tableName, BKObjectInstShardingTablePrefix)
    // 检查是否以 "cc_ObjectBase_" 开头
}

// IsObjectInstAsstShardingTable returns if the target table is an object instance association sharding table.
func IsObjectInstAsstShardingTable(tableName string) bool {
    return strings.HasPrefix(tableName, BKObjectInstAsstShardingTablePrefix)
    // 检查是否以 "cc_InstAsst_" 开头
}

4.3 分片表路由重定向

src/storage/dal/mongo/local/mongo.go 第 811-818 行,有一个 redirectTable() 函数,用于处理分片表名的路由重定向:

func (c *Mongo) redirectTable(tableName string) string {
    if common.IsObjectInstShardingTable(tableName) {
        tableName = common.BKTableNameBaseInst
    } else if common.IsObjectInstAsstShardingTable(tableName) {
        tableName = common.BKTableNameInstAsst
    }
    return tableName
}

这个路由逻辑在 NextSequence()NextSequences() 方法中被调用,确保分片表也能正确获取自增 ID。

分片表 ID 生成机制

分片表共享一套 ID 生成器(cc_idgenerator)。虽然数据存在不同的物理集合中,但 ID 是全局唯一的,避免了不同分片表之间的 ID 冲突。

4.4 分片表的删除归档

蓝鲸 CMDB 支持删除归档功能,被删除的数据会记录到归档表中。看 src/common/util/table/table.go 第 27-63 行定义的归档表映射:

var delArchiveCollMap = map[string]string{
    common.BKTableNameModuleHostConfig:        common.BKTableNameDelArchive,
    common.BKTableNameBaseHost:                common.BKTableNameDelArchive,
    common.BKTableNameBaseApp:                 common.BKTableNameDelArchive,
    common.BKTableNameBaseSet:                 common.BKTableNameDelArchive,
    common.BKTableNameBaseModule:              common.BKTableNameDelArchive,
    // ...
    common.BKTableNameBaseInst:         common.BKTableNameDelArchive,
    common.BKTableNameMainlineInstance: common.BKTableNameDelArchive,
    common.BKTableNameInstAsst:         common.BKTableNameDelArchive,
    // ...
}

Why — 为什么需要删除归档?解决了什么问题?

审计追溯:配置平台的操作需要满足审计要求,删除的数据不能真的"消失",需要记录删除前的状态。

数据恢复:误删的数据可以通过归档表进行恢复。

没有删除归档会发生什么?

  • 管理员误删主机记录,无法追溯是谁删的、删了什么
  • 合规审计时无法证明数据删除前的状态
  • 误操作无法回滚

五、核心结构体源码解读

5.1 Mongo 结构体

src/storage/dal/mongo/local/mongo.go 第 46-53 行的 Mongo 结构体定义:

// Mongo TODO
type Mongo struct {
    dbc    *mongo.Client    // MongoDB 客户端连接
    dbname string           // 数据库名
    sess   mongo.Session    // 事务会话
    tm     *TxnManager      // 事务管理器
    conf   *mongoCliConf    // MongoDB 客户端配置
}

5.2 Collection 结构体

src/storage/dal/mongo/local/mongo.go 第 347-351 行的 Collection 结构体定义:

// Collection implement client.Collection interface
type Collection struct {
    collName string // 集合名
    *Mongo           // 嵌入 Mongo 结构体,复用连接等资源
}

5.3 Find 结构体

src/storage/dal/mongo/local/mongo.go 第 366-377 行的 Find 结构体定义:

// Find define a find operation
type Find struct {
    *Collection           // 嵌入 Collection,获取集合名和数据库连接

    projection map[string]int  // 投影字段(指定返回哪些字段)
    filter     types.Filter    // 查询条件
    start      int64           // 跳过的文档数(分页偏移)
    limit      int64           // 返回的文档数量(分页大小)
    sort       bson.D          // 排序规则

    option types.FindOpts      // 查询选项(如是否返回 _id)
}

5.4 Table 函数实现

src/storage/driver/mongodb/monogdb.go 第 52-55 行的 Table 函数实现:

// Table 获取操作db table的对象
func Table(name string) dbType.Table {
    return Client().Table(name)
}

它内部调用 Client() 获取全局单例,然后调用该单例的 Table() 方法。

5.5 Client 全局单例

src/storage/driver/mongodb/monogdb.go 第 35-50 行的客户端单例实现:

var (
    dbMap = make(map[string]dal.DB)  // 按 prefix 存储多个 DB 实例

    // 在并发的情况下,这里存在panic的问题
    lastInitErr   errors.CCErrorCoder
    lastConfigErr errors.CCErrorCoder
)

// Client get default error
func Client(prefix ...string) dal.DB {
    var pre string
    if len(prefix) > 0 {
        pre = prefix[0]
    }
    return dbMap[pre]
}

注意:注释中提到"在并发的情况下,这里存在 panic 的问题",说明当前实现对并发访问 dbMap 没有加锁保护。初始化应该在服务启动阶段完成,避免运行时并发写入。

六、my-idea:源码视角的设计精髓

我理解源码的意思是说

蓝鲸 CMDB 的表结构设计,本质上是工厂模式 + 接口抽象 + 分片策略的组合应用。三个设计要点可以从源码中直接读出来:

源码视角一:工厂函数封装表名

src/storage/driver/mongodb/table.go 第 32-34 行,TBHostBase() 函数的实现极其简单:

func TBHostBase() types.Table {
    return Table(common.BKTableNameBaseHost)
}

为什么这样写?因为表名 common.BKTableNameBaseHost 是常量 "cc_HostBase" 的别名。业务代码调用 TBHostBase() 时,IDE 会提示返回类型是 types.Table,而不是具体的集合名字符串。这就是类型安全。

源码视角二:接口继承实现链式调用

src/storage/dal/types/types.go 第 91-111 行,Find 接口的每个方法都返回 Find 自身:

type Find interface {
    Fields(fields ...string) Find   // 返回 Find,可继续调用
    Sort(sort string) Find          // 返回 Find,可继续调用
    Start(start uint64) Find        // 返回 Find,可继续调用
    Limit(limit uint64) Find        // 返回 Find,可继续调用
    // ...
}

src/storage/dal/mongo/local/mongo.go 第 380-434 行的实现,每个方法都返回 f(即 *Find 指针):

func (f *Find) Fields(fields ...string) types.Find {
    for _, field := range fields {
        if len(field) <= 0 {
            continue
        }
        f.projection[field] = 1
    }
    return f
}

func (f *Find) Sort(sort string) types.Find {
    // 设置排序规则
    f.sort = bson.D{}
    // ...
    return f
}

Go 语言没有继承,但通过返回自身指针实现了"链式调用"的效果。

源码视角三:分片表前缀匹配判断

src/common/tablenames.go 第 232-240 行,分片表的判断逻辑是字符串前缀匹配:

func IsObjectInstShardingTable(tableName string) bool {
    return strings.HasPrefix(tableName, BKObjectInstShardingTablePrefix)
    // BKObjectInstShardingTablePrefix = "cc_ObjectBase_"
}

这种设计简单高效,只需要一次字符串比较就能判断是否是分片表。但需要注意,如果表名格式发生变化(如添加中间层级),需要同步修改常量。

源码视角总结:3 个设计原则

  • 1 个工厂封装:TB 函数族将表名封装为类型安全的函数调用
  • 1 套接口抽象:dal 层接口定义屏蔽了底层存储引擎差异
  • 1 种分片策略:前缀匹配 + 动态表名生成实现自定义模型物理隔离

避坑提醒(源码视角):

  • 不要直接硬编码表名:业务代码应始终通过 TB 函数获取 Table 实例,而非直接写 "cc_HostBase" 字符串
  • 不要在分片表判断中使用精确匹配:表名是动态生成的,只能用前缀匹配判断
  • 不要在运行时修改 dbMap:全局单例应在启动时初始化完成

FAQ 问答(20 组)

Q1. TB 构造器的 "TB" 是什么意思?

Table Builder 的缩写,表示用于构建 Table 实例的工厂函数。src/storage/driver/mongodb/table.go 第 25 行的注释中明确说明:"TB 前缀为 Table 缩写,表示为 collection 对象类型的数据"。

Q2. 为什么需要 TB 函数族?直接写表名字符串不行吗?

可以实现类型安全、编译期检查和统一修改。如果直接写 "cc_HostBase" 字符串,敲错字符要运行时才能发现。通过 TB 函数,IDE 可以提示返回类型,编译期就能检查错误。

Q3. dal 层的"三层接口"是哪三层?

DB 接口、Table 接口、Find 接口。DB 接口(src/storage/dal/dal.go)负责数据库连接管理;Table 接口(src/storage/dal/types/types.go)负责集合级操作;Find 接口负责查询链式调用。

Q4. 为什么 Find 接口的方法都返回 Find 自身?

为了支持链式调用。src/storage/dal/mongo/local/mongo.go 第 380-434 行,每个方法的实现都是返回 f*Find 指针),这样可以连续调用多个方法,如 .Fields().Sort().Limit()

Q5. 什么是分片表?为什么需要分片表?

分片表是按自定义模型 ID 动态生成的物理集合。src/common/tablenames.go 第 201-207 行,GetObjectInstTableName() 函数生成分片表名,格式为 cc_ObjectBase_{supplierAccount}_pub_{objectID}。每个自定义模型有独立的物理集合,避免数据全部堆积在一张表里。

Q6. 分片表的命名规则是什么?

cc_ObjectBase_{供应商账号}_pub_{模型ID}。前缀常量是 BKObjectInstShardingTablePrefixsrc/common/tablenames.go L191),展开后为 "cc_ObjectBase_"

Q7. IsObjectShardingTable 函数是如何判断分片表的?

通过字符串前缀匹配。src/common/tablenames.go 第 232-240 行,IsObjectInstShardingTable() 使用 strings.HasPrefix() 判断表名是否以 "cc_ObjectBase_" 开头。

Q8. 为什么分片表需要路由重定向?

确保 ID 生成器和事务操作能够正常工作。src/storage/dal/mongo/local/mongo.go 第 811-818 行,redirectTable() 函数将分片表名重定向到基础表名(如 cc_ObjectBase),确保 ID 生成器只操作基础表。

Q9. 删除归档机制的作用是什么?

满足审计追溯和数据恢复需求。src/storage/dal/mongo/local/mongo.go 第 765-809 行,tryArchiveDeletedDoc() 函数在删除数据前,将数据插入归档表 cc_DelArchive

Q10. Mongo 结构体中的 TxnManager 是什么?

分布式事务管理器。src/storage/dal/mongo/local/mongo.go 第 46-53 行,Mongo 结构体嵌入了 *TxnManager,用于管理 MongoDB 事务上下文。

Q11. Table 接口和 Find 接口的区别是什么?

Table 是集合级操作,Find 是查询级操作。Table 接口(src/storage/dal/types/types.go L38)定义 Insert/Update/Delete 等集合级操作;Find 接口(L91)定义 Fields/Sort/Limit 等查询级操作。Find 通过 Table.Find() 方法获得。

Q12. 为什么客户端使用单例模式?

减少连接开销和资源占用。src/storage/driver/mongodb/monogdb.go 第 35-50 行,dbMap 存储全局单例。MongoDB 连接池的建立成本较高,复用连接可以提升性能。

Q13. NewMgo 函数做了哪些初始化?

创建客户端连接、设置连接池参数、初始化 ID 生成器。src/storage/dal/mongo/local/mongo.go 第 78-142 行,NewMgo() 函数解析 URI、设置连接池参数、建立连接、初始化 ID 生成器。

Q14. ID 生成器的 step 参数是什么意思?

每次获取 ID 时的增量步长。src/storage/dal/mongo/local/mongo.go 第 145-204 行,initIDGenerator() 从配置中读取 step 值,后续每次 NextSequence() 调用都会按 step 递增。

Q15. 为什么查询条件为空时需要设置为 bson.M{}

因为 MongoDB driver 要求 filter 参数不能为 nil。src/storage/dal/mongo/local/mongo.go 第 461-464 行和第 494-497 行,代码会检查 filter 是否为 nil,如果是则设置为空查询条件 bson.M{}

Q16. CreateIndex 和 BatchCreateIndexes 的区别是什么?

单索引创建 vs 批量创建。src/storage/dal/mongo/local/mongo.go 第 951-1004 行,CreateIndex() 创建单个索引,BatchCreateIndexes() 批量创建多个索引,减少与数据库的交互次数。

Q17. AddColumn 是如何实现动态添加字段的?

使用 MongoDB 的 $exists 操作符和 $set 更新。src/storage/dal/mongo/local/mongo.go 第 1069-1088 行,AddColumn() 使用 $set 操作符为不存在的字段设置默认值。

Q18. TableSpecifierPublic 常量是什么?

表示分片表中的公开标识。src/common/tablenames.go 第 201-207 行,分片表名格式为 {prefix}{supplierAccount}_{specifier}_{objID}specifier 固定为 "pub"(Public)。

Q19. IsDuplicatedError 函数是如何判断重复错误的?

通过字符串匹配 MongoDB 错误信息。src/storage/dal/mongo/local/mongo.go 第 297-320 行,函数检查错误信息是否包含 "E11000 duplicate""IndexOptionsConflict" 等关键词。

Q20. 为什么 Upsert 注释说"非原子操作"?

因为没有使用唯一索引保证,存在插入多条相同数据的理论风险。src/storage/dal/mongo/local/mongo.go 第 675-700 行,Upsert() 使用 MongoDB 的 UpdateOne 配合 Upsert: true 选项实现,存在并发竞争条件。

全篇总纲

  • 一条调用链:TB 函数 → Table() → Client().Table() → Collection 实例
  • 三层接口:DB 接口(连接管理)→ Table 接口(集合操作)→ Find 接口(链式查询)
  • 两种分片:cc_ObjectBase_{supplier}_pub_{objID}(实例表)和 cc_InstAsst_{supplier}_pub_{objID}(关联表)
  • 一个安全:通过 TB 函数族实现类型安全的表名管理

Roadmap 后续预告

  • 下篇预告:storage/dal:MongoDB+MySQL 双存储抽象层(#23)—— 深入分析 dal 层如何同时支持 MongoDB 和 MySQL 两种存储引擎
  • 相关阅读:host_controller:主机资源的原子接口层(#10)—— 看业务层如何使用 TB 函数访问主机数据
  • 扩展阅读:object_controller:通用对象资源(#11)—— 看自定义模型如何通过 TBModelInstance 访问分片表

posted @ 2026-07-14 11:37  左扬  阅读(14)  评论(0)    收藏  举报