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?
- Atomic Operations: Scripts execute as a single atomic operation, ensuring no other clients can interrupt them.
- Reduced Network Overhead: Instead of sending multiple commands over the network, you send a single script.
- Complex Logic: Implement more complex operations that aren't possible with Redis commands alone.
- Reusability: Scripts can be stored and reused across your application.
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
- RedisJSON: Native JSON data type with path syntax for efficient access.
- RediSearch: Full-text search engine with querying, filtering, and aggregation.
- RedisGraph: Graph database implementation for Redis.
- RedisTimeSeries: Time series data structure with aggregation.
- RedisBloom: Probabilistic data structures like Bloom filters and Cuckoo filters.
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).
- Advantages: Data redundancy, read scaling (queries can be distributed across replicas), and fault tolerance.
- Limitations: Manual failover required, no automatic sharding.
Redis Sentinel
Redis Sentinel provides high availability for Redis through automatic failover when the master becomes unavailable.
- Advantages: Automatic master monitoring, notification, and failover.
- Use Case: High availability setups without sharding needs.
// 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
- Advantages: Horizontal scaling, automatic data sharding, and built-in replication.
- Limitations: More complex setup, multi-key operations are limited to keys in the same hash slot.
// 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.
- Use appropriate data structures: For example, hashes are more memory-efficient than individual keys for object properties.
- Set expiration times: Use TTL for transient data to automatically free up memory.
- Use Redis compression: Enable compression for keys that contain duplicated strings or patterns.
- Monitor memory usage: Regularly check for large keys and memory fragmentation.
// 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 |
|
| Slow Commands | High latency, commands appearing in SLOWLOG |
|
| Network Bottlenecks | High latency despite low CPU, increased cmd/s without response |
|
| High CPU Usage | Redis using significant CPU resources |
|
| Replication Lag | Replicas falling behind the master |
|
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