- Published at
Build một cái K-V Database đơn giản như thế nào? (P7 - Database Layer)

Khám phá Database Layer - lớp orchestrator chính của Bolt, kết nối tất cả components và cung cấp unified API cho developers
Table of Contents
- DB Structure - The Central Coordinator
- Database Lifecycle - Open và Close
- Mở một Database
- Closing và Cleanup
- API Layer - Giao diện người dùng
- Managed Transactions - Đơn Giản và An Toàn
- Explicit Transactions - Kiểm Soát Hoàn Toàn
- Batch Operations
- Component Orchestration - Making It All Work
- MVCC Coordination
- Memory Management Coordination
- Freelist Coordination
- Concurrency Control
- Single Writer Pattern
- Multiple Readers
- Kết hợp tất cả mọi thứ
- Kết - Hành trình tìm 7 viên ngọc rồng kết thúc
- Một vài điểm chính
Sau 6 phần, chúng ta đã tìm hiểu về các building blocks của Bolt: file format, storage management, B+ tree, buckets, cursors và transactions. Nhưng làm sao tất cả những components này "dính"
lại với nhau thành một database hoàn chỉnh? Câu trả lời chính là DB Layer - lớp orchestrator cao nhất chịu trách nhiệm quản lý lifecycle, cung cấp API, và đảm bảo tính nhất quán của toàn bộ hệ thống.
Phần này sẽ chủ yếu nói về code được expose ở Database Layer, vì chỉ có code mới biết nó kết hợp các component nào.
DB Structure - The Central Coordinator
DB struct trong Bolt chứa tất cả những gì cần thiết để quản lý một database instance:
type DB struct {
// Configuration & behavior
StrictMode bool
NoSync bool // Skip fsync for performance
ReadOnly bool // Read-only access mode
// File system integration
path string // Database file path
file *os.File // File handle
// Memory management
data *[maxMapSize]byte // mmap array
datasz int // Current mapped size
pageSize int // Page size alignment
// Core components
meta0 *meta // Primary metadata
meta1 *meta // Backup metadata
freelist *freelist // Free page management
// Transaction coordination
rwtx *Tx // Current read-write transaction
txs []*Tx // Active read-only transactions
// Concurrency control
rwlock sync.Mutex // Single writer enforcement
mmaplock sync.RWMutex // Memory mapping protection
// Performance optimization
batch *batch // Batch operation context
batchMu sync.Mutex // Batch coordination
pagePool sync.Pool // Page buffer recycling
}
DB struct có 4 trách nhiệm chính:
- File Management: Quản lý file system operations, file locking, permissions
- Memory Mapping: Điều phối memory-mapped access, remapping khi cần
- Transaction Coordination: Thực thi single-writer rule, quản lý MVCC
- Component Orchestration: Kết nối freelist, meta pages, B+ trees, buckets
Database Lifecycle - Open và Close
Mở một Database
Việc mở một database Bolt không chỉ đơn giản là mở file. Đây là một quá trình phức tạp gồm nhiều bước quan trọng:
func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
db := &DB{opened: true}
// 1. File system setup
if err := db.open(path, mode, options); err != nil {
return nil, err
}
// 2. Memory mapping initialization
if err := db.mmap(options.InitialMmapSize); err != nil {
return nil, err
}
// 3. Load or initialize metadata
if fileSize == 0 {
if err := db.init(); err != nil { // New database
return nil, err
}
}
// 4. Restore freelist state
db.freelist = newFreelist()
db.freelist.read(db.page(db.meta().freelist))
return db, nil
}
Tại bước 3, ta nhận thấy Bolt có 2 trường hợp xảy ra là load database đã có hoặc init một database mới.
Với tạo database mới, Bolt phải setup initial state:
- Tạo 2 meta pages (cho redundancy)
- Khởi tạo root bucket
- Setup initial freelist
- Ghi tất cả mọi thứ vào disk
Với mở database có sẵn:
- Validate file format và compatibility
- Load metadata pages
- Restore freelist state
- Xác minh toàn vẹn dữ liệu của database.
Closing và Cleanup
Quá trình đóng database đảm bảo không mất dữ liệu và cleanup resources:
- Transaction Completion: Chờ tất cả active transactions hoàn thành hoặc timeout
- Memory Unmapping: Giải phóng memory-mapped regions một cách an toàn
- File Handle Cleanup: Đóng file descriptors và release OS locks
func (db *DB) Close() error {
// 1. Wait for all transactions to complete
db.metalock.Lock()
defer db.metalock.Unlock()
// 2. Unmap memory
if err := db.munmap(); err != nil {
return err
}
// 3. Close file handles
return db.file.Close()
}
API Layer - Giao diện người dùng
Bolt cung cấp 3 cách tương tác khác nhau, mỗi cách phù hợp với use case riêng:
Managed Transactions - Đơn Giản và An Toàn
Các transaction được quản lý sẵn đã được cài sẵn các hàm xử lý khi gặp vấn đề:
- Transaction lifecycle (begin, commit, rollback)
- Error handling và cleanup
- Panic recovery
- Resource management
// Read-write transaction
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
return b.Put([]byte("key"), []byte("value"))
})
// Read-only transaction
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
val := b.Get([]byte("key"))
fmt.Printf("Value: %s\n", val)
return nil
})
Explicit Transactions - Kiểm Soát Hoàn Toàn
Như đã đề cập ở chương Transaction
explicit transactions cho phép ta hoàn toàn tự kiểm soát transaction như sau:
// Manual transaction management
tx, err := db.Begin(true) // read-write
if err != nil {
return err
}
defer tx.Rollback() // Safety net
// Do work...
b := tx.Bucket([]byte("MyBucket"))
err = b.Put([]byte("key"), []byte("value"))
if err != nil {
return err // tx.Rollback() called by defer
}
return tx.Commit() // Success
Batch Operations
Bolt cũng cung cấp Batch operations để tối ưu write throughput bằng cách kết hợp nhiều operations:
err := db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key%d", i)
val := fmt.Sprintf("value%d", i)
if err := b.Put([]byte(key), []byte(val)); err != nil {
return err
}
}
return nil
})
Batch intelligence: Nếu nhiều goroutines gọi Batch()
đồng thời, Bolt sẽ kết hợp chúng thành một transaction bự để tối ưu performance.
Component Orchestration - Making It All Work
Database Layer phải điều phối tất cả các thành phần một cách cẩn thận:
MVCC Coordination
Database Layer đảm bảo mỗi transaction đều có góc nhìn nhất quán với nhau:
- Transaction Isolation: Mỗi transaction capture metadata snapshot tại thời điểm bắt đầu, đảm bảo duy trì góc nhìn nhất quán khi đọc dữ liệu suốt vòng đời transaction.
- Snapshot versioning: Database duy trì nhiều phiên bản metadata khác nhau để hỗ trợ đồng thời nhiều reader trong khi writer đang cập nhật cấu trúc.
func (db *DB) beginTx() (*Tx, error) {
// 1. Create transaction with current metadata snapshot
t := &Tx{
db: db,
meta: db.meta(), // Snapshot at transaction start
}
// 2. Add to active transaction list
db.txs = append(db.txs, t)
return t, nil
}
Memory Management Coordination
Database Layer quản lý memory mapping và page allocation như sau:
func (db *DB) allocate(count int) (*page, error) {
// 1. Try to get pages from freelist first
if p := db.freelist.allocate(count); p != nil {
return p, nil
}
// 2. Need to grow database file
if err := db.grow(count * db.pageSize); err != nil {
return nil, err
}
// 3. Possibly need to remap memory
if db.datasz < db.filesz {
if err := db.mmap(0); err != nil {
return nil, err
}
}
// 4. Allocate from end of file
return db.allocateAt(db.meta().pgid, count), nil
}
Freelist Coordination
Freelist management requires careful coordination với transactions:
func (db *DB) freepages(txid txid, pages []pgid) {
// Pages can't be immediately freed - other transactions
// might still be reading them. Mark as pending.
db.freelist.free(txid, pages)
}
func (db *DB) releasePages() {
// Find oldest active transaction
minTxid := db.oldestActiveTransaction()
// Release pages from transactions older than minTxid
db.freelist.release(minTxid)
}
Concurrency Control
Database Layer đảm bảo tuân thủ concurrency rules:
Single Writer Pattern
func (db *DB) beginRWTx() (*Tx, error) {
// Only one read-write transaction allowed
db.rwlock.Lock() // Will be released on tx.Commit() or tx.Rollback()
t := &Tx{
db: db,
writable: true,
meta: db.meta().copy(), // Private metadata copy
}
db.rwtx = t
return t, nil
}
Multiple Readers
func (db *DB) beginTx() (*Tx, error) {
// Multiple readers allowed, no locking needed
// MVCC provides isolation
t := &Tx{
db: db,
writable: false,
meta: db.meta(), // Shared metadata snapshot
}
// Add to active transactions list
db.metalock.Lock()
db.txs = append(db.txs, t)
db.metalock.Unlock()
return t, nil
}
Kết hợp tất cả mọi thứ
Hãy xem một ví dụ hoàn chỉnh về cách mà Database Layer điều phối tất cả các thành phần:
// User code
err := db.Update(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte("users"))
if err != nil {
return err
}
return bucket.Put([]byte("john"), []byte(`{"name":"John","age":30}`))
})
Behind the scenes:
- DB Layer bắt đầu một managed transaction
- Transaction Layer acquire rwlock, tạo MVCC snapshot
- Bucket Layer tìm hoặc tạo bucket trong B+ Tree
- Storage Layer allocate pages nếu cần (qua freelist)
- Data Structure cập nhật B+ tree nodes
- Transaction Layer thu thập các dirty page
- DB Layer điều phối quá trình commit
- Storage Layer ghi các page vào disk theo thứ tự được sắp xếp
- DB Layer sync dữ liệu từ application vào disk
- Transaction Layer release rwlock
Quy trình được mô tả chung theo kiến trúc sau:

Kết - Hành trình tìm 7 viên ngọc rồng kết thúc
Qua 7 phần, chúng ta đã hoàn thành cuộc hành trình khám phá Bolt DB và cách tạo ra một K-V Database đơn giản từ A tới Á:
- P1 - Database File Format: Foundation - cách tổ chức metadata, index và raw data trong một file trên disk
- P2 - Storage and Cache Management: Memory management với mmap và freelist cho disk space
- P3 - Data Structure: Triển khai B+ Tree với cơ chế rebalance/spill
- P4 - Bucket: Quản lý các namespace cùng với kĩ thuật inline-bucket để tối ưu disk space.
- P5 - Cursor: Truy cập dữ liệu hiệu quả và iteration patterns
- P6 - Transaction: Tuân thủ ACID, MVCC và concurrency control
- P7 - Database Layer: Điều phối tất cả các thành phần thành một cái Database system hoàn chỉnh
Một vài điểm chính
Đơn giản mà tinh tế: Bolt cung cấp các API đơn giản nhưng đằng sau là các kĩ thuật tinh tế. Single file format, coarse-grained locking và OS page cache delegation tạo ra một giải pháp đủ xịn để giải quyết các vấn đề.
Kĩ thuật tích hợp các thành phần: Mỗi thành phần có trách nhiệm rõ ràng nhưng lại hoạt động cùng nhau 1 cách mướt mườn mượt.
Hiệu suất thông qua cách thiết kế: Bolt đạt được gud performance không qua micro-optimizations mà qua cách lựa chọn kĩ thuật: memory mapping, sequential I/O, batch operations và zero-copy access.
ACID thông qua sự phối hợp: Các thuộc tính này của Transaction đạt được từ sự phối hợp giữa các phần: freelist cho isolation, dual metadata cho durability, single-writer cho consistency.
Cuối cùng, việc mình tìm hiểu sâu vào trong hệ thống để:
- Đưa ra quyết định có căn cứ khi chọn cơ sở dữ liệu
- Debug mấy cái vấn đề về hiệu suất một cách hiệu quả
- Làm cái gì cũng có trade-off, nhưng biết chọn đánh đổi nào là phù hợp
- Thiết kế hệ thống tốt hơn lấy cảm hứng từ mấy cái đã được cải tiến và chứng minh
Gud job Bolt - một masterclass trong việc cân bằng các trade-off.