RocksCache

repository·main·Indexed 20 days ago

https://github.com/dtm-labs/rockscache

A Redis-based Go library designed to ensure eventual and strong consistency between a database and a cache. It provides built-in protections against cache breakdown, penetration, and avalanche effects. Key features include a cache-aside pattern via Fetch and FetchBatch, a 'TagAsDeleted' management policy for invalidation, and configurable consistency modes to balance performance and data accuracy.

Tokens
4.5K
Snippets
19
Records
24
Agent score
68%

What's inside rockscache

  1. How RocksCache prevents common cache issues

    main

    RocksCache includes built-in protections against several common distributed caching problems:

    Anti-Breakdown (Cache Breakdown)

    Prevents multiple processes from hammering the database when a hot key expires. It uses singleflight within a single process and distributed locks in Redis to ensure only one request reaches the database for a specific key. Unlike standard solutions, it can return data immediately to waiting requests.

    Anti-Penetration

    Prevents requests for non-existent data from hitting the database repeatedly. If the fetch function fn returns an empty string, RocksCache caches this empty result for a duration defined by EmptyExpire (default: 60s). Set EmptyExpire to 0 to disable this.

    Anti-Avalanche

    Prevents mass cache expiration (thundering herd) by adding jitter to expiration times. The RandomExpireAdjustment option (default: 0.1) adjusts the requested expiration time by a random amount within a range to spread out expirations.

  2. Initialize a RocksCache client

    main

    To use RocksCache, create a new client using NewClient. You must provide an existing Redis client and an options object created via NewDefaultOptions().

    RocksCache follows the update DB and then delete cache management policy. To ensure eventual consistency, you must call TagAsDeleted after your database update is successful.

    import "github.com/dtm-labs/rockscache"
    
    // rc is the initialized client
    rc := rockscache.NewClient(redisClient, NewDefaultOptions())
  3. Enable strong consistency in RocksCache

    main

    By default, RocksCache provides eventual consistency. If your application requires strong consistency (ensuring the cache always reflects the latest DB state), enable the StrongConsisteny option on the client.

    rc.Options.StrongConsisteny = true
  4. How FetchBatch handles consistency and cache disabling

    main

    The behavior of FetchBatch (and FetchBatch2) is determined by the Client.Options configuration:

    1. Cache Disabled: If Options.DisableCacheRead is true, the client bypasses the cache entirely and calls the provided fn for all keys.
    2. Strong Consistency: If Options.StrongConsistency is true, the client uses strongFetchBatch. This mode ensures that if a key is currently being updated (locked), the client will wait/retry to ensure it gets the most recent data.
    3. Weak Consistency (Default): If strong consistency is disabled, the client uses weakFetchBatch. In this mode, if a key is locked for an update, the client may return the existing (potentially stale) data immediately to reduce latency, while triggering an asynchronous background fetch to update the cache.
  5. Configure cache degradation (Circuit Breaking)

    main

    If Redis becomes unavailable or unstable, you can degrade the cache functionality using two flags in the options object:

    • DisableCacheRead: If true, Fetch will bypass the cache and always call the provided function fn directly.
    • DisableCacheDelete: If true, TagAsDeleted will perform no operation and return immediately.
  6. Configure cache protection settings

    main

    RocksCache includes built-in protections against common cache issues. You can tune them via the options:

    • Cache Penetration (Empty Results): When the callback fn returns an empty string, RocksCache treats it as an empty result and caches it for a specific duration. Use EmptyExpire to set this duration (default is 60s). Set EmptyExpire to 0 to disable this protection.
    • Cache Avalanche (Random Expiration): To prevent many keys from expiring at the same time, RocksCache applies a random jitter to the expiration time. The RandomExpireAdjustment option (default 0.1) determines the range of this jitter.
  7. Configure cache downgrade switches

    main

    RocksCache supports downgrading to maintain availability if Redis encounters issues. You can control this via two boolean switches in the options:

    • DisableCacheRead: If true, Fetch will bypass the cache and call the fetch function directly. Default is false.
    • DisableCacheDelete: If true, TagAsDeleted will do nothing. Default is false.
  8. Delete a cache key using TagAsDeleted

    main

    To invalidate a cache entry (typically after a database update), use TagAsDeleted. This is a critical step for maintaining eventual consistency between your database and Redis.

    rc.TagAsDeleted(key)
  9. Delete a cache key with TagAsDeleted

    main

    RocksCache uses a "Mark as Deleted" strategy to ensure eventual consistency. Instead of a standard cache deletion, use TagAsDeleted(key) after updating your database. This ensures that stale data writes are rejected, solving the classic database-cache inconsistency problem.

    rc.TagAsDeleted(key)
  10. Batch read cache using FetchBatch

    main

    Use FetchBatch to retrieve multiple keys efficiently. If some keys are missing from the cache, the provided batch fetch function is called with a list of indices representing the missing keys.

    Parameters:

    1. keys ([]string): A list of keys to fetch.
    2. expiration (time.Duration): The expiration time for the fetched data.
    3. fn (func(idxs []int) (map[int]string, error)): The batch fetch function. It receives a slice of indices (idxs) corresponding to the missing keys in the input list. It must return a map where the key is the index and the value is the data string.

    Returns:

    • A map of results and an error.
    v, err := rc.FetchBatch([]string{"key1", "key2", "key3"}, 300 * time.Second, func(idxs []int) (map[int]string, error) {
        values := make(map[int]string)
        for _, i := range idxs {
            values[i] = fmt.Sprintf("value%d", i)
        }
        return values, nil
    })
  11. Read data from cache using Fetch

    main

    Use the Fetch method to retrieve data. If the key does not exist in the cache, the provided fetch function (fn) is executed to retrieve the data from the database or other sources, and the result is then cached.

    Parameters:

    1. key (string): The unique identifier for the data.
    2. expiration (time.Duration): How long the data should remain in the cache.
    3. fn (func() (string, error)): The callback function to fetch data on a cache miss.

    Returns:

    • The fetched value (string) and an error.
    // use Fetch to fetch data
    v, err := rc.Fetch("key1", 300 * time.Second, func() (string, error) {
      // fetch data from database or other sources
      return "value1", nil
    })