Back to Blog

Redis Beyond Caching: How I Use It in Real Projects

Hey there! Today's blog post is about Redis, the Swiss Army Knife of backend systems, but not just for caching. We'll explore how to use it as a message queue, pub/sub system, and even for distributed locks and rate limiting. Plus, some lesser-known uses like session storage and leaderboards.

Message Queues with Redis

One of my go-to solutions when building a reliable asynchronous workflow is using Redis as a message broker. It's super lightweight compared to full-blown messaging systems like RabbitMQ or Kafka. For small teams or projects with moderate traffic, it works perfectly fine.

Let's say you need to send out emails in the background without blocking user requests. You can create a list in Redis and push email tasks into it as they come in. Then, have a separate job processor that consumes from this queue continuously:

const redis = require('redis');
const client = redis.createClient();

client.lpush("email_queue", "[email protected]");

This setup is simple to implement and can scale well for small loads.

Pub/Sub for Real-Time Features

Pub/Sub in Redis is another cool feature I use a lot. It's perfect for real-time notifications, chat messages, or any scenario where you need immediate updates between different clients or processes. For example, if you have a notification system, users could subscribe to their own channels and receive instant alerts when something happens.

Here’s how it works with Node.js:

const redis = require('redis');
const client = redis.createClient();

// Subscribe to a channel
client.subscribe("user_notifications", (message) => {
    console.log(`Received message: ${message}`);
});

// Publish a message
client.publish("user_notifications", "New notification for you!");

Streams for Complex Workflows

Redis Streams are like an enhanced version of Pub/Sub. They're durable, support ordering and grouping messages, making them ideal for more complex workflows where order matters or you need to track events over time.

For example, in a microservices architecture, one service might produce events that other services consume. Redis Streams can handle this by ensuring the order of events is maintained across different consumers:

// Producer (writing)
client.xadd("events_stream", "*", "event_type", "order_placed");

// Consumer (reading)
client.xread({streams: ["events_stream"], count: 1, block: 0});

Distributed Locks for Concurrency Control

Distributed locks are crucial when you have multiple instances of the same service running and need to coordinate access to a shared resource. Redis provides a simple API for implementing distributed mutexes using Lua scripts or just atomic operations.

A common use case is ensuring only one process can run a specific job at any given time:

const redis = require('redis');
const client = redis.createClient();

// Acquire lock
client.set("lock_key", "value", {nx: true, ex: 60}, (err, result) => {
    if (!result) return console.log("Lock already taken");

    // Do work and release lock
    setTimeout(() => client.del('lock_key'), 59000);
});

Rate Limiting to Protect Services

Rate limiting is essential for protecting your services from abuse or spikes in traffic. Redis offers a simple way to implement rate limits using sets, sorted sets, or hashes depending on the exact requirement.

Here’s an example of how you might limit requests per minute:

const redis = require('redis');
const client = redis.createClient();

function checkRateLimit(userId) {
    const now = Date.now();
    return new Promise((resolve, reject) => {
        client.zrangebyscore("ratelimit", now - 60000, '+inf', 'WITHSCORES', (err, res) => {
            if (!res || res.length < 10) resolve(true);

            // If limit exceeded
            reject(new Error('Rate Limit Exceeded'));
        });
    });
}

Session Storage for State Management

Storing session data in Redis is another way to manage state across different server instances. It’s particularly useful when you’re dealing with stateful applications like chat systems or games, where maintaining user context between requests is critical.

const redis = require('redis');
const client = redis.createClient();

function getSession(userId) {
    return new Promise((resolve, reject) => {
        client.get(`session:${userId}`, (err, data) => {
            if (!data) resolve(null);

            resolve(JSON.parse(data));
        });
    });
}

Leaderboards and Rankings

Lastly, Redis shines for leaderboards or rankings where you need to maintain a list of top users based on some metric. Sorted sets in Redis are perfect for this since they allow efficient ranking operations.

const redis = require('redis');
const client = redis.createClient();

function updateUserScore(userId, score) {
    return new Promise((resolve, reject) => {
        client.zadd("leaderboard", score, userId);

        resolve(score); // Return the updated score if needed
    });
}

function getTopUsers() {
    return new Promise((resolve, reject) => {
        client.zrevrange("leaderboard", 0, 9, 'WITHSCORES', (err, res) => {
            if (!res || !res.length) resolve([]);

            const users = [];
            for(let i=0; i<res.length; i+=2){
                users.push({userId: res[i], score: parseInt(res[i+1])});
            }

            resolve(users);
        });
    });
}

Real-World Example: Building a Chat Application

Let’s put some of these concepts together in a real-world scenario. Imagine you’re building a chat application that needs to support multiple users, private messaging, and notifications.

This setup ensures your chat application is scalable, reliable, and capable of handling real-time communication needs.

Conclusion

Redis offers a lot more than just caching. From message queues and pub/sub to distributed locks, rate limiting, session management, and leaderboards, it's an incredibly versatile tool in the backend engineer’s toolkit. The examples above should give you a good starting point for integrating Redis into your projects beyond its basic use as a cache.

My Practical Takeaway

Redis is like a Swiss Army Knife for backend developers—it has multiple tools that can be used creatively to solve complex problems efficiently. While caching is its most popular use case, leveraging it for other tasks like message queues and distributed systems management can significantly enhance your application's performance and scalability. Don't just stick with the basics; explore Redis’s full feature set to see what else you can do!