A Go LRU cache library with distributed cache invalidation, based on hashicorp/golang-lru.
- Drop-in replacement for
hashicorp/golang-lruwith the same API - Distributed cache invalidation via pub/sub
- Support for both standard LRU and expirable (TTL-based) caches
- Pluggable broker interface for different message transports
- Built-in serializers for string and JSON keys
- Cascade prevention (events don't re-publish)
go get github.com/vogo/vlrupackage main
import (
"github.com/vogo/vlru"
"github.com/vogo/vlru/examples/inmemory"
"github.com/vogo/vogo/vsync/vrun"
)
func main() {
// Configure global broker (once at startup)
runner := vrun.New()
defer runner.Stop()
broker := inmemory.New()
vlru.StartEventBroker(runner, broker)
// Create cache - same API as hashicorp/golang-lru
// Cache name is auto-generated from call site (file:function:line)
cache, _ := vlru.New[string, User](1000)
defer cache.Close()
// Use normally - invalidations auto-propagate
cache.Add("user:1", user)
cache.Remove("user:1") // publishes event, other instances remove key
}When one cache instance evicts or removes a key, it publishes an invalidation event via the configured Broker. Other instances receive the event and remove the key locally without re-publishing (preventing cascade).
Publish Path (local eviction → remote invalidation):
Add() causes eviction → onEvict callback
→ serialize key
→ Broker.Publish(event)
→ other instances receive via their broker
→ broker routes to registry
→ registry calls cache.InvalidateKey()
Receive Path (remote event → local removal):
Broker receives event from network
→ routes to registry
→ registry checks Instance != self
→ finds cache by CacheName
→ cache.InvalidateKey() with suppressPublish=true
→ NO cascade event published
// StartEventBroker sets the global broker and starts the event loop
runner := vrun.New()
vlru.StartEventBroker(runner, broker)Cache names are auto-generated from the call site (file:function:line). This means:
- Same code location → same cache name → events sync correctly
- Different code locations → different cache names → events don't cross
For distributed invalidation to work between multiple cache instances, they must share the same cache name. Use WithCacheName to override the auto-generated name:
// Two caches that should sync must have the same name
cache1, _ := vlru.New[string, User](1000, vlru.WithCacheName[string, User]("users"))
cache2, _ := vlru.New[string, User](1000, vlru.WithCacheName[string, User]("users"))// Create a cache with capacity of 1000 items
// Cache name is auto-generated from call site
cache, err := vlru.New[string, User](1000)
// Create with eviction callback
cache, err := vlru.NewWithEvict[string, User](1000, func(key string, value User) {
log.Printf("evicted: %s", key)
})
// With custom options
cache, err := vlru.New[string, User](1000,
vlru.WithCacheName[string, User]("users"), // override auto-name
vlru.WithSerializer[string, User](vlru.StringKeySerializer{}),
)import "github.com/vogo/vlru/vexpirable"
// Create cache with 5-minute TTL
cache := vexpirable.NewLRU[string, User](1000, nil, 5*time.Minute)
defer cache.Close()
// With eviction callback
cache := vexpirable.NewLRU[string, User](1000, func(key string, value User) {
log.Printf("expired or evicted: %s", key)
}, 5*time.Minute)
// With custom options
cache := vexpirable.NewLRU[string, User](1000, nil, 5*time.Minute,
vexpirable.WithCacheName[string, User]("users"),
)All standard hashicorp/golang-lru methods are supported:
cache.Add(key, value) // Add or update
cache.Get(key) // Get with LRU update
cache.Peek(key) // Get without LRU update
cache.Contains(key) // Check existence
cache.Remove(key) // Remove (publishes event)
cache.Len() // Current size
cache.Keys() // All keys
cache.Values() // All values
cache.Purge() // Clear all
cache.Resize(newSize) // Change capacity
cache.Close() // Cleanup and unregisterThe Broker interface handles event publishing and receiving:
type Broker interface {
// Publish sends an invalidation event to other instances
Publish(ctx context.Context, event *InvalidationEvent) error
// StartReceive starts the event receiver and returns a channel for receiving
// invalidation events from other instances.
// The runner is used to manage the receiver lifecycle.
StartReceive(runner *vrun.Runner) <-chan *InvalidationEvent
// Close releases resources
Close() error
}runner := vrun.New()
defer runner.Stop()
broker := inmemory.New()
vlru.StartEventBroker(runner, broker)import (
"context"
"encoding/json"
"github.com/redis/go-redis/v9"
"github.com/vogo/vlru"
"github.com/vogo/vogo/vlog"
"github.com/vogo/vogo/vsync/vrun"
)
const vlruTopic = "vlru:invalidation"
// RedisBroker implements vlru.Broker using Redis pub/sub for cache invalidation.
type RedisBroker struct {
cli *redis.Client
pubsub *redis.PubSub
receiveChan chan *vlru.InvalidationEvent
receiveRunner *vrun.Runner
}
// NewRedisBroker creates a new RedisBroker with the given Redis client.
func NewRedisBroker(cli *redis.Client) *RedisBroker {
return &RedisBroker{
cli: cli,
receiveChan: make(chan *vlru.InvalidationEvent, 100),
}
}
// StartReceive starts the event receiver and returns a channel for receiving
// invalidation events from other instances.
func (b *RedisBroker) StartReceive(runner *vrun.Runner) <-chan *vlru.InvalidationEvent {
ctx := context.Background()
b.pubsub = b.cli.Subscribe(ctx, vlruTopic)
msgCh := b.pubsub.Channel()
b.receiveRunner = runner.NewChild()
b.receiveRunner.Loop(func() {
select {
case msg, ok := <-msgCh:
if !ok {
vlog.Infof("vlru pubsub channel closed")
return
}
vlog.Debugf("vlru pubsub message received | payload: %s", msg.Payload)
var event vlru.InvalidationEvent
if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil {
vlog.Errorf("failed to unmarshal vlru event | payload: %s | err: %v", msg.Payload, err)
return
}
b.receiveChan <- &event
case <-b.receiveRunner.C:
return
}
})
return b.receiveChan
}
// Publish sends an invalidation event to other instances via Redis pub/sub.
func (b *RedisBroker) Publish(ctx context.Context, event *vlru.InvalidationEvent) error {
data, err := json.Marshal(event)
if err != nil {
return err
}
return b.cli.Publish(ctx, vlruTopic, data).Err()
}
// Close releases resources held by the broker.
func (b *RedisBroker) Close() error {
if b.pubsub != nil {
return b.pubsub.Close()
}
if b.receiveRunner != nil {
b.receiveRunner.Stop()
}
return nil
}
// InitVlru initializes the vlru event broker with a Redis-backed broker.
func InitVlru(runner *vrun.Runner) {
broker := NewRedisBroker(getCurrentRedisClient())
vlru.StartEventBroker(runner, broker)
}Keys must be serializable for network transport. By default, the following key types are supported without configuration:
string- zero-copy serializationint- converted viastrconvint64- converted viastrconv- Other types - JSON encoding (fallback)
Built-in serializers for explicit configuration:
StringKeySerializer- for string keys (zero-copy)JSONKeySerializer[K]- for any JSON-serializable key type
Custom serializers implement KeySerializer[K]:
type KeySerializer[K comparable] interface {
Serialize(key K) (string, error)
Deserialize(s string) (K, error)
}Events are published for:
- Explicit
Remove()calls - LRU capacity evictions (when cache is full)
- TTL expirations (expirable variant)
Events are NOT published for:
Add()operations (just local)Purge()operations (just local)- Remote invalidations (prevents cascade)
Apache 2.0