feat: 请求日志记录

This commit is contained in:
tbphp
2025-07-12 10:15:07 +08:00
parent 70554b8fe5
commit 13d7c4dbad
13 changed files with 411 additions and 115 deletions

View File

@@ -14,12 +14,9 @@ type memoryStoreItem struct {
}
// MemoryStore is an in-memory key-value store that is safe for concurrent use.
// It now supports simple K/V, HASH, and LIST data types.
type MemoryStore struct {
mu sync.RWMutex
data map[string]any
// For Pub/Sub
mu sync.RWMutex
data map[string]any
muSubscribers sync.RWMutex
subscribers map[string]map[chan *Message]struct{}
}
@@ -30,14 +27,11 @@ func NewMemoryStore() *MemoryStore {
data: make(map[string]any),
subscribers: make(map[string]map[chan *Message]struct{}),
}
// The cleanup loop was removed as it's not compatible with multiple data types
// without a unified expiration mechanism, and the KeyPool feature does not rely on TTLs.
return s
}
// Close cleans up resources.
func (s *MemoryStore) Close() error {
// Nothing to close for now.
return nil
}
@@ -73,9 +67,7 @@ func (s *MemoryStore) Get(key string) ([]byte, error) {
return nil, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
}
// Check for expiration
if item.expiresAt > 0 && time.Now().UnixNano() > item.expiresAt {
// Lazy deletion
s.mu.Lock()
delete(s.data, key)
s.mu.Unlock()
@@ -93,6 +85,16 @@ func (s *MemoryStore) Delete(key string) error {
return nil
}
// Del removes multiple values by their keys.
func (s *MemoryStore) Del(keys ...string) error {
s.mu.Lock()
defer s.mu.Unlock()
for _, key := range keys {
delete(s.data, key)
}
return nil
}
// Exists checks if a key exists.
func (s *MemoryStore) Exists(key string) (bool, error) {
s.mu.RLock()
@@ -103,10 +105,8 @@ func (s *MemoryStore) Exists(key string) (bool, error) {
return false, nil
}
// Check for expiration only if it's a simple K/V item
if item, ok := rawItem.(memoryStoreItem); ok {
if item.expiresAt > 0 && time.Now().UnixNano() > item.expiresAt {
// Lazy deletion
s.mu.Lock()
delete(s.data, key)
s.mu.Unlock()
@@ -122,12 +122,10 @@ func (s *MemoryStore) SetNX(key string, value []byte, ttl time.Duration) (bool,
s.mu.Lock()
defer s.mu.Unlock()
// In memory store, we need to manually check for existence and expiration
rawItem, exists := s.data[key]
if exists {
if item, ok := rawItem.(memoryStoreItem); ok {
if item.expiresAt == 0 || time.Now().UnixNano() < item.expiresAt {
// Key exists and is not expired
return false, nil
}
} else {
@@ -179,7 +177,6 @@ func (s *MemoryStore) HGetAll(key string) (map[string]string, error) {
rawHash, exists := s.data[key]
if !exists {
// Per Redis convention, HGETALL on a non-existent key returns an empty map, not an error.
return make(map[string]string), nil
}
@@ -188,7 +185,6 @@ func (s *MemoryStore) HGetAll(key string) (map[string]string, error) {
return nil, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
}
// Return a copy to prevent race conditions on the returned map
result := make(map[string]string, len(hash))
for k, v := range hash {
result[k] = v
@@ -265,9 +261,7 @@ func (s *MemoryStore) LRem(key string, count int64, value any) error {
strValue := fmt.Sprint(value)
newList := make([]string, 0, len(list))
// LREM with count = 0: Remove all elements equal to value.
if count != 0 {
// For now, only implement count = 0 behavior as it's what we need.
return fmt.Errorf("LRem with non-zero count is not implemented in MemoryStore")
}
@@ -298,7 +292,6 @@ func (s *MemoryStore) Rotate(key string) (string, error) {
return "", ErrNotFound
}
// "RPOP"
lastIndex := len(list) - 1
item := list[lastIndex]
@@ -309,6 +302,63 @@ func (s *MemoryStore) Rotate(key string) (string, error) {
return item, nil
}
// --- SET operations ---
// SAdd adds members to a set.
func (s *MemoryStore) SAdd(key string, members ...any) error {
s.mu.Lock()
defer s.mu.Unlock()
var set map[string]struct{}
rawSet, exists := s.data[key]
if !exists {
set = make(map[string]struct{})
s.data[key] = set
} else {
var ok bool
set, ok = rawSet.(map[string]struct{})
if !ok {
return fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
}
}
for _, member := range members {
set[fmt.Sprint(member)] = struct{}{}
}
return nil
}
// SPopN randomly removes and returns the given number of members from a set.
func (s *MemoryStore) SPopN(key string, count int64) ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
rawSet, exists := s.data[key]
if !exists {
return []string{}, nil
}
set, ok := rawSet.(map[string]struct{})
if !ok {
return nil, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
}
if count > int64(len(set)) {
count = int64(len(set))
}
popped := make([]string, 0, count)
for member := range set {
if int64(len(popped)) >= count {
break
}
popped = append(popped, member)
delete(set, member)
}
return popped, nil
}
// --- Pub/Sub operations ---
// memorySubscription implements the Subscription interface for the in-memory store.
@@ -350,11 +400,10 @@ func (s *MemoryStore) Publish(channel string, message []byte) error {
if subs, ok := s.subscribers[channel]; ok {
for subCh := range subs {
// Non-blocking send
go func(c chan *Message) {
select {
case c <- msg:
case <-time.After(1 * time.Second): // Prevent goroutine leak if receiver is stuck
case <-time.After(1 * time.Second):
}
}(subCh)
}

View File

@@ -42,6 +42,14 @@ func (s *RedisStore) Delete(key string) error {
return s.client.Del(context.Background(), key).Err()
}
// Del removes multiple values from Redis.
func (s *RedisStore) Del(keys ...string) error {
if len(keys) == 0 {
return nil
}
return s.client.Del(context.Background(), keys...).Err()
}
// Exists checks if a key exists in Redis.
func (s *RedisStore) Exists(key string) (bool, error) {
val, err := s.client.Exists(context.Background(), key).Result()
@@ -96,6 +104,16 @@ func (s *RedisStore) Rotate(key string) (string, error) {
return val, nil
}
// --- SET operations ---
func (s *RedisStore) SAdd(key string, members ...any) error {
return s.client.SAdd(context.Background(), key, members...).Err()
}
func (s *RedisStore) SPopN(key string, count int64) ([]string, error) {
return s.client.SPopN(context.Background(), key, count).Result()
}
// --- Pipeliner implementation ---
type redisPipeliner struct {
@@ -121,7 +139,7 @@ func (s *RedisStore) Pipeline() Pipeliner {
}
// Eval executes a Lua script on Redis.
func (s *RedisStore) Eval(script string, keys []string, args ...interface{}) (interface{}, error) {
func (s *RedisStore) Eval(script string, keys []string, args ...any) (any, error) {
return s.client.Eval(context.Background(), script, keys, args...).Result()
}
@@ -165,7 +183,6 @@ func (s *RedisStore) Publish(channel string, message []byte) error {
func (s *RedisStore) Subscribe(channel string) (Subscription, error) {
pubsub := s.client.Subscribe(context.Background(), channel)
// Wait for confirmation that subscription is created.
_, err := pubsub.Receive(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to subscribe to channel %s: %w", channel, err)

View File

@@ -16,34 +16,28 @@ type Message struct {
// Subscription represents an active subscription to a pub/sub channel.
type Subscription interface {
// Channel returns the channel for receiving messages.
Channel() <-chan *Message
// Close unsubscribes and releases any resources associated with the subscription.
Close() error
}
// Store is a generic key-value store interface.
// Implementations of this interface must be safe for concurrent use.
type Store interface {
// Set stores a key-value pair with an optional TTL.
// - key: The key (string).
// - value: The value ([]byte).
// - ttl: The expiration time. If ttl is 0, the key never expires.
Set(key string, value []byte, ttl time.Duration) error
// Get retrieves a value by its key.
// It must return store.ErrNotFound if the key does not exist.
Get(key string) ([]byte, error)
// Delete removes a value by its key.
// If the key does not exist, this operation should be considered successful (idempotent) and not return an error.
Delete(key string) error
// Del deletes multiple keys.
Del(keys ...string) error
// Exists checks if a key exists in the store.
Exists(key string) (bool, error)
// SetNX sets a key-value pair if the key does not already exist.
// It returns true if the key was set, false otherwise.
SetNX(key string, value []byte, ttl time.Duration) (bool, error)
// HASH operations
@@ -56,6 +50,10 @@ type Store interface {
LRem(key string, count int64, value any) error
Rotate(key string) (string, error)
// SET operations
SAdd(key string, members ...any) error
SPopN(key string, count int64) ([]string, error)
// Close closes the store and releases any underlying resources.
Close() error
@@ -63,7 +61,6 @@ type Store interface {
Publish(channel string, message []byte) error
// Subscribe listens for messages on a given channel.
// It returns a Subscription object that can be used to receive messages and to close the subscription.
Subscribe(channel string) (Subscription, error)
}
@@ -80,5 +77,5 @@ type RedisPipeliner interface {
// LuaScripter is an optional interface that a Store can implement to provide Lua script execution.
type LuaScripter interface {
Eval(script string, keys []string, args ...interface{}) (interface{}, error)
Eval(script string, keys []string, args ...any) (any, error)
}