Skip to content

kydenul/kcache

Repository files navigation

kcache

Go Reference Go Report Card License

A thread-safe in-memory key-value cache library for Go with expiration support and automatic cleanup.

Features

  • Thread-Safe: All operations are safe for concurrent use by multiple goroutines
  • Automatic Expiration: Set expiration times for cache items
  • Background Cleanup: Optional automatic cleanup of expired items via janitor goroutine
  • Type Flexible: Store values of any type
  • Numeric Operations: Increment and decrement numeric values atomically
  • Persistence: Serialize and deserialize cache data using Gob encoding
  • Eviction Callbacks: Set callbacks for when items are evicted from the cache
  • Batch Operations: Efficiently set, get, or delete multiple items in a single operation
  • Cache Statistics: Monitor cache performance with hit/miss rates and eviction counts
  • Key Pattern Operations: Find and delete keys by prefix for bulk management
  • TTL Management: Update expiration times and query remaining time-to-live
  • Atomic Operations: Thread-safe GetOrSet and CompareAndSwap operations

Installation

go get github.com/kydenul/kcache

Quick Start

package main

import (
    "fmt"
    "time"
    "github.com/kydenul/kcache"
)

func main() {
    // Create a cache with 5 minute default expiration and cleanup every 10 minutes
    c := kcache.New(5*time.Minute, 10*time.Minute)

    // Set a value with default expiration
    c.Set("key", "value", kcache.DefaultExpiration)

    // Get a value
    if val, found := c.Get("key"); found {
        fmt.Println(val)
    }

    // Set a value that never expires
    c.Set("permanent", "data", kcache.NoExpiration)

    // Set a value with custom expiration
    c.Set("custom", "expires in 1 minute", 1*time.Minute)
}

Core Features

Basic Operations

// Create a cache
c := kcache.New(5*time.Minute, 10*time.Minute)

// Set a value
c.Set("key", "value", kcache.DefaultExpiration)
c.SetDefault("key", "value") // Use default expiration

// Get a value
value, found := c.Get("key")

// Get a value with its expiration time
value, expiration, found := c.GetWithExpiration("key")

// Delete a value
c.Delete("key")

// Clear all cache items
c.Flush()

// Get the number of items in cache
count := c.ItemCount()

Conditional Operations

// Add only if the key doesn't exist
err := c.Add("key", "value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemExists) {
    // Key already exists
}

// Replace only if the key exists
err := c.Replace("key", "new value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemNotFound) {
    // Key doesn't exist
}

Numeric Operations

// Set a counter
c.Set("counter", 10, kcache.NoExpiration)

// Increment the value
err := c.Increment("counter", 5)  // counter is now 15

// Decrement the value
err := c.Decrement("counter", 3)  // counter is now 12

// Supported numeric types:
// int, int8, int16, int32, int64
// uint, uintptr, uint8, uint16, uint32, uint64
// float32, float64

Expiration Management

// Manually delete all expired items
c.DeleteExpired()

// Set an eviction callback
c.OnEvicted(func(key string, value any) {
    fmt.Printf("Item %s was evicted\n", key)
})

Persistence

// Save to file
err := c.WriteFile("/tmp/cache.dat")

// Load from file
err := c.LoadFile("/tmp/cache.dat")

// Use io.Writer/io.Reader
var buf bytes.Buffer
err := c.Write(&buf)
err := c.Load(&buf)

Iterating Over Cache

// Get all non-expired items
items := c.Items()
for key, item := range items {
    fmt.Printf("%s: %v (expires: %v)\n", key, item.Object, item.Expiration)
}

Advanced Usage

Creating Cache from Existing Data

items := map[string]kcache.Item{
    "key1": {
        Object:     "value1",
        Expiration: 0, // Never expires
    },
    "key2": {
        Object:     "value2",
        Expiration: time.Now().Add(5 * time.Minute).UnixNano(),
    },
}

c := kcache.NewFrom(5*time.Minute, 10*time.Minute, items)

Disabling Automatic Cleanup

// No background cleanup, must call DeleteExpired() manually
c := kcache.New(5*time.Minute, 0)

// Manual cleanup
c.DeleteExpired()

Cache Without Expiration

// Create a cache with no default expiration
c := kcache.New(kcache.NoExpiration, 0)

// All items won't expire unless explicitly set
c.Set("key", "value", kcache.DefaultExpiration) // Won't expire
c.Set("temp", "data", 1*time.Minute)            // Expires in 1 minute

Advanced Features

Batch Operations

Batch operations are more efficient than individual operations because they acquire the lock only once:

// Set multiple items at once
items := map[string]any{
    "user:1": "Alice",
    "user:2": "Bob",
    "user:3": "Charlie",
}
c.SetMultiple(items, 5*time.Minute)

// Get multiple items at once
keys := []string{"user:1", "user:2", "user:3"}
results := c.GetMultiple(keys)
for key, value := range results {
    fmt.Printf("%s: %v\n", key, value)
}

// Delete multiple items at once
c.DeleteMultiple(keys)

Cache Statistics

Monitor cache performance to optimize your application:

// Get current statistics
stats := c.Stats()
fmt.Printf("Hits: %d\n", stats.Hits)
fmt.Printf("Misses: %d\n", stats.Misses)
fmt.Printf("Evictions: %d\n", stats.Evictions)
fmt.Printf("Items: %d\n", stats.Items)
fmt.Printf("Hit Rate: %.2f%%\n", stats.HitRate*100)

// Reset statistics
c.ResetStats()

Key Pattern Operations

Manage related cache items using prefix-based operations:

// Get all keys in the cache
allKeys := c.Keys()
fmt.Printf("Total keys: %d\n", len(allKeys))

// Get all keys with a specific prefix
userKeys := c.KeysByPrefix("user:")
for _, key := range userKeys {
    fmt.Println(key)  // e.g., "user:1", "user:2", etc.
}

// Delete all keys with a specific prefix
deletedCount := c.DeleteByPrefix("session:")
fmt.Printf("Deleted %d session entries\n", deletedCount)

TTL Management

Update expiration times and query remaining time-to-live:

// Extend the expiration of an item (sliding expiration)
err := c.Touch("session:123", 30*time.Minute)
if errors.Is(err, kcache.ErrItemNotFound) {
    fmt.Println("Session not found or expired")
}

// Check how much time remains before expiration
ttl, err := c.GetTTL("session:123")
if err == nil {
    if ttl == -1 {
        fmt.Println("Session never expires")
    } else {
        fmt.Printf("Session expires in %v\n", ttl)
    }
}

Atomic Operations

Perform thread-safe conditional operations:

// Get existing value or set a new one atomically
value, wasSet := c.GetOrSet("counter", 0, kcache.NoExpiration)
if wasSet {
    fmt.Println("Counter initialized to 0")
} else {
    fmt.Printf("Counter already exists: %v\n", value)
}

// Compare-and-swap: update only if current value matches expected
success := c.CompareAndSwap("counter", 5, 6, kcache.NoExpiration)
if success {
    fmt.Println("Counter updated from 5 to 6")
} else {
    fmt.Println("Counter was not 5, update failed")
}

Performance Considerations

  • All operations use read-write locks for protection; read operations can execute concurrently
  • Background janitor runs in a separate goroutine and doesn't block main operations
  • Uses runtime.SetFinalizer to ensure the janitor stops when the cache is garbage collected
  • Adjust cleanup interval based on your use case to avoid overly frequent cleanup
  • Batch operations (SetMultiple, GetMultiple, DeleteMultiple) are significantly more efficient than individual operations when working with multiple items
  • Statistics tracking uses atomic operations for minimal performance overhead
  • Cache statistics are updated atomically without blocking cache operations

Best Practices

Use Batch Operations for Multiple Items

// ❌ Less efficient - multiple lock acquisitions
for _, key := range keys {
    c.Set(key, values[key], expiration)
}

// ✅ More efficient - single lock acquisition
c.SetMultiple(values, expiration)

Monitor Cache Performance

// Periodically check cache statistics to optimize expiration settings
ticker := time.NewTicker(1 * time.Minute)
go func() {
    for range ticker.C {
        stats := c.Stats()
        if stats.HitRate < 0.8 {
            log.Printf("Low hit rate: %.2f%% - consider adjusting TTL", stats.HitRate*100)
        }
    }
}()

Implement Sliding Expiration with Touch

// Extend expiration for actively used items
value, found := c.Get("session:123")
if found {
    // Reset the expiration timer
    c.Touch("session:123", 30*time.Minute)
}

Use Atomic Operations for Concurrent Updates

// Initialize a value only once across multiple goroutines
value, wasSet := c.GetOrSet("config", loadConfig(), kcache.NoExpiration)

// Safe concurrent counter updates
for {
    current, found := c.Get("counter")
    if !found {
        current = 0
    }
    if c.CompareAndSwap("counter", current, current.(int)+1, kcache.NoExpiration) {
        break // Successfully incremented
    }
    // Retry if another goroutine modified the value
}

Error Handling

The library defines the following error types:

kcache.ErrItemNotFound  // Item doesn't exist or has expired
kcache.ErrItemExists    // Item already exists (Add operation)
kcache.ErrNotNumeric    // Value is not a numeric type (Increment/Decrement operations)

Usage example:

err := c.Add("key", "value", kcache.DefaultExpiration)
if errors.Is(err, kcache.ErrItemExists) {
    // Handle key already exists
}

Thread Safety

All public methods are thread-safe and can be used safely from multiple goroutines:

c := kcache.New(5*time.Minute, 10*time.Minute)

// Concurrent access from multiple goroutines
for i := 0; i < 100; i++ {
    go func(n int) {
        c.Set(fmt.Sprintf("key%d", n), n, kcache.DefaultExpiration)
    }(i)
}

License

This project is licensed under the MIT License. See the LICENSE file for details.

Contributing

Issues and Pull Requests are welcome!

Acknowledgments

This project is inspired by go-cache.

About

In-Memory Cache

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors