Cachex Documentation

repository·main·Indexed 23 days ago

https://github.com/whitfin/cachex

A high-performance, in-memory key/value store for Elixir. Cachex provides features such as time-based expiration, transactions, row locking, and distribution across nodes. It supports basic operations like put, get, and del, as well as advanced patterns including atomic updates, lazy fetching, custom commands, and hooks via the Cachex.Hook behaviour.

Tokens
20K
Snippets
45
Records
122
Agent score
81%

What's inside Cachex

  1. How distributed caches work in Cachex

    main

    A distributed cache spans multiple nodes, allowing each node to store a subset of the total data while maintaining cluster-wide access. For example, in a 3-node cluster, writing 100 keys will result in approximately 33 keys per node. If a key is written on Node A, it may be stored on Node B; a subsequent search on Node C will automatically fetch it from Node B.

    Important: Cachex is a caching library, not a database. Data is not replicated to every node. It provides an ephemeral data layer. If you require data persistence across a cluster, use a dedicated database tool.

  2. How Cache Routers work in Cachex

    main
    Introduced in Cachex v4.x, Cache Routers allow developers to determine how keys are assigned to nodes in a distributed caching cluster. This provides flexibility for scenarios like dynamically scaling caches, which was a limitation in v3.x. By choosing a specific routing algorithm, you can optimize how your cluster handles key distribution and node membership changes.
  3. Configure Hooks in v3.x

    main
    In v3.x, Hooks are driven by module behaviours rather than structs. When registering a hook, you provide a hook record containing the hook module, the hook state, and an optional name. The logic for the hook is implemented within the functions defined in the registered module's behaviour.
  4. How proactive warming works in Cachex

    main

    Proactive warming (via Cachex.Warmer) is an eager way to populate a cache. Unlike reactive warming (e.g., Cachex.fetch/4), which waits for a cache miss to retrieve data, proactive warming pulls data into the cache upfront.

    This is best used when you know exactly what data will be requested (e.g., a fixed set of database rows or API paths) rather than dealing with arbitrary, user-generated data. Warmers run periodically for the lifetime of the cache to refresh data from a source.

  5. Implement LRU (Least Recently Used) caching

    main

    While Cachex defaults to LRW (Least Recently Written), you can implement LRU (Least Recently Used) by combining Cachex.Limit.Accessed with an LRW pruning hook.

    Cachex.Limit.Accessed attaches a lifecycle hook that updates the access time of each record. To make this work, you must place the Cachex.Limit.Accessed hook before your LRW pruning hook in the hooks list.

    Note: This incurs a performance cost due to the heavy read/write activity required to update access times on every read. Use LRW whenever possible and only use LRU if absolutely necessary.

    import Cachex.Spec
    
    Cachex.start(:my_cache, 
      hooks: [
        hook(module: Cachex.Limit.Accessed),
        hook(module: Cachex.Limit.Scheduled, args: {
          500, 
          [], 
          []
        })
      ]
    )
  6. Use the :local option for cluster-wide actions

    main

    Most Cachex actions in a distributed setup are transparent and aggregate results from the whole cluster (e.g., Cachex.size/2 returns the total count across all nodes).

    If you need to perform an action specifically on the current node only, pass the :local option set to true. This applies to:

    • Aggregation functions (like Cachex.size/2)
    • Actions that are otherwise unavailable in distributed mode (like Cachex.stream/3)
    • Saving data (via Cachex.save/3)
  7. Understand the structure of hook notifications

    main

    When a cache action is performed, the hook receives a notification in the form of a tuple: { action, args, result } (conceptually) or specifically within handle_notify/3 as (action, result, state).

    In the context of a Cachex.get/3 call:

    • Action: An atom representing the operation (e.g., :get).
    • Args: A list of arguments passed to the cache call (e.g., [:my_cache, "key"]).
    • Result: The value returned by the cache (e.g., "value").

    Example notification for Cachex.get(:my_cache, "key") returning "value": { :get, [ :my_cache, "key" ], "value" }

  8. How Cachex handles concurrent warmer contention

    main

    A common issue with manual cache loading (e.g., get followed by put) is that multiple concurrent requests for a missing key can all trigger the warming logic simultaneously, causing redundant work (like multiple database queries).

    Cachex.fetch/4 prevents this via an internal Courier service. If multiple processes request the same missing key at the same time, Cachex ensures that only the first warmer executes. All subsequent requests for that same key are queued and will resolve with the result produced by the first warmer once it completes. This provides a per-key queue that prevents 'thundering herd' problems on backing systems.

    # Using fetch/4 ensures the warmer only runs once even with 10 concurrent calls
    for _ <- 1..10 do
      spawn(fn ->
        Cachex.fetch(:cache, "key2", fn key ->
          IO.puts("Running warmer in fetch/4")
          value = :timer.sleep(1000)
          value
        end)
      end)
    end
  9. Use unsafe (!) functions for error raising

    main

    For cache actions that can fail (returning error tuples like {:error, reason}), Cachex provides an 'unsafe' version of the function appended with !. These versions unpack the result and raise an error if the operation fails.

    Note: While useful for testing or simple scripts, it is recommended to use the standard versions and explicitly handle error tuples in production code.

    # Standard version returns error tuple
    {:error, :non_numeric_value} = Cachex.incr(:my_cache, "one")
    
    # Unsafe version raises a Cachex.Error
    Cachex.incr!(:my_cache, "one")
    # ** (Cachex.Error) Attempted arithmetic operations on a non-numeric value
    {:error, :non_numeric_value} = Cachex.incr(:my_cache, "one")
    Cachex.incr!(:my_cache, "one")
  10. Choose the right Default Router for your cluster

    main

    Cachex provides several built-in routers. Choose one based on your cluster topology:

    ModuleDescriptionBest Use Case
    Cachex.Router.LocalRoutes keys to the local node only.Single node clusters (Default).
    Cachex.Router.ModUses basic modulo hashing (hash(key) % len(nodes)).Statically sized clusters.
    Cachex.Router.JumpUses the Jump Consistent hash algorithm.Statically sized clusters or v3.x compatibility.
    Cachex.Router.RingUses Discord's hash ring implementation.Dynamically sized clusters.
  11. What are Custom Commands in Cachex

    main

    Custom Commands allow you to attach application-specific logic or domain-specific operations directly to a cache. This simplifies common logic by allowing you to perform complex operations on cached values without needing to wrap every cache call in external modules.

    Commands are categorized into two types:

    • :read: Transforms a cached value and returns the result to the user without modifying the value stored in the cache.
    • :write: Modifies the value stored in the cache and returns a result to the user.

    As a rule of thumb, use commands for general actions and keep highly specific application logic outside the caching layer.