commit 2918f0424901bd72ba060fce739357e527d0babe Author: kingecg Date: Sun Feb 23 12:46:27 2025 +0800 init diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af8b418 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.pyer.club/kingecg/gocache + +go 1.23.1 diff --git a/kvstore.go b/kvstore.go new file mode 100644 index 0000000..62d8000 --- /dev/null +++ b/kvstore.go @@ -0,0 +1,241 @@ +package gocache + +import ( + "bytes" + "encoding/json" + "os" + "sync" +) + +// KVStore represents a simple in-memory key/value store. +type KVStore struct { + mu sync.RWMutex + store map[string]string + file string + transaction *Transaction + logFile string // 添加日志文件路径 +} + +// Transaction represents an ongoing transaction +type Transaction struct { + store map[string]string +} + +// NewKVStore creates a new instance of KVStore. +func NewKVStore(file string) *KVStore { + store := &KVStore{ + store: make(map[string]string), + file: file, + logFile: file + ".log", + } + // 启动时自动恢复日志 + if err := store.RecoverFromLog(); err != nil { + panic(err) + } + return store +} + +// RecoverFromLog recovers data from log file +func (k *KVStore) RecoverFromLog() error { + k.mu.Lock() + defer k.mu.Unlock() + + // 检查日志文件是否存在 + if _, err := os.Stat(k.logFile); os.IsNotExist(err) { + return nil + } + + // 读取日志文件 + data, err := os.ReadFile(k.logFile) + if err != nil { + return err + } + + // 逐行处理日志 + lines := bytes.Split(data, []byte{'\n'}) + for _, line := range lines { + if len(line) == { + continue + } + var logEntry map[string]string + if err := json.Unmarshal(line, &logEntry); err != nil { + return err + } + switch logEntry["op"] { + case "put": + k.store[logEntry["key"]] = logEntry["value"] + case "delete": + delete(k.store, logEntry["key"]) + } + } + + // 清空日志文件 + return os.Truncate(k.logFile, 0) +} + +// SaveToFile saves the current store to a file. +func (k *KVStore) SaveToFile() error { + k.mu.RLock() + defer k.mu.RUnlock() + data, err := json.Marshal(k.store) + if err != nil { + return err + } + return os.WriteFile(k.file, data, 0644) +} + +// LogOperation logs a key/value operation to the log file +func (k *KVStore) LogOperation(op string, key, value string) error { + logEntry := map[string]string{ + "op": op, + "key": key, + "value": value, + } + data, err := json.Marshal(logEntry) + if err != nil { + return err + } + data = append(data, '\n') + f, err := os.OpenFile(k.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write(data) + return err +} + +// LoadFromFile loads the store from a file with partial loading support +func (k *KVStore) LoadFromFile(keys ...string) error { + k.mu.Lock() + defer k.mu.Unlock() + + // 先加载主数据文件 + if _, err := os.Stat(k.file); err == nil { + data, err := os.ReadFile(k.file) + if err != nil { + return err + } + + var tempStore map[string]string + if err := json.Unmarshal(data, &tempStore); err != nil { + return err + } + + // Partial loading + if len(keys) > 0 { + for _, key := range keys { + if value, exists := tempStore[key]; exists { + k.store[key] = value + } + } + } + } + + // 然后应用日志文件中的操作 + if _, err := os.Stat(k.logFile); err == nil { + data, err := os.ReadFile(k.logFile) + if err != nil { + return err + } + + lines := bytes.Split(data, []byte{'\n'}) + for _, line := range lines { + if len(line) == 0 { + continue + } + var logEntry map[string]string + if err := json.Unmarshal(line, &logEntry); err != nil { + return err + } + switch logEntry["op"] { + case "put": + k.store[logEntry["key"]] = logEntry["value"] + case "delete": + delete(k.store, logEntry["key"]) + } + } + } + + return nil +} + +// Put adds a key/value pair to the store. +func (k *KVStore) Put(key, value string) { + k.mu.Lock() + defer k.mu.Unlock() + k.store[key] = value + go func() { + k.mu.Lock() + defer k.mu.Unlock() + k.LogOperation("put", key, value) // 异步记录操作日志 + }() +} + +// Get retrieves the value associated with the given key. +func (k *KVStore) Get(key string) (string, bool) { + k.mu.RLock() + defer k.mu.RUnlock() + if k.transaction != nil { + value, exists := k.transaction.store[key] + if exists { + return value, true + } + } + value, exists := k.store[key] + return value, exists +} + +// Delete removes the key/value pair associated with the given key. +func (k *KVStore) Delete(key string) { + k.mu.Lock() + defer k.mu.Unlock() + delete(k.store, key) + go func() { + k.mu.Lock() + defer k.mu.Unlock() + k.LogOperation("delete", key, "") // 异步记录操作日志 + }() +} + +// BeginTransaction starts a new transaction +func (k *KVStore) BeginTransaction() { + k.mu.Lock() + defer k.mu.Unlock() + k.transaction = &Transaction{ + store: make(map[string]string), + } +} + +// Commit commits the current transaction +func (k *KVStore) Commit() error { + k.mu.Lock() + defer k.mu.Unlock() + if k.transaction == nil { + return nil + } + + for key, value := range k.transaction.store { + k.store[key] = value + } + k.transaction = nil + return nil +} + +// Rollback rolls back the current transaction +func (k *KVStore) Rollback() { + k.mu.Lock() + defer k.mu.Unlock() + k.transaction = nil +} + +// PutInTransaction puts a key/value pair in the current transaction +func (k *KVStore) PutInTransaction(key, value string) { + k.mu.Lock() + defer k.mu.Unlock() + if k.transaction != nil { + k.transaction.store[key] = value + } else { + k.store[key] = value + } +} diff --git a/kvstore_test.go b/kvstore_test.go new file mode 100644 index 0000000..550604b --- /dev/null +++ b/kvstore_test.go @@ -0,0 +1,106 @@ +package gocache + +import ( + "os" + "testing" +) + +func TestNewKVStore(t *testing.T) { + store := NewKVStore("test.db") + if store == nil { + t.Error("Expected a new KVStore instance, got nil") + } +} + +func TestPutAndGet(t *testing.T) { + store := NewKVStore("test.db") + store.Put("key1", "value1") + value, exists := store.Get("key1") + if !exists || value != "value1" { + t.Error("Expected value 'value1' for key 'key1'") + } +} + +func TestDelete(t *testing.T) { + store := NewKVStore("test.db") + store.Put("key1", "value1") + store.Delete("key1") + _, exists := store.Get("key1") + if exists { + t.Error("Expected key 'key1' to be deleted") + } +} + +func TestSaveAndLoadFromFile(t *testing.T) { + store := NewKVStore("test.db") + store.Put("key1", "value1") + err := store.SaveToFile() + if err != nil { + t.Error("Failed to save store to file") + } + + newStore := NewKVStore("test.db") + err = newStore.LoadFromFile() + if err != nil { + t.Error("Failed to load store from file") + } + + value, exists := newStore.Get("key1") + if !exists || value != "value1" { + t.Error("Expected value 'value1' for key 'key1' after loading from file") + } + + // Clean up + os.Remove("test.db") + os.Remove("test.db.log") +} + +func TestLogOperation(t *testing.T) { + store := NewKVStore("test.db") + err := store.LogOperation("put", "key1", "value1") + if err != nil { + t.Error("Failed to log operation") + } + + // Clean up + os.Remove("test.db.log") +} + +func TestTransaction(t *testing.T) { + store := NewKVStore("test.db") + store.BeginTransaction() + store.PutInTransaction("key1", "value1") + store.Commit() + + value, exists := store.Get("key1") + if !exists || value != "value1" { + t.Error("Expected value 'value1' for key 'key1' after commit") + } + + store.BeginTransaction() + store.PutInTransaction("key2", "value2") + store.Rollback() + + _, exists = store.Get("key2") + if exists { + t.Error("Expected key 'key2' to be rolled back") + } + + // 新增测试:验证事务中的PutInTransaction + store.BeginTransaction() + store.PutInTransaction("key3", "value3") + value, exists = store.Get("key3") + if !exists || value != "value3" { + t.Error("Expected value 'value3' for key 'key3' during transaction") + } + store.Commit() + + value, exists = store.Get("key3") + if !exists || value != "value3" { + t.Error("Expected value 'value3' for key 'key3' after commit") + } + + // Clean up + os.Remove("test.db") + os.Remove("test.db.log") +}