[手写系列]Go手写db — — 第七版(实现Disk存储引擎、Docker化支持)

[手写系列]Go手写db --- --- 第七版(实现Disk存储引擎、Docker化支持)

第一版文章:[手写系列]Go手写db --- --- 完整教程_go手写数据库-CSDN博客

第二版文章:[手写系列]Go手写db --- --- 第二版-CSDN博客

第三版文章:[手写系列]Go手写db --- --- 第三版(实现分组、排序、聚合函数等)-CSDN博客

第四版文章:[手写系列]Go手写db --- --- 第四版(实现事务、网络模块)

第五版文章:[手写系列]Go手写db --- --- 第五版(实现数据库操作模块)

第六版文章:[手写系列]Go手写db --- --- 第六版(实现表连接)

  • 整体项目Github地址:https://github.com/ziyifast/ZiyiDB
  • 请大家多多支持,也欢迎大家star⭐️和共同维护这个项目~

本文主要介绍如何在 ZiyiDB 第六版的基础上,实现持久化存储引擎,将数据保存到磁盘,使数据库在重启后依然能够保留数据,同时支持Docker方式部署。

🚀 同时ZiyiDB的核心功能,至此也基本完成,感谢大家一直以来的支持~

前置

本文会涉及存储引擎与本地磁盘文件的关系,Disk存储引擎底层参考MySQL8.0版本,大家感兴趣的也可以去了解MySQL的存储系统发展。

MySQL底层存储系统发展之路-CSDN博客

一、功能列表

  1. 实现其他存储引擎,与本地磁盘交互,实现数据持久化。
  2. 实现配置文件,配置程序默认server端端口、默认选择的存储引擎等
  3. 打包到docker

实现Disk存储引擎,与本地文件进行交互。

sql 复制代码
-- 通过下面SQL可查看MySQL支持的存储引擎
show engines;

二、实现细节

功能点一:实现磁盘存储引擎

实现思路

1.新增持久化存储引擎结构

  • 新增internal/storage/engine.go文件,抽取Engine接口,定义存储引擎必要接口
  • 新增internal/storage/disk.go文件,创建 DiskBackend 结构体,实现 Engine 接口,与现有的 MemoryBackend 并行存在,保证ZiyiDB支持多个引擎,实现类MySQL多引擎能力。
  • 新增internal/storage/base.go文件,抽象baseEngine,因为MemoryBackend和DiskBackend解析SQL、调用函数、拼装结果等步骤是类似的,只是对结果的保存方式不同。因此我们可以抽取一个baseEngine,复用通用的逻辑。
    2.数据组织结构设计

参考MySQL最新的InnoDB引擎设计,采用数据字典存储表元数据相关信息,每个表的数据单独存储为.ibd文件。这里的数据字典为了简单方便,我直接采取.json方式统一存储表的名称、表的列名等元数据信息,实际MySQL将其分散在多张表中存储。

详情可参考:MySQL底层存储系统发展之路-CSDN博客

  • 使用 data_dictionary.json 文件存储所有数据库和表的元数据
  • 每个数据库对应一个目录
  • 每个表对应一个 .ibd 文件,存储实际数据行

首先internal/storage/disk.go文件新增数据字典相关结构:

然后在disk.go中实现对数据字典的维护,比如加载和保存等,底层逻辑就是本地磁盘创建文件、读取、更新文件。

最后针对每个数据库下的表创建对应的.ibd文件,用于存储每个表中的数据,同时表数据更新时也对应更新.ibd文件。

MySQL底层.ibd实际是采用二进制方式存储,且做过压缩。ZiyiDB的.ibd文件这里为了简单也采用json方式存储。

  1. 实现核心操作
  • CreateDatabase: 创建数据库目录并更新数据字典

每个数据库会在本地磁盘创建一个目录

  • CreateTable: 创建表结构并保存到数据字典和.ibd文件
  • Insert/Update/Delete: 修改数据后保存到.ibd文件

以更新数据为例:加载表 - 更新数据到内存 - 内存数据保存到.ibd文件中(更新.ibd文件)


  • Select: 从.ibd文件加载数据后进行查询

读取.ibd文件中的表数据,加载到内存,然后select逻辑与之前一样。


  • loadTable/saveTable: 核心的加载和保存表数据的方法

loadTable:从磁盘加载表数据到内存

saveTable:将内存中的数据保存更新到本地磁盘中

  1. 事务支持

与Memory存储引擎相比,Disk引擎主要新增了Cache层,存储事务内提交的变动,因为Disk引擎的数据主要来源于磁盘,但如果事务未提交我们先不写磁盘,先写入缓存,提交后再刷新缓存表数据到磁盘中。



代码实现

因为代码行数比较多,本次主要展示核心代码

1. 存储引擎接口定义

internal/storage/engine.go:

go 复制代码
// internal/storage/engine.go
package storage

import (
    "ziyi.db.com/internal/ast"
    "ziyi.db.com/internal/context"
)

// Engine 定义存储引擎接口
type Engine interface {
    // 数据库操作
    CreateDatabase(stmt *ast.CreateDatabaseStatement) error
    DropDatabase(stmt *ast.DropDatabaseStatement) error
    UseDatabase(stmt *ast.UseDatabaseStatement, connCtx context.DBContext) error
    ShowDatabases() *Results
    ShowTables(connCtx context.DBContext) *Results

    // 表操作
    CreateTable(databaseName string, stmt *ast.CreateTableStatement) error
    DropTable(databaseName string, stmt *ast.DropTableStatement) error

    // 数据操作
    Insert(databaseName string, stmt *ast.InsertStatement, txn *Transaction) error
    Select(databaseName string, stmt *ast.SelectStatement, txn *Transaction) (*Results, error)
    Update(databaseName string, stmt *ast.UpdateStatement, txn *Transaction) error
    Delete(databaseName string, stmt *ast.DeleteStatement, txn *Transaction) error

    // 事务支持
    BeginTransaction() *Transaction

    // 提交事务
    CommitTransaction(txn *Transaction) error

    // 回滚事务
    RollbackTransaction(txn *Transaction) error
}
2. disk存储引擎实现

internal/storage/disk.go:

go 复制代码
// internal/storage/disk.go
package storage

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    "sync"
    "time"
    "ziyi.db.com/internal/ast"
    "ziyi.db.com/internal/context"
)

type DiskBackend struct {
    dataPath string
    mu       sync.RWMutex
    txnMgr   *TransactionManager
    base     *BaseEngine
    // 数据字典,存储所有表的元数据。参考MySQL8.0设计
    dataDictionary map[string]map[string]*TableMetadata
    dictMutex      sync.RWMutex

    // 事务表缓存,存储事务期间的表修改
    // key: txnID, value: map[tableName]*Table
    txnTableCache map[uint64]map[string]*Table
    cacheMutex    sync.RWMutex
}

// TableMetadata 表示表的元数据(替代.frm文件)
type TableMetadata struct {
    Name    string
    Columns []ast.ColumnDefinition
    Indexes map[string]*IndexMetadata
}

// IndexMetadata 索引元数据
type IndexMetadata struct {
    Column string
    Type   string // "PRIMARY", "UNIQUE", "INDEX"
}

func NewDiskBackend(dataPath string) *DiskBackend {
    // 确保数据目录存在
    if err := os.MkdirAll(dataPath, 0755); err != nil {
       panic(fmt.Sprintf("无法创建数据目录: %v", err))
    }

    backend := &DiskBackend{
       dataPath:       dataPath,
       txnMgr:         NewTransactionManager(),
       base:           &BaseEngine{},
       dataDictionary: make(map[string]map[string]*TableMetadata),
       txnTableCache:  make(map[uint64]map[string]*Table),
    }

    // 初始化数据字典
    backend.initDataDictionary()

    return backend
}

// initDataDictionary 初始化数据字典
func (d *DiskBackend) initDataDictionary() {
    dictPath := filepath.Join(d.dataPath, "data_dictionary.json")

    // 尝试加载现有的数据字典
    if file, err := os.Open(dictPath); err == nil {
       defer file.Close()
       var dict map[string]map[string]*TableMetadata
       if err := json.NewDecoder(file).Decode(&dict); err == nil {
          d.dataDictionary = dict
          return
       }
    }

    // 如果没有现有字典,创建新的
    d.dataDictionary = make(map[string]map[string]*TableMetadata)
}

// saveDataDictionary 保存数据字典到磁盘
func (d *DiskBackend) saveDataDictionary() error {
    dictPath := filepath.Join(d.dataPath, "data_dictionary.json")
    tempPath := dictPath + ".tmp"

    file, err := os.Create(tempPath)
    if err != nil {
       return err
    }

    if err := json.NewEncoder(file).Encode(d.dataDictionary); err != nil {
       file.Close()
       os.Remove(tempPath)
       return err
    }

    if err := file.Close(); err != nil {
       os.Remove(tempPath)
       return err
    }

    return os.Rename(tempPath, dictPath)
}

// getDatabasePath 获取数据库路径
func (d *DiskBackend) getDatabasePath(dbName string) string {
    return filepath.Join(d.dataPath, dbName)
}

// getTableDataPath 获取表数据文件路径 (.ibd)
func (d *DiskBackend) getTableDataPath(dbName, tableName string) string {
    return filepath.Join(d.getDatabasePath(dbName), tableName+".ibd")
}

// loadTable 加载表(从.ibd文件和数据字典)
func (d *DiskBackend) loadTable(dbName, tableName string, txn *Transaction) (*Table, error) {
    // 如果在事务中,首先检查事务缓存
    if txn != nil {
       d.cacheMutex.RLock()
       if tables, exists := d.txnTableCache[txn.ID]; exists {
          if table, exists := tables[tableName]; exists {
             d.cacheMutex.RUnlock()
             // 直接返回缓存中的表引用,而不是创建副本
             // 这样可以确保在事务中看到最新的修改
             return table, nil
          }
       }
       d.cacheMutex.RUnlock()
    }
    d.dictMutex.RLock()
    // 从数据字典获取表结构
    dbTables, dbExists := d.dataDictionary[dbName]
    if !dbExists {
       d.dictMutex.RUnlock()
       return nil, fmt.Errorf("database '%s' not exist", dbName)
    }
    metadata, tableExists := dbTables[tableName]
    if !tableExists {
       d.dictMutex.RUnlock()
       return nil, fmt.Errorf("table '%s' not exist", tableName)
    }
    d.dictMutex.RUnlock()

    // 创建表结构
    table := &Table{
       Name:     metadata.Name,
       Columns:  metadata.Columns,
       Indexes:  make(map[string]*Index),
       RowLocks: make(map[int]*sync.RWMutex),
       Rows:     make([][]VersionedCell, 0),
    }
    // 恢复索引结构
    for name, idxMeta := range metadata.Indexes {
       table.Indexes[name] = &Index{
          Column: idxMeta.Column,
          Values: make(map[string][]int),
       }
    }

    // 加载表数据
    dataPath := d.getTableDataPath(dbName, tableName)
    if dataFile, err := os.Open(dataPath); err == nil {
       defer dataFile.Close()

       var tableData struct {
          Rows [][]VersionedCell `json:"rows"`
       }
       if err := json.NewDecoder(dataFile).Decode(&tableData); err == nil {
          table.Rows = tableData.Rows
       }
    }

    // 重建索引数据
    d.rebuildIndexes(table)

    // 如果在事务中,将加载的表放入缓存
    if txn != nil {
       d.cacheMutex.Lock()
       if d.txnTableCache[txn.ID] == nil {
          d.txnTableCache[txn.ID] = make(map[string]*Table)
       }
       // 存储表的引用到缓存中
       d.txnTableCache[txn.ID][tableName] = table
       d.cacheMutex.Unlock()
    }

    return table, nil
}

// saveTable 保存表(保存到.ibd文件,元数据保存在数据字典中)
func (d *DiskBackend) saveTable(dbName, tableName string, table *Table) error {
    dbPath := d.getDatabasePath(dbName)
    // 确保数据库目录存在
    if err := os.MkdirAll(dbPath, 0755); err != nil {
       return err
    }
    // 更新数据字典中的表元数据
    d.dictMutex.Lock()
    if d.dataDictionary[dbName] == nil {
       d.dataDictionary[dbName] = make(map[string]*TableMetadata)
    }
    // 创建表元数据
    metadata := &TableMetadata{
       Name:    table.Name,
       Columns: table.Columns,
       Indexes: make(map[string]*IndexMetadata),
    }
    // 保存索引元数据
    for name, idx := range table.Indexes {
       indexType := "INDEX"
       for _, col := range table.Columns {
          if col.Name == idx.Column && col.Primary {
             indexType = "PRIMARY"
             break
          }
       }
       metadata.Indexes[name] = &IndexMetadata{
          Column: idx.Column,
          Type:   indexType,
       }
    }
    d.dataDictionary[dbName][tableName] = metadata
    // 保存数据字典
    if err := d.saveDataDictionary(); err != nil {
       d.dictMutex.Unlock()
       return err
    }
    d.dictMutex.Unlock()
    // 保存表数据到.ibd文件
    dataPath := d.getTableDataPath(dbName, tableName)
    tempPath := dataPath + ".tmp"
    dataFile, err := os.Create(tempPath)
    if err != nil {
       return err
    }
    // 只保存数据行
    tableData := struct {
       Rows [][]VersionedCell `json:"rows"`
    }{
       Rows: table.Rows,
    }
    if err := json.NewEncoder(dataFile).Encode(tableData); err != nil {
       dataFile.Close()
       os.Remove(tempPath)
       return err
    }
    if err := dataFile.Close(); err != nil {
       os.Remove(tempPath)
       return err
    }
    // 原子性替换表数据文件
    return os.Rename(tempPath, dataPath)
}

// rebuildIndexes 重建索引
func (d *DiskBackend) rebuildIndexes(table *Table) {
    // 清空现有索引值
    for _, index := range table.Indexes {
       index.Values = make(map[string][]int)
    }

    // 重建索引数据
    for i, row := range table.Rows {
       for j, col := range table.Columns {
          if col.Primary && j < len(row) {
             key := row[j].Data.String()
             if index, exists := table.Indexes[col.Name]; exists {
                index.Values[key] = append(index.Values[key], i)
             }
          }
       }
    }
}

// 实现 Engine 接口的方法
func (d *DiskBackend) CreateDatabase(stmt *ast.CreateDatabaseStatement) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    dbPath := d.getDatabasePath(stmt.Name)

    // 检查数据库是否已存在
    if _, err := os.Stat(dbPath); !os.IsNotExist(err) {
       return fmt.Errorf("数据库 '%s' 已存在", stmt.Name)
    }

    // 创建数据库目录
    if err := os.MkdirAll(dbPath, 0755); err != nil {
       return err
    }

    // 更新数据字典
    d.dictMutex.Lock()
    d.dataDictionary[stmt.Name] = make(map[string]*TableMetadata)
    err := d.saveDataDictionary()
    d.dictMutex.Unlock()

    return err
}

func (d *DiskBackend) DropDatabase(stmt *ast.DropDatabaseStatement) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    dbPath := d.getDatabasePath(stmt.Name)

    // 检查数据库是否存在
    if _, err := os.Stat(dbPath); os.IsNotExist(err) {
       return fmt.Errorf("database '%s' does not exist", stmt.Name)
    }

    // 删除数据库目录
    if err := os.RemoveAll(dbPath); err != nil {
       return err
    }

    // 更新数据字典
    d.dictMutex.Lock()
    delete(d.dataDictionary, stmt.Name)
    err := d.saveDataDictionary()
    d.dictMutex.Unlock()

    return err
}

func (d *DiskBackend) UseDatabase(stmt *ast.UseDatabaseStatement, connCtx context.DBContext) error {
    d.mu.RLock()
    defer d.mu.RUnlock()

    // 检查数据库是否存在(通过数据字典)
    d.dictMutex.RLock()
    _, exists := d.dataDictionary[stmt.Name]
    d.dictMutex.RUnlock()

    if !exists {
       return fmt.Errorf("database '%s' does not exist", stmt.Name)
    }

    // 更新连接上下文中的当前数据库
    connCtx.SetDBName(stmt.Name)
    return nil
}

func (d *DiskBackend) ShowDatabases() *Results {
    d.mu.RLock()
    defer d.mu.RUnlock()

    results := &Results{
       Columns: []ResultColumn{
          {Name: "Database", Type: "TEXT"},
       },
       Rows: make([][]Cell, 0),
    }

    d.dictMutex.RLock()
    for dbName := range d.dataDictionary {
       results.Rows = append(results.Rows, []Cell{
          {Type: CellTypeText, TextValue: dbName},
       })
    }
    d.dictMutex.RUnlock()

    return results
}

func (d *DiskBackend) ShowTables(connCtx context.DBContext) *Results {
    d.mu.RLock()
    defer d.mu.RUnlock()

    results := &Results{
       Columns: []ResultColumn{
          {Name: "Tables", Type: "TEXT"},
       },
       Rows: make([][]Cell, 0),
    }

    dbName := connCtx.GetDBName()
    if dbName == "" {
       return results
    }

    d.dictMutex.RLock()
    if dbTables, exists := d.dataDictionary[dbName]; exists {
       for tableName := range dbTables {
          results.Rows = append(results.Rows, []Cell{
             {Type: CellTypeText, TextValue: tableName},
          })
       }
    }
    d.dictMutex.RUnlock()

    return results
}

// CreateTable 创建表
func (d *DiskBackend) CreateTable(databaseName string, stmt *ast.CreateTableStatement) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 检查数据库是否存在
    d.dictMutex.RLock()
    _, dbExists := d.dataDictionary[databaseName]
    d.dictMutex.RUnlock()

    if !dbExists {
       return fmt.Errorf("database '%s' does not exist", databaseName)
    }

    // 检查表是否已存在
    d.dictMutex.RLock()
    _, tableExists := d.dataDictionary[databaseName][stmt.TableName]
    d.dictMutex.RUnlock()

    if tableExists {
       return fmt.Errorf("table '%s' 已存在", stmt.TableName)
    }

    // 创建新表
    table := &Table{
       Name:     stmt.TableName,
       Columns:  stmt.Columns,
       Rows:     make([][]VersionedCell, 0),
       Indexes:  make(map[string]*Index),
       RowLocks: make(map[int]*sync.RWMutex),
    }

    // 为主键创建索引
    for _, col := range stmt.Columns {
       if col.Primary {
          table.Indexes[col.Name] = &Index{
             Column: col.Name,
             Values: make(map[string][]int),
          }
       }
    }

    // 保存表到磁盘
    return d.saveTable(databaseName, stmt.TableName, table)
}

func (d *DiskBackend) DropTable(databaseName string, stmt *ast.DropTableStatement) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 检查数据库是否存在
    d.dictMutex.RLock()
    _, dbExists := d.dataDictionary[databaseName]
    d.dictMutex.RUnlock()

    if !dbExists {
       return fmt.Errorf("database '%s' does not exist", databaseName)
    }

    // 检查表是否存在
    d.dictMutex.RLock()
    _, tableExists := d.dataDictionary[databaseName][stmt.TableName]
    d.dictMutex.RUnlock()

    if !tableExists {
       return fmt.Errorf("table '%s' does not exist", stmt.TableName)
    }

    // 删除表数据文件
    dataPath := d.getTableDataPath(databaseName, stmt.TableName)
    if err := os.Remove(dataPath); err != nil && !os.IsNotExist(err) {
       return fmt.Errorf("can not delete table data '%s': %v", stmt.TableName, err)
    }

    // 从数据字典中移除表
    d.dictMutex.Lock()
    delete(d.dataDictionary[databaseName], stmt.TableName)
    err := d.saveDataDictionary()
    d.dictMutex.Unlock()

    return err
}

// Select 查询数据
func (d *DiskBackend) Select(databaseName string, stmt *ast.SelectStatement, txn *Transaction) (*Results, error) {
    d.mu.RLock()
    defer d.mu.RUnlock()

    // 加载表(支持事务缓存)
    table, err := d.loadTable(databaseName, stmt.TableName, txn)
    if err != nil {
       return nil, err
    }

    // 使用通用逻辑处理查询
    return d.base.Select(table, stmt, txn, d.getVisibleRow, d.base.evaluateWhereCondition)
}

// Insert 插入数据
func (d *DiskBackend) Insert(databaseName string, stmt *ast.InsertStatement, txn *Transaction) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 加载表(支持事务缓存)
    table, err := d.loadTable(databaseName, stmt.TableName, txn)
    if err != nil {
       return err
    }

    // 使用通用逻辑处理插入
    err = d.base.Insert(table, stmt, txn, d.getVisibleRow, d.base.convertToCell, d.base.evaluateExpression)
    if err != nil {
       return err
    }

    // 在非事务模式下直接保存表,否则更新事务缓存
    if txn == nil {
       return d.saveTable(databaseName, stmt.TableName, table)
    } else {
       // 更新事务缓存中的表
       d.cacheMutex.Lock()
       if d.txnTableCache[txn.ID] == nil {
          d.txnTableCache[txn.ID] = make(map[string]*Table)
       }
       d.txnTableCache[txn.ID][stmt.TableName] = table
       d.cacheMutex.Unlock()
    }

    return nil
}

// Update 更新数据
func (d *DiskBackend) Update(databaseName string, stmt *ast.UpdateStatement, txn *Transaction) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 加载表(支持事务缓存)
    table, err := d.loadTable(databaseName, stmt.TableName, txn)
    if err != nil {
       return err
    }

    // 使用通用逻辑处理更新
    err = d.base.Update(table, stmt, txn, d.getVisibleRow, d.base.convertToCell,
       d.base.evaluateExpression, d.base.evaluateWhereCondition)
    if err != nil {
       return err
    }

    // 在非事务模式下直接保存表,否则更新事务缓存
    if txn == nil {
       return d.saveTable(databaseName, stmt.TableName, table)
    } else {
       // 更新事务缓存中的表
       d.cacheMutex.Lock()
       if d.txnTableCache[txn.ID] == nil {
          d.txnTableCache[txn.ID] = make(map[string]*Table)
       }
       d.txnTableCache[txn.ID][stmt.TableName] = table
       d.cacheMutex.Unlock()
    }

    return nil
}

// Delete 删除数据
func (d *DiskBackend) Delete(databaseName string, stmt *ast.DeleteStatement, txn *Transaction) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 加载表(支持事务缓存)
    table, err := d.loadTable(databaseName, stmt.TableName, txn)
    if err != nil {
       return err
    }

    // 使用通用逻辑处理删除
    err = d.base.Delete(table, stmt, txn, d.getVisibleRow, d.base.evaluateWhereCondition)
    if err != nil {
       return err
    }

    // 在非事务模式下直接保存表,否则更新事务缓存
    if txn == nil {
       return d.saveTable(databaseName, stmt.TableName, table)
    } else {
       // 更新事务缓存中的表
       d.cacheMutex.Lock()
       if d.txnTableCache[txn.ID] == nil {
          d.txnTableCache[txn.ID] = make(map[string]*Table)
       }
       d.txnTableCache[txn.ID][stmt.TableName] = table
       d.cacheMutex.Unlock()
    }

    return nil
}

// updateIndexesAfterInsert 在插入后更新索引
func (d *DiskBackend) updateIndexesAfterInsert(table *Table, oldRowCount int) {
    // 只需要更新新插入行的索引
    for i := oldRowCount; i < len(table.Rows); i++ {
       row := table.Rows[i]
       for _, col := range table.Columns {
          if col.Primary && i < len(row) {
             key := row[i].Data.String()
             if index, exists := table.Indexes[col.Name]; exists {
                index.Values[key] = append(index.Values[key], i)
             }
          }
       }
    }
}

// BeginTransaction 开启事务
func (d *DiskBackend) BeginTransaction() *Transaction {
    return d.txnMgr.BeginTransaction(d)
}

// CommitTransaction 提交事务中的更改
func (d *DiskBackend) CommitTransaction(txn *Transaction) error {
    d.mu.Lock()
    defer d.mu.Unlock()

    // 首先调用事务管理器的提交方法
    if err := d.txnMgr.CommitTransaction(txn); err != nil {
       return err
    }

    // 获取事务缓存中的表
    d.cacheMutex.RLock()
    tables, cacheExists := d.txnTableCache[txn.ID]
    d.cacheMutex.RUnlock()

    if cacheExists {
       // 遍历事务缓存中的所有表,将修改持久化到磁盘
       for tableName, table := range tables {
          // 查找表所在的数据库
          var dbName string
          var found bool

          d.dictMutex.RLock()
          for db, dbTables := range d.dataDictionary {
             if _, exists := dbTables[tableName]; exists {
                dbName = db
                found = true
                break
             }
          }
          d.dictMutex.RUnlock()

          if !found {
             continue
          }

          // 更新表中所有版本化单元格的状态
          for i := range table.Rows {
             for j := range table.Rows[i] {
                if table.Rows[i][j].TxnID == txn.ID {
                   table.Rows[i][j].Committed = true
                   table.Rows[i][j].Timestamp = time.Now()
                }
             }
          }

          // 重建索引以确保一致性
          d.rebuildIndexes(table)

          // 保存表到磁盘
          if err := d.saveTable(dbName, tableName, table); err != nil {
             return err
          }
       }
    }

    // 清理事务缓存
    d.cacheMutex.Lock()
    delete(d.txnTableCache, txn.ID)
    d.cacheMutex.Unlock()

    // 保存数据字典
    return d.saveDataDictionary()
}

// RollbackTransaction 回滚事务
func (d *DiskBackend) RollbackTransaction(txn *Transaction) error {
    // 先执行事务管理器的回滚逻辑(更新事务状态等)
    if err := d.txnMgr.RollbackTransaction(txn); err != nil {
       return err
    }

    // 清理事务缓存(回滚时直接丢弃所有未提交的修改)
    d.cacheMutex.Lock()
    delete(d.txnTableCache, txn.ID)
    d.cacheMutex.Unlock()

    // 保存数据字典(确保元数据一致性)
    return d.saveDataDictionary()
}

// getVisibleRow 获取可见行版本
func (d *DiskBackend) getVisibleRow(versionedRow []VersionedCell, txn *Transaction) []Cell {
    if len(versionedRow) == 0 {
       return nil
    }

    if txn == nil {
       // 非事务查询,返回最新提交的版本
       for i := len(versionedRow) - 1; i >= 0; i-- {
          if versionedRow[i].Committed {
             row := make([]Cell, len(versionedRow))
             for j, v := range versionedRow {
                row[j] = v.Data
             }
             return row
          }
       }
       return nil
    }

    // 事务查询,根据读已提交隔离级别规则
    // 查找对当前事务可见的最新版本
    for i := len(versionedRow) - 1; i >= 0; i-- {
       version := versionedRow[i]

       // 如果是当前事务自己的修改,可见(即使未提交)
       if version.TxnID == txn.ID {
          row := make([]Cell, len(versionedRow))
          for j, v := range versionedRow {
             row[j] = v.Data
          }
          return row
       }

       // 对于读已提交隔离级别,只能看到已提交的数据
       if version.Committed {
          row := make([]Cell, len(versionedRow))
          for j, v := range versionedRow {
             row[j] = v.Data
          }
          return row
       }
    }

    return nil
}

测试

测试命令:

sql 复制代码
-- 创建数据库test
create database test;
use test;
create table users (id INT PRIMARY KEY,name text,age INT);
insert into users values (1, 'Tom', 20);
select * from users;
-- 重启数据库,然后观察数据库及表中数据是否存在
use test;
select * from users;

效果:

可以看到我们插入数据,重启数据库之后依然可以看到之前的数据,并且本地生成了对应的持久化文件,整体符合预期。

功能点二:配置文件支持

MySQL中可通过my.cnf等文件进行系统配置,我们也可参考MySQL,提供一个配置文件用于配置服务默认端口、默认存储引擎等信息。

实现思路

只需在程序启动时,读取本地的配置文件即可。然后判断程序监听的端口和默认初始化的存储引擎

1.新建config/config.go文件:

  1. main.go中读取配置文件:

代码实现

config/config.go:

go 复制代码
// config/config.go
package config

import (
    "encoding/json"
    "os"
)

type Config struct {
    Storage struct {
       Type     string `json:"type"`     // "memory" 或 "disk"
       DataPath string `json:"dataPath"` // 磁盘存储的数据路径
    } `json:"storage"`
    Server struct {
       Port string `json:"port"`
    } `json:"server"`
}

func LoadConfig(configPath string) (*Config, error) {
    file, err := os.Open(configPath)
    if err != nil {
       return nil, err
    }
    defer file.Close()

    var config Config
    if err := json.NewDecoder(file).Decode(&config); err != nil {
       return nil, err
    }

    return &config, nil
}

cmd/main.go:

go 复制代码
// cmd/main.go
package main

import (
    "flag"
    "fmt"
    "github.com/c-bata/go-prompt"
    "log"
    "os"
    "strings"
    "ziyi.db.com/config"
    "ziyi.db.com/internal/ast"
    "ziyi.db.com/internal/lexer"
    "ziyi.db.com/internal/parser"
    "ziyi.db.com/internal/storage"
    "ziyi.db.com/network"
)

var history []string                // 存储命令历史
var backend storage.Engine          // 存储引擎实例
var currentTxn *storage.Transaction // 当前事务
var historyIndex int                // 当前历史记录索引
var currentDatabase string          // 当前用户选择的数据库

type dbContextAdapter struct {
    dbName *string
}

func (d *dbContextAdapter) GetDBName() string {
    return *d.dbName
}

func (d *dbContextAdapter) SetDBName(dbName string) {
    *d.dbName = dbName
}

func executor(t string) {
    // 分割多个SQL语句(用分号分隔)
    statements := strings.Split(t, ";")

    for _, stmt := range statements {
       stmt = strings.TrimSpace(stmt)
       if stmt == "" {
          continue
       }

       // 添加到历史记录
       history = append(history, stmt)
       historyIndex = len(history) // 重置历史记录索引

       // 处理退出命令
       if strings.ToLower(stmt) == "exit" {
          fmt.Println("Bye!")
          os.Exit(0)
       }

       // 处理事务相关命令
       if strings.HasPrefix(strings.ToLower(stmt), "begin") {
          currentTxn = backend.BeginTransaction()
          fmt.Printf("Transaction %d started\n", currentTxn.ID)
          continue
       }

       if strings.HasPrefix(strings.ToLower(stmt), "commit") {
          if currentTxn == nil {
             fmt.Println("Error: No active transaction")
             continue
          }
          if err := backend.CommitTransaction(currentTxn); err != nil {
             fmt.Printf("Error: %v\n", err)
          } else {
             fmt.Printf("Transaction %d committed\n", currentTxn.ID)
          }
          currentTxn = nil
          continue
       }

       if strings.HasPrefix(strings.ToLower(stmt), "rollback") {
          if currentTxn == nil {
             fmt.Println("Error: No active transaction")
             continue
          }
          if err := backend.RollbackTransaction(currentTxn); err != nil {
             fmt.Printf("Error: %v\n", err)
          } else {
             fmt.Printf("Transaction %d rolled back\n", currentTxn.ID)
          }
          currentTxn = nil
          continue
       }

       // 创建词法分析器
       l := lexer.NewLexer(strings.NewReader(stmt))

       // 创建语法分析器
       p := parser.NewParser(l)

       // 解析SQL语句
       parsedStmt, err := p.ParseProgram()
       if err != nil {
          fmt.Printf("Parse error: %v\n", err)
          continue
       }

       // 执行SQL语句
       for _, statement := range parsedStmt.Statements {
          if currentDatabase == "" {
             // 检查是否是非数据库操作语句
             _, isCreateDB := statement.(*ast.CreateDatabaseStatement)
             _, isShowDBs := statement.(*ast.ShowDatabasesStatement)
             _, isDropDB := statement.(*ast.DropDatabaseStatement)
             _, isUseDB := statement.(*ast.UseDatabaseStatement)
             _, isShowTables := statement.(*ast.ShowTablesStatement)
             // 如果不是允许的语句类型,则提示需要选择数据库
             if !isCreateDB && !isShowDBs && !isDropDB && !isUseDB && !isShowTables {
                fmt.Println("No database selected. Use 'USE database_name' to select a database.")
                continue
             }
          }
          switch s := statement.(type) {
          case *ast.CreateDatabaseStatement:
             if err := backend.CreateDatabase(s); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Database created successfully")
             }
          case *ast.DropDatabaseStatement:
             if err := backend.DropDatabase(s); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Database dropped successfully")
             }
          case *ast.ShowDatabasesStatement:
             result := backend.ShowDatabases()
             printResults(result)
          case *ast.ShowTablesStatement:
             result := backend.ShowTables(&dbContextAdapter{&currentDatabase})
             printResults(result)
          case *ast.UseDatabaseStatement:
             if err := backend.UseDatabase(s, &dbContextAdapter{&currentDatabase}); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Printf("Database changed to '%s'\n", currentDatabase)
             }
          case *ast.CreateTableStatement:
             if err := backend.CreateTable(currentDatabase, s); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Table created successfully")
             }
          case *ast.InsertStatement:
             if err := backend.Insert(currentDatabase, s, currentTxn); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("1 row inserted")
             }
          case *ast.SelectStatement:
             results, err := backend.Select(currentDatabase, s, currentTxn)
             if err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                printResults(results)
             }
          case *ast.UpdateStatement:
             if err := backend.Update(currentDatabase, s, currentTxn); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Query OK")
             }
          case *ast.DeleteStatement:
             if err := backend.Delete(currentDatabase, s, currentTxn); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Query OK")
             }
          case *ast.DropTableStatement:
             if err := backend.DropTable(currentDatabase, s); err != nil {
                fmt.Printf("Error: %v\n", err)
             } else {
                fmt.Println("Table dropped successfully")
             }
          default:
             fmt.Printf("Unsupported statement type: %T\n", s)
          }
       }
    }
}

func printResults(results *storage.Results) {
    // 计算每列的最大宽度
    colWidths := make([]int, len(results.Columns))
    for i, col := range results.Columns {
       colWidths[i] = len(col.Name)
    }
    for _, row := range results.Rows {
       for i, cell := range row {
          cellLen := len(cell.String())
          if cellLen > colWidths[i] {
             colWidths[i] = cellLen
          }
       }
    }

    // 打印表头
    fmt.Print("+")
    for _, width := range colWidths {
       fmt.Print(strings.Repeat("-", width+2))
       fmt.Print("+")
    }
    fmt.Println()

    // 打印列名
    fmt.Print("|")
    for i, col := range results.Columns {
       fmt.Printf(" %-*s |", colWidths[i], col.Name)
    }
    fmt.Println()

    // 打印分隔线
    fmt.Print("+")
    for _, width := range colWidths {
       fmt.Print(strings.Repeat("-", width+2))
       fmt.Print("+")
    }
    fmt.Println()

    // 打印数据行
    for _, row := range results.Rows {
       fmt.Print("|")
       for i, cell := range row {
          fmt.Printf(" %-*s |", colWidths[i], cell.String())
       }
       fmt.Println()
    }

    // 打印底部边框
    fmt.Print("+")
    for _, width := range colWidths {
       fmt.Print(strings.Repeat("-", width+2))
       fmt.Print("+")
    }
    fmt.Println()

    // 打印行数统计
    fmt.Printf("%d rows in set\n", len(results.Rows))
}

// 提供命令补全功能
func completer(d prompt.Document) []prompt.Suggest {
    s := []prompt.Suggest{}
    return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}

func main() {
    // 添加配置文件参数
    configPath := flag.String("config", "config.json", "Path to config file")
    port := flag.String("port", "3118", "Port to listen on")
    flag.Parse()

    // 加载配置
    config, err := config.LoadConfig(*configPath)
    if err != nil {
       fmt.Printf("无法加载配置文件: %v\n", err)
       os.Exit(1)
    }

    // 初始化存储引擎
    switch config.Storage.Type {
    case "memory":
       backend = storage.NewMemoryBackend()
    case "disk":
       backend = storage.NewDiskBackend(config.Storage.DataPath)
    default:
       fmt.Printf("未知的存储引擎类型: %s\n", config.Storage.Type)
       os.Exit(1)
    }

    // 检查是否以服务器模式运行
    args := flag.Args()
    if len(args) > 0 && args[0] == "server" {
       // 启动服务器模式
       portEnv := os.Getenv("ZIYIDB_PORT")
       if portEnv != "" {
          *port = portEnv
       } else if config.Server.Port != "" {
          *port = config.Server.Port
       }

       // 类型断言获取 MemoryBackend(如果使用的是内存引擎)
       if memoryBackend, ok := backend.(*storage.MemoryBackend); ok {
          server := network.NewServer(memoryBackend, *port)
          fmt.Printf("Starting ZiyiDB server on port %s with %s storage...\n", *port, config.Storage.Type)
          log.Fatal(server.Start())
       } else {
          fmt.Println("Server mode only supports memory storage engine")
          os.Exit(1)
       }
    }

    fmt.Println("Welcome to ZiyiDB!")
    fmt.Println("Type your SQL commands (type 'exit' to quit)")

    p := prompt.New(
       executor,
       completer,
       prompt.OptionTitle("ZiyiDB: A Simple SQL Database"),
       prompt.OptionPrefix("ziyidb> "),
       prompt.OptionHistory(history),
       prompt.OptionLivePrefix(func() (string, bool) {
          return "ziyidb> ", true
       }),
       //实现方向键上下翻阅历史命令
       // 上键绑定
       prompt.OptionAddKeyBind(prompt.KeyBind{
          Key: prompt.Up,
          Fn: func(buf *prompt.Buffer) {
             if historyIndex > 0 {
                historyIndex--
                buf.DeleteBeforeCursor(len(buf.Text()))
                buf.InsertText(history[historyIndex], false, true)
             }
          },
       }),
       // 下键绑定
       prompt.OptionAddKeyBind(prompt.KeyBind{
          Key: prompt.Down,
          Fn: func(buf *prompt.Buffer) {
             if historyIndex < len(history)-1 {
                historyIndex++
                buf.DeleteBeforeCursor(len(buf.Text()))
                buf.InsertText(history[historyIndex], false, true)
             } else if historyIndex == len(history)-1 {
                historyIndex++
                buf.DeleteBeforeCursor(len(buf.Text()))
             }
          },
       }),
    )
    p.Run()
}

拓展:docker化支持

为了适应现在互联网开发的要求,ZiyiDB也需要提供docker部署的方式。

  1. 项目根目录下,新建Dockerfile文件

Dockerfile:

yaml 复制代码
# Dockerfile
FROM golang:1.23.8-alpine AS builder

# 设置ZiyiDB工作目录
WORKDIR /app

# 复制go mod和sum文件
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 编译服务端程序
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ziyidb cmd/main.go

# 编译客户端程序
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ziyidb-cli cmd/client.go

# 使用基础镜像
FROM alpine:latest

# 安装ca-certificates
RUN apk --no-cache add ca-certificates

# 设置工作目录
WORKDIR /root/

# 从builder阶段复制编译好的程序
COPY --from=builder /app/ziyidb .
COPY --from=builder /app/ziyidb-cli .

# 复制配置文件
COPY --from=builder /app/config.json .

# 暴露默认端口
EXPOSE 3118

# 启动服务器模式
ENTRYPOINT ["./ziyidb", "server", "-port=3118"]
  1. 执行docker命令打包镜像
bash 复制代码
# 在Dockerfile同一目录,构建镜像
docker build -t ziyidb .

查看是否构建成功:

bash 复制代码
docker images | grep ziyidb
  1. 创建容器
bash 复制代码
# 创建容器并挂载本地目录
docker run -d \
  --name ziyidb \
  -p 3118:3118 \
  --restart unless-stopped \
  -v /Users/ziyi/GolandProjects/ZiyiDB/config.json:/root/config.json \
  -v /Users/ziyi/GolandProjects/ZiyiDB/data:/root/data \
  ziyidb

# 查看容器运行状态
docker ps | grep ziyidb

PS:同样的,除了docker方式搭建,也可以编写docker-compose.yml启动。

docker-compose.yml:

yaml 复制代码
services:
  ziyidb:
    build: .
    container_name: ziyidb
    ports:
      - "3118:3118"
    restart: unless-stopped
    environment:
      - ZIYIDB_PORT=3118
bash 复制代码
# 通过docker-compose方式启动
docker-compose up -d 

效果:

  1. 测试功能
  • 验证memory存储引擎
bash 复制代码
# 进入容器
docker exec -it ziyidb /bin/sh

# 执行客户端,连接服务端(后续可优化:默认链接本地的服务端)
./ziyidb-cli localhost:3118

# 测试memory存储引擎
CREATE DATABASE test_memory;
USE test_memory;
CREATE TABLE users (id INT PRIMARY KEY, name TEXT, age INT);
INSERT INTO users VALUES (1, 'Alice', 25);
SELECT * FROM users;
exit
exit

# 重启容器,观察之前创建的数据是否还存在
docker restart ziyidb
docker exec -it ziyidb /bin/sh
./ziyidb-cli localhost:3118
show databases;


  • 验证disk存储引擎

调整本地挂载的config.json配置文件,然后重启容器

重启容器,并插入数据:

bash 复制代码
# 重启容器
docker restart ziyidb

# 进入ziyidb,连接到客户端
docker exec -it ziyidb /bin/sh
./ziyidb-cli localhost:3118

# 创建数据库、表,并插入数据
CREATE DATABASE test_disk;
USE test_disk;
CREATE TABLE users (id INT PRIMARY KEY, name TEXT, age INT);
INSERT INTO users VALUES (1, 'Alice', 25);
SELECT * FROM users;

观察挂载的本地磁盘是否有对应目录生成:

重启ziyidb,验证之前插入的数据是否还能查询:

bash 复制代码
# 进入ziyidb
docker exec -it ziyidb /bin/sh -c "./ziyidb-cli localhost:3118"
# 查询之前数据
show databases;
use test_disk;
select * from users;
  1. 打包镜像到docker-hub

在Dockerfile同级目录执行下面命令:

bash 复制代码
# ziyigun是我dockerhub的用户名
# ziyidb是镜像名,latest是版本号,代表最新
docker build -t ziyigun/ziyidb:latest .

# 查询是否打包成功
docker images | grep ziyigun


登录dockerhub:

bash 复制代码
# 登录dockerhub
docker login


推送镜像到远程仓库:

bash 复制代码
# 推送到dockerhub
docker push ziyigun/ziyidb:latest 

在仓库搜索ziyidb,查看是否能搜索到镜像:https://hub.docker.com/search?q=ziyidb

参考文章:

https://blog.csdn.net/weixin_45565886/article/details/154035863

https://dev.mysql.com/doc/refman/8.0/en/system-schema.html

https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html#mysql-nutshell-ddl

相关推荐
Victor3562 小时前
Redis(109)Redis的Pipeline如何使用?
后端
Victor3562 小时前
Redis(108)Redis的事务机制如何实现?
后端
JaguarJack2 小时前
PHP 开发中 你可能不知道的非常好用 PhpStorm 插件
后端·php
9ilk2 小时前
【基于one-loop-per-thread的高并发服务器】--- 前置技术
运维·服务器·c++·笔记·后端·中间件
qq_343247032 小时前
docker 下搭建 nacos
运维·docker·容器
人工智能训练2 小时前
Ubuntu系统中Docker的常用命令总结
linux·运维·人工智能·ubuntu·docker·ai
workflower2 小时前
FDD与其他方法的相似和区别
数据库·算法·需求分析·个人开发
Felix_XXXXL3 小时前
IDEA + Spring Boot 的三种热加载方案
java·后端