Skip to content

Commit 6860a0c

Browse files
committed
Add documentation for Redis read routing and replica support
Details the `ReadMode` configuration for Redis providers, including usage examples and safety considerations regarding replication lag for cache, queue, and file storage implementations.
1 parent 62a4a4c commit 6860a0c

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

‎docs/guide/implementations/redis.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ var cache = new RedisCacheClient(options =>
8383
options.ConnectionMultiplexer = redis;
8484
options.LoggerFactory = loggerFactory;
8585
options.Serializer = new SystemTextJsonSerializer();
86+
options.ReadMode = CommandFlags.PreferReplica; // Route reads to replicas
8687
});
8788
```
8889

@@ -249,6 +250,9 @@ var queue = new RedisQueue<WorkItem>(options =>
249250
// Run maintenance (cleanup dead letters)
250251
options.RunMaintenanceTasks = true;
251252

253+
// Route reads to replicas (see Read Routing section for caveats)
254+
options.ReadMode = CommandFlags.PreferReplica;
255+
252256
options.LoggerFactory = loggerFactory;
253257
});
254258
```
@@ -344,6 +348,7 @@ var storage = new RedisFileStorage(options =>
344348
options.ConnectionMultiplexer = redis;
345349
options.LoggerFactory = loggerFactory;
346350
options.Serializer = serializer;
351+
options.ReadMode = CommandFlags.PreferReplica; // Route reads to replicas
347352
});
348353
```
349354

@@ -569,6 +574,81 @@ await cache.SetAsync("session", data,
569574
expiresIn: TimeSpan.FromMinutes(30));
570575
```
571576

577+
## Read Routing (Replica Reads)
578+
579+
All Redis providers support a `ReadMode` option that controls how read operations are routed in a master-replica topology. By default, reads go to the master node (`CommandFlags.None`). Set `ReadMode` to `CommandFlags.PreferReplica` to distribute reads to replica nodes, reducing load on the master and improving read throughput.
580+
581+
### Configuration
582+
583+
```csharp
584+
using StackExchange.Redis;
585+
586+
// Enable replica reads on cache
587+
var cache = new RedisCacheClient(o => o
588+
.ConnectionMultiplexer(redis)
589+
.ReadMode(CommandFlags.PreferReplica));
590+
591+
// Enable replica reads on queue
592+
var queue = new RedisQueue<WorkItem>(o => o
593+
.ConnectionMultiplexer(redis)
594+
.ReadMode(CommandFlags.PreferReplica));
595+
596+
// Enable replica reads on file storage
597+
var storage = new RedisFileStorage(o => o
598+
.ConnectionMultiplexer(redis)
599+
.ReadMode(CommandFlags.PreferReplica));
600+
```
601+
602+
`PreferReplica` is safe on single-node deployments -- it falls back to the master when no replica exists. Write operations always go to the master regardless of this setting. Distributed locks are not affected (all lock operations use writes or Lua scripts on the master).
603+
604+
### ReadMode Values
605+
606+
| Value | Behavior | Use case |
607+
|-------|----------|----------|
608+
| `CommandFlags.None` (default) | Read from master | Backward compatible; strict consistency |
609+
| `CommandFlags.PreferReplica` | Read from replica if available, fall back to master | Recommended for master-replica topologies |
610+
| `CommandFlags.DemandReplica` | Replica only; error if none available | Dedicated read-scaling scenarios |
611+
| `CommandFlags.DemandMaster` | Master only; error if unavailable | Critical path operations |
612+
613+
### Operation Routing by Provider
614+
615+
| Provider | Operation | Routing |
616+
|----------|-----------|---------|
617+
| **RedisCacheClient** | `GetAsync`, `GetAllAsync`, `GetListAsync` | Via ReadMode |
618+
| | `ExistsAsync`, `GetExpirationAsync` | Via ReadMode |
619+
| | `SetAsync`, `RemoveAsync`, `IncrementAsync` | Always master |
620+
| | `GetAllExpirationAsync` | Always master (Lua script) |
621+
| | Lua scripts (`SetIfHigher`, `ReplaceIfEqual`, etc.) | Always master |
622+
| **RedisQueue** | Internal payload/metadata reads | Via ReadMode |
623+
| | Enqueue, dequeue, complete, abandon | Always master |
624+
| | Maintenance (work list, wait list scans) | Always master |
625+
| **RedisFileStorage** | `GetFileStreamAsync`, `GetFileInfoAsync`, `ExistsAsync` | Via ReadMode |
626+
| | `GetFileListAsync` | Via ReadMode |
627+
| | `SaveFileAsync`, `DeleteFileAsync` | Always master |
628+
| **RedisMessageBus** | Pub/sub | N/A (not routable) |
629+
630+
### Replication Lag Considerations
631+
632+
::: warning
633+
Redis/Valkey replication is asynchronous. When using `PreferReplica`, reads may return stale data during the replication lag window (typically sub-millisecond on AWS ElastiCache, but variable under load). Review the scenarios below before enabling replica reads.
634+
:::
635+
636+
| Scenario | Risk | Impact |
637+
|----------|------|--------|
638+
| **Queue: dequeue payload read** | **High** | After enqueue writes a payload, a dequeue on another process reads it back. If the replica hasn't replicated yet, the payload is `null`, the item is removed from the work list, and the message is silently lost. |
639+
| **Queue: abandon retry count** | Medium | The attempts counter is incremented on master, then read back during abandon. A stale replica read returns an old count, giving the item one extra retry before dead-lettering. |
640+
| **Queue: maintenance renewal check** | Medium | Lock renewal writes a timestamp to master. Maintenance reads it to check timeout. A stale read may auto-abandon an item that was just renewed, causing spurious re-processing. |
641+
| **Queue: maintenance wait time** | Low | Wait times for retry delays are read from cache. A stale read makes an item wait slightly longer before retry. |
642+
| **File storage: rename after save** | Low-Medium | `RenameFileAsync` reads file content immediately after save. A stale replica read could miss the just-written content. |
643+
| **Cache: sorted set expiration** | Low | Reads the highest score from a sorted set to determine TTL. A stale read sets a slightly inaccurate expiration. |
644+
| **Distributed locks** | **None** | Lock acquire, release, and renewal all use writes or Lua scripts that execute on master. |
645+
646+
**Per-provider guidance:**
647+
648+
- **RedisCacheClient**: `PreferReplica` is safe for most read-heavy workloads. Risk exists only if you read a key immediately after writing it from a different process.
649+
- **RedisQueue**: Use caution. Under very high throughput, dequeue can fail to read a just-enqueued payload, causing message loss. Consider keeping `CommandFlags.None` for queues processing critical work items.
650+
- **RedisFileStorage**: Generally safe. The rename-after-save edge case is unlikely in practice.
651+
572652
## Next Steps
573653

574654
- [Azure Implementation](./azure) - Azure Storage and Service Bus

0 commit comments

Comments
 (0)