欢迎光临
我们一直在努力

09-第9章-数据库封装与索引设计

第9章:数据库封装与索引设计

9.1 数据库封装层设计

9.1.1 封装目标

数据库封装层的目标:

  • 简化操作:提供简洁的 API
  • 类型安全:使用 Go 类型系统
  • 错误处理:统一的错误处理策略
  • 性能优化:内置批量操作和缓存
  • 功能扩展:加密、备份、导入导出
  • 9.1.2 核心封装结构

    // database/database.go:16-20
    var Table *engine.Table
    var AuthTable *engine.Table

    使用全局变量的原因:

    • 简化访问,无需传递表实例
    • 保证全局唯一
    • 便于测试和 mock

    9.2 数据库初始化

    9.2.1 初始化流程

    // database/database.go:23-162
    func Init(dbPath string, useEncryption bool, encryptionKey, algorithm string) error {
    // 1. 确保数据库目录存在
    if err := os.MkdirAll(dbPath, 0755); err != nil {
    return fmt.Errorf("failed to create database directory: %v", err)
    }

    // 2. 获取场景配置
    var dbScenario string
    cfgMgr := config.GetConfigManager()
    if cfgMgr != nil && cfgMgr.GetConfig() != nil {
    dbScenario = cfgMgr.GetConfig().DBScenario
    } else {
    dbScenario = storage.ScenarioEdge
    }

    // 3. 设置存储配置
    scenarioOptions := storage.GetConfigManager().GetScenarioOptions(dbScenario)
    storageConfig := storage.Config{
    WriteBuffer: scenarioOptions.WriteBuffer,
    OpenFilesCacheCapacity: scenarioOptions.OpenFilesCacheCapacity,
    BlockCacheCapacity: scenarioOptions.BlockCacheCapacity,
    Compression: scenarioOptions.Compression,
    }
    storage.SetConfig(storageConfig)

    // 4. 打开数据库(支持加密)
    if useEncryption {
    // 加密数据库初始化
    masterKey := make([]byte, 32)
    copy(masterKey, []byte(encryptionKey))
    for i := len(encryptionKey); i < 32; i++ {
    masterKey[i] = 0
    }
    encryptConfig := &storage.EncryptionConfig{
    Enabled: true,
    Algorithm: algorithm,
    MasterKey: masterKey,
    }
    _, err = storage.GetDBManager().OpenDBWithEncryption(dbPath, encryptConfig)
    } else {
    _, err = storage.GetDBManager().OpenDB(dbPath)
    }

    // 5. 创建数据表
    tableName := "edgex_readings"
    Table, createErr = engine.TableNew(tableName)

    // 6. 设置表字段
    fields := map[string]any{
    "id": "",
    "deviceName": "",
    "reading": "",
    "value": 0.0,
    "valueType": "",
    "baseType": "",
    "timestamp": int64(0),
    "metadata": "",
    }
    if err := Table.SetFields(fields); err != nil {
    return fmt.Errorf("failed to set table fields: %v", err)
    }

    // 7. 创建索引
    primaryKey, err := engine.DefaultPrimaryKeyNew("pk")
    primaryKey.AddFields("deviceName", "timestamp")
    if err := Table.CreateIndex(primaryKey); err != nil {
    if err.Error() != "index already exists" {
    return fmt.Errorf("failed to create primary key index: %v", err)
    }
    }

    // 8. 创建认证表(类似流程)
    // …

    return nil
    }

    9.2.2 表字段设计

    字段类型说明
    id string 读数唯一 ID
    deviceName string 设备名称(索引字段)
    reading string 读数名称
    value float64 读数数值
    valueType string 值类型
    baseType string 基础类型
    timestamp int64 时间戳(纳秒,索引字段)
    metadata string 元数据 JSON

    9.3 索引设计

    9.3.1 组合主键索引

    // database/database.go:112-123
    primaryKey, err := engine.DefaultPrimaryKeyNew("pk")
    if err != nil {
    return fmt.Errorf("failed to create primary key: %v", err)
    }
    primaryKey.AddFields("deviceName", "timestamp")
    if err := Table.CreateIndex(primaryKey); err != nil {
    if err.Error() != "index already exists" {
    return fmt.Errorf("failed to create primary key index: %v", err)
    }
    }

    为什么选择 (deviceName + timestamp)?

  • 查询模式匹配:

    • 按设备查询:WHERE deviceName = ?
    • 按时间范围查询:WHERE deviceName = ? AND timestamp BETWEEN ? AND ?
  • 数据局部性:

    • 同一设备的数据物理存储在一起
    • 范围查询效率高
  • 前缀查询优化:

    • 仅用 deviceName 也能利用索引
    • 最左前缀原则
  • 9.3.2 索引存储结构

    主键索引存储格式:
    pk:{deviceName}:{timestamp} → 完整记录数据

    示例:
    pk:Device001:1677721600000000000 → {id: "1", deviceName: "Device001", …}
    pk:Device001:1677721601000000000 → {id: "2", deviceName: "Device001", …}
    pk:Device002:1677721600000000000 → {id: "3", deviceName: "Device002", …}

    9.3.3 设备名称格式化

    // common/utils.go 中的设备名称格式化
    func FormatDeviceName(deviceName string) string {
    // 确保设备名称长度固定为 64 字符
    // 这样可以作为索引的一部分
    const fixedLength = 64
    if len(deviceName) >= fixedLength {
    return deviceName[:fixedLength]
    }
    // 右侧补零或空格
    return deviceName + strings.Repeat("0", fixedLength–len(deviceName))
    }

    为什么需要格式化?

    • sfsDb 组合主键要求固定长度数据类型,字符串属于不定长类型。
    • 确保索引大小一致
    • 提高查询效率

    9.4 数据操作封装

    9.4.1 批量插入

    // database/database.go:208-222
    func BatchInsertWithRetry(tbl *engine.Table, records []*map[string]any, maxRetries int, retryInterval time.Duration) error {
    for i := 0; i < maxRetries; i++ {
    _, err := tbl.BatchInsertNoInc(records)
    if err == nil {
    return nil
    }

    log.Printf("Failed to batch insert data (attempt %d/%d): %v", i+1, maxRetries, err)
    if i < maxRetries–1 {
    time.Sleep(retryInterval)
    }
    }

    return fmt.Errorf("failed to batch insert data after %d attempts", maxRetries)
    }

    9.4.2 范围查询

    // database/database.go:225-288
    func QueryRecords(tbl *engine.Table, deviceName, startTime, endTime string) (record.Records, error) {
    formattedDeviceName := common.FormatDeviceName(deviceName)

    var startTimestamp, endTimestamp *int64

    if startTime != "" {
    start, err := time.Parse(time.RFC3339, startTime)
    if err == nil {
    ts := start.UnixNano()
    startTimestamp = &ts
    }
    }

    if endTime != "" {
    end, err := time.Parse(time.RFC3339, endTime)
    if err == nil {
    ts := end.UnixNano()
    endTimestamp = &ts
    }
    }

    startRange := make(map[string]any)
    endRange := make(map[string]any)

    startRange["deviceName"] = formattedDeviceName
    endRange["deviceName"] = formattedDeviceName

    if startTimestamp != nil {
    startRange["timestamp"] = *startTimestamp
    } else {
    startRange["timestamp"] = nil
    }

    if endTimestamp != nil {
    endRange["timestamp"] = *endTimestamp
    } else {
    endRange["timestamp"] = nil
    }

    iter, err := tbl.SearchRange(nil, &startRange, &endRange)
    if err != nil {
    return nil, fmt.Errorf("failed to search readings: %v", err)
    }
    defer iter.Release()

    records := iter.GetRecords(true)
    return records, nil
    }

    9.4.3 查询范围构建

    查询场景startRangeendRange
    查询设备所有数据 {deviceName: “D001”, timestamp: nil} {deviceName: “D001”, timestamp: nil}
    查询设备时间范围内数据 {deviceName: “D001”, timestamp: 1000} {deviceName: “D001”, timestamp: 2000}
    查询设备某时间之后数据 {deviceName: “D001”, timestamp: 1000} {deviceName: “D001”, timestamp: nil}

    9.5 加密存储

    9.5.1 加密配置

    // database/database.go:54-76
    if useEncryption {
    if encryptionKey == "" {
    return fmt.Errorf("encryption enabled but no encryption key provided")
    }
    masterKey := make([]byte, 32)
    copy(masterKey, []byte(encryptionKey))
    for i := len(encryptionKey); i < 32; i++ {
    masterKey[i] = 0
    }
    encryptConfig := &storage.EncryptionConfig{
    Enabled: true,
    Algorithm: algorithm,
    MasterKey: masterKey,
    }
    _, err = storage.GetDBManager().OpenDBWithEncryption(dbPath, encryptConfig)
    } else {
    _, err = storage.GetDBManager().OpenDB(dbPath)
    }

    9.5.2 密钥轮换

    // database/database.go:165-187
    func RotateEncryptionKey(newKey string) error {
    store := storage.GetDBManager().GetDB()
    if store == nil {
    return fmt.Errorf("database not initialized")
    }

    encryptedStore, ok := store.(*storage.EncryptedStoreWrapper)
    if !ok {
    return fmt.Errorf("database is not encrypted")
    }

    masterKey := make([]byte, 32)
    copy(masterKey, []byte(newKey))
    for i := len(newKey); i < 32; i++ {
    masterKey[i] = 0
    }

    return encryptedStore.ReEncrypt(masterKey)
    }

    9.6 导入导出

    9.6.1 导出功能

    // database/database.go:291-313
    func ExportTableToCSV(tbl *engine.Table, filePath string) error {
    return tbl.ExportToCSV(filePath)
    }

    func ExportTableToJSON(tbl *engine.Table, filePath string) error {
    return tbl.ExportToJSON(filePath)
    }

    func ExportTableToSQL(tbl *engine.Table, filePath string) error {
    return tbl.ExportToSQL(filePath)
    }

    9.6.2 导入功能

    // database/database.go:296-308
    func ImportTableFromCSV(tbl *engine.Table, filePath string, batchSize int) error {
    return tbl.ImportFromCSV(filePath, batchSize)
    }

    func ImportTableFromJSON(tbl *engine.Table, filePath string, batchSize int) error {
    return tbl.ImportFromJSON(filePath, batchSize)
    }

    9.7 实战练习

    练习 9.1:二级索引设计

    为 reading 字段添加二级索引,优化按读数名称查询。

    练习 9.2:查询优化

    分析一个慢查询,使用 EXPLAIN 或日志找出瓶颈并优化。

    练习 9.3:批量操作优化

    对比不同批量大小的性能差异,找出最优批量大小。

    9.8 本章小结

    本章深入讲解了数据库封装与索引设计:

    • 数据库封装层的设计目标和结构
    • 数据库初始化流程
    • 组合主键索引的设计原理
    • 数据操作的封装实现
    • 加密存储和密钥轮换
    • 导入导出功能

    合理的索引设计是数据库性能的关键。


    本书版本:1.0.0 最后更新:2026-03-08 sfsEdgeStore – 让边缘数据存储更简单!🚀 技术栈 – Go语言、sfsDb与EdgeX Foundry。纯golang工业物联网边缘计算技术栈 项目地址:GitHub

    赞(0)
    未经允许不得转载:171主机测评 » 09-第9章-数据库封装与索引设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址