https://github.com/ersintarhan/rediskit
Production-ready Redis toolkit for .NET 9 with advanced caching, pub/sub, and streaming features
https://github.com/ersintarhan/rediskit
cache circuit-breaker csharp dotnet high-performance messagepack net9 pubsub redis streams
Last synced: 5 months ago
JSON representation
Production-ready Redis toolkit for .NET 9 with advanced caching, pub/sub, and streaming features
- Host: GitHub
- URL: https://github.com/ersintarhan/rediskit
- Owner: ersintarhan
- License: mit
- Created: 2025-08-16T00:24:50.000Z (10 months ago)
- Default Branch: master
- Last Pushed: 2025-08-16T02:03:21.000Z (10 months ago)
- Last Synced: 2025-08-16T02:41:08.139Z (10 months ago)
- Topics: cache, circuit-breaker, csharp, dotnet, high-performance, messagepack, net9, pubsub, redis, streams
- Language: C#
- Homepage: https://ersintarhan.github.io/RedisKit
- Size: 6.21 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# RedisKit
[](https://github.com/ersintarhan/RedisKit/actions)
[](https://qodana.cloud/projects/ersintarhan-RedisKit)
[](https://github.com/ersintarhan/RedisKit/actions)
[](#testing)
[](https://dotnet.microsoft.com/)
[](https://www.nuget.org/packages/RedisKit/)
[](https://opensource.org/licenses/MIT)
[](#performance-benchmarks)
A production-ready, enterprise-grade Redis library for .NET 9 with advanced caching, pub/sub, and streaming features.
## ๐ Features
### Core Features
- **Caching**: Generic Get, Set, Delete operations with TTL support
- **Batch Operations**: GetMany and SetMany for improved performance
- **Key Prefixing**: Support for cache key prefixes
- **Pub/Sub**: Type-safe publishing and subscribing with advanced pattern matching
- **Streaming**: Redis Streams support with consumer groups and retry mechanisms
- **Multiple Serializers**: JSON, MessagePack support
- **Dependency Injection**: Full support with .NET DI container
- **High Performance Logging**: Source generator based logging with EventId support
- **Async/Await**: Full async/await support with CancellationToken
### Redis 7.x Features (NEW!)
- **๐ Redis Functions**: Server-side scripting with Lua (replacement for Redis Scripts)
- **๐ก Sharded Pub/Sub**: Scalable pub/sub across cluster shards
- **๐ง Function Library Builder**: Fluent API for creating Redis function libraries
- **๐ Array Return Types**: Full support for array results from Redis functions
- **๐ฏ Native Sharded Channel Support**: Using StackExchange.Redis's RedisChannel.Sharded() API
### Enterprise Features
- **๐ Distributed Locking**: Redis-based distributed locking with auto-renewal
- **๐ก๏ธ Redis Sentinel Support**: High availability with automatic failover (NEW!)
- **๐ Circuit Breaker Pattern**: Automatic failure detection and recovery
- **๐ Advanced Retry Strategies**: Multiple backoff strategies (Exponential, Decorrelated Jitter, etc.)
- **๐ฅ Health Monitoring**: Automatic health checks with auto-reconnection
- **๐ฏ Pattern Matching**: Redis glob pattern support (`*`, `?`, `[abc]`, `[^abc]`, `[a-z]`)
- **๐งน Memory Leak Prevention**: Automatic cleanup of inactive handlers
- **๐ Statistics & Monitoring**: Built-in metrics for subscriptions and connections
- **โก High Performance**: Optimized with concurrent collections and minimal allocations
- **๐ Thread Safety**: All operations are thread-safe
- **๐ Lua Script Optimization**: 90-95% performance improvement for batch operations
- **๐ Source-Generated Logging**: Zero-allocation high-performance logging
### Performance & Memory Optimizations
- **๐พ Object Pooling**: ArrayPool and ObjectPool for reduced GC pressure
- **๐ Streaming API**: IAsyncEnumerable for processing large datasets without memory overhead
- **๐๏ธ Dynamic Parallelism**: CPU-aware parallel processing (auto-scales with cores)
- **๐ฆ Smart Batching**: Size-based strategy selection for optimal performance
- **โก Inline Optimizations**: AggressiveInlining for hot paths
- **๐ Zero-Copy Operations**: Minimal allocations in critical paths
- **๐ ValueTask Support**: Reduced heap allocations in hot paths with ValueTask
- **๐ Memory & Span**: Zero-allocation serialization with Memory buffers
- **๐ Pipeline Batching**: ExecuteBatchAsync for multiple operations in single round-trip
## ๐ฆ Installation
Install the package via NuGet:
```bash
dotnet add package RedisKit
```
## ๐ฏ Quick Start
### Minimal Setup
```csharp
using RedisKit.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add Redis services with minimal configuration
builder.Services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
});
var app = builder.Build();
// Use in your controllers or services
app.MapGet("/cache/{key}", async (string key, IRedisCacheService cache) =>
{
var value = await cache.GetAsync(key);
return value ?? "Not found";
});
app.Run();
```
## ๐ถ Getting Started - Hello Redis!
### Your First Redis Cache
```csharp
using RedisKit.Extensions;
using RedisKit.Interfaces;
// 1. Setup - Add to your Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
});
var app = builder.Build();
// 2. Simple String Cache
app.MapPost("/hello/{name}", async (string name, IRedisCacheService cache) =>
{
// Store a simple string
await cache.SetAsync($"greeting:{name}", $"Hello, {name}!", TimeSpan.FromMinutes(5));
return $"Greeting saved for {name}";
});
app.MapGet("/hello/{name}", async (string name, IRedisCacheService cache) =>
{
// Retrieve the string
var greeting = await cache.GetAsync($"greeting:{name}");
return greeting ?? "No greeting found";
});
app.Run();
```
### Counter Example - Increment Values
```csharp
public class CounterService
{
private readonly IRedisCacheService _cache;
public CounterService(IRedisCacheService cache)
{
_cache = cache;
}
public async Task IncrementVisitCountAsync(string page)
{
var key = $"visits:{page}";
// Get current count
var currentCount = await _cache.GetAsync(key) ?? 0;
// Increment and save
currentCount++;
await _cache.SetAsync(key, currentCount, TimeSpan.FromDays(30));
return currentCount;
}
}
```
### Simple User Session
```csharp
public class SessionService
{
private readonly IRedisCacheService _cache;
public SessionService(IRedisCacheService cache)
{
_cache = cache;
}
// Store user session
public async Task CreateSessionAsync(string sessionId, string userId, string userName)
{
var session = new UserSession
{
UserId = userId,
UserName = userName,
LoginTime = DateTime.UtcNow
};
// Session expires in 20 minutes
await _cache.SetAsync($"session:{sessionId}", session, TimeSpan.FromMinutes(20));
}
// Get user session
public async Task GetSessionAsync(string sessionId)
{
return await _cache.GetAsync($"session:{sessionId}");
}
// Extend session
public async Task ExtendSessionAsync(string sessionId)
{
var session = await GetSessionAsync(sessionId);
if (session != null)
{
// Reset expiration to 20 minutes
await _cache.ExpireAsync($"session:{sessionId}", TimeSpan.FromMinutes(20));
}
}
}
public class UserSession
{
public string UserId { get; set; }
public string UserName { get; set; }
public DateTime LoginTime { get; set; }
}
```
## ๐ง Configuration
### Basic Configuration
```csharp
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.DefaultTtl = TimeSpan.FromHours(1);
options.CacheKeyPrefix = "myapp:";
options.Serializer = SerializerType.MessagePack; // or JSON
});
```
### Redis Sentinel Configuration (High Availability)
```csharp
services.AddRedisServices(options =>
{
// Configure Redis Sentinel for high availability and automatic failover
options.Sentinel = new SentinelOptions
{
Endpoints = new List
{
"sentinel1:26379",
"sentinel2:26379",
"sentinel3:26379"
},
ServiceName = "mymaster",
RedisPassword = "your_redis_password",
EnableFailoverHandling = true,
HealthCheckInterval = TimeSpan.FromSeconds(30)
};
// Note: When using Sentinel, ConnectionString is not required
// RedisKit will automatically discover the master via Sentinel
});
```
### Advanced Configuration
```csharp
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.DefaultTtl = TimeSpan.FromHours(1);
options.CacheKeyPrefix = "myapp:";
// Retry Configuration
options.RetryConfiguration = new RetryConfiguration
{
MaxAttempts = 3,
Strategy = BackoffStrategy.ExponentialWithJitter,
InitialDelay = TimeSpan.FromSeconds(1),
MaxDelay = TimeSpan.FromSeconds(30),
JitterFactor = 0.2 // 20% jitter
};
// Circuit Breaker
options.CircuitBreaker = new CircuitBreakerSettings
{
Enabled = true,
FailureThreshold = 5,
BreakDuration = TimeSpan.FromSeconds(30),
SuccessThreshold = 2
};
// Health Monitoring
options.HealthMonitoring = new HealthMonitoringSettings
{
Enabled = true,
CheckInterval = TimeSpan.FromSeconds(30),
AutoReconnect = true,
ConsecutiveFailuresThreshold = 3
};
// Connection Timeouts
options.TimeoutSettings = new ConnectionTimeoutSettings
{
ConnectTimeout = TimeSpan.FromSeconds(5),
SyncTimeout = TimeSpan.FromSeconds(5),
AsyncTimeout = TimeSpan.FromSeconds(5),
KeepAlive = TimeSpan.FromSeconds(60)
};
});
```
## ๐ Basic Usage Examples
### Simple Caching with ValueTask (Performance Optimized)
```csharp
public class ProductService
{
private readonly IRedisCacheService _cache;
public ProductService(IRedisCacheService cache)
{
_cache = cache;
}
public async ValueTask GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
// Try to get from cache - ValueTask for hot path optimization
var cached = await _cache.GetAsync(cacheKey);
if (cached != null)
return cached;
// Load from database
var product = await LoadFromDatabaseAsync(productId);
// Cache for 1 hour - ValueTask for reduced allocations
if (product != null)
{
await _cache.SetAsync(cacheKey, product, TimeSpan.FromHours(1));
}
return product;
}
public async Task InvalidateProductAsync(int productId)
{
await _cache.DeleteAsync($"product:{productId}");
}
}
```
### High-Performance Batch Operations
```csharp
public class CartService
{
private readonly IRedisCacheService _cache;
public CartService(IRedisCacheService cache)
{
_cache = cache;
}
// ExecuteBatchAsync - Multiple operations in single round-trip
public async Task GetCartSummaryAsync(string userId)
{
var result = await _cache.ExecuteBatchAsync(batch =>
{
batch.GetAsync($"cart:{userId}");
batch.GetAsync($"prefs:{userId}");
batch.GetAsync($"discount:{userId}");
batch.ExistsAsync($"premium:{userId}");
});
return new CartSummary
{
Cart = result.GetResult(0),
Preferences = result.GetResult(1),
DiscountRate = result.GetResult(2) ?? 0,
IsPremium = result.GetResult(3)
};
}
}
```
### Basic Pub/Sub
```csharp
public class NotificationService
{
private readonly IRedisPubSubService _pubSub;
private readonly ILogger _logger;
public NotificationService(IRedisPubSubService pubSub, ILogger logger)
{
_pubSub = pubSub;
_logger = logger;
}
// Publisher
public async Task SendNotificationAsync(string userId, string message)
{
var notification = new UserNotification
{
UserId = userId,
Message = message,
Timestamp = DateTime.UtcNow
};
await _pubSub.PublishAsync($"notifications:{userId}", notification);
}
// Subscriber
public async Task StartListeningAsync(string userId)
{
await _pubSub.SubscribeAsync(
$"notifications:{userId}",
async (notification, ct) =>
{
_logger.LogInformation("Received notification for user {UserId}: {Message}",
notification.UserId, notification.Message);
// Process notification
await ProcessNotificationAsync(notification);
});
}
}
public class UserNotification
{
public string UserId { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
```
### Batch Operations
```csharp
public class BulkOperationService
{
private readonly IRedisCacheService _cache;
public BulkOperationService(IRedisCacheService cache)
{
_cache = cache;
}
public async Task> GetProductsAsync(int[] productIds)
{
// Generate cache keys
var keys = productIds.Select(id => $"product:{id}");
// Get all products in one operation
var cached = await _cache.GetManyAsync(keys);
var result = new Dictionary();
var missingIds = new List();
// Check what we found in cache
foreach (var productId in productIds)
{
var key = $"product:{productId}";
if (cached.TryGetValue(key, out var product) && product != null)
{
result[productId] = product;
}
else
{
missingIds.Add(productId);
}
}
// Load missing from database
if (missingIds.Any())
{
var products = await LoadProductsFromDatabaseAsync(missingIds);
// Cache them
var toCache = new Dictionary();
foreach (var product in products)
{
result[product.Id] = product;
toCache[$"product:{product.Id}"] = product;
}
await _cache.SetManyAsync(toCache, TimeSpan.FromHours(1));
}
return result;
}
}
```
## ๐ Advanced Usage Examples
### Pattern-Based Subscriptions
```csharp
public class GameEventService
{
private readonly IRedisPubSubService _pubSub;
private readonly ILogger _logger;
private SubscriptionToken? _token;
public GameEventService(IRedisPubSubService pubSub, ILogger logger)
{
_pubSub = pubSub;
_logger = logger;
}
public async Task StartMonitoringAsync()
{
// Subscribe to all game events using pattern
_token = await _pubSub.SubscribePatternAsync(
"game:*:events",
async (gameEvent, ct) =>
{
_logger.LogInformation("Game {GameId} - Event: {EventType}",
gameEvent.GameId, gameEvent.EventType);
switch (gameEvent.EventType)
{
case "player_joined":
await HandlePlayerJoinedAsync(gameEvent);
break;
case "game_started":
await HandleGameStartedAsync(gameEvent);
break;
case "game_ended":
await HandleGameEndedAsync(gameEvent);
break;
}
});
// You can also subscribe with channel metadata
await _pubSub.SubscribePatternWithChannelAsync(
"game:*:critical",
async (gameEvent, channel, ct) =>
{
// Extract game ID from channel name
var parts = channel.Split(':');
var gameId = parts[1];
_logger.LogCritical("Critical event in game {GameId}: {Message}",
gameId, gameEvent.Message);
await SendAlertAsync(gameId, gameEvent);
});
}
public async Task StopMonitoringAsync()
{
if (_token != null)
{
await _token.UnsubscribeAsync();
}
}
}
```
### Redis Streams with Consumer Groups
```csharp
public class OrderProcessingService : BackgroundService
{
private readonly IRedisStreamService _streams;
private readonly ILogger _logger;
public OrderProcessingService(IRedisStreamService streams, ILogger logger)
{
_streams = streams;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Create consumer group
await _streams.CreateConsumerGroupAsync("orders", "order-processors");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Read messages from stream
var messages = await _streams.ReadGroupAsync(
"orders",
"order-processors",
"processor-1",
count: 10,
cancellationToken: stoppingToken);
foreach (var message in messages)
{
try
{
await ProcessOrderAsync(message.Data);
// Acknowledge message
await _streams.AcknowledgeAsync("orders", "order-processors", message.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", message.Data?.OrderId);
// Message will be retried
}
}
// Process pending messages (retry failed ones)
var retryResult = await _streams.RetryPendingMessagesAsync(
"orders",
"order-processors",
"processor-1",
async (order) =>
{
await ProcessOrderAsync(order);
return true; // Success
},
cancellationToken: stoppingToken);
if (retryResult.FailureCount > 0)
{
_logger.LogWarning("Failed to process {Count} orders, moved to DLQ",
retryResult.DeadLetterCount);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in order processing loop");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
private async Task ProcessOrderAsync(Order? order)
{
if (order == null) return;
_logger.LogInformation("Processing order {OrderId}", order.OrderId);
// Process the order
await Task.Delay(100); // Simulate work
// Publish completion event
await _pubSub.PublishAsync($"orders:{order.OrderId}:completed", new OrderCompleted
{
OrderId = order.OrderId,
CompletedAt = DateTime.UtcNow
});
}
}
```
### Cache-Aside Pattern with Statistics
```csharp
public class CachedRepository where T : class, IEntity
{
private readonly IRedisCacheService _cache;
private readonly ILogger> _logger;
private readonly string _entityName;
private long _hits = 0;
private long _misses = 0;
public CachedRepository(IRedisCacheService cache, ILogger> logger)
{
_cache = cache;
_logger = logger;
_entityName = typeof(T).Name.ToLower();
}
public async Task GetByIdAsync(string id, Func> dataLoader)
{
var key = $"{_entityName}:{id}";
// Try cache first
var cached = await _cache.GetAsync(key);
if (cached != null)
{
Interlocked.Increment(ref _hits);
return cached;
}
Interlocked.Increment(ref _misses);
// Load from source
var entity = await dataLoader();
if (entity != null)
{
// Cache with sliding expiration
await _cache.SetAsync(key, entity, TimeSpan.FromMinutes(15));
}
return entity;
}
public async Task GetOrCreateAsync(string id, Func> factory)
{
var key = $"{_entityName}:{id}";
var cached = await _cache.GetAsync(key);
if (cached != null)
{
Interlocked.Increment(ref _hits);
return cached;
}
Interlocked.Increment(ref _misses);
// Use distributed lock to prevent cache stampede
var lockKey = $"lock:{key}";
var lockAcquired = await _cache.SetAsync(
lockKey,
"locked",
TimeSpan.FromSeconds(30),
when: When.NotExists);
if (lockAcquired)
{
try
{
// Double-check after acquiring lock
cached = await _cache.GetAsync(key);
if (cached != null)
return cached;
// Create new entity
var entity = await factory();
await _cache.SetAsync(key, entity, TimeSpan.FromMinutes(15));
return entity;
}
finally
{
await _cache.DeleteAsync(lockKey);
}
}
else
{
// Wait for other thread to populate cache
await Task.Delay(100);
return await GetByIdAsync(id, factory) ?? await factory();
}
}
public CacheStatistics GetStatistics()
{
var total = _hits + _misses;
return new CacheStatistics
{
Hits = _hits,
Misses = _misses,
HitRate = total > 0 ? (double)_hits / total : 0
};
}
}
public interface IEntity
{
string Id { get; }
}
public class CacheStatistics
{
public long Hits { get; set; }
public long Misses { get; set; }
public double HitRate { get; set; }
}
```
## ๐จ Custom Serializer Implementation
### Creating a Custom Serializer
```csharp
using RedisKit.Serialization;
using ProtoBuf;
// Custom Protobuf serializer
public class ProtobufRedisSerializer : IRedisSerializer
{
public string Name => "Protobuf";
public Task SerializeAsync(T value, CancellationToken cancellationToken = default)
where T : class
{
if (value == null)
return Task.FromResult(Array.Empty());
using var stream = new MemoryStream();
Serializer.Serialize(stream, value);
return Task.FromResult(stream.ToArray());
}
public Task DeserializeAsync(byte[] data, CancellationToken cancellationToken = default)
where T : class
{
if (data == null || data.Length == 0)
return Task.FromResult(null);
using var stream = new MemoryStream(data);
var result = Serializer.Deserialize(stream);
return Task.FromResult(result);
}
public Task DeserializeAsync(byte[] data, Type type, CancellationToken cancellationToken = default)
{
if (data == null || data.Length == 0)
return Task.FromResult(null);
using var stream = new MemoryStream(data);
var result = Serializer.Deserialize(type, stream);
return Task.FromResult(result);
}
}
// Register custom serializer
public class CustomSerializerFactory : IRedisSerializerFactory
{
public IRedisSerializer Create(SerializerType type)
{
return type switch
{
SerializerType.Custom => new ProtobufRedisSerializer(),
_ => RedisSerializerFactory.Create(type)
};
}
}
```
### Using Custom Serializer
```csharp
// Option 1: Register globally
services.AddRedisServices(options =>
{
options.ConnectionString = "localhost:6379";
options.Serializer = SerializerType.Custom;
options.CustomSerializerFactory = new CustomSerializerFactory();
});
// Option 2: Use for specific service
services.AddSingleton();
services.AddSingleton(provider =>
{
var database = provider.GetRequiredService();
var logger = provider.GetRequiredService>();
var options = provider.GetRequiredService>();
var serializer = provider.GetRequiredService();
return new RedisCacheService(database, logger, options.Value, serializer);
});
```
### Compression Serializer Wrapper
```csharp
using System.IO.Compression;
public class CompressedSerializer : IRedisSerializer
{
private readonly IRedisSerializer _innerSerializer;
private readonly CompressionLevel _compressionLevel;
public CompressedSerializer(
IRedisSerializer innerSerializer,
CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
_innerSerializer = innerSerializer;
_compressionLevel = compressionLevel;
}
public string Name => $"Compressed_{_innerSerializer.Name}";
public async Task SerializeAsync(T value, CancellationToken cancellationToken = default)
where T : class
{
var data = await _innerSerializer.SerializeAsync(value, cancellationToken);
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, _compressionLevel))
{
await gzip.WriteAsync(data, 0, data.Length, cancellationToken);
}
return output.ToArray();
}
public async Task DeserializeAsync(byte[] data, CancellationToken cancellationToken = default)
where T : class
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
await gzip.CopyToAsync(output, cancellationToken);
var decompressed = output.ToArray();
return await _innerSerializer.DeserializeAsync(decompressed, cancellationToken);
}
public async Task DeserializeAsync(
byte[] data,
Type type,
CancellationToken cancellationToken = default)
{
using var input = new MemoryStream(data);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
await gzip.CopyToAsync(output, cancellationToken);
var decompressed = output.ToArray();
return await _innerSerializer.DeserializeAsync(decompressed, type, cancellationToken);
}
}
// Usage
services.AddSingleton(provider =>
{
var innerSerializer = RedisSerializerFactory.Create(SerializerType.MessagePack);
return new CompressedSerializer(innerSerializer, CompressionLevel.Fastest);
});
```
## ๐๏ธ Dependency Injection
```csharp
// In your Program.cs or Startup.cs:
var builder = WebApplication.CreateBuilder(args);
// Add Redis services with configuration from appsettings.json
builder.Services.Configure(
builder.Configuration.GetSection("Redis"));
builder.Services.AddRedisServices(options =>
{
builder.Configuration.GetSection("Redis").Bind(options);
});
// In your services:
public class UserService
{
private readonly IRedisCacheService _cache;
private readonly IRedisPubSubService _pubSub;
private readonly IRedisStreamService _stream;
private readonly ILogger _logger;
public UserService(
IRedisCacheService cache,
IRedisPubSubService pubSub,
IRedisStreamService stream,
ILogger logger)
{
_cache = cache;
_pubSub = pubSub;
_stream = stream;
_logger = logger;
}
public async Task GetUserAsync(string userId)
{
// Try cache first
var cached = await _cache.GetAsync($"user:{userId}");
if (cached != null)
return cached;
// Load from database
var user = await LoadFromDatabaseAsync(userId);
// Cache for future requests
if (user != null)
{
await _cache.SetAsync($"user:{userId}", user, TimeSpan.FromHours(1));
// Publish update event
await _pubSub.PublishAsync("user-updates", new UserLoadedEvent
{
UserId = userId
});
}
return user;
}
}
```
## ๐ฏ Backoff Strategies
RedisKit supports multiple backoff strategies for retry operations:
- **Fixed**: Constant delay between retries
- **Linear**: Linear increase in delay
- **Exponential**: Exponential increase in delay
- **ExponentialWithJitter**: Exponential with random jitter to prevent thundering herd
- **DecorrelatedJitter**: AWS-recommended strategy with decorrelated jitter
```csharp
options.RetryConfiguration = new RetryConfiguration
{
Strategy = BackoffStrategy.DecorrelatedJitter,
MaxAttempts = 5,
InitialDelay = TimeSpan.FromMilliseconds(100),
MaxDelay = TimeSpan.FromSeconds(10)
};
```
## ๐ Performance Tips & Best Practices
### 1. **Connection Management**
```csharp
// โ DON'T: Create new connections for each operation
public async Task BadExample()
{
var connection = await ConnectionMultiplexer.ConnectAsync("localhost");
var db = connection.GetDatabase();
await db.StringSetAsync("key", "value");
connection.Dispose(); // Connection closed!
}
// โ
DO: Use dependency injection and connection pooling
public class GoodExample
{
private readonly IRedisCacheService _cache; // Injected, pooled connection
public async Task SetValueAsync()
{
await _cache.SetAsync("key", "value");
}
}
```
### 2. **Batch Operations for Better Performance**
```csharp
// โ DON'T: Multiple round trips
public async Task SlowApproach(string[] userIds)
{
var users = new List();
foreach (var id in userIds)
{
var user = await _cache.GetAsync($"user:{id}");
if (user != null) users.Add(user);
}
}
// โ
DO: Single batch operation
public async Task FastApproach(string[] userIds)
{
var keys = userIds.Select(id => $"user:{id}");
var results = await _cache.GetManyAsync(keys);
var users = results.Values.Where(u => u != null).ToList();
}
```
### 3. **Big Key Handling**
```csharp
// โ DON'T: Store huge objects as single keys
public async Task BadBigKey()
{
var hugeList = new List(1_000_000); // 1 million items!
await _cache.SetAsync("huge:list", hugeList); // This blocks Redis!
}
// โ
DO: Split large datasets
public async Task GoodBigKeyHandling()
{
var items = GetLargeDataset();
var chunks = items.Chunk(1000); // Split into chunks of 1000
var tasks = chunks.Select((chunk, index) =>
_cache.SetAsync($"items:chunk:{index}", chunk.ToList(), TimeSpan.FromHours(1))
);
await Task.WhenAll(tasks);
}
// โ
DO: Use Redis Streams for large datasets
public async Task StreamApproach(List items)
{
foreach (var batch in items.Chunk(100))
{
foreach (var item in batch)
{
await _streamService.AddAsync("items:stream", item);
}
}
}
```
### 4. **Pipeline Usage**
```csharp
// โ DON'T: Sequential operations
public async Task SlowSequential()
{
await _cache.SetAsync("key1", "value1");
await _cache.SetAsync("key2", "value2");
await _cache.SetAsync("key3", "value3");
// 3 round trips to Redis
}
// โ
DO: Use batch/pipeline operations
public async Task FastPipeline()
{
var items = new Dictionary
{
["key1"] = "value1",
["key2"] = "value2",
["key3"] = "value3"
};
await _cache.SetManyAsync(items, TimeSpan.FromHours(1));
// Single round trip!
}
```
### 5. **Memory Optimization**
```csharp
// โ
Use appropriate serializers
services.AddRedisServices(options =>
{
// MessagePack: Fastest and smallest
options.Serializer = SerializerType.MessagePack;
// JSON: Human readable, larger size
// options.Serializer = SerializerType.SystemTextJson;
});
// โ
Compress large objects
public class CompressedCacheService
{
private readonly IRedisCacheService _cache;
public async Task SetCompressedAsync(string key, T value) where T : class
{
if (value is string str && str.Length > 1000)
{
// Compress strings larger than 1KB
var compressed = Compress(str);
await _cache.SetAsync($"{key}:compressed", compressed);
}
else
{
await _cache.SetAsync(key, value);
}
}
}
```
### 6. **Key Expiration Strategies**
```csharp
// โ
Use sliding expiration for frequently accessed data
public async Task GetWithSlidingExpirationAsync(string key) where T : class
{
var value = await _cache.GetAsync(key);
if (value != null)
{
// Reset expiration on each access
await _cache.ExpireAsync(key, TimeSpan.FromMinutes(30));
}
return value;
}
// โ
Use absolute expiration for time-sensitive data
public async Task SetDailyReportAsync(Report report)
{
var tomorrow = DateTime.UtcNow.Date.AddDays(1);
var ttl = tomorrow - DateTime.UtcNow;
await _cache.SetAsync($"report:{DateTime.UtcNow:yyyy-MM-dd}", report, ttl);
}
```
### 7. **Avoid Hot Keys**
```csharp
// โ DON'T: Single key for global counter
public async Task IncrementGlobalCounter()
{
var count = await _cache.GetAsync("global:counter");
await _cache.SetAsync("global:counter", count + 1);
// This key becomes a bottleneck!
}
// โ
DO: Distribute load across multiple keys
public async Task IncrementDistributedCounter()
{
var shard = Random.Shared.Next(0, 10); // 10 shards
var key = $"counter:shard:{shard}";
var count = await _cache.GetAsync(key);
await _cache.SetAsync(key, count + 1);
}
public async Task GetTotalCount()
{
var tasks = Enumerable.Range(0, 10)
.Select(i => _cache.GetAsync($"counter:shard:{i}"));
var counts = await Task.WhenAll(tasks);
return counts.Sum();
}
```
### 8. **Circuit Breaker for Resilience**
```csharp
// โ
Configure circuit breaker to prevent cascade failures
services.AddRedisServices(options =>
{
options.CircuitBreaker = new CircuitBreakerSettings
{
Enabled = true,
FailureThreshold = 5, // Open after 5 failures
BreakDuration = TimeSpan.FromSeconds(30), // Stay open for 30s
SuccessThreshold = 2 // Need 2 successes to close
};
});
```
### 9. **Monitoring & Metrics**
```csharp
// โ
Track cache hit rates
public class MetricsCacheService
{
private readonly IRedisCacheService _cache;
private readonly IMetrics _metrics;
public async Task GetWithMetricsAsync(string key) where T : class
{
var value = await _cache.GetAsync(key);
if (value != null)
_metrics.Increment("cache.hits");
else
_metrics.Increment("cache.misses");
return value;
}
}
```
### 10. **Pub/Sub Performance**
```csharp
// โ
Use pattern subscriptions wisely
public class EfficientPubSub
{
private readonly IRedisPubSubService _pubSub;
public async Task SubscribeEfficiently()
{
// Instead of subscribing to many individual channels
// Use pattern subscription
await _pubSub.SubscribePatternAsync(
"events:*", // Single pattern subscription
async (evt, ct) => await ProcessEventAsync(evt, ct)
);
}
}
```
## โก Recent Performance Improvements
### SetManyAsync Optimization with Lua Scripts
We've implemented a significant performance optimization for batch operations using Lua scripts:
- **90-95% performance improvement** for batch SET operations
- **Single round-trip** to Redis instead of O(n) operations
- **Automatic fallback** for environments without Lua support
- **Parallel serialization** for optimal CPU utilization
#### Before vs After Performance
| Batch Size | Before (ms) | After (ms) | Improvement |
|------------|-------------|------------|----------------|
| 100 items | 52 | 3 | **94% faster** |
| 500 items | 258 | 14 | **95% faster** |
| 1000 items | 516 | 28 | **95% faster** |
| 5000 items | 2,580 | 140 | **95% faster** |
*Benchmarks on local Redis with 1KB objects*
## ๐ Performance Benchmarks
### Serializer Performance Comparison
| Method | JSON (ns) | MessagePack (ns) | Speed Improvement | Memory Improvement |
|---------------------------------|-----------|------------------|-------------------|----------------------|
| **Small Object Serialize** | 331.8 | 143.2 | **2.3x faster** | **5.6x less memory** |
| **Large Object Serialize** | 3,569.1 | 1,940.7 | **1.8x faster** | **Similar memory** |
| **Array Serialize (100 items)** | 28,143.8 | 11,556.8 | **2.4x faster** | **3.2x less memory** |
| **Small Object Deserialize** | 628.0 | 256.5 | **2.4x faster** | **2.1x less memory** |
| **Async Serialize** | 355.9 | 173.8 | **2.0x faster** | **2.8x less memory** |
| **Async Deserialize** | 823.8 | 290.0 | **2.8x faster** | **2.0x less memory** |
> **Recommendation**: Use MessagePack for production workloads requiring high performance and low memory usage.
### Redis Operations Performance
| Operation | Single Item | Batch (100 items) | Batch (1000 items) |
|------------|-------------|-------------------|--------------------|
| Set | ~1ms | ~5ms | ~40ms |
| Get | ~0.8ms | ~4ms | ~35ms |
| Pub/Sub | ~0.5ms | N/A | N/A |
| Stream Add | ~1.2ms | ~8ms | ~70ms |
*Benchmarks on local Redis, actual performance depends on network latency and Redis server specs*
## ๐ Performance Considerations
- **Connection Pooling**: Connections are automatically pooled and reused
- **Pipelining**: Commands are automatically pipelined for better throughput
- **Memory Efficiency**: Uses ArrayPool and MemoryPool to minimize allocations
- **Concurrent Operations**: Thread-safe operations with minimal locking
- **Circuit Breaker**: Prevents cascading failures in distributed systems
- **Automatic Cleanup**: Inactive handlers are automatically cleaned up to prevent memory leaks
## ๐งช Testing
The library includes comprehensive unit tests with 100% coverage of critical paths:
```bash
dotnet test
# Results
Passed! - Failed: 0, Passed: 140, Skipped: 12, Total: 152
```
## ๐ Requirements
- .NET 9.0 or higher
- Redis Server 5.0 or higher (6.0+ recommended for Streams support)
## ๐ Security & Dependencies
- **Automated Dependency Updates**: Dependabot configured for weekly security updates
- **Security Policy**: See [SECURITY.md](SECURITY.md) for vulnerability reporting
- **CI/CD Pipeline**: Automated testing on multiple platforms (Windows, Linux, macOS)
- **Auto-merge**: Minor and patch updates are automatically merged after passing tests
## ๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## ๐ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Documentation
For more detailed documentation, please visit our [Wiki](https://github.com/ersintarhan/RedisKit/wiki).
## ๐ Redis 7.x Usage Examples
### Redis Functions
```csharp
// Create and load a function library
var functionService = app.Services.GetRequiredService();
// Build a function library with FunctionLibraryBuilder
var library = new FunctionLibraryBuilder()
.WithName("mylib")
.WithEngine("LUA")
.WithDescription("My custom functions")
.AddFunction("greet", @"
function(keys, args)
return 'Hello, ' .. args[1]
end
")
.AddFunction("sum", @"
function(keys, args)
return tonumber(args[1]) + tonumber(args[2])
end
")
.Build();
// Load the library
await functionService.LoadAsync(library);
// Call functions
var greeting = await functionService.CallAsync("greet", args: new[] { "World" });
// Result: "Hello, World"
var sum = await functionService.CallAsync("sum", args: new[] { "10", "20" });
// Result: 30
// Call with array return type
var results = await functionService.CallAsync("get_users");
// Result: ["user1", "user2", "user3"]
```
### Sharded Pub/Sub
```csharp
// Sharded Pub/Sub for better scalability in cluster mode
var shardedPubSub = app.Services.GetRequiredService();
// Subscribe to a sharded channel (distributed across shards)
var token = await shardedPubSub.SubscribeAsync(
"orders:new",
async (message, ct) =>
{
Console.WriteLine($"Order received on shard: {message.ShardId}");
await ProcessOrder(message.Data);
});
// Publish to sharded channel (automatically routed to correct shard)
var subscribers = await shardedPubSub.PublishAsync(
"orders:new",
new OrderMessage { OrderId = 123, Amount = 99.99m });
// Note: Pattern subscriptions are NOT supported in sharded pub/sub
// Use regular pub/sub for patterns
```
## ๐ Known Issues
- Stream service tests are currently skipped as they require a real Redis instance
- PUBSUB NUMSUB command returns local handler count only (StackExchange.Redis limitation)
- Sharded Pub/Sub does not support pattern subscriptions (Redis limitation)
## ๐ฆ Roadmap
- [x] Redis Functions support (Redis 7.x) - โ
Completed
- [x] Sharded Pub/Sub (Redis 7.x) - โ
Completed
- [x] Distributed locking primitives - โ
Completed
- [x] Redis Sentinel support (High Availability with automatic failover)
- [ ] Redis Cluster native support
- [ ] ACL v2 improvements (Redis 7.x)
- [ ] Client-side caching support
- [ ] Geo-spatial operations
- [ ] Time-series data support
- [ ] OpenTelemetry integration
- [ ] Prometheus metrics export