diff --git a/content/develop/clients/client-side-caching.md b/content/develop/clients/client-side-caching.md index e3e588ee18..97ba57eeb3 100644 --- a/content/develop/clients/client-side-caching.md +++ b/content/develop/clients/client-side-caching.md @@ -21,6 +21,7 @@ relatedPages: - /develop/clients/nodejs/connect#connect-using-client-side-caching - /develop/clients/jedis/connect#connect-using-client-side-caching - /develop/clients/go/connect#connect-using-client-side-caching +- /develop/clients/dotnet/connect#connect-using-client-side-caching topics: - client-side-caching - performance @@ -94,6 +95,7 @@ The following client libraries support CSC from the stated version onwards: | [`Jedis`](/content/develop/clients/jedis/connect.md#connect-using-client-side-caching) | v5.2.0 | | [`node-redis`](/content/develop/clients/nodejs/connect.md#connect-using-client-side-caching) | v5.1.0 | | [`go-redis`](/content/develop/clients/go/connect.md#connect-using-client-side-caching) | v9.22.0 | +| [`StackExchange.Redis`](/content/develop/clients/dotnet/connect.md#connect-using-client-side-caching) | v4.0 (pre-release) | Note that some other clients support the [`CLIENT TRACKING`](/content/commands/client-tracking.md) command to configure CSC on the server, but this does not mean they support the features required for CSC themselves. @@ -116,6 +118,12 @@ will use cached data, except for the following: - Redis Search commands (with the `FT.*` prefix), such as [`FT.SEARCH`](/content/commands/ft.search.md). +The details vary between client libraries, so check the client-side caching +section for your client. For example, +[StackExchange.Redis](/content/develop/clients/dotnet/connect.md#connect-using-client-side-caching) +caches `HSCAN` pages, and by default it caches a reply the second time you read +it rather than the first. + You can use the [`MONITOR`](/content/commands/monitor.md) command to check the server's behavior when you are using client-side caching. Because `MONITOR` only reports activity from the server, you should find the first cacheable diff --git a/content/develop/clients/dotnet/connect.md b/content/develop/clients/dotnet/connect.md index 8815a1fe1a..36b0c9aab6 100644 --- a/content/develop/clients/dotnet/connect.md +++ b/content/develop/clients/dotnet/connect.md @@ -301,6 +301,198 @@ muxer.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) => { `MaintenanceType` property, so you can tell whether a failure happened because of ongoing maintenance. +## Connect using client-side caching + +Client-side caching is a technique to reduce network traffic between +the client and server, resulting in better performance. See +[Client-side caching introduction](/content/develop/clients/client-side-caching.md) +for more information about how client-side caching works and how to use it effectively. + + +> [!NOTE] +> Client-side caching requires StackExchange.Redis v4.0, which is currently +> available as the `4.0.127-alpha` pre-release, so its API may change before +> the final release. +> To maximize compatibility with all Redis products, client-side caching +> is supported by Redis v7.4 or later. +> +> Client-side caching requires version 3 of the +> [Redis serialization protocol (RESP3)](/content/develop/reference/protocol-spec.md#resp-versions) +> and a server that allows the [`CLIENT TRACKING`](/content/commands/client-tracking.md) +> command. If either is missing, the connection fails with an error rather +> than running without cache invalidation. + +Client-side caching is disabled by default. To enable it, set `Protocol` to +`RedisProtocol.Resp3` and pass a `CacheOptions` object in the `ClientCache` +property of `ConfigurationOptions`. You can only configure the cache in code, +not with a configuration string. The cache works below the API, so it applies +to all of the `IDatabase` methods, both synchronous and asynchronous: + +```csharp +using StackExchange.Redis; +using StackExchange.Redis.Caching; + +var options = ConfigurationOptions.Parse("localhost:6379"); +options.Protocol = RedisProtocol.Resp3; // RESP3 required for client-side caching +options.ClientCache = new CacheOptions { + Prefixes = ["city:"], + MaxBytes = 64 * 1024 * 1024, +}; + +ConnectionMultiplexer muxer = ConnectionMultiplexer.Connect(options); +IDatabase db = muxer.GetDatabase(); + +db.StringSet("city:1", "New York"); +db.StringGet("city:1"); // Retrieved from the server +db.StringGet("city:1"); // Retrieved from the server and cached +db.StringGet("city:1"); // Retrieved from the cache +``` + +By default, StackExchange.Redis caches a reply the second time you read it, +not the first, so values you read only once never take up space in the cache. +You can see this if you connect to the same Redis database +with [`redis-cli`](/content/develop/tools/cli.md) and run the +[`MONITOR`](/content/commands/monitor.md) command. With caching enabled, +the server sees the first two `StringGet("city:1")` calls but not the third, +which the client satisfies from the cache. Set `Admission` to +`CacheAdmission.OnFirstMiss` to cache every reply the first time you read it. + +There is one cache for each `ConnectionMultiplexer`, shared by all of +its databases. + +### Tracking modes + +The `TrackingMode` option sets how the server decides which changes to announce: + +- `CacheTrackingMode.Broadcast` (the default): the server announces every + change to keys that match the `Prefixes` you set, and doesn't record which + keys you read. This is cheap for the server, but the client receives + invalidations for keys it never cached. Without `Prefixes`, the client + receives an invalidation for every write to every key, which is a lot of + traffic on a busy server. Keys outside the prefixes are not cached. + Prefixes must not overlap (for example, `"app:"` and `"app:user:"` together + are rejected), and they are compared with the key as it is sent, so include + any key prefix that you add with `WithKeyPrefix()`. +- `CacheTrackingMode.PerKey`: the server records which keys this client read + and announces changes only to those keys. This is more precise, but uses + server memory for each client. You can't use `Prefixes` in this mode. + +### Configuration options + +The `CacheOptions` object accepts the following options: + +| Name | Description | +| :-- | :-- | +| `Enabled` | Whether the cache is enabled. The default is `true`. Use this to keep your options in place but switch the cache off. | +| `TrackingMode` | The tracking mode (see [Tracking modes](#tracking-modes)). The default is `CacheTrackingMode.Default`, which is currently `Broadcast`. | +| `Prefixes` | The key prefixes to track and cache in `Broadcast` mode. By default, all keys are tracked. | +| `MaxBytes` | A memory limit for cached replies. When the cache exceeds it, the client evicts the oldest entries. By default, there is no limit. | +| `MaxEntries` | The maximum number of entries the cache can hold. By default, there is no limit. Use this with `MaxBytes` when your replies are small and numerous. | +| `MaxPayloadBytes` | The largest single reply that can be cached. The default is 1 MiB. | +| `EvictionSampleSize` | The number of entries that eviction considers each time it chooses one to remove. The default is 8. Larger values make better choices but cost more. | +| `SweepInterval` | How often the client reclaims memory held by invalidated or expired entries. The default is 10 seconds. | +| `DefaultPolicy` | A `CachePolicy` object that sets how entries behave as they age (see the next table). The default is `CachePolicy.Default`. | +| `Admission` | When a reply is added to the cache. The default is `CacheAdmission.Default`, which is currently `OnRepeatedMiss` (cache a reply the second time it's read). Use `OnFirstMiss` to cache every reply the first time it's read. | + +The `CachePolicy` object accepts the following options: + +| Name | Description | +| :-- | :-- | +| `TimeToLive` | The longest time the client serves an entry, whether or not it was invalidated. The default is 1 minute. | +| `RefreshAfter` | The age at which the client starts refreshing an entry in the background, while it continues to serve the cached value. Off by default. | +| `InvalidationGracePeriod` | How long the client continues to serve an entry after another client's change invalidates it, while a background refresh runs. This deliberately serves values that are out of date, so keep it short. It never applies to your own writes. Off by default. | + +`RefreshAfter` and `InvalidationGracePeriod` stop every reader of a frequently +read key from going to the server at the same moment when its entry expires +or is invalidated. The example below sets both: + +```csharp +options.ClientCache = new CacheOptions { + Prefixes = ["catalog:"], + DefaultPolicy = new CachePolicy { + TimeToLive = TimeSpan.FromMinutes(1), + RefreshAfter = TimeSpan.FromSeconds(45), + InvalidationGracePeriod = TimeSpan.FromMilliseconds(250), + }, +}; +``` + +### Bypassing the cache + +Pass `CommandFlags.NoClientCache` to stop a single command from reading from +or writing to the cache: + +```csharp +RedisValue exact = db.StringGet("city:1", CommandFlags.NoClientCache); +``` + +You can also create a *context* that limits how the cache is used for all the +commands you send through it. Contexts are part of the command group API that +StackExchange.Redis v4.0 introduces, so the examples below use the command +group methods rather than the `IDatabase` methods: + +```csharp +// Accept cached replies only if they are less than one second old. +var fresh = db.Context.WithMaxCacheAge(TimeSpan.FromSeconds(1)); +RedisValue recent = await fresh.Strings.GetAsync("city:1"); + +// Bypass the cache completely. +var uncached = db.Context.WithoutCache(); +RedisValue latest = await uncached.Strings.GetAsync("city:1"); +``` + +### Which commands use the cache + +StackExchange.Redis caches the replies from read commands on keys, including +`HSCAN`, `SSCAN`, and `ZSCAN` pages, nil replies for missing keys, and the +read-only script commands +([`EVAL_RO`](/content/commands/eval_ro.md) and [`EVALSHA_RO`](/content/commands/evalsha_ro.md)). +It never caches the following: + +- Write commands. +- Commands that don't name a key, such as [`SCAN`](/content/commands/scan.md) + and [`INFO`](/content/commands/info.md). +- Random reads, such as [`SRANDMEMBER`](/content/commands/srandmember.md) and + [`HRANDFIELD`](/content/commands/hrandfield.md). +- Commands that report time or access, such as [`TTL`](/content/commands/ttl.md) + and [`OBJECT FREQ`](/content/commands/object-freq.md). +- Reads inside a transaction, and commands sent with `CommandFlags.FireAndForget`. +- Error replies and replies larger than `MaxPayloadBytes`. + +A read-only script is invalidated only when a key in its `KEYS` array changes. +If a script reads a key that it doesn't declare, its cached result can be out +of date until `TimeToLive` expires, so declare every key or use +`CommandFlags.NoClientCache` for that script. + +### Consistency + +StackExchange.Redis guarantees the following: + +- You always read your own writes. A write through the multiplexer + invalidates the keys it names before it is sent to the server. +- You see writes from other clients shortly after they happen, when the + server's invalidation message arrives. +- If a connection fails, the client empties the whole cache. The + [`FLUSHDB`](/content/commands/flushdb.md) and + [`FLUSHALL`](/content/commands/flushall.md) commands also empty it. + +The server doesn't send an invalidation message when a key expires, so a +cached value can outlive its key until `TimeToLive` expires. If your app relies +on keys disappearing on time, use a short `TimeToLive` or use +`CommandFlags.NoClientCache` for those reads. + +### Monitoring the cache + +StackExchange.Redis doesn't provide statistics for the cache. Instead, check +the server: + +- Run [`CLIENT TRACKINGINFO`](/content/commands/client-trackinginfo.md) to + confirm that tracking is enabled. +- Compare the `total_commands_processed` field from + [`INFO stats`](/content/commands/info.md) before and after a workload. The + server never sees commands that the cache satisfies. +- Use `INFO commandstats` to see the same count for each command. + ## Multiplexing Although example code typically works with a single connection, diff --git a/content/develop/clients/dotnet/produsage.md b/content/develop/clients/dotnet/produsage.md index 2dbcc427cd..848d2db51c 100644 --- a/content/develop/clients/dotnet/produsage.md +++ b/content/develop/clients/dotnet/produsage.md @@ -25,6 +25,7 @@ for a recommendation. Use the checklist icons to record your progress in implementing the recommendations. ```checklist {id="dotnetprodlist"} +- [ ] [Client-side caching](#client-side-caching) - [ ] [Event handling](#event-handling) - [ ] [Timeouts](#timeouts) - [ ] [Exception handling](#exception-handling) @@ -36,6 +37,16 @@ progress in implementing the recommendations. The sections below offer recommendations for your production environment. Some of them may not apply to your particular use case. +### Client-side caching + +[Client-side caching](/content/develop/clients/client-side-caching.md) +involves storing the results from read-only commands in a local cache. If the +same command is executed again later, the results can be obtained from the cache, +without contacting the server. This improves command execution time on the client, +while also reducing network traffic and server load. See +[Connect using client-side caching](/content/develop/clients/dotnet/connect.md#connect-using-client-side-caching) +for more information and example code. + ### Event handling The `ConnectionMultiplexer` class publishes several different types of