正在加载,请稍候…

Redis Advanced Patterns: Pub/Sub, Streams, Lua Scripts, and Rate Limiting

Go beyond simple caching with Redis. Implement pub/sub messaging, Redis Streams, rate limiting, distributed locks, and Lua scripts for atomic operations.

Redis Advanced Patterns

Pub/Sub Messaging

import { createClient } from 'redis';

// Publisher
const publisher = createClient();
await publisher.connect();

async function publishEvent(channel: string, event: object): Promise<void> {
  await publisher.publish(channel, JSON.stringify(event));
}

// Subscriber
const subscriber = createClient();
await subscriber.connect();

await subscriber.subscribe('user-events', (message) => {
  const event = JSON.parse(message);
  console.log('Received:', event);
});

await subscriber.subscribe('order-events', (message) => {
  handleOrderEvent(JSON.parse(message));
});

// Publish
await publishEvent('user-events', { type: 'user.created', userId: '123' });

Redis Streams (Persistent Pub/Sub)

// Producer: add to stream
await client.xAdd('events', '*', {
  type: 'order.placed',
  orderId: 'order_123',
  userId: 'user_456',
  amount: '99.99',
  timestamp: Date.now().toString(),
});

// Consumer group setup
await client.xGroupCreate('events', 'order-processor', '
#39;, { MKSTREAM: true }); // Consumer: read from group while (true) { const messages = await client.xReadGroup( 'order-processor', 'consumer-1', [{ key: 'events', id: '>' }], { COUNT: 10, BLOCK: 2000 } ); if (messages) { for (const { name, messages: msgs } of messages) { for (const { id, message } of msgs) { await processEvent(message); // Acknowledge processed message await client.xAck('events', 'order-processor', id); } } } }

Rate Limiting with Sliding Window

async function checkRateLimit(
  userId: string,
  limit: number,
  windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
  const key = `rate:${userId}`;
  const now = Date.now();
  const windowStart = now - windowMs;

  // Atomic operation with pipeline
  const pipeline = client.multi();
  pipeline.zRemRangeByScore(key, '-inf', windowStart.toString());  // Remove old requests
  pipeline.zAdd(key, { score: now, value: now.toString() });        // Add current request
  pipeline.zCard(key);                                               // Count requests in window
  pipeline.expire(key, Math.ceil(windowMs / 1000));                  // Set TTL

  const results = await pipeline.exec();
  const count = results[2] as number;

  return {
    allowed: count <= limit,
    remaining: Math.max(0, limit - count),
    resetIn: windowMs,
  };
}

// Middleware
app.use(async (req, res, next) => {
  const result = await checkRateLimit(req.ip, 100, 60000);
  res.setHeader('X-RateLimit-Limit', 100);
  res.setHeader('X-RateLimit-Remaining', result.remaining);

  if (!result.allowed) {
    return res.status(429).json({ error: 'Too many requests' });
  }
  next();
});

Distributed Lock

class RedisLock {
  constructor(private client: RedisClient) {}

  async acquire(resource: string, ttlMs: number): Promise<string | null> {
    const token = crypto.randomUUID();
    const key = `lock:${resource}`;

    const acquired = await this.client.set(key, token, {
      NX: true,          // Only set if not exists
      PX: ttlMs,         // Expire in ttlMs
    });

    return acquired ? token : null;
  }

  async release(resource: string, token: string): Promise<boolean> {
    // Atomic check-and-delete with Lua
    const script = `
      if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
      else
        return 0
      end
    `;
    const result = await this.client.eval(script, { keys: [`lock:${resource}`], arguments: [token] });
    return result === 1;
  }
}

// Usage
const lock = new RedisLock(client);

async function processOrder(orderId: string): Promise<void> {
  const token = await lock.acquire(`order:${orderId}`, 30000);
  if (!token) {
    throw new Error('Order is already being processed');
  }

  try {
    await doProcessOrder(orderId);
  } finally {
    await lock.release(`order:${orderId}`, token);
  }
}

Sorted Sets for Leaderboards

// Add/update score
await client.zAdd('game:leaderboard', { score: 1500, value: 'player:alice' });

// Get top 10
const top10 = await client.zRangeWithScores('game:leaderboard', 0, 9, { REV: true });
// Returns: [{ value: 'player:alice', score: 1500 }, ...]

// Get player rank (0-indexed)
const rank = await client.zRevRank('game:leaderboard', 'player:alice');

// Get players in score range
const midLevel = await client.zRangeByScoreWithScores('game:leaderboard', '1000', '2000');

Redis's data structures enable elegant solutions to complex real-time problems.