Redis Setup

Athena.Cache supports Redis for distributed caching across multiple application instances. This guide covers Redis configuration, connection management, and best practices for production deployments.

Why Redis?

Redis enables distributed caching for scenarios like:

Basic Redis Setup

Install Package

dotnet add package Athena.Cache.Redis

Simple Configuration

// Program.cs
using Athena.Cache.Redis.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Replace MemoryCache setup with Redis
builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => {
        athenaOptions.Namespace = "MyApp";
        athenaOptions.DefaultExpirationMinutes = 30;
    },
    redisOptions => {
        redisOptions.ConnectionString = "localhost:6379";
        redisOptions.DatabaseId = 1;
        redisOptions.InstanceName = "MyApp";
    });

var app = builder.Build();

app.UseRouting();
app.UseAthenaCache();
app.MapControllers();

app.Run();

Redis Connection Options

Connection String Formats

// Single server
redisOptions.ConnectionString = "localhost:6379";

// With authentication
redisOptions.ConnectionString = "localhost:6379,password=mypassword";

// Multiple servers (Redis Cluster)
redisOptions.ConnectionString = "server1:6379,server2:6379,server3:6379";

// With SSL
redisOptions.ConnectionString = "redis.example.com:6380,ssl=true,password=secret";

// Azure Redis Cache
redisOptions.ConnectionString = "cachename.redis.cache.windows.net:6380,password=key,ssl=true";

Advanced Connection Options

builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => { /* ... */ },
    redisOptions => {
        redisOptions.ConnectionString = "localhost:6379";
        redisOptions.DatabaseId = 2;
        redisOptions.InstanceName = "MyApp_Prod";
        
        // Timeout settings
        redisOptions.ConnectTimeout = 10000;  // 10 seconds
        redisOptions.SyncTimeout = 2000;      // 2 seconds
        redisOptions.AsyncTimeout = 5000;     // 5 seconds
        
        // Retry settings
        redisOptions.ConnectRetry = 5;
        redisOptions.AbortOnConnectFail = false;
        
        // Performance settings
        redisOptions.AllowAdmin = false;
        redisOptions.ChannelPrefix = "cache:";
    });

Configuration from appsettings.json

Configuration File

{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  },
  "AthenaCache": {
    "Namespace": "MyApp_Prod",
    "DefaultExpirationMinutes": 60,
    "Logging": {
      "LogCacheHitMiss": true,
      "LogCacheInvalidation": true
    }
  },
  "RedisCacheOptions": {
    "ConnectionString": "localhost:6379",
    "DatabaseId": 1,
    "InstanceName": "MyApp",
    "ConnectTimeout": 5000,
    "SyncTimeout": 1000,
    "AsyncTimeout": 3000,
    "ConnectRetry": 3,
    "AbortOnConnectFail": false,
    "AllowAdmin": false,
    "ChannelPrefix": "cache:"
  }
}

Using Configuration

// Program.cs
builder.Services.AddAthenaCacheRedisComplete(
    builder.Configuration.GetSection("AthenaCache"),
    builder.Configuration.GetSection("RedisCacheOptions"));

// Or using connection string
builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => {
        athenaOptions.Namespace = "MyApp";
    },
    redisOptions => {
        redisOptions.ConnectionString = builder.Configuration.GetConnectionString("Redis");
        redisOptions.DatabaseId = 1;
    });

High Availability Setup

Redis Sentinel

For automatic failover with Redis Sentinel:

redisOptions.ConnectionString = "sentinel1:26379,sentinel2:26379,sentinel3:26379";
redisOptions.ServiceName = "mymaster";
redisOptions.AbortOnConnectFail = false;
redisOptions.ConnectRetry = 10;

Redis Cluster

For Redis Cluster deployment:

redisOptions.ConnectionString = 
    "node1:7000,node2:7000,node3:7000,node4:7000,node5:7000,node6:7000";
redisOptions.AbortOnConnectFail = false;

Cloud Redis Services

Azure Redis Cache

{
  "RedisCacheOptions": {
    "ConnectionString": "yourcache.redis.cache.windows.net:6380,password=yourkey,ssl=true",
    "DatabaseId": 0,
    "ConnectTimeout": 30000,
    "SyncTimeout": 5000
  }
}

AWS ElastiCache

{
  "RedisCacheOptions": {
    "ConnectionString": "your-cluster.cache.amazonaws.com:6379",
    "DatabaseId": 0,
    "ConnectTimeout": 15000
  }
}

Distributed Cache Invalidation

One of the major benefits of Redis is automatic cache invalidation across all instances.

Automatic Cross-instance Invalidation

// Instance A
[HttpPost]
[CacheInvalidateOn("Products")]
public async Task<ProductDto> CreateProduct([FromBody] CreateProductRequest request)
{
    var product = await _productService.CreateProductAsync(request);
    // This automatically invalidates "Products" cache on Instance B, C, etc.
    return product;
}

// Instance B, C, etc. - their "Products" caches are automatically cleared
[HttpGet]
[AthenaCache(ExpirationMinutes = 30)]
[CacheInvalidateOn("Products")]
public async Task<ProductDto[]> GetProducts()
{
    return await _productService.GetProductsAsync();
}

Manual Distributed Invalidation

[ApiController]
public class CacheAdminController : ControllerBase
{
    private readonly IDistributedCacheInvalidator _distributedInvalidator;

    public CacheAdminController(IDistributedCacheInvalidator distributedInvalidator)
    {
        _distributedInvalidator = distributedInvalidator;
    }

    [HttpDelete("cache/global/table/{tableName}")]
    public async Task<IActionResult> ClearTableGlobally(string tableName)
    {
        await _distributedInvalidator.InvalidateByTableAsync(tableName);
        return Ok($"Cleared {tableName} cache across ALL instances");
    }

    [HttpDelete("cache/global/pattern/{pattern}")]
    public async Task<IActionResult> ClearPatternGlobally(string pattern)
    {
        await _distributedInvalidator.InvalidateByPatternAsync(pattern);
        return Ok($"Cleared pattern {pattern} across ALL instances");
    }
}

Connection Management

Connection Health Monitoring

[ApiController]
public class RedisHealthController : ControllerBase
{
    private readonly IConnectionMultiplexer _redis;

    public RedisHealthController(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    [HttpGet("redis/health")]
    public IActionResult GetRedisHealth()
    {
        return Ok(new
        {
            IsConnected = _redis.IsConnected,
            ConnectionCount = _redis.GetCounters().Interactive.ConnectionCount,
            ServerEndpoints = _redis.GetEndPoints().Select(ep => ep.ToString()),
            ClientName = _redis.ClientName,
            Configuration = _redis.Configuration
        });
    }

    [HttpGet("redis/info")]
    public async Task<IActionResult> GetRedisInfo()
    {
        var db = _redis.GetDatabase();
        var server = _redis.GetServer(_redis.GetEndPoints().First());
        
        return Ok(new
        {
            DatabaseSize = await db.ExecuteAsync("DBSIZE"),
            ServerInfo = await server.InfoAsync("server"),
            MemoryInfo = await server.InfoAsync("memory"),
            StatsInfo = await server.InfoAsync("stats")
        });
    }
}

Connection Events

Monitor connection events for better observability:

// Program.cs
builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => { /* ... */ },
    redisOptions => { /* ... */ },
    connectionSetup => {
        connectionSetup.ConnectionFailed += (sender, args) =>
        {
            Console.WriteLine($"Redis connection failed: {args.FailureType} - {args.Exception?.Message}");
        };
        
        connectionSetup.ConnectionRestored += (sender, args) =>
        {
            Console.WriteLine($"Redis connection restored: {args.ConnectionType}");
        };
        
        connectionSetup.ErrorMessage += (sender, args) =>
        {
            Console.WriteLine($"Redis error: {args.Message}");
        };
    });

Performance Optimization

Connection Pooling

// Redis connections are automatically pooled, but you can tune pool settings
redisOptions.ConnectTimeout = 5000;   // Faster connection timeout
redisOptions.SyncTimeout = 1000;      // Faster sync operations
redisOptions.AsyncTimeout = 3000;     // Reasonable async timeout

Pipeline Operations

Athena.Cache automatically uses Redis pipelining for bulk operations:

// These operations are automatically pipelined for better performance
public async Task<Dictionary<string, UserDto>> GetMultipleUsers(int[] userIds)
{
    var tasks = userIds.Select(id => GetUserAsync(id));
    var users = await Task.WhenAll(tasks);
    
    return userIds.Zip(users, (id, user) => new { id, user })
                  .Where(x => x.user != null)
                  .ToDictionary(x => x.id.ToString(), x => x.user);
}

Memory Optimization

// Configure Redis-specific memory settings
redisOptions.ChannelPrefix = "cache:";  // Shorter prefix saves memory
redisOptions.DatabaseId = 1;            // Use specific database for cache data

Error Handling and Resilience

Fallback to Memory Cache

Configure fallback when Redis is unavailable:

builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => {
        athenaOptions.Resilience.EnableFallbackToMemory = true;
        athenaOptions.Resilience.FallbackToMemoryOnError = true;
        athenaOptions.ErrorHandling.OnCacheError = CacheErrorAction.LogAndContinue;
    },
    redisOptions => { /* ... */ });

Circuit Breaker Pattern

athenaOptions.Resilience.EnableCircuitBreaker = true;
athenaOptions.Resilience.FailureThreshold = 5;      // Open circuit after 5 failures
athenaOptions.Resilience.RecoveryTimeSeconds = 30;  // Try reconnecting after 30s

Retry Configuration

redisOptions.ConnectRetry = 5;           // Retry connection 5 times
redisOptions.AbortOnConnectFail = false; // Don't abort, keep retrying

Multi-tenant Setup

Database Separation

// Configure different databases per tenant
builder.Services.AddAthenaCacheRedisComplete(
    athenaOptions => {
        athenaOptions.Namespace = $"Tenant_{tenantId}";
    },
    redisOptions => {
        redisOptions.ConnectionString = "localhost:6379";
        redisOptions.DatabaseId = tenantId;  // Each tenant gets its own database
    });

Key Prefix Separation

// Use prefixes to separate tenant data within same database
athenaOptions.Namespace = $"Tenant_{tenantId}";

// Or use custom key patterns
[AthenaCache(KeyPattern = "tenant_{tenantId}_user_{id}")]
public async Task<UserDto> GetUser(string tenantId, int id) { ... }

Docker Setup

Docker Compose Example

version: '3.8'
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
      
  app:
    build: .
    ports:
      - "5000:80"
    depends_on:
      - redis
    environment:
      - ConnectionStrings__Redis=redis:6379
      - AthenaCache__Namespace=MyApp_Docker

volumes:
  redis_data:

Redis Configuration

# redis.conf for production
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec

Monitoring and Troubleshooting

Enable Redis Logging

athenaOptions.Logging.LogCacheOperations = true;  // Log Redis operations
athenaOptions.Logging.LogConnectionEvents = true; // Log connection events

Common Issues

Connection Timeouts

// Increase timeouts for slow networks
redisOptions.ConnectTimeout = 30000;  // 30 seconds
redisOptions.SyncTimeout = 5000;      // 5 seconds

Memory Issues

# Monitor Redis memory usage
redis-cli INFO memory

# Set appropriate maxmemory in redis.conf
maxmemory 1gb
maxmemory-policy allkeys-lru

Network Partitions

// Configure for network resilience
redisOptions.AbortOnConnectFail = false;
redisOptions.ConnectRetry = 10;
athenaOptions.Resilience.EnableFallbackToMemory = true;

Performance Monitoring

[HttpGet("redis/performance")]
public async Task<IActionResult> GetRedisPerformance()
{
    var server = _redis.GetServer(_redis.GetEndPoints().First());
    var info = await server.InfoAsync("stats");
    
    return Ok(new
    {
        TotalConnectionsReceived = info.FirstOrDefault(x => x.Key == "total_connections_received").Value,
        TotalCommandsProcessed = info.FirstOrDefault(x => x.Key == "total_commands_processed").Value,
        InstantaneousOpsPerSec = info.FirstOrDefault(x => x.Key == "instantaneous_ops_per_sec").Value,
        KeyspaceHits = info.FirstOrDefault(x => x.Key == "keyspace_hits").Value,
        KeyspaceMisses = info.FirstOrDefault(x => x.Key == "keyspace_misses").Value
    });
}

Next Steps