Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/user-guide/client-side-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Client-Side Caching

Lettuce supports server-assisted client-side caching as described in the
[Redis client-side caching documentation](https://redis.io/docs/latest/develop/reference/client-side-caching/).
The `ClientSideCaching` utility creates a `CacheFrontend` that represents a
two-level cache: values are first looked up in a local, application-provided
cache and only fetched from Redis on a cache miss. Redis notifies the client
through `CLIENT TRACKING` invalidation messages when a cached key is modified,
and the corresponding local entry is evicted.

## Standalone connections

```java
Map<String, String> clientCache = new ConcurrentHashMap<>();

StatefulRedisConnection<String, String> connection = redisClient.connect();

CacheFrontend<String, String> frontend = ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connection,
TrackingArgs.Builder.enabled());

String value = frontend.get(key);
```

The `CacheFrontend` is associated with the Redis connection. Close the
frontend through `CacheFrontend.close()` to release the connection after use.

The example above requires RESP3: invalidation messages are delivered as push
messages on the connection itself, and the frontend evicts local entries
automatically. With RESP2, Redis delivers invalidations only to a redirected
client (`TrackingArgs.Builder.enabled().redirect(clientId)`), so the
frontend's built-in invalidation listener never receives them. In that case
subscribe to the `__redis__:invalidate` Pub/Sub channel on the redirect target
connection and evict entries from the client-side cache
(`CacheAccessor.evict(...)`) in the Pub/Sub listener yourself. Full
invalidations such as `FLUSHALL`/`FLUSHDB` deliver a `null` message without
keys — clear the entire client-side cache (`CacheAccessor.clear()`) when the
invalidation message is `null`.

## Redis Cluster connections

Since version 7.7, client-side caching is also supported for Redis Cluster
connections:

```java
Map<String, String> clientCache = new ConcurrentHashMap<>();

StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();

CacheFrontend<String, String> frontend = ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connection,
TrackingArgs.Builder.enabled());

String value = frontend.get(key);
```

`CLIENT TRACKING` is enabled on each cluster node connection because
invalidation messages for a key are emitted by the node that serves the key's
slot. Upstream (master) nodes are tracked through their write-intent
connections. When a `ReadFrom` setting routes reads to replicas, the replicas
that the read policy can select are tracked through their read-intent
connections as well.

The following constraints apply to Redis Cluster:

- **RESP3 is required.** Enabling the cache fails with an
`IllegalStateException` if a node connection did not negotiate RESP3.
- **`REDIRECT` is not supported** as invalidation messages can originate from
any node. Redirected `TrackingArgs` are rejected with an
`IllegalArgumentException`.
- **`OPTIN` is not supported** as the cache frontend does not issue
`CLIENT CACHING yes` before reads. Opt-in `TrackingArgs` are rejected with an
`IllegalArgumentException`.
- **Prefix-limited `BCAST` is not supported** as the cache frontend caches all
keys regardless of prefix, so keys outside the configured prefixes would
never be invalidated. Prefixed `TrackingArgs` are rejected with an
`IllegalArgumentException`.
- **Topology and read-policy changes are not tracked.** Tracking is configured
for the topology and `ReadFrom` setting present when the cache is enabled.
Nodes added to the cluster afterwards do not have tracking enabled, and
changing the read policy through `setReadFrom(...)` does not track newly
selectable replicas. Applications that must observe such changes should
re-enable tracking afterwards.

`FLUSHALL` and `FLUSHDB` emit a full invalidation, which clears the entire
client-side cache.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ nav:
- Kotlin API: user-guide/kotlin-api.md
- Publish/Subscribe: user-guide/pubsub.md
- Transactions/Multi: user-guide/transactions-multi.md
- Client-Side Caching: user-guide/client-side-caching.md
- Redis Query Engine: user-guide/redis-search.md
- Redis JSON: user-guide/redis-json.md
- Redis Vector Sets: user-guide/vector-sets.md
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/io/lettuce/core/TrackingArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,56 @@ public TrackingArgs redirect(long clientId) {
return this;
}

/**
* Return whether key tracking is enabled.
*
* @return {@code true} if {@link #enabled(boolean)} was configured with {@code true}.
* @since 7.7
*/
public boolean isEnabled() {
return enabled;
}

/**
* Return whether invalidation messages are redirected to another client connection through {@code REDIRECT}.
*
* @return {@code true} if {@link #redirect(long)} was configured.
* @since 7.7
*/
public boolean isRedirect() {
return redirect != null;
}

/**
* Return whether broadcasting invalidation messages are limited to key prefixes through {@code PREFIX}.
*
* @return {@code true} if {@link #prefixes(String...)} was configured with at least one prefix.
* @since 7.7
*/
public boolean hasPrefixes() {
return prefixes != null && prefixes.length > 0;
}

/**
* Create a copy of {@code this} {@link TrackingArgs} to guard against later modifications of a shared, mutable instance.
*
* @return a new {@link TrackingArgs} with the same configuration.
* @since 7.7
*/
public TrackingArgs copy() {

TrackingArgs copy = new TrackingArgs();
copy.enabled = this.enabled;
copy.redirect = this.redirect;
copy.bcast = this.bcast;
copy.prefixes = this.prefixes == null ? null : this.prefixes.clone();
copy.prefixCharset = this.prefixCharset;
copy.optin = this.optin;
copy.optout = this.optout;
copy.noloop = this.noloop;
return copy;
}

/**
* Enable tracking in broadcasting mode. In this mode invalidation messages are reported for all the prefixes specified,
* regardless of the keys requested by the connection. Instead when the broadcasting mode is not enabled, Redis will track
Expand Down Expand Up @@ -145,6 +195,16 @@ public TrackingArgs optin() {
return this;
}

/**
* Return whether tracking requires a preceding {@code CLIENT CACHING yes} opt-in per read.
*
* @return {@code true} if {@link #optin()} was configured.
* @since 7.7
*/
public boolean isOptin() {
return optin;
}

/**
* When broadcasting is NOT active, normally track keys in read only commands, unless they are called immediately after a
* CLIENT CACHING no command.
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/lettuce/core/support/caching/CacheAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,16 @@ static <K, V> CacheAccessor<K, V> forMap(Map<K, V> map) {
*/
void evict(K key);

/**
* Evict all mappings from this cache. Invoked when Redis reports a full invalidation, for example after {@code FLUSHALL} or
* {@code FLUSHDB}.
* <p>
* The default implementation is a no-op; implementations should override this method to avoid serving stale entries after a
* flush.
*
* @since 7.7
*/
default void clear() {
}

}
Loading
Loading