Redis: in-memory data store для production
Кэширование, pub/sub, распределенные блокировки и очереди
Redis — не просто кэш. Это мощная in-memory база данных с persistence, pub/sub, transactions, Lua scripting. Twitter, GitHub, Stack Overflow используют Redis в production. Разберем паттерны использования Redis. Основы Redis Установка и подключение: # Установка brew install redis # macOS sudo apt install redis # Ubuntu # Запуск redis-server # CLI redis-cli # Node.js npm install redis const redis = require('redis'); const client = redis.createClient({ host: 'localhost', port: 6379 }); await client.connect(); Data Types 1. Strings: // Set/Get await client.set('user:1000:name', 'John Doe'); const name = await client.get('user:1000:name'); // Multiple await client.mSet({ 'user:1001:name': 'Jane', 'user:1002:name': 'Bob' }); // Counter await client.incr('page:views'); await client.incrBy('score', 10); // Expiration await client.setEx('session:abc', 3600, 'data'); // 1 hour await client.expire('key', 60); // 60 seconds 2. Hashes (objects): // Set hash fields await client.hSet('user:1000', { name: 'John Doe', email: 'john@example.com', age: 30 }); // Get field const email = await client.hGet('user:1000', 'email'); // Get all const user = await client.hGetAll('user:1000'); // Increment field await client.hIncrBy('user:1000', 'login_count', 1); 3. Lists (queues): // Push await client.lPush('queue:tasks', 'task1'); await client.rPush('queue:tasks', 'task2'); // Pop const task = await client.lPop('queue:tasks'); const task = await client.rPop('queue:tasks'); // Blocking pop (for worker queues) const task = await client.blPop('queue:tasks', 0); // Wait forever // Range const tasks = await client.lRange('queue:tasks', 0, -1); 4. Sets (unique values): // Add members await client.sAdd('tags:post:1', ['javascript', 'react', 'nodejs']); // Check membership const isMember = await client.sIsMember('tags:post:1', 'react'); // Get all const tags = await client.sMembers('tags:post:1'); // Operations const common = await client.sInter('tags:post:1', 'tags:post:2'); // Intersection const all = await client.sUnion('tags:post:1', 'tags:post:2'); // Union const diff = await client.sDiff('tags:post:1', 'tags:post:2'); // Difference 5. Sorted Sets (leaderboards): // Add with score await client.zAdd('leaderboard', [ { score: 100, value: 'player1' }, { score: 200, value: 'player2' }, { score: 150, value: 'player3' } ]); // Get by rank const top10 = await client.zRange('leaderboard', 0, 9, { REV: true }); // Get by score const highScorers = await client.zRangeByScore('leaderboard', 150, 200); // Rank of member const rank = await client.zRevRank('leaderboard', 'player1'); // Increment score await client.zIncrBy('leaderboard', 10, 'player1'); Caching Patterns 1. Cache-Aside: async function getUser(userId) { // Try cache first const cached = await client.get(`user:${userId}`); if (cached) { return JSON.parse(cached); } // Cache miss - fetch from DB const user = await db.users.findById(userId); // Store in cache await client.setEx( `user:${userId}`, 3600, // 1 hour JSON.stringify(user) ); return user; } 2. Write-Through: async function updateUser(userId, data) { // Update DB const user = await db.users.update(userId, data); // Update cache await client.setEx( `user:${userId}`, 3600, JSON.stringify(user) ); return user; } 3. Write-Behind (async): async function updateUser(userId, data) { // Update cache immediately await client.setEx( `user:${userId}`, 3600, JSON.stringify(data) ); // Queue DB write await client.lPush('queue:db-writes', JSON.stringify({ type: 'user:update', userId, data })); return data; } // Background worker async function dbWriteWorker() { while (true) { const task = await client.blPop('queue:db-writes', 0); const { type, userId, data } = JSON.parse(task.element); if (type === 'user:update') { await db.users.update(userId, data); } } } Pub/Sub Pattern // Publisher const publisher = redis.createClient(); await publisher.connect(); await publisher.publish('notifications', JSON.stringify({ type: 'new_message', userId: 123, message: 'Hello!' })); // Subscriber const subscriber = redis.createClient(); await subscriber.connect(); await subscriber.subscribe('notifications', (message) => { const data = JSON.parse(message); console.log('Received:', data); // Handle notification if (data.type === 'new_message') { notifyUser(data.userId, data.message); } }); // Pattern subscribe await subscriber.pSubscribe('user:*:notifications', (message, channel) => { console.log(`Message on ${channel}:`, message); }); Distributed Locks async function acquireLock(resource, ttl = 10000) { const lockKey = `lock:${resource}`; const token = crypto.randomUUID(); // Try to acquire lock const result = await client.set(lockKey, token, { NX: true, // Only if not exists PX: ttl // Expire in milliseconds }); if (!result) { return null; // Lock not acquired } return { token, release: async () => { // Release only if we own the lock const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; await client.eval(script, { keys: [lockKey], arguments: [token] }); } }; } // Usage const lock = await acquireLock('payment:user:123', 5000); if (!lock) { throw new Error('Could not acquire lock'); } try { // Critical section await processPayment(userId); } finally { await lock.release(); } Rate Limiting Fixed Window: async function isRateLimited(userId, limit = 100, window = 60) { const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / window)}`; const count = await client.incr(key); if (count === 1) { await client.expire(key, window); } return count > limit; } // Usage if (await isRateLimited(req.userId, 100, 60)) { return res.status(429).json({ error: 'Rate limit exceeded' }); } Sliding Window: async function isRateLimited(userId, limit = 100, window = 60) { const key = `rate:${userId}`; const now = Date.now(); const windowStart = now - window * 1000; // Remove old entries await client.zRemRangeByScore(key, 0, windowStart); // Count requests in window const count = await client.zCard(key); if (count >= limit) { return true; } // Add current request await client.zAdd(key, [{ score: now, value: `${now}` }]); await client.expire(key, window); return false; } Session Store // Express session with Redis const session = require('express-session'); const RedisStore = require('connect-redis').default; app.use(session({ store: new RedisStore({ client }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, maxAge: 24 * 60 * 60 * 1000 // 24 hours } })); Lua Scripting // Atomic operations with Lua const script = ` local current = redis.call('GET', KEYS[1]) if current and tonumber(current) >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 else return 0 end `; const success = await client.eval(script, { keys: ['balance:user:123'], arguments: ['100'] }); if (success) { console.log('Payment processed'); } else { console.log('Insufficient balance'); } Transactions // MULTI/EXEC const multi = client.multi(); multi.decrBy('balance:user:123', 100); multi.incrBy('balance:user:456', 100); multi.lPush('transactions', JSON.stringify({ from: 123, to: 456, amount: 100, timestamp: Date.now() })); const results = await multi.exec(); console.log('Transaction complete', results); Persistence RDB (snapshotting): # redis.conf save 900 1 # After 900 sec if 1 key changed save 300 10 # After 300 sec if 10 keys changed save 60 10000 # After 60 sec if 10000 keys changed AOF (append-only file): # redis.conf appendonly yes appendfsync everysec # always, everysec, or no Monitoring // Get stats const info = await client.info(); console.log(info); // Monitor commands const monitor = redis.createClient(); await monitor.connect(); await monitor.monitor(); monitor.on('monitor', (time, args) => { console.log(`${time}: ${args.join(' ')}`); }); Best Practices Use connection pooling Set appropriate TTL — prevent memory bloat Key naming convention — object:id:field Pipeline для batch операций Monitor memory usage Use Redis Cluster для horizontal scaling Enable persistence для important data Заключение: Redis — универсальный инструмент для production. Кэширование ускоряет приложения в разы. Pub/Sub для real-time коммуникации. Distributed locks для consistency. Rate limiting из коробки. Начните с простого кэширования, добавляйте advanced features по необходимости. Redis — must-have в modern stack.