Nebulex.Adapters.Redis (Nebulex.Adapters.Redis v3.0.0-rc.2)
View SourceNebulex adapter for Redis. This adapter is implemented using Redix
(a Redis driver for Elixir).
The adapter provides three setup alternatives:
Standalone - The adapter establishes a pool of connections with a single Redis node. The
:standaloneis the default mode.Redis Cluster - Redis Cluster is a built-in feature in Redis since version 3, and it may be the most convenient and recommendable way to set up Redis in a cluster and have a distributed cache storage out-of-box. This adapter provides the
:redis_clustermode to set up Redis Cluster from the client-side automatically and be able to use it transparently.Built-in client-side cluster - The
:client_side_clustermode provides a simple client-side cluster implementation based on sharding distribution model.
Standalone
A cache that uses Redis is defined as follows:
defmodule MyApp.RedisCache do
use Nebulex.Cache,
otp_app: :nebulex,
adapter: Nebulex.Adapters.Redis
endThe configuration for the cache must be in your application environment,
usually defined in your config/config.exs:
config :my_app, MyApp.RedisCache,
conn_opts: [
host: "127.0.0.1",
port: 6379
]Redis Cluster
A cache that uses Redis Cluster can be defined as follows:
defmodule MyApp.RedisClusterCache do
use Nebulex.Cache,
otp_app: :nebulex,
adapter: Nebulex.Adapters.Redis
endAs you may notice, nothing has changed, it is defined the same as the standalone mode. The change is in the configuration:
config :my_app, MyApp.RedisClusterCache,
mode: :redis_cluster,
redis_cluster: [
# Configuration endpoints
# The client connects to these endpoints to fetch cluster topology
# using "CLUSTER SHARDS" (Redis >= 7) or "CLUSTER SLOTS" (Redis < 7).
# Multiple endpoints can be provided for redundancy. The adapter will
# try each endpoint until it successfully retrieves the topology.
configuration_endpoints: [
endpoint1_conn_opts: [
host: "127.0.0.1",
port: 6379,
# Add the password if 'requirepass' is enabled
password: "password"
],
endpoint2_conn_opts: [
host: "127.0.0.1",
port: 6380,
password: "password"
]
],
# Optional: Override master host addresses returned by the cluster.
# Useful when running Redis in Docker or behind NAT where the
# advertised addresses differ from the actual connection addresses.
# Defaults to `false`.
override_master_host: false
]Client-side Cluster
Same as the previous modes, a cache is defined as:
defmodule MyApp.ClusteredCache do
use Nebulex.Cache,
otp_app: :nebulex,
adapter: Nebulex.Adapters.Redis
endThe configuration:
config :my_app, MyApp.ClusteredCache,
mode: :client_side_cluster,
client_side_cluster: [
nodes: [
node1: [
pool_size: 10,
conn_opts: [
host: "127.0.0.1",
port: 9001
]
],
node2: [
pool_size: 4,
conn_opts: [
url: "redis://127.0.0.1:9002"
]
],
node3: [
conn_opts: [
host: "127.0.0.1",
port: 9003
]
],
...
]
]Redis Proxy Alternative
Consider using a proxy instead, since it may provide more and better features. See the "Redis Proxy" section below for more information.
Redis Proxy
Another option for "Redis Cluster" or the built-in "Client-side cluster" is using a proxy such as Envoy proxy or Twemproxy on top of Redis. In this case, the proxy does the distribution work, and from the adapter's side (Nebulex.Adapters.Redis), it would be only configuration. Instead of connecting the adapter against the Redis nodes, we connect it against the proxy nodes, this means, in the configuration, we set up the pool with the host and port pointing to the proxy.
Configuration options
In addition to Nebulex.Cache config options, the adapter supports the
following options:
:mode- Defines the Redis configuration mode, which determines how the adapter connects to and distributes data across Redis instances.:standalone- Single Redis instance mode. Use this for simple setups or development environments. Creates a pool of connections to one Redis server. See the "Standalone" section for more options.:redis_cluster- Native Redis Cluster mode. Use this for production distributed caching with automatic sharding, replication, and failover provided by Redis Cluster. The adapter automatically discovers cluster topology and routes commands to the correct shard. See the "Redis Cluster" section for more options.:client_side_cluster- Client-side sharding mode. Use this when you need to distribute data across multiple independent Redis instances without Redis Cluster. The adapter uses consistent hashing to distribute keys. See the "Client-side Cluster" section for more options.
The default value is
:standalone.:pool_size(pos_integer/0) - The number of connections per Redis instance or shard.- In
:standalonemode: Total number of connections to the single Redis instance. - In
:redis_clustermode: Number of connections per shard (master node). - In
:client_side_clustermode: Number of connections per node (can be overridden per node in the node configuration).
Defaults to
System.schedulers_online(), which matches the number of available CPU cores and provides good concurrency.- In
:serializer- Custom serializer module implementing theNebulex.Adapters.Redis.Serializerbehaviour.See the "Custom Serializers" section in the module documentation for examples.
:serializer_opts(keyword/0) - Options passed to the serializer module's encode/decode functions. These options are forwarded to your custom serializer and can be used to configure serialization behavior. The default value is[].:encode_key(keyword/0) - Options passed toNebulex.Adapters.Redis.Serializer.encode_key/2when encoding cache keys before storing them in Redis. The default value is[].:encode_value(keyword/0) - Options passed toNebulex.Adapters.Redis.Serializer.encode_value/2when encoding cache values before storing them in Redis. The default value is[].:decode_key(keyword/0) - Options passed toNebulex.Adapters.Redis.Serializer.decode_key/2when decoding cache keys retrieved from Redis. The default value is[].:decode_value(keyword/0) - Options passed toNebulex.Adapters.Redis.Serializer.decode_value/2when decoding cache values retrieved from Redis. The default value is[].
:conn_opts(keyword/0) - Redis connection options for:standalonemode. These options are passed directly toRedixwhen establishing connections.For cluster modes, connection options are specified within
:redis_clusteror:client_side_clusterconfiguration.See
Redix.start_link/1for the complete list of available options.The default value is
[host: "127.0.0.1", port: 6379].:redis_cluster- Required only when:modeis set to:redis_cluster. A keyword list of options.See "Redis Cluster options" section below.
:client_side_cluster- Required only when:modeis set to:client_side_cluster. A keyword list of options.See "Client-side Cluster options" section below.
Redis Cluster options
The available options are:
:configuration_endpoints- Required. A keyword list of Redis Cluster nodes used to discover and fetch the cluster topology.Each endpoint is identified by an atom key and configured with connection options (same format as
:conn_opts). The adapter tries each endpoint in order until it successfully retrieves the cluster topology using "CLUSTER SHARDS" (Redis >= 7) or "CLUSTER SLOTS" (Redis < 7).Providing multiple endpoints improves reliability; if one node is unavailable, the adapter will try the next one.
See "Redis Cluster" for configuration examples.
:override_master_host(boolean/0) - Determines whether to override master host addresses returned by Redis Cluster with the configuration endpoint's host.By default (
false), the adapter uses host addresses returned by "CLUSTER SHARDS" (Redis >= 7) or "CLUSTER SLOTS" (Redis < 7). Set totruewhen:- Running Redis in Docker, where advertised addresses differ from actual connection addresses.
- Redis nodes are behind NAT or a load balancer.
- The cluster advertises internal IPs unreachable from your application.
When
true, the adapter replaces cluster-advertised hosts with the host from the configuration endpoint that provided the topology.The default value is
false.:keyslot(function of arity 2) - Custom function to compute the Redis Cluster hash slot for a given key.The function receives two arguments:
key- The cache key (after serialization).range- The total number of hash slots (typically16384).
It should return an integer between
0andrange - 1.The default implementation uses CRC16-XMODEM algorithm with support for hash tags (e.g.,
{user}:123and{user}:456map to the same slot). Only provide a custom function if you need different hash slot calculation logic.The default value is
&Nebulex.Adapters.Redis.Cluster.Keyslot.hash_slot/2.
Client-side Cluster options
The available options are:
:nodes- Required. A keyword list of independent Redis nodes that form the client-side cluster.Each node is identified by an atom key and configured with connection options. The adapter uses consistent hashing to distribute cache keys across these nodes. Each node can optionally override the global
:pool_sizesetting.See "Client-side Cluster" for configuration examples.
Shared runtime options
Since the adapter runs on top of Redix, all commands accept their options
(e.g.: :timeout, and :telemetry_metadata). See Redix docs for more
information.
Redis Cluster runtime options
The following options are only for the :redis_cluster mode and apply to all
commands:
:lock_retries- When the config manager is running and setting up the hash slot map, all Redis commands get blocked until the cluster is properly configured and the hash slot map is ready to use. This option defines the max retry attempts to acquire the lock before executing the command. Defaults to:infinity.
Query API
The queryable API is implemented using Redis KEYS command for pattern
matching and operations like get_all, delete_all, and count_all.
Performance Warning
The KEYS command can cause performance issues in production environments
because it blocks Redis while scanning all keys in the database. Consider
the following:
- Avoid using pattern queries (
get_all("pattern*")) in production with large datasets. - Prefer explicit key lists (
get_all(in: [key1, key2])) when possible. - Use
stream/2instead ofget_all/2for large result sets to reduce memory usage.
This adapter currently uses the KEYS command. A refactoring to use
SCAN (the Redis-recommended approach for production) is planned for
the next release.
Keep in mind the following limitations:
- Only keys can be queried (not values or other attributes).
- Only strings and predefined queries are allowed as query values.
- Pattern queries scan the entire keyspace.
See "KEYS" command for pattern syntax details.
Examples
iex> MyApp.RedisCache.put_all(%{
...> "firstname" => "Albert",
...> "lastname" => "Einstein",
...> "age" => 76
...> })
:ok
# returns key/value pairs by default
iex> MyApp.RedisCache.get_all!("**name**") |> Map.new()
%{"firstname" => "Albert", "lastname" => "Einstein"}
iex> MyApp.RedisCache.get_all!("**name**", select: :key)
["firstname", "lastname"]
iex> MyApp.RedisCache.get_all!("a??", select: :key)
["age"]
iex> MyApp.RedisCache.get_all!(select: :key)
["age", "firstname", "lastname"]
iex> MyApp.RedisCache.stream!("**name**", select: :key) |> Enum.to_list()
["firstname", "lastname"]Deleting/counting keys
iex> MyApp.RedisCache.delete_all!(in: ["foo", "bar"])
2
iex> MyApp.RedisCache.count_all!(in: ["foo", "bar"])
2Transactions
Nebulex Transaction API
The Nebulex.Adapter.Transaction behaviour (for multi-operation
transactions via the transaction/3 callback) is not currently implemented
in this adapter, but it is planned for future releases.
However, the adapter does use Redis transactions (MULTI/EXEC) internally
to ensure atomicity for operations like take/2, put_all/2, and
update_counter/3. These internal transactions are transparent to users
and happen automatically when needed.
Using the adapter as a Redis client
Since the Redis adapter works on top of Redix and provides features like
connection pools, "Redis Cluster", etc., it may also work as a Redis client.
The Redis API is quite extensive, and there are many useful commands we may
want to run, leveraging the Redis adapter features. Therefore, the adapter
provides additional functions to do so.
fetch_conn(opts \\ [])
The function accepts the following options:
:name(atom/0) - The name of the cache (in case you are using dynamic caches), otherwise it is not required (defaults to the cache module name).:key(term/0) - A cache key used to determine which Redis instance/shard to connect to in cluster modes.Required for
:redis_clusterand:client_side_clustermodes, where the adapter uses the key to:- In
:redis_clustermode: Calculate the hash slot to find the correct shard. - In
:client_side_clustermode: Use consistent hashing to select the appropriate node.
When executing custom Redis commands (e.g., list operations, sorted sets), provide a key to ensure all operations target the same Redis instance. Not required for
:standalonemode.- In
Let's see some examples:
iex> MyCache.fetch_conn!()
...> |> Redix.command!(["LPUSH", "mylist", "hello"])
1
iex> MyCache.fetch_conn!()
...> |> Redix.command!(["LPUSH", "mylist", "world"])
2
iex> MyCache.fetch_conn!()
...> |> Redix.command!(["LRANGE", "mylist", "0", "-1"])
["hello", "world"]When working with :redis_cluster or :client_side_cluster modes the option
:key is required:
iex> {:ok, conn} = MyCache.fetch_conn(key: "mylist")
iex> Redix.pipeline!(conn, [
...> ["LPUSH", "mylist", "hello"],
...> ["LPUSH", "mylist", "world"],
...> ["LRANGE", "mylist", "0", "-1"]
...> ])
[1, 2, ["hello", "world"]]Since these functions run on top of Redix, they also accept their options
(e.g.: :timeout, and :telemetry_metadata). See Redix docs for more
information.
Encoding/decoding functions
The following functions are available to encode/decode Elixir terms. It is useful whenever you want to work with Elixir terms in addition to strings or other specific Redis data types.
encode_key(name \\ __MODULE__, key)- Encodes an Elixir term into a string. The argumentnameis optional and should be used in case of dynamic caches (Defaults to the defined cache module).encode_value(name \\ __MODULE__, value)- Same asencode_keybut it is specific for encoding values, in case the encoding for keys and values are different.decode_key(name \\ __MODULE__, key)- Decodes binary into an Elixir term. The argumentnameis optional and should be used in case of dynamic caches (Defaults to the defined cache module).decode_value(name \\ __MODULE__, value)- Same asdecode_keybut it is specific for decoding values, in case the decoding for keys and values are different.
Let's see some examples:
iex> conn = MyCache.fetch_conn!()
iex> key = MyCache.encode_key({:key, "key"})
iex> value = MyCache.encode_value({:value, "value"})
iex> Redix.command!(conn, ["SET", key, value], timeout: 5000)
"OK"
iex> Redix.command!(conn, ["GET", key]) |> MyCache.decode_value()
{:value, "value"}Custom Serializers
By default, the adapter uses Erlang's term format (:erlang.term_to_binary/2)
for serialization, with strings being stored as-is. You can implement a
custom serializer by creating a module that implements the
Nebulex.Adapters.Redis.Serializer behaviour.
This is useful when you need:
- JSON serialization for interoperability with other systems.
- Custom compression algorithms.
- Different encoding strategies for keys vs values.
- Integration with external serialization libraries.
Example custom serializer using JSON:
defmodule MyApp.JSONSerializer do
@behaviour Nebulex.Adapters.Redis.Serializer
@impl true
def encode_key(key, _opts), do: JSON.encode!(key)
@impl true
def encode_value(value, _opts), do: JSON.encode!(value)
@impl true
def decode_key(key, _opts), do: JSON.decode!(key)
@impl true
def decode_value(value, _opts), do: JSON.decode!(value)
endThen configure your cache to use the custom serializer:
config :my_app, MyApp.RedisCache,
serializer: MyApp.JSONSerializerAdapter-specific telemetry events for the :redis_cluster mode
Aside from the recommended Telemetry events by Nebulex.Cache, this adapter
exposes the following Telemetry events for the :redis_cluster mode:
telemetry_prefix ++ [:redis_cluster, :setup, :start]- This event is specific to the:redis_clustermode. Before the configuration manager calls Redis to set up the cluster shards, this event should be invoked.The
:measurementsmap will include the following::system_time- The current system time in native units from calling:System.system_time().
A Telemetry
:metadatamap including the following fields::adapter_meta- The adapter metadata.:pid- The configuration manager PID.
telemetry_prefix ++ [:redis_cluster, :setup, :stop]- This event is specific to the:redis_clustermode. After the configuration manager set up the cluster shards, this event should be invoked.The
:measurementsmap will include the following::duration- The time spent configuring the cluster. The measurement is given in the:nativetime unit. You can read more about it in the docs forSystem.convert_time_unit/3.
A Telemetry
:metadatamap including the following fields::adapter_meta- The adapter metadata.:pid- The configuration manager PID.:status- The cluster setup status. If the cluster was configured successfully, the status will be set to:ok, otherwise, will be set to:error.:reason- The status reason. When the status is:ok, the reason is:succeeded, otherwise, it is the error reason.
telemetry_prefix ++ [:redis_cluster, :setup, :exception]- This event is specific to the:redis_clustermode. When an exception is raised while configuring the cluster, this event should be invoked.The
:measurementsmap will include the following::duration- The time spent configuring the cluster. The measurement is given in the:nativetime unit. You can read more about it in the docs forSystem.convert_time_unit/3.
A Telemetry
:metadatamap including the following fields::adapter_meta- The adapter metadata.:pid- The configuration manager PID.:kind- The type of the error::error,:exit, or:throw.:reason- The reason of the error.:stacktrace- The stacktrace.