go-cache

repository·master·Indexed 41 days ago

https://github.com/viney-shih/go-cache

A flexible, multi-layered caching library for Go that coordinates between local in-memory caches and shared distributed caches like Redis. It implements the Cache-Aside pattern and utilizes Pub-Sub to maintain consistency across distributed instances. Features include support for custom Adapter interfaces, bulk refills via MGetter, and a GetByFunc method to automate cache refilling on misses.

Tokens
9.4K
Snippets
41
Records
48
Agent score
99%

What's inside go-cache

  1. How go-cache works: Multi-layered caching and Cache-Aside

    master

    go-cache is a multi-layered caching library that coordinates between a private (in-memory) cache and a shared cache (e.g., Redis).

    Cache-Aside Data Flow

    When an application requests data via Cache.Get() or Cache.MGet():

    1. Local Cache Hit: Data is returned immediately from the private in-memory cache.
    2. Local Miss / Shared Hit: If the local cache misses but the shared cache (Redis) hits, the data is fetched from the shared cache and then stored in the local cache for future requests.
    3. Total Miss: If both caches miss, the library executes a provided getter function (e.g., a database query). The resulting data is then stored in both the shared cache and the local cache before being returned to the application.

    Cache Eviction

    When Cache.Del() is called:

    1. The key is deleted from the shared cache.
    2. The key is deleted from the local cache.
    3. A Pub-Sub message is broadcast to other distributed instances to ensure their local caches are also evicted, maintaining consistency across the system.
  2. Initialize the Cache Factory

    master

    To use go-cache, follow the Singleton pattern: initialize a Factory in your main.go and pass it to your business logic. You need to define your local cache (e.g., TinyLFU) and your shared cache (e.g., Redis).

    // Initialize the Factory in main.go
    tinyLfu := cache.NewTinyLFU(10000)
    rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{
        Addrs: map[string]string{
            "server1": ":6379",
        },
    }))
    
    cacheFactory := cache.NewFactory(rds, tinyLfu)
  3. Understand Cache Types

    master

    The Type enum determines which layers are used in a multi-layer cache structure:

    • NoneType: No cache layer.
    • SharedCacheType: Distributed caching (e.g., Redis, Memcached). Ensures consistency across multiple application instances.
    • LocalCacheType: In-memory caching within a single process. Fast, but limited by the process's memory and requires eviction policies.
  4. Configure Factory options

    master

    When initializing a Factory (via the internal newFactory which is typically called by a package-level constructor), you can provide FactoryOptions to configure global behaviors for all caches created by that factory.

    Supported options include:

    • Serialization: Global marshalFunc and unmarshalFunc (must be provided as a pair).
    • Callbacks: Global event hooks that trigger when cache events occur across any cache created by this factory:
      • onCacheHit(prefix string, key string, count int)
      • onCacheMiss(prefix string, key string, count int)
      • onLCCostAdd(prefix string, key string, cost int)
      • onLCCostEvict(prefix string, key string, cost int)
    • PubSub: A pubsub mechanism used by the internal message broker to handle cache eviction events across distributed instances.
  5. Understand event types and topics

    master

    The go-cache library uses an enumeration of eventType to communicate via a Pub/Sub system. Each eventType is associated with a specific string-based topic. You can retrieve the topic name for a given event type by calling the .Topic() method.

    Currently supported event types:

    • None: The default state for unregistered events.
    • Evict: Represents an eviction event.
    // Example of getting a topic for an event type
    topic := cache.EventTypeEvict.Topic()
  6. Advanced usage: GetByFunc (Cache-Aside)

    master

    The GetByFunc() method simplifies the Cache-Aside pattern. You provide a getter function that is automatically executed if the key is missing from both local and shared caches. This function is responsible for refilling the cache.

    You can also provide custom MarshalFunc and UnmarshalFunc (e.g., using msgpack) within the Setting to control data serialization.

    func ExampleCache_GetByFunc() {
        // Create a cache group named "get-by-func" using only the local cache.
        c := cacheFactory.NewCache([]cache.Setting{
            {
                Prefix: "get-by-func",
                CacheAttributes: map[cache.Type]cache.Attribute{
                    cache.LocalCacheType: {TTL: 10 * time.Minute},
                },
                MarshalFunc:   msgpack.Marshal,
                UnmarshalFunc: msgpack.Unmarshal,
            },
        })
    
        ctx := context.TODO()
        container2 := &Object{}
    
        // GetByFunc automatically refills the cache if it misses
        err := c.GetByFunc(ctx, "get-by-func", "key2", container2, func() (interface{}, error) {
            // This logic runs on cache miss (e.g., DB query)
            return Object{Str: "value2", Num: 2}, nil
        })
        if err != nil {
            panic("not expected")
        }
    
        fmt.Println(container2) // Output: &{value2 2}
    }
  7. Advanced usage: MGetter for bulk refills

    master

    If you need to handle multiple keys at once during a cache miss, you can define an MGetter function in your cache.Setting. The MGetter must return a slice of values where the order of items corresponds to the order of the input keys.

    func ExampleService_Create_mGetter() {
        // Create a cache group named "mgetter" using both shared and local caches.
        c := cacheFactory.NewCache([]cache.Setting{
            {
                Prefix: "mgetter",
                CacheAttributes: map[cache.Type]cache.Attribute{
                    cache.SharedCacheType: {TTL: time.Hour},
                    cache.LocalCacheType:  {TTL: 10 * time.Minute},
                },
                MGetter: func(keys ...string) (interface{}, error) {
                    // Refill logic for multiple keys. 
                    // Return a slice where order matches 'keys'.
                    return []Object{{Str: "value3", Num: 3}}, nil
                },
                MarshalFunc:   cache.Marshal,
                UnmarshalFunc: cache.Unmarshal,
            },
        })
    
        ctx := context.TODO()
        container3 := &Object{}
        if err := c.Get(ctx, "mgetter", "key3", container3); err != nil {
            panic("not expected")
        }
    
        fmt.Println(container3) // Output: &{value3 3}
    }
  8. Basic usage: Set-And-Get

    master

    You can treat a cache group as a standard key-value store. Use NewCache with a Setting to define a prefix and specific CacheAttributes (like TTL) for different cache types (Local vs Shared).

    type Object struct {
        Str string
        Num int
    }
    
    func Example_setAndGetPattern() {
        // Create a cache group named "set-and-get" using only the shared cache.
        c := cacheFactory.NewCache([]cache.Setting{
            {
                Prefix: "set-and-get",
                CacheAttributes: map[cache.Type]cache.Attribute{
                    cache.SharedCacheType: {TTL: 10 * time.Second},
                },
            },
        })
    
        ctx := context.TODO()
    
        // Set the cache
        obj := &Object{Str: "value1", Num: 1}
        if err := c.Set(ctx, "set-and-get", "key", obj); err != nil {
            panic("not expected")
        }
    
        // Read the cache
        container := &Object{}
        if err := c.Get(ctx, "set-and-get", "key", container); err != nil {
            panic("not expected")
        }
        fmt.Println(container) // Output: &{value1 1}
    }
  9. Configure cache layers and TTL with Setting

    master

    A Setting defines how a specific group of keys (identified by a Prefix) behaves. You can define different Attribute configurations (like TTL) for different Type layers (e.g., LocalCacheType vs SharedCacheType).

    Key fields in Setting:

    • Prefix: The unique identifier for the cache group.
    • CacheAttributes: A map linking Type to Attribute (e.g., TTL).
    • MGetter: A MGetterFunc used to reload multiple keys during an MGet cache miss.
    • MarshalFunc / UnmarshalFunc: Custom functions for data serialization.
    setting := cache.Setting{
        Prefix: "session",
        CacheAttributes: map[cache.Type]cache.Attribute{
            cache.LocalCacheType:  {TTL: time.Minute * 1},
            cache.SharedCacheType: {TTL: time.Hour * 24},
        },
        MGetter: func(keys ...string) (interface{}, error) {
            // Implementation to fetch multiple keys from DB
            return values, nil
        },
    }
  10. Use the Pubsub interface to manage message queues

    master

    The Pubsub interface allows you to interact with a message queue for publishing and subscribing to cache events.

    • Pub(context context.Context, topic string, message []byte) error: Publishes a byte slice message to a specific topic.
    • Sub(context context.Context, topic ...string) <-chan Message: Subscribes to one or more topics and returns a read-only channel of Message objects.
    • Close(): Closes the subscription. This should be called if Sub() was used to ensure resources are cleaned up. The implementation should safely handle cases where Sub() was never called.
    // Example of using Pubsub
    var ps cache.Pubsub
    
    // Subscribing to a topic
    msgs := ps.Sub(ctx, "cache-eviction")
    
    // Consuming messages
    go func() {
        for msg := range msgs {
            fmt.Printf("Received on %s: %s\n", msg.Topic(), string(msg.Content()))
        }
    }()
    
    // Publishing a message
    err := ps.Pub(ctx, "cache-eviction", []byte("key-123"))
    if err != nil {
        // handle error
    }
    
    // Cleanup
    ps.Close()
  11. Perform bulk operations with MGet and MSet

    master

    For high-performance batch operations, use MGet and MSet:

    • MGet: Retrieves multiple values. If a cache miss occurs, it attempts to reload values using the MGetter function defined in the Setting for that prefix. It returns a Result object which must be iterated over.
    • MSet: Sets multiple key-value pairs at once using a map[string]interface{}.
    // MGet usage
    results, err := myCache.MGet(ctx, "user_profile", "user_1", "user_2")
    if err == nil {
        for i := 0; i < results.Len(); i++ {
            var user User
            if err := results.Get(ctx, i, &user); err != nil {
                // handle error
            }
        }
    }
    
    // MSet usage
    err := myCache.MSet(ctx, "user_profile", map[string]interface{}{
        "user_1": user1,
        "user_2": user2,
    })