Advanced Redis Usage

Extending Redis for complex application requirements

Beyond Basic Redis

While Redis is powerful even with its core functionality, this supplementary guide will explore more advanced features and usage patterns that can help you leverage Redis for complex application needs.

mindmap
  root((Redis Advanced
Features)) Lua Scripting Atomic Operations Complex Logic Reduce Network Trips Redis Modules RedisJSON RediSearch RedisGraph RedisTimeSeries High Availability Sentinel Cluster Replication Enterprise Features ACID Transactions Multi-tenancy Active-Active Geo Optimization Pipelining Memory Optimization Command Analysis

Lua Scripting in Redis

Redis supports the execution of Lua scripts, which provide a way to execute multiple commands atomically and can reduce network round-trips. Think of Lua scripts as stored procedures for Redis.

Why Use Lua Scripts?

Basic Lua Script Example


// Simple increment and get script
const incrementScript = `
    local key = KEYS[1]
    local increment = tonumber(ARGV[1])
    
    -- Increment the value
    redis.call('INCRBY', key, increment)
    
    -- Get the new value
    return redis.call('GET', key)
`;

// Execute the script
async function incrementAndGet(key, increment) {
  return client.eval(
    incrementScript,
    {
      keys: [key],
      arguments: [increment.toString()]
    }
  );
}

// Usage
async function testScript() {
  await client.set('counter', '10');
  
  const newValue = await incrementAndGet('counter', 5);
  console.log(`New counter value: ${newValue}`);  // "New counter value: 15"
}
            

Script Loading and Execution

For frequently used scripts, it's more efficient to load them once and then execute them by their SHA1 hash.


// Load a script and store its SHA1 hash
async function loadScripts() {
  const atomicTransferScript = `
    local sourceKey = KEYS[1]
    local destKey = KEYS[2]
    local amount = tonumber(ARGV[1])
    
    -- Get current balances
    local sourceBalance = tonumber(redis.call('GET', sourceKey) or 0)
    local destBalance = tonumber(redis.call('GET', destKey) or 0)
    
    -- Check if source has enough balance
    if sourceBalance < amount then
      return {err = "Insufficient balance"}
    end
    
    -- Perform the transfer
    redis.call('DECRBY', sourceKey, amount)
    redis.call('INCRBY', destKey, amount)
    
    -- Return new balances
    return {
      sourceBalance - amount,
      destBalance + amount
    }
  `;
  
  // Load the script and get its SHA1 hash
  const scriptHash = await client.scriptLoad(atomicTransferScript);
  console.log(`Loaded script with hash: ${scriptHash}`);
  
  return scriptHash;
}

// Execute the script using its hash
async function transferMoney(sourceUserId, destUserId, amount, scriptHash) {
  const sourceKey = `user:${sourceUserId}:balance`;
  const destKey = `user:${destUserId}:balance`;
  
  try {
    const result = await client.evalSha(
      scriptHash,
      {
        keys: [sourceKey, destKey],
        arguments: [amount.toString()]
      }
    );
    
    console.log(`Transfer complete. New balances: Source=${result[0]}, Dest=${result[1]}`);
    return { success: true, newSourceBalance: result[0], newDestBalance: result[1] };
  } catch (error) {
    console.error('Transfer error:', error);
    return { success: false, error: error.message };
  }
}

// Usage
async function testMoneyTransfer() {
  // Set initial balances
  await client.set('user:100:balance', '500');
  await client.set('user:200:balance', '300');
  
  // Load the script
  const scriptHash = await loadScripts();
  
  // Perform transfers
  await transferMoney(100, 200, 100, scriptHash);  // Should succeed
  await transferMoney(100, 200, 1000, scriptHash); // Should fail - insufficient balance
}
            

Complex Lua Script Example: Rate Limiter with Token Bucket

This example implements a token bucket rate limiter, which is more flexible than simple counters.


// Token bucket rate limiter script
const tokenBucketScript = `
    local key = KEYS[1]
    local maxTokens = tonumber(ARGV[1])
    local tokensPerSecond = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    local requestTokens = tonumber(ARGV[4])
    
    -- Create bucket if it doesn't exist
    local exists = redis.call('EXISTS', key)
    if exists == 0 then
      redis.call('HSET', key, 'tokens', maxTokens, 'last_refill', now)
    end
    
    -- Get bucket info
    local tokens = tonumber(redis.call('HGET', key, 'tokens'))
    local lastRefill = tonumber(redis.call('HGET', key, 'last_refill'))
    
    -- Calculate token refill
    local timePassed = math.max(0, now - lastRefill)
    local newTokens = math.min(maxTokens, tokens + (timePassed * tokensPerSecond))
    
    -- Set default values
    local allowed = 0
    local newTokensAfterRequest = newTokens
    
    -- Check if enough tokens and consume
    if newTokens >= requestTokens then
      newTokensAfterRequest = newTokens - requestTokens
      allowed = 1
    end
    
    -- Update bucket
    redis.call('HSET', key, 'tokens', newTokensAfterRequest, 'last_refill', now)
    redis.call('EXPIRE', key, 60) -- Auto-cleanup after 60 seconds of inactivity
    
    -- Return allowed status and remaining tokens
    return {allowed, newTokensAfterRequest}
`;

// Rate limiter implementation
class TokenBucketRateLimiter {
  constructor(client, options = {}) {
    this.client = client;
    this.maxTokens = options.maxTokens || 10;
    this.tokensPerSecond = options.tokensPerSecond || 1;
    this.script = tokenBucketScript;
    this.scriptHash = null;
  }
  
  // Initialize the rate limiter
  async init() {
    this.scriptHash = await this.client.scriptLoad(this.script);
    return this;
  }
  
  // Check if a request is allowed
  async allowRequest(userId, tokens = 1) {
    const key = `ratelimit:${userId}`;
    const now = Math.floor(Date.now() / 1000);
    
    const result = await this.client.evalSha(
      this.scriptHash,
      {
        keys: [key],
        arguments: [
          this.maxTokens.toString(),
          this.tokensPerSecond.toString(),
          now.toString(),
          tokens.toString()
        ]
      }
    );
    
    return {
      allowed: result[0] === 1,
      remainingTokens: result[1]
    };
  }
}

// Express middleware using token bucket rate limiter
async function createRateLimiterMiddleware() {
  const rateLimiter = await new TokenBucketRateLimiter(client, {
    maxTokens: 20,         // Burst capacity
    tokensPerSecond: 0.2   // 1 token every 5 seconds (0.2 per second)
  }).init();
  
  return async (req, res, next) => {
    const userId = req.ip || 'anonymous';
    
    // Some endpoints might cost more tokens
    let tokenCost = 1;
    if (req.path.includes('/api/expensive-operation')) {
      tokenCost = 5;
    }
    
    const result = await rateLimiter.allowRequest(userId, tokenCost);
    
    // Set headers
    res.setHeader('X-RateLimit-Limit', rateLimiter.maxTokens);
    res.setHeader('X-RateLimit-Remaining', result.remainingTokens);
    res.setHeader('X-RateLimit-Reset', Math.ceil(
      (rateLimiter.maxTokens - result.remainingTokens) / rateLimiter.tokensPerSecond
    ));
    
    if (!result.allowed) {
      return res.status(429).json({
        error: 'Too many requests',
        retryAfter: Math.ceil(tokenCost / rateLimiter.tokensPerSecond)
      });
    }
    
    next();
  };
}
            

Best Practices for Lua Scripts

  • Keep scripts idempotent when possible (same input always produces same output).
  • Avoid long-running scripts that could block Redis.
  • Use EVALSHA for scripts that are executed frequently.
  • Include error handling in your scripts to make debugging easier.
  • Limit the number of keys accessed by a script for better performance in Redis Cluster.

Redis Modules and Redis Stack

Redis modules extend Redis with new data types and commands. Redis Stack is a collection of modules that add powerful capabilities to Redis.

Key Redis Modules

Using RedisJSON

RedisJSON allows you to store and manipulate JSON documents natively in Redis, with efficient path-based operations.


// Note: Requires Redis Stack or Redis with the RedisJSON module loaded
// Install: npm install redis @redis/json

const { createClient } = require('redis');

async function redisJsonExample() {
  const client = createClient();
  await client.connect();
  
  // Store a JSON document
  await client.json.set('user:1000', '$', {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30,
    address: {
      city: 'New York',
      country: 'USA'
    },
    tags: ['developer', 'nodejs', 'redis']
  });
  
  // Get the entire document
  const user = await client.json.get('user:1000', {
    path: '$'
  });
  console.log('User:', user);
  
  // Get specific fields using path expressions
  const name = await client.json.get('user:1000', {
    path: '$.name'
  });
  console.log('Name:', name);
  
  // Get nested object
  const address = await client.json.get('user:1000', {
    path: '$.address'
  });
  console.log('Address:', address);
  
  // Update a specific field
  await client.json.set('user:1000', '$.age', 31);
  
  // Update multiple fields
  await client.json.merge('user:1000', '$', {
    subscription: 'premium',
    lastLogin: new Date().toISOString()
  });
  
  // Array operations
  await client.json.arrAppend('user:1000', '$.tags', 'redis-json');
  
  // Get array length
  const tagsLength = await client.json.arrLen('user:1000', '$.tags');
  console.log('Tags count:', tagsLength);
  
  // Object key operations
  const keys = await client.json.objKeys('user:1000', '$');
  console.log('Object keys:', keys);
  
  // Check if a key exists
  const hasSubscription = await client.json.objKeys('user:1000', '$').then(
    keys => keys.includes('subscription')
  );
  console.log('Has subscription:', hasSubscription);
}
            

Using RediSearch

RediSearch provides full-text search capabilities for Redis, enabling complex queries and filtering.


// Note: Requires Redis Stack or Redis with the RediSearch module loaded
// Install: npm install redis @redis/search

async function rediSearchExample() {
  const client = createClient();
  await client.connect();
  
  // Create a search index
  try {
    await client.ft.create('productIdx', {
      '$.name': {
        type: 'TEXT',
        SORTABLE: true
      },
      '$.description': {
        type: 'TEXT',
        WEIGHT: 0.5
      },
      '$.price': {
        type: 'NUMERIC',
        SORTABLE: true
      },
      '$.category': {
        type: 'TAG'
      },
      '$.in_stock': {
        type: 'TAG'
      }
    }, {
      ON: 'JSON',
      PREFIX: 'product:'
    });
  } catch (err) {
    // Index might already exist
    if (!err.message.includes('Index already exists')) {
      throw err;
    }
  }
  
  // Add some products
  await client.json.set('product:1', '$', {
    name: 'iPhone 13',
    description: 'The latest iPhone with A15 Bionic chip',
    price: 799,
    category: 'electronics',
    in_stock: 'true'
  });
  
  await client.json.set('product:2', '$', {
    name: 'MacBook Pro',
    description: 'Powerful laptop for professionals',
    price: 1299,
    category: 'electronics',
    in_stock: 'true'
  });
  
  await client.json.set('product:3', '$', {
    name: 'Coffee Maker',
    description: 'Automatic drip coffee maker with timer',
    price: 49.99,
    category: 'kitchen',
    in_stock: 'true'
  });
  
  // Simple text search
  const results1 = await client.ft.search('productIdx', 'iphone');
  console.log('iPhone search results:', results1);
  
  // Combined text search
  const results2 = await client.ft.search('productIdx', 'laptop | coffee');
  console.log('Laptop or coffee results:', results2);
  
  // With tag filtering
  const results3 = await client.ft.search(
    'productIdx',
    '@category:{electronics} @in_stock:{true}'
  );
  console.log('In-stock electronics:', results3);
  
  // With numeric range and sorting
  const results4 = await client.ft.search(
    'productIdx',
    '@price:[0 100]',
    {
      SORTBY: {
        BY: '$.price',
        DIRECTION: 'DESC'
      }
    }
  );
  console.log('Products under $100 (sorted by price):', results4);
  
  // Complex query with highlighting
  const results5 = await client.ft.search(
    'productIdx',
    'professional',
    {
      RETURN: ['$.name', '$.price'],
      HIGHLIGHT: {
        FIELDS: ['$.description'],
        TAGS: ['', '']
      }
    }
  );
  console.log('Highlighted results:', results5);
  
  // Aggregations
  const agg = await client.ft.aggregate(
    'productIdx',
    '*',
    {
      GROUPBY: {
        properties: ['@category'],
        REDUCE: [{
          type: 'AVG',
          property: '@price',
          AS: 'avg_price'
        }, {
          type: 'COUNT',
          AS: 'count'
        }]
      },
      SORTBY: {
        BY: 'avg_price',
        DIRECTION: 'DESC'
      }
    }
  );
  console.log('Price aggregation by category:', agg);
}
            

Using RedisTimeSeries

RedisTimeSeries is perfect for time series data like metrics, monitoring, and IoT sensor data.


// Note: Requires Redis Stack or Redis with the RedisTimeSeries module loaded
// Install: npm install redis @redis/time-series

async function timeSeriesExample() {
  const client = createClient();
  await client.connect();
  
  // Create a time series
  await client.ts.create('temperature:sensor1', {
    RETENTION: 86400000,  // Retain data for 24 hours (in ms)
    LABELS: {
      sensor_id: 'sensor1',
      location: 'living_room',
      type: 'temperature'
    }
  });
  
  await client.ts.create('humidity:sensor1', {
    RETENTION: 86400000,
    LABELS: {
      sensor_id: 'sensor1',
      location: 'living_room',
      type: 'humidity'
    }
  });
  
  // Add data points
  const now = Date.now();
  
  // Add temperature readings
  await client.ts.add('temperature:sensor1', now - 3600000, 22.5);  // 1 hour ago
  await client.ts.add('temperature:sensor1', now - 2700000, 23.0);  // 45 min ago
  await client.ts.add('temperature:sensor1', now - 1800000, 23.2);  // 30 min ago
  await client.ts.add('temperature:sensor1', now - 900000, 23.5);   // 15 min ago
  await client.ts.add('temperature:sensor1', now, 24.0);            // now
  
  // Add humidity readings
  await client.ts.add('humidity:sensor1', now - 3600000, 45);
  await client.ts.add('humidity:sensor1', now - 2700000, 47);
  await client.ts.add('humidity:sensor1', now - 1800000, 48);
  await client.ts.add('humidity:sensor1', now - 900000, 50);
  await client.ts.add('humidity:sensor1', now, 52);
  
  // Get the latest reading
  const latestTemp = await client.ts.get('temperature:sensor1');
  console.log('Latest temperature:', latestTemp);
  
  // Get range of data
  const tempHistory = await client.ts.range('temperature:sensor1', 
    now - 3600000, now);
  console.log('Temperature history:', tempHistory);
  
  // Get data with aggregation (avg temperature every 15 minutes)
  const tempAgg = await client.ts.range('temperature:sensor1',
    now - 3600000, now, {
      AGGREGATION: {
        type: 'avg',
        timeBucket: 900000  // 15 minutes in ms
      }
    });
  console.log('Temperature aggregation (15-min avg):', tempAgg);
  
  // Query multiple time series by labels
  const allTemperatures = await client.ts.mrange(now - 3600000, now, {
    FILTER_BY_LABELS: {
      type: 'temperature'
    },
    AGGREGATION: {
      type: 'avg',
      timeBucket: 1800000  // 30 minutes
    }
  });
  console.log('All temperature sensors (30-min avg):', allTemperatures);
  
  // Create a rule for downsampling (automatic aggregation)
  await client.ts.createRule('temperature:sensor1', 'temperature:sensor1:hourly', {
    AGGREGATION: {
      type: 'avg',
      timeBucket: 3600000  // 1 hour
    }
  });
  
  console.log('Time series rule created for downsampling');
}
            

High Availability and Scaling Redis

For production applications, Redis offers several options for high availability and scaling.

flowchart TB
    subgraph "Redis Replication"
        Master[Master] --> Replica1[Replica 1]
        Master --> Replica2[Replica 2]
        Master --> Replica3[Replica 3]
    end
    
    subgraph "Redis Sentinel"
        Sentinel1[Sentinel 1] -.-> Master
        Sentinel2[Sentinel 2] -.-> Master
        Sentinel3[Sentinel 3] -.-> Master
        Sentinel1 -.-> Replica1
        Sentinel1 -.-> Replica2
        Sentinel1 -.-> Replica3
        Sentinel2 -.-> Replica1
        Sentinel2 -.-> Replica2
        Sentinel2 -.-> Replica3
        Sentinel3 -.-> Replica1
        Sentinel3 -.-> Replica2
        Sentinel3 -.-> Replica3
        Client[Client] --> Sentinel1
        Client --> Sentinel2
        Client --> Sentinel3
    end
                

Redis Replication

Redis replication allows you to have multiple copies of your data. One Redis instance acts as a master, and one or more instances act as replicas (formerly called slaves).

Redis Sentinel

Redis Sentinel provides high availability for Redis through automatic failover when the master becomes unavailable.


// Connecting to Redis Sentinel from Node.js
const Redis = require('ioredis');

// Create a client that connects to Sentinel
const redis = new Redis({
  sentinels: [
    { host: 'sentinel1', port: 26379 },
    { host: 'sentinel2', port: 26379 },
    { host: 'sentinel3', port: 26379 }
  ],
  name: 'mymaster',       // Master group name
  password: 'password',   // If authentication is required
  db: 0
});

// Handle reconnection events
redis.on('connect', () => {
  console.log('Connected to Redis via Sentinel');
});

redis.on('error', (err) => {
  console.error('Redis connection error:', err);
});

// Creating a read-only connection to a replica
const redisReadOnly = new Redis({
  sentinels: [
    { host: 'sentinel1', port: 26379 },
    { host: 'sentinel2', port: 26379 },
    { host: 'sentinel3', port: 26379 }
  ],
  name: 'mymaster',
  role: 'slave',         // Connect to a replica for read-only operations
  preferredSlaves: [     // Optional preferred replica selection
    { ip: '192.168.1.3', port: 6379, prio: 1 },
    { ip: '192.168.1.4', port: 6379, prio: 2 }
  ]
});

// Example function to use the appropriate connection based on operation type
function getRedisClient(operation) {
  return ['get', 'hget', 'smembers', 'zrange'].includes(operation)
    ? redisReadOnly  // Read operations go to replicas
    : redis;         // Write operations go to master
}

async function example() {
  // Write to master
  await getRedisClient('set').set('key', 'value');
  
  // Read from replica
  const value = await getRedisClient('get').get('key');
  console.log('Value:', value);
}
            

Redis Cluster

Redis Cluster provides horizontal scaling through data sharding across multiple Redis instances.

graph TD
    Client((Client))
    Client --> Node1
    Client --> Node2
    Client --> Node3
    
    subgraph "Redis Cluster"
        Node1[Node 1
Slots 0-5460] Node2[Node 2
Slots 5461-10922] Node3[Node 3
Slots 10923-16383] Node1R[Node 1 Replica] Node2R[Node 2 Replica] Node3R[Node 3 Replica] Node1 --> Node1R Node2 --> Node2R Node3 --> Node3R end

// Connecting to Redis Cluster from Node.js
const Redis = require('ioredis');

// Create a cluster client
const cluster = new Redis.Cluster([
  { host: 'node1', port: 6379 },
  { host: 'node2', port: 6379 },
  { host: 'node3', port: 6379 }
], {
  redisOptions: {
    password: 'password'  // If authentication is required
  },
  // Read from replicas for increased performance
  scaleReads: 'slave',
  // Important cluster-specific options
  maxRedirections: 16,
  retryDelayOnFailover: 100,
  retryDelayOnClusterDown: 100,
  enableOfflineQueue: true
});

// Handle connection events
cluster.on('ready', () => {
  console.log('Connected to Redis Cluster');
});

cluster.on('error', (err) => {
  console.error('Redis Cluster error:', err);
});

// Using the cluster for operations
async function clusterExample() {
  // Basic operations work the same way as with a single Redis instance
  await cluster.set('user:1001', 'John Doe');
  
  // For multi-key operations, ensure keys are in the same hash slot
  // by using hash tags (strings inside curly braces)
  await cluster.mset(
    'order:{user:1001}:1', 'Order details 1',
    'order:{user:1001}:2', 'Order details 2'
  );
  
  // Read scaling (automatically reads from replicas)
  const keys = await cluster.keys('order:{user:1001}:*');
  console.log('User orders:', keys);
  
  // Pipeline example with the cluster
  const pipeline = cluster.pipeline();
  
  pipeline.set('key1', 'value1');
  pipeline.set('key2', 'value2');
  pipeline.get('key1');
  
  const results = await pipeline.exec();
  console.log('Pipeline results:', results);
}
            

Multi-key Operations in Cluster Mode

In Redis Cluster, multi-key operations are only allowed when all keys belong to the same hash slot. Hash tags (strings within curly braces) can be used to force keys to be assigned to the same slot.


// Using hash tags for multi-key operations
async function multiKeyExample() {
  // These keys will be in the same hash slot due to the hash tag {user:1001}
  await cluster.sadd('cart:{user:1001}', 'product:101');
  await cluster.sadd('cart:{user:1001}', 'product:102');
  await cluster.hset('profile:{user:1001}', 'name', 'John Doe');
  
  // This transaction works because all keys are in the same hash slot
  const multi = cluster.multi();
  multi.scard('cart:{user:1001}');
  multi.hgetall('profile:{user:1001}');
  multi.sismember('cart:{user:1001}', 'product:101');
  
  const results = await multi.exec();
  console.log('Transaction results:', results);
  
  // This will FAIL because keys are in different hash slots
  try {
    await cluster.mget('user:1001', 'user:1002');
  } catch (error) {
    console.error('Multi-key error:', error.message);
    // Error: "CROSSSLOT Keys in request don't hash to the same slot"
  }
  
  // Solution: Use promise.all for operations on keys in different slots
  const [user1, user2] = await Promise.all([
    cluster.get('user:1001'),
    cluster.get('user:1002')
  ]);
  
  console.log('Users:', user1, user2);
}
            

Performance Monitoring and Optimization

Monitoring Redis Performance

Effective monitoring is crucial for identifying and resolving performance issues in Redis.


// Basic Redis info monitoring
async function monitorRedis() {
  // Get general Redis information
  const info = await client.info();
  console.log('Redis Info:', info);
  
  // Get memory usage stats
  const memory = await client.info('memory');
  console.log('Memory Info:', memory);
  
  // Get stats about clients
  const clients = await client.info('clients');
  console.log('Client Info:', clients);
  
  // Get stats about commands
  const commandStats = await client.info('commandstats');
  console.log('Command Stats:', commandStats);
}

// Using the SLOWLOG to identify slow commands
async function checkSlowLog() {
  // Get the slow log
  const slowLog = await client.slowlog('GET', 10);  // Get the last 10 slow commands
  
  console.log('Slow Log Entries:');
  slowLog.forEach(entry => {
    console.log(`ID: ${entry.id}`);
    console.log(`Timestamp: ${new Date(entry.timestamp * 1000).toISOString()}`);
    console.log(`Execution Time: ${entry.executionTime} microseconds`);
    console.log(`Command: ${entry.command.join(' ')}`);
    console.log('---');
  });
  
  // Reset the slow log if needed
  await client.slowlog('RESET');
}

// Monitor memory usage of keys
async function analyzeKeyMemoryUsage() {
  // Find big keys
  console.log('Scanning for large keys...');
  
  let cursor = 0;
  let largeKeys = [];
  
  do {
    const result = await client.scan(cursor, {
      COUNT: 100
    });
    
    cursor = result.cursor;
    
    // Check memory usage of each key
    await Promise.all(result.keys.map(async (key) => {
      const size = await client.memoryUsage(key);
      if (size > 1000000) {  // Keys larger than 1MB
        largeKeys.push({ key, size });
      }
    }));
  } while (cursor !== 0);
  
  // Sort by size, largest first
  largeKeys.sort((a, b) => b.size - a.size);
  
  console.log('Large keys:');
  largeKeys.forEach(({ key, size }) => {
    console.log(`${key}: ${(size / 1024 / 1024).toFixed(2)} MB`);
  });
}

// Custom monitoring function that could be run periodically
async function periodicMonitoring() {
  const stats = {
    timestamp: new Date().toISOString(),
    memory: {},
    keyspace: {},
    performance: {}
  };
  
  // Get memory stats
  const memoryInfo = await client.info('memory');
  const memoryLines = memoryInfo.split('\n');
  
  memoryLines.forEach(line => {
    if (line.startsWith('used_memory_human:')) {
      stats.memory.used = line.split(':')[1].trim();
    } else if (line.startsWith('used_memory_peak_human:')) {
      stats.memory.peak = line.split(':')[1].trim();
    } else if (line.startsWith('used_memory_lua_human:')) {
      stats.memory.lua = line.split(':')[1].trim();
    }
  });
  
  // Get keyspace stats
  const keyspaceInfo = await client.info('keyspace');
  const keyspaceLines = keyspaceInfo.split('\n');
  
  keyspaceLines.forEach(line => {
    if (line.startsWith('db')) {
      const db = line.split(':')[0].trim();
      const values = line.split(':')[1].split(',').reduce((acc, item) => {
        const [key, value] = item.split('=');
        acc[key] = value;
        return acc;
      }, {});
      
      stats.keyspace[db] = values;
    }
  });
  
  // Get performance stats
  const commandStats = await client.info('commandstats');
  const commandLines = commandStats.split('\n');
  
  let totalCalls = 0;
  let totalUsec = 0;
  
  commandLines.forEach(line => {
    if (line.startsWith('cmdstat_')) {
      const command = line.split(':')[0].replace('cmdstat_', '');
      const stats = line.split(':')[1].split(',').reduce((acc, item) => {
        const [key, value] = item.split('=');
        acc[key] = value;
        return acc;
      }, {});
      
      totalCalls += parseInt(stats.calls || 0);
      totalUsec += parseInt(stats.usec || 0);
    }
  });
  
  stats.performance.totalCommands = totalCalls;
  stats.performance.avgLatency = totalCalls > 0 ? (totalUsec / totalCalls).toFixed(2) + ' us' : '0 us';
  
  console.log('Redis Monitoring Stats:', JSON.stringify(stats, null, 2));
  
  // In a real application, you might:
  // 1. Store these stats in a time-series database
  // 2. Send alerts if certain thresholds are exceeded
  // 3. Generate visualization dashboards
  return stats;
}
            

Memory Optimization Techniques

Redis is an in-memory database, so optimizing memory usage is crucial for performance and cost efficiency.


// Memory optimization examples

// Example 1: Using hashes instead of individual keys
// Less efficient: 3 separate keys
await client.set('user:1000:name', 'John Doe');
await client.set('user:1000:email', 'john@example.com');
await client.set('user:1000:age', '30');

// More efficient: 1 hash
await client.hSet('user:1000', {
  name: 'John Doe',
  email: 'john@example.com',
  age: '30'
});

// Example 2: Using expiration for temporary data
await client.set('verification:code:user1000', '123456', {
  EX: 600  // Expires in 10 minutes
});

// Example 3: Using EXPIRE at for specific expiration time
const midnight = new Date();
midnight.setHours(23, 59, 59, 999);
const secondsUntilMidnight = Math.floor((midnight - new Date()) / 1000);

await client.set('daily:stats', JSON.stringify(stats));
await client.expire('daily:stats', secondsUntilMidnight);

// Example 4: Using compression for large strings
const largeData = 'very large string with lots of repetition...';

// Enable compression with "redis-lz4" if not saving keys
// This helps when just passing data through Redis
await client.set('compressed:data', largeData, {
  EX: 3600,
  COMPRESSION: true  // Not a standard Redis option, but available in some clients
});

// Example 5: Using bit operations for compact data storage
// Store whether users are active (1 bit per user)
await client.setBit('active:users', 1000, 1);  // User 1000 is active
await client.setBit('active:users', 1001, 0);  // User 1001 is inactive

// Check if a user is active
const isActive = await client.getBit('active:users', 1000);
console.log(`User 1000 is active: ${isActive === 1}`);

// Count active users
const activeCount = await client.bitCount('active:users');
console.log(`Active users: ${activeCount}`);
            

Common Performance Issues and Solutions

Issue Symptoms Potential Solutions
Memory Fragmentation High ratio between used_memory_rss and used_memory
  • Restart Redis during off-peak hours
  • Optimize key patterns to reduce allocation/deallocation
  • Consider using Redis 6+ with jemalloc improvements
Slow Commands High latency, commands appearing in SLOWLOG
  • Avoid O(N) commands on large collections (KEYS, SMEMBERS, etc.)
  • Use SCAN/HSCAN/SSCAN instead of KEYS/HGETALL/SMEMBERS
  • Limit Lua scripts' complexity and execution time
Network Bottlenecks High latency despite low CPU, increased cmd/s without response
  • Use pipelining to reduce round trips
  • Place Redis closer to your application (same region/data center)
  • Use connection pooling
  • Implement batch operations
High CPU Usage Redis using significant CPU resources
  • Identify expensive commands using INFO COMMANDSTATS
  • Cache computation results rather than computing on-the-fly
  • Distribute load across multiple Redis instances
Replication Lag Replicas falling behind the master
  • Increase replication buffer size
  • Use faster network between master and replicas
  • Scale horizontally with Redis Cluster

Advanced Real-World Examples

Distributed Locks with Redis

Distributed locks are essential for coordinating actions across multiple instances of an application.


// Distributed lock implementation
class RedisLock {
  constructor(client, options = {}) {
    this.client = client;
    this.retryDelay = options.retryDelay || 200;
    this.retryCount = options.retryCount || 10;
    this.lockTimeout = options.lockTimeout || 10000;  // 10 seconds
  }
  
  // Acquire a lock
  async acquire(lockName, lockValue = Math.random().toString(36).substring(2)) {
    let retries = 0;
    
    while (retries < this.retryCount) {
      // Try to set the lock with NX option (only if not exists)
      const result = await this.client.set(`lock:${lockName}`, lockValue, {
        NX: true,
        PX: this.lockTimeout
      });
      
      if (result === 'OK') {
        // Lock acquired successfully
        return {
          acquired: true,
          value: lockValue
        };
      }
      
      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, this.retryDelay));
      retries++;
    }
    
    return {
      acquired: false,
      value: null
    };
  }
  
  // Release a lock (using Lua script for atomicity)
  async release(lockName, lockValue) {
    // This script ensures we only delete the lock if it's still ours
    const script = `
      if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
      else
        return 0
      end
    `;
    
    return this.client.eval(
      script,
      {
        keys: [`lock:${lockName}`],
        arguments: [lockValue]
      }
    );
  }
  
  // Extend a lock's expiry time
  async extend(lockName, lockValue, extensionTime = 10000) {
    const script = `
      if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("PEXPIRE", KEYS[1], ARGV[2])
      else
        return 0
      end
    `;
    
    return this.client.eval(
      script,
      {
        keys: [`lock:${lockName}`],
        arguments: [lockValue, extensionTime.toString()]
      }
    );
  }
}

// Usage example
async function processWithLock() {
  const lock = new RedisLock(client, {
    retryDelay: 100,
    retryCount: 5,
    lockTimeout: 30000  // 30 seconds
  });
  
  console.log('Trying to acquire lock...');
  
  const { acquired, value } = await lock.acquire('resource:123');
  
  if (!acquired) {
    console.error('Failed to acquire lock after retries');
    return false;
  }
  
  console.log('Lock acquired, processing...');
  
  try {
    // Simulate some work
    await new Promise(resolve => setTimeout(resolve, 5000));
    
    // If processing takes longer, extend the lock
    await lock.extend('resource:123', value, 30000);
    console.log('Lock extended');
    
    // More processing...
    await new Promise(resolve => setTimeout(resolve, 10000));
    
    console.log('Processing complete');
    return true;
  } catch (error) {
    console.error('Error during processing:', error);
    return false;
  } finally {
    // Always release the lock when done
    await lock.release('resource:123', value);
    console.log('Lock released');
  }
}
            

Sliding Window Rate Limiter with Tokens

A more sophisticated rate limiter that provides a sliding window with token budgeting.


// Advanced sliding window rate limiter with Redis
class SlidingWindowRateLimiter {
  constructor(client, options = {}) {
    this.client = client;
    this.windowSize = options.windowSize || 60;  // Window size in seconds
    this.maxTokens = options.maxTokens || 10;    // Maximum tokens per window
    this.tokenReserve = options.tokenReserve || 0.2;  // Portion of tokens reserved for burst
    
    // Initialize script hash
    this.scriptHash = null;
    this.initialized = false;
    
    // Lua script for atomic rate limiting
    this.script = `
      local key = KEYS[1]
      local now = tonumber(ARGV[1])
      local windowSize = tonumber(ARGV[2])
      local maxTokens = tonumber(ARGV[3])
      local tokenReserve = tonumber(ARGV[4])
      local requestTokens = tonumber(ARGV[5])
      
      -- Calculate window bounds
      local windowStart = now - windowSize
      
      -- Remove events outside the current window
      redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
      
      -- Count events in the current window
      local windowEvents = redis.call('ZCARD', key)
      
      -- Calculate available tokens with an exponential decay function
      -- More tokens available for the beginning of the window
      local availableTokens = maxTokens
      
      if windowEvents > 0 then
        local eventTimes = redis.call('ZRANGE', key, 0, -1, 'WITHSCORES')
        local usedTokens = 0
        local reserveTokens = math.floor(maxTokens * tokenReserve)
        
        for i = 1, #eventTimes, 2 do
          local timestamp = tonumber(eventTimes[i+1])
          local age = (now - timestamp) / windowSize
          local weight = math.min(1, age * 2)  -- Linear decay up to 2x speed
          
          usedTokens = usedTokens + 1 - weight
        end
        
        availableTokens = math.max(0, math.floor(maxTokens - usedTokens))
        
        -- Ensure we always have some reserve tokens available
        availableTokens = math.max(reserveTokens, availableTokens)
      end
      
      -- Check if we have enough tokens
      if availableTokens >= requestTokens then
        -- Add the current request to the window
        redis.call('ZADD', key, now, now .. '-' .. math.random())
        -- Set expiry on the key to auto-cleanup
        redis.call('EXPIRE', key, windowSize * 2)
        
        -- Return remaining tokens and true (allowed)
        return {availableTokens - requestTokens, 1}
      else
        -- Return available tokens and false (denied)
        return {availableTokens, 0}
      end
    `;
  }
  
  // Initialize the rate limiter
  async init() {
    if (!this.initialized) {
      this.scriptHash = await this.client.scriptLoad(this.script);
      this.initialized = true;
    }
    return this;
  }
  
  // Check if a request is allowed
  async checkLimit(userId, tokens = 1) {
    if (!this.initialized) {
      await this.init();
    }
    
    const key = `rate:${userId}:sliding`;
    const now = Math.floor(Date.now() / 1000);
    
    const result = await this.client.evalSha(
      this.scriptHash,
      {
        keys: [key],
        arguments: [
          now.toString(),
          this.windowSize.toString(),
          this.maxTokens.toString(),
          this.tokenReserve.toString(),
          tokens.toString()
        ]
      }
    );
    
    return {
      allowed: result[1] === 1,
      remainingTokens: result[0],
      resetAfter: this.windowSize - (now % this.windowSize)
    };
  }
}

// Express middleware using the sliding window rate limiter
async function createAdvancedRateLimiterMiddleware() {
  // Different rate limits for different endpoints/methods
  const rateLimiters = {
    // Standard API endpoints (10 requests per minute)
    standard: await new SlidingWindowRateLimiter(client, {
      windowSize: 60,
      maxTokens: 10,
      tokenReserve: 0.2
    }).init(),
    
    // Search endpoints (5 requests per minute)
    search: await new SlidingWindowRateLimiter(client, {
      windowSize: 60,
      maxTokens: 5,
      tokenReserve: 0.2
    }).init(),
    
    // Admin endpoints (30 requests per minute)
    admin: await new SlidingWindowRateLimiter(client, {
      windowSize: 60,
      maxTokens: 30,
      tokenReserve: 0.1
    }).init()
  };
  
  return async (req, res, next) => {
    // Determine which rate limiter to use based on the route
    let limiterKey = 'standard';
    
    if (req.path.includes('/api/search')) {
      limiterKey = 'search';
    } else if (req.path.includes('/api/admin')) {
      limiterKey = 'admin';
    }
    
    const rateLimiter = rateLimiters[limiterKey];
    const identifier = req.ip || 'anonymous';
    
    // Check the rate limit
    const result = await rateLimiter.checkLimit(identifier);
    
    // Set headers
    res.setHeader('X-RateLimit-Limit', rateLimiter.maxTokens);
    res.setHeader('X-RateLimit-Remaining', result.remainingTokens);
    res.setHeader('X-RateLimit-Reset', result.resetAfter);
    
    if (!result.allowed) {
      return res.status(429).json({
        error: 'Too many requests',
        message: `Rate limit exceeded. Try again in ${result.resetAfter} seconds.`
      });
    }
    
    next();
  };
}
            

Real-time Analytics with Redis

Redis can be used to build real-time analytics dashboards with high performance.


// Real-time analytics tracking
class AnalyticsTracker {
  constructor(client) {
    this.client = client;
    this.dayFormat = 'YYYY-MM-DD';
    this.hourFormat = 'YYYY-MM-DD:HH';
    this.minuteFormat = 'YYYY-MM-DD:HH:mm';
  }
  
  // Format date according to specified format
  formatDate(date, format) {
    const d = date || new Date();
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const hour = String(d.getHours()).padStart(2, '0');
    const minute = String(d.getMinutes()).padStart(2, '0');
    
    if (format === this.dayFormat) {
      return `${year}-${month}-${day}`;
    } else if (format === this.hourFormat) {
      return `${year}-${month}-${day}:${hour}`;
    } else if (format === this.minuteFormat) {
      return `${year}-${month}-${day}:${hour}:${minute}`;
    }
    
    return `${year}-${month}-${day}:${hour}:${minute}`;
  }
  
  // Track a page view
  async trackPageView(page, userId = 'anonymous') {
    const now = new Date();
    const day = this.formatDate(now, this.dayFormat);
    const hour = this.formatDate(now, this.hourFormat);
    const minute = this.formatDate(now, this.minuteFormat);
    
    const pipeline = this.client.multi();
    
    // Increment counters at different granularities
    pipeline.incr(`stats:pageviews:total`);
    pipeline.incr(`stats:pageviews:${day}`);
    pipeline.incr(`stats:pageviews:${hour}`);
    pipeline.incr(`stats:pageviews:${minute}`);
    
    // Increment page-specific counters
    pipeline.incr(`stats:pageviews:${page}:total`);
    pipeline.incr(`stats:pageviews:${page}:${day}`);
    
    // Add to sorted set for most popular pages
    pipeline.zIncrBy('stats:popular:pages', 1, page);
    
    // Track unique visitors with HyperLogLog
    pipeline.pfAdd(`stats:unique:${day}`, userId);
    pipeline.pfAdd(`stats:unique:${hour}`, userId);
    
    // Add to time series if using RedisTimeSeries
    // pipeline.ts.add(`stats:ts:pageviews`, now.getTime(), 1);
    
    // Execute all commands
    await pipeline.exec();
  }
  
  // Track an event (button click, form submission, etc.)
  async trackEvent(eventName, properties = {}, userId = 'anonymous') {
    const now = new Date();
    const day = this.formatDate(now, this.dayFormat);
    
    const pipeline = this.client.multi();
    
    // Increment event counters
    pipeline.incr(`stats:events:${eventName}:total`);
    pipeline.incr(`stats:events:${eventName}:${day}`);
    
    // Add to sorted set for most common events
    pipeline.zIncrBy('stats:popular:events', 1, eventName);
    
    // Store event details in a stream (good for later analysis)
    const eventData = {
      userId,
      eventName,
      timestamp: now.toISOString(),
      ...properties
    };
    
    pipeline.xAdd(
      'stats:events:stream',
      '*',  // Auto-generate ID
      {
        ...eventData,
        // Convert object properties to string
        properties: JSON.stringify(properties)
      }
    );
    
    // Execute all commands
    await pipeline.exec();
  }
  
  // Get real-time analytics
  async getRealTimeStats() {
    const now = new Date();
    const day = this.formatDate(now, this.dayFormat);
    const hour = this.formatDate(now, this.hourFormat);
    const prevHour = this.formatDate(new Date(now.getTime() - 3600000), this.hourFormat);
    
    // Get various stats in parallel
    const [
      totalPageviews,
      todayPageviews,
      currentHourPageviews,
      prevHourPageviews,
      uniqueVisitorsToday,
      uniqueVisitorsHour,
      topPages,
      topEvents
    ] = await Promise.all([
      this.client.get('stats:pageviews:total'),
      this.client.get(`stats:pageviews:${day}`),
      this.client.get(`stats:pageviews:${hour}`),
      this.client.get(`stats:pageviews:${prevHour}`),
      this.client.pfCount(`stats:unique:${day}`),
      this.client.pfCount(`stats:unique:${hour}`),
      this.client.zRevRangeWithScores('stats:popular:pages', 0, 9),
      this.client.zRevRangeWithScores('stats:popular:events', 0, 9)
    ]);
    
    // Calculate hour-over-hour change
    const hourChange = prevHourPageviews 
      ? ((currentHourPageviews - prevHourPageviews) / prevHourPageviews) * 100 
      : 0;
    
    return {
      pageviews: {
        total: parseInt(totalPageviews || 0),
        today: parseInt(todayPageviews || 0),
        currentHour: parseInt(currentHourPageviews || 0),
        hourChange: parseFloat(hourChange.toFixed(2))
      },
      uniqueVisitors: {
        today: uniqueVisitorsToday,
        currentHour: uniqueVisitorsHour
      },
      topPages: topPages.map(({ value, score }) => ({
        page: value,
        views: parseInt(score)
      })),
      topEvents: topEvents.map(({ value, score }) => ({
        event: value,
        count: parseInt(score)
      }))
    };
  }
  
  // Get historical analytics data
  async getHistoricalData(metric, days = 7) {
    const result = [];
    const now = new Date();
    
    for (let i = 0; i < days; i++) {
      const date = new Date(now);
      date.setDate(now.getDate() - i);
      const day = this.formatDate(date, this.dayFormat);
      
      const value = await this.client.get(`stats:${metric}:${day}`);
      
      result.push({
        date: day,
        value: parseInt(value || 0)
      });
    }
    
    // Sort by date ascending
    return result.sort((a, b) => a.date