omgkit 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,901 +1,94 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Redis caching
|
|
4
|
-
category: databases
|
|
5
|
-
triggers:
|
|
6
|
-
- redis
|
|
7
|
-
- caching
|
|
8
|
-
- cache
|
|
9
|
-
- pub sub
|
|
10
|
-
- session storage
|
|
11
|
-
- rate limiting
|
|
12
|
-
- ioredis
|
|
2
|
+
name: Developing with Redis
|
|
3
|
+
description: The agent implements Redis caching, data structures, and real-time messaging patterns. Use when implementing caching layers, session storage, rate limiting, pub/sub messaging, or distributed data structures.
|
|
13
4
|
---
|
|
14
5
|
|
|
15
|
-
# Redis
|
|
6
|
+
# Developing with Redis
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Purpose
|
|
20
|
-
|
|
21
|
-
Build high-performance applications with Redis:
|
|
22
|
-
|
|
23
|
-
- Implement efficient caching strategies
|
|
24
|
-
- Use appropriate data structures
|
|
25
|
-
- Build real-time features with pub/sub
|
|
26
|
-
- Process events with Redis Streams
|
|
27
|
-
- Manage user sessions
|
|
28
|
-
- Implement rate limiting
|
|
29
|
-
- Deploy for high availability
|
|
30
|
-
|
|
31
|
-
## Features
|
|
32
|
-
|
|
33
|
-
### 1. Redis Client Setup
|
|
8
|
+
## Quick Start
|
|
34
9
|
|
|
35
10
|
```typescript
|
|
36
|
-
// src/lib/redis.ts
|
|
37
11
|
import Redis from 'ioredis';
|
|
38
12
|
|
|
39
|
-
const
|
|
13
|
+
const redis = new Redis({
|
|
40
14
|
host: process.env.REDIS_HOST || 'localhost',
|
|
41
15
|
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
42
|
-
password: process.env.REDIS_PASSWORD,
|
|
43
|
-
db: parseInt(process.env.REDIS_DB || '0'),
|
|
44
16
|
maxRetriesPerRequest: 3,
|
|
45
|
-
|
|
46
|
-
const delay = Math.min(times * 50, 2000);
|
|
47
|
-
return delay;
|
|
48
|
-
},
|
|
49
|
-
reconnectOnError: (err: Error) => {
|
|
50
|
-
const targetErrors = ['READONLY', 'ECONNRESET'];
|
|
51
|
-
return targetErrors.some(e => err.message.includes(e));
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Singleton instance
|
|
56
|
-
let redis: Redis | null = null;
|
|
57
|
-
|
|
58
|
-
export function getRedis(): Redis {
|
|
59
|
-
if (!redis) {
|
|
60
|
-
redis = new Redis(redisConfig);
|
|
61
|
-
|
|
62
|
-
redis.on('connect', () => {
|
|
63
|
-
console.log('Redis connected');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
redis.on('error', (err) => {
|
|
67
|
-
console.error('Redis error:', err);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
redis.on('close', () => {
|
|
71
|
-
console.log('Redis connection closed');
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return redis;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function closeRedis(): Promise<void> {
|
|
79
|
-
if (redis) {
|
|
80
|
-
await redis.quit();
|
|
81
|
-
redis = null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Health check
|
|
86
|
-
export async function checkRedisHealth(): Promise<boolean> {
|
|
87
|
-
try {
|
|
88
|
-
const client = getRedis();
|
|
89
|
-
const result = await client.ping();
|
|
90
|
-
return result === 'PONG';
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### 2. Caching Service
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
// src/services/cache.service.ts
|
|
101
|
-
import Redis from 'ioredis';
|
|
102
|
-
import { getRedis } from '../lib/redis';
|
|
103
|
-
|
|
104
|
-
interface CacheOptions {
|
|
105
|
-
ttl?: number; // seconds
|
|
106
|
-
prefix?: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export class CacheService {
|
|
110
|
-
private redis: Redis;
|
|
111
|
-
private defaultTTL: number;
|
|
112
|
-
private prefix: string;
|
|
113
|
-
|
|
114
|
-
constructor(options: CacheOptions = {}) {
|
|
115
|
-
this.redis = getRedis();
|
|
116
|
-
this.defaultTTL = options.ttl || 3600; // 1 hour
|
|
117
|
-
this.prefix = options.prefix || 'cache:';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private getKey(key: string): string {
|
|
121
|
-
return `${this.prefix}${key}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Basic get/set
|
|
125
|
-
async get<T>(key: string): Promise<T | null> {
|
|
126
|
-
const data = await this.redis.get(this.getKey(key));
|
|
127
|
-
if (!data) return null;
|
|
128
|
-
return JSON.parse(data) as T;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
132
|
-
const serialized = JSON.stringify(value);
|
|
133
|
-
const expiry = ttl || this.defaultTTL;
|
|
134
|
-
await this.redis.setex(this.getKey(key), expiry, serialized);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async delete(key: string): Promise<void> {
|
|
138
|
-
await this.redis.del(this.getKey(key));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Get or set pattern
|
|
142
|
-
async getOrSet<T>(
|
|
143
|
-
key: string,
|
|
144
|
-
factory: () => Promise<T>,
|
|
145
|
-
ttl?: number
|
|
146
|
-
): Promise<T> {
|
|
147
|
-
const cached = await this.get<T>(key);
|
|
148
|
-
if (cached !== null) {
|
|
149
|
-
return cached;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const value = await factory();
|
|
153
|
-
await this.set(key, value, ttl);
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Cache invalidation patterns
|
|
158
|
-
async invalidatePattern(pattern: string): Promise<number> {
|
|
159
|
-
const keys = await this.redis.keys(`${this.prefix}${pattern}`);
|
|
160
|
-
if (keys.length === 0) return 0;
|
|
161
|
-
return this.redis.del(...keys);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async invalidateByTags(tags: string[]): Promise<void> {
|
|
165
|
-
const pipeline = this.redis.pipeline();
|
|
166
|
-
|
|
167
|
-
for (const tag of tags) {
|
|
168
|
-
const tagKey = `tag:${tag}`;
|
|
169
|
-
const members = await this.redis.smembers(tagKey);
|
|
170
|
-
|
|
171
|
-
for (const key of members) {
|
|
172
|
-
pipeline.del(key);
|
|
173
|
-
}
|
|
174
|
-
pipeline.del(tagKey);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
await pipeline.exec();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Set with tags for invalidation
|
|
181
|
-
async setWithTags<T>(
|
|
182
|
-
key: string,
|
|
183
|
-
value: T,
|
|
184
|
-
tags: string[],
|
|
185
|
-
ttl?: number
|
|
186
|
-
): Promise<void> {
|
|
187
|
-
const fullKey = this.getKey(key);
|
|
188
|
-
const pipeline = this.redis.pipeline();
|
|
189
|
-
|
|
190
|
-
pipeline.setex(fullKey, ttl || this.defaultTTL, JSON.stringify(value));
|
|
191
|
-
|
|
192
|
-
for (const tag of tags) {
|
|
193
|
-
pipeline.sadd(`tag:${tag}`, fullKey);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
await pipeline.exec();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Bulk operations
|
|
200
|
-
async mget<T>(keys: string[]): Promise<(T | null)[]> {
|
|
201
|
-
const fullKeys = keys.map(k => this.getKey(k));
|
|
202
|
-
const results = await this.redis.mget(...fullKeys);
|
|
203
|
-
return results.map(r => (r ? JSON.parse(r) : null));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async mset<T>(entries: Array<{ key: string; value: T; ttl?: number }>): Promise<void> {
|
|
207
|
-
const pipeline = this.redis.pipeline();
|
|
208
|
-
|
|
209
|
-
for (const entry of entries) {
|
|
210
|
-
const fullKey = this.getKey(entry.key);
|
|
211
|
-
const serialized = JSON.stringify(entry.value);
|
|
212
|
-
pipeline.setex(fullKey, entry.ttl || this.defaultTTL, serialized);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
await pipeline.exec();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### 3. Session Management
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// src/services/session.service.ts
|
|
224
|
-
import Redis from 'ioredis';
|
|
225
|
-
import { getRedis } from '../lib/redis';
|
|
226
|
-
import { randomUUID } from 'crypto';
|
|
227
|
-
|
|
228
|
-
interface Session {
|
|
229
|
-
id: string;
|
|
230
|
-
userId: string;
|
|
231
|
-
data: Record<string, unknown>;
|
|
232
|
-
createdAt: number;
|
|
233
|
-
expiresAt: number;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export class SessionService {
|
|
237
|
-
private redis: Redis;
|
|
238
|
-
private prefix = 'session:';
|
|
239
|
-
private userSessionsPrefix = 'user_sessions:';
|
|
240
|
-
private defaultTTL = 86400 * 7; // 7 days
|
|
241
|
-
|
|
242
|
-
constructor() {
|
|
243
|
-
this.redis = getRedis();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async create(userId: string, data: Record<string, unknown> = {}): Promise<Session> {
|
|
247
|
-
const sessionId = randomUUID();
|
|
248
|
-
const now = Date.now();
|
|
249
|
-
|
|
250
|
-
const session: Session = {
|
|
251
|
-
id: sessionId,
|
|
252
|
-
userId,
|
|
253
|
-
data,
|
|
254
|
-
createdAt: now,
|
|
255
|
-
expiresAt: now + this.defaultTTL * 1000,
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const pipeline = this.redis.pipeline();
|
|
259
|
-
|
|
260
|
-
// Store session
|
|
261
|
-
pipeline.hset(`${this.prefix}${sessionId}`, {
|
|
262
|
-
userId,
|
|
263
|
-
data: JSON.stringify(data),
|
|
264
|
-
createdAt: now.toString(),
|
|
265
|
-
expiresAt: session.expiresAt.toString(),
|
|
266
|
-
});
|
|
267
|
-
pipeline.expire(`${this.prefix}${sessionId}`, this.defaultTTL);
|
|
268
|
-
|
|
269
|
-
// Track user sessions
|
|
270
|
-
pipeline.sadd(`${this.userSessionsPrefix}${userId}`, sessionId);
|
|
271
|
-
pipeline.expire(`${this.userSessionsPrefix}${userId}`, this.defaultTTL);
|
|
272
|
-
|
|
273
|
-
await pipeline.exec();
|
|
274
|
-
|
|
275
|
-
return session;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async get(sessionId: string): Promise<Session | null> {
|
|
279
|
-
const data = await this.redis.hgetall(`${this.prefix}${sessionId}`);
|
|
280
|
-
|
|
281
|
-
if (!data || !data.userId) {
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
id: sessionId,
|
|
287
|
-
userId: data.userId,
|
|
288
|
-
data: JSON.parse(data.data || '{}'),
|
|
289
|
-
createdAt: parseInt(data.createdAt),
|
|
290
|
-
expiresAt: parseInt(data.expiresAt),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async update(sessionId: string, data: Record<string, unknown>): Promise<void> {
|
|
295
|
-
const exists = await this.redis.exists(`${this.prefix}${sessionId}`);
|
|
296
|
-
if (!exists) {
|
|
297
|
-
throw new Error('Session not found');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
await this.redis.hset(`${this.prefix}${sessionId}`, 'data', JSON.stringify(data));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async refresh(sessionId: string): Promise<void> {
|
|
304
|
-
const session = await this.get(sessionId);
|
|
305
|
-
if (!session) {
|
|
306
|
-
throw new Error('Session not found');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const newExpiresAt = Date.now() + this.defaultTTL * 1000;
|
|
310
|
-
|
|
311
|
-
const pipeline = this.redis.pipeline();
|
|
312
|
-
pipeline.hset(`${this.prefix}${sessionId}`, 'expiresAt', newExpiresAt.toString());
|
|
313
|
-
pipeline.expire(`${this.prefix}${sessionId}`, this.defaultTTL);
|
|
314
|
-
await pipeline.exec();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async destroy(sessionId: string): Promise<void> {
|
|
318
|
-
const session = await this.get(sessionId);
|
|
319
|
-
if (!session) return;
|
|
320
|
-
|
|
321
|
-
const pipeline = this.redis.pipeline();
|
|
322
|
-
pipeline.del(`${this.prefix}${sessionId}`);
|
|
323
|
-
pipeline.srem(`${this.userSessionsPrefix}${session.userId}`, sessionId);
|
|
324
|
-
await pipeline.exec();
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async destroyAllUserSessions(userId: string): Promise<number> {
|
|
328
|
-
const sessionIds = await this.redis.smembers(`${this.userSessionsPrefix}${userId}`);
|
|
329
|
-
|
|
330
|
-
if (sessionIds.length === 0) return 0;
|
|
331
|
-
|
|
332
|
-
const pipeline = this.redis.pipeline();
|
|
333
|
-
for (const sessionId of sessionIds) {
|
|
334
|
-
pipeline.del(`${this.prefix}${sessionId}`);
|
|
335
|
-
}
|
|
336
|
-
pipeline.del(`${this.userSessionsPrefix}${userId}`);
|
|
337
|
-
|
|
338
|
-
await pipeline.exec();
|
|
339
|
-
return sessionIds.length;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async getUserSessions(userId: string): Promise<Session[]> {
|
|
343
|
-
const sessionIds = await this.redis.smembers(`${this.userSessionsPrefix}${userId}`);
|
|
344
|
-
|
|
345
|
-
const sessions: Session[] = [];
|
|
346
|
-
for (const sessionId of sessionIds) {
|
|
347
|
-
const session = await this.get(sessionId);
|
|
348
|
-
if (session) {
|
|
349
|
-
sessions.push(session);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
17
|
+
});
|
|
352
18
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
19
|
+
// Basic caching
|
|
20
|
+
await redis.setex('user:123', 3600, JSON.stringify(userData));
|
|
21
|
+
const cached = await redis.get('user:123');
|
|
356
22
|
```
|
|
357
23
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
// src/services/rate-limiter.service.ts
|
|
362
|
-
import Redis from 'ioredis';
|
|
363
|
-
import { getRedis } from '../lib/redis';
|
|
364
|
-
|
|
365
|
-
interface RateLimitResult {
|
|
366
|
-
allowed: boolean;
|
|
367
|
-
remaining: number;
|
|
368
|
-
resetAt: number;
|
|
369
|
-
retryAfter?: number;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
interface RateLimitConfig {
|
|
373
|
-
points: number; // Number of requests
|
|
374
|
-
duration: number; // Time window in seconds
|
|
375
|
-
blockDuration?: number; // Block duration when exceeded
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export class RateLimiter {
|
|
379
|
-
private redis: Redis;
|
|
380
|
-
private prefix = 'ratelimit:';
|
|
381
|
-
|
|
382
|
-
constructor() {
|
|
383
|
-
this.redis = getRedis();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Fixed window rate limiting
|
|
387
|
-
async checkFixedWindow(
|
|
388
|
-
key: string,
|
|
389
|
-
config: RateLimitConfig
|
|
390
|
-
): Promise<RateLimitResult> {
|
|
391
|
-
const fullKey = `${this.prefix}fixed:${key}`;
|
|
392
|
-
const now = Math.floor(Date.now() / 1000);
|
|
393
|
-
const windowStart = now - (now % config.duration);
|
|
394
|
-
const windowKey = `${fullKey}:${windowStart}`;
|
|
395
|
-
|
|
396
|
-
const current = await this.redis.incr(windowKey);
|
|
397
|
-
|
|
398
|
-
if (current === 1) {
|
|
399
|
-
await this.redis.expire(windowKey, config.duration);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const remaining = Math.max(0, config.points - current);
|
|
403
|
-
const resetAt = (windowStart + config.duration) * 1000;
|
|
404
|
-
|
|
405
|
-
if (current > config.points) {
|
|
406
|
-
return {
|
|
407
|
-
allowed: false,
|
|
408
|
-
remaining: 0,
|
|
409
|
-
resetAt,
|
|
410
|
-
retryAfter: resetAt - Date.now(),
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return { allowed: true, remaining, resetAt };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Sliding window rate limiting
|
|
418
|
-
async checkSlidingWindow(
|
|
419
|
-
key: string,
|
|
420
|
-
config: RateLimitConfig
|
|
421
|
-
): Promise<RateLimitResult> {
|
|
422
|
-
const fullKey = `${this.prefix}sliding:${key}`;
|
|
423
|
-
const now = Date.now();
|
|
424
|
-
const windowStart = now - config.duration * 1000;
|
|
425
|
-
|
|
426
|
-
// Use sorted set for sliding window
|
|
427
|
-
const pipeline = this.redis.pipeline();
|
|
428
|
-
pipeline.zremrangebyscore(fullKey, '-inf', windowStart);
|
|
429
|
-
pipeline.zadd(fullKey, now, `${now}:${Math.random()}`);
|
|
430
|
-
pipeline.zcard(fullKey);
|
|
431
|
-
pipeline.expire(fullKey, config.duration);
|
|
432
|
-
|
|
433
|
-
const results = await pipeline.exec();
|
|
434
|
-
const count = results?.[2]?.[1] as number;
|
|
435
|
-
|
|
436
|
-
const remaining = Math.max(0, config.points - count);
|
|
437
|
-
const resetAt = now + config.duration * 1000;
|
|
438
|
-
|
|
439
|
-
if (count > config.points) {
|
|
440
|
-
// Remove the request we just added
|
|
441
|
-
await this.redis.zremrangebyscore(fullKey, now, now);
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
allowed: false,
|
|
445
|
-
remaining: 0,
|
|
446
|
-
resetAt,
|
|
447
|
-
retryAfter: config.duration * 1000,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return { allowed: true, remaining, resetAt };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Token bucket rate limiting
|
|
455
|
-
async checkTokenBucket(
|
|
456
|
-
key: string,
|
|
457
|
-
config: { capacity: number; refillRate: number }
|
|
458
|
-
): Promise<RateLimitResult> {
|
|
459
|
-
const fullKey = `${this.prefix}bucket:${key}`;
|
|
460
|
-
const now = Date.now();
|
|
461
|
-
|
|
462
|
-
const luaScript = `
|
|
463
|
-
local key = KEYS[1]
|
|
464
|
-
local capacity = tonumber(ARGV[1])
|
|
465
|
-
local refillRate = tonumber(ARGV[2])
|
|
466
|
-
local now = tonumber(ARGV[3])
|
|
467
|
-
|
|
468
|
-
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
|
|
469
|
-
local tokens = tonumber(bucket[1]) or capacity
|
|
470
|
-
local lastRefill = tonumber(bucket[2]) or now
|
|
471
|
-
|
|
472
|
-
-- Calculate refill
|
|
473
|
-
local elapsed = (now - lastRefill) / 1000
|
|
474
|
-
local refill = elapsed * refillRate
|
|
475
|
-
tokens = math.min(capacity, tokens + refill)
|
|
476
|
-
|
|
477
|
-
-- Try to consume a token
|
|
478
|
-
local allowed = 0
|
|
479
|
-
if tokens >= 1 then
|
|
480
|
-
tokens = tokens - 1
|
|
481
|
-
allowed = 1
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
|
|
485
|
-
redis.call('EXPIRE', key, 3600)
|
|
486
|
-
|
|
487
|
-
return {allowed, tokens}
|
|
488
|
-
`;
|
|
489
|
-
|
|
490
|
-
const result = await this.redis.eval(
|
|
491
|
-
luaScript,
|
|
492
|
-
1,
|
|
493
|
-
fullKey,
|
|
494
|
-
config.capacity,
|
|
495
|
-
config.refillRate,
|
|
496
|
-
now
|
|
497
|
-
) as [number, number];
|
|
498
|
-
|
|
499
|
-
return {
|
|
500
|
-
allowed: result[0] === 1,
|
|
501
|
-
remaining: Math.floor(result[1]),
|
|
502
|
-
resetAt: now + Math.ceil((config.capacity - result[1]) / config.refillRate) * 1000,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Express middleware
|
|
508
|
-
export function createRateLimitMiddleware(config: RateLimitConfig) {
|
|
509
|
-
const limiter = new RateLimiter();
|
|
510
|
-
|
|
511
|
-
return async (req: any, res: any, next: any) => {
|
|
512
|
-
const key = req.ip || req.connection.remoteAddress;
|
|
513
|
-
const result = await limiter.checkSlidingWindow(key, config);
|
|
514
|
-
|
|
515
|
-
res.setHeader('X-RateLimit-Limit', config.points);
|
|
516
|
-
res.setHeader('X-RateLimit-Remaining', result.remaining);
|
|
517
|
-
res.setHeader('X-RateLimit-Reset', result.resetAt);
|
|
24
|
+
## Features
|
|
518
25
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
26
|
+
| Feature | Description | Guide |
|
|
27
|
+
|---------|-------------|-------|
|
|
28
|
+
| Caching | High-speed key-value storage with TTL | Use `setex` for auto-expiration, `get` for retrieval |
|
|
29
|
+
| Session Storage | Distributed session management | Store sessions with user ID index for multi-device |
|
|
30
|
+
| Rate Limiting | Request throttling with sliding windows | Use sorted sets or token bucket algorithms |
|
|
31
|
+
| Pub/Sub | Real-time messaging between services | Separate subscriber connections from publishers |
|
|
32
|
+
| Streams | Event sourcing and message queues | Consumer groups for reliable message processing |
|
|
33
|
+
| Data Structures | Lists, sets, sorted sets, hashes | Choose structure based on access patterns |
|
|
526
34
|
|
|
527
|
-
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
```
|
|
35
|
+
## Common Patterns
|
|
531
36
|
|
|
532
|
-
###
|
|
37
|
+
### Cache-Aside Pattern
|
|
533
38
|
|
|
534
39
|
```typescript
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
type MessageHandler = (message: unknown, channel: string) => void | Promise<void>;
|
|
540
|
-
|
|
541
|
-
export class PubSubService {
|
|
542
|
-
private publisher: Redis;
|
|
543
|
-
private subscriber: Redis;
|
|
544
|
-
private handlers: Map<string, Set<MessageHandler>> = new Map();
|
|
545
|
-
|
|
546
|
-
constructor() {
|
|
547
|
-
this.publisher = getRedis();
|
|
548
|
-
this.subscriber = getRedis().duplicate();
|
|
549
|
-
|
|
550
|
-
this.subscriber.on('message', (channel, message) => {
|
|
551
|
-
this.handleMessage(channel, message);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
this.subscriber.on('pmessage', (pattern, channel, message) => {
|
|
555
|
-
this.handleMessage(pattern, message, channel);
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
private async handleMessage(channel: string, message: string, actualChannel?: string): Promise<void> {
|
|
560
|
-
const handlers = this.handlers.get(channel);
|
|
561
|
-
if (!handlers) return;
|
|
562
|
-
|
|
563
|
-
const parsed = JSON.parse(message);
|
|
564
|
-
const targetChannel = actualChannel || channel;
|
|
40
|
+
async function getOrSet<T>(key: string, factory: () => Promise<T>, ttl = 3600): Promise<T> {
|
|
41
|
+
const cached = await redis.get(key);
|
|
42
|
+
if (cached) return JSON.parse(cached);
|
|
565
43
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
} catch (error) {
|
|
570
|
-
console.error(`Error handling message on ${targetChannel}:`, error);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Subscribe to channel
|
|
576
|
-
async subscribe(channel: string, handler: MessageHandler): Promise<() => void> {
|
|
577
|
-
if (!this.handlers.has(channel)) {
|
|
578
|
-
this.handlers.set(channel, new Set());
|
|
579
|
-
await this.subscriber.subscribe(channel);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
this.handlers.get(channel)!.add(handler);
|
|
583
|
-
|
|
584
|
-
return () => {
|
|
585
|
-
const handlers = this.handlers.get(channel);
|
|
586
|
-
if (handlers) {
|
|
587
|
-
handlers.delete(handler);
|
|
588
|
-
if (handlers.size === 0) {
|
|
589
|
-
this.handlers.delete(channel);
|
|
590
|
-
this.subscriber.unsubscribe(channel);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Subscribe to pattern
|
|
597
|
-
async psubscribe(pattern: string, handler: MessageHandler): Promise<() => void> {
|
|
598
|
-
if (!this.handlers.has(pattern)) {
|
|
599
|
-
this.handlers.set(pattern, new Set());
|
|
600
|
-
await this.subscriber.psubscribe(pattern);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
this.handlers.get(pattern)!.add(handler);
|
|
604
|
-
|
|
605
|
-
return () => {
|
|
606
|
-
const handlers = this.handlers.get(pattern);
|
|
607
|
-
if (handlers) {
|
|
608
|
-
handlers.delete(handler);
|
|
609
|
-
if (handlers.size === 0) {
|
|
610
|
-
this.handlers.delete(pattern);
|
|
611
|
-
this.subscriber.punsubscribe(pattern);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Publish message
|
|
618
|
-
async publish<T>(channel: string, message: T): Promise<number> {
|
|
619
|
-
return this.publisher.publish(channel, JSON.stringify(message));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Cleanup
|
|
623
|
-
async close(): Promise<void> {
|
|
624
|
-
await this.subscriber.quit();
|
|
625
|
-
}
|
|
44
|
+
const value = await factory();
|
|
45
|
+
await redis.setex(key, ttl, JSON.stringify(value));
|
|
46
|
+
return value;
|
|
626
47
|
}
|
|
627
|
-
|
|
628
|
-
// Usage example
|
|
629
|
-
const pubsub = new PubSubService();
|
|
630
|
-
|
|
631
|
-
// Subscribe to user events
|
|
632
|
-
const unsubscribe = await pubsub.subscribe('user:events', (message, channel) => {
|
|
633
|
-
console.log('User event:', message);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// Publish event
|
|
637
|
-
await pubsub.publish('user:events', {
|
|
638
|
-
type: 'user.created',
|
|
639
|
-
userId: '123',
|
|
640
|
-
timestamp: Date.now(),
|
|
641
|
-
});
|
|
642
48
|
```
|
|
643
49
|
|
|
644
|
-
###
|
|
50
|
+
### Sliding Window Rate Limiter
|
|
645
51
|
|
|
646
52
|
```typescript
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
interface StreamMessage {
|
|
652
|
-
id: string;
|
|
653
|
-
fields: Record<string, string>;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
export class StreamService {
|
|
657
|
-
private redis: Redis;
|
|
658
|
-
|
|
659
|
-
constructor() {
|
|
660
|
-
this.redis = getRedis();
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Add message to stream
|
|
664
|
-
async add(
|
|
665
|
-
stream: string,
|
|
666
|
-
data: Record<string, string | number>,
|
|
667
|
-
maxLen?: number
|
|
668
|
-
): Promise<string> {
|
|
669
|
-
const fields = Object.entries(data).flat().map(String);
|
|
670
|
-
|
|
671
|
-
if (maxLen) {
|
|
672
|
-
return this.redis.xadd(stream, 'MAXLEN', '~', maxLen, '*', ...fields);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return this.redis.xadd(stream, '*', ...fields);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Read messages
|
|
679
|
-
async read(
|
|
680
|
-
stream: string,
|
|
681
|
-
options: { count?: number; block?: number; lastId?: string } = {}
|
|
682
|
-
): Promise<StreamMessage[]> {
|
|
683
|
-
const { count = 10, block, lastId = '$' } = options;
|
|
684
|
-
|
|
685
|
-
const args: (string | number)[] = ['COUNT', count];
|
|
686
|
-
if (block !== undefined) {
|
|
687
|
-
args.push('BLOCK', block);
|
|
688
|
-
}
|
|
53
|
+
async function checkRateLimit(key: string, limit: number, windowSec: number): Promise<boolean> {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const windowStart = now - windowSec * 1000;
|
|
689
56
|
|
|
690
|
-
|
|
57
|
+
const pipeline = redis.pipeline();
|
|
58
|
+
pipeline.zremrangebyscore(key, '-inf', windowStart);
|
|
59
|
+
pipeline.zadd(key, now, `${now}:${Math.random()}`);
|
|
60
|
+
pipeline.zcard(key);
|
|
61
|
+
pipeline.expire(key, windowSec);
|
|
691
62
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
id,
|
|
696
|
-
fields: this.parseFields(fields),
|
|
697
|
-
}));
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Consumer group operations
|
|
701
|
-
async createConsumerGroup(
|
|
702
|
-
stream: string,
|
|
703
|
-
group: string,
|
|
704
|
-
startId: string = '$'
|
|
705
|
-
): Promise<void> {
|
|
706
|
-
try {
|
|
707
|
-
await this.redis.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
|
|
708
|
-
} catch (error: any) {
|
|
709
|
-
if (!error.message.includes('BUSYGROUP')) {
|
|
710
|
-
throw error;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async readGroup(
|
|
716
|
-
stream: string,
|
|
717
|
-
group: string,
|
|
718
|
-
consumer: string,
|
|
719
|
-
options: { count?: number; block?: number } = {}
|
|
720
|
-
): Promise<StreamMessage[]> {
|
|
721
|
-
const { count = 10, block = 5000 } = options;
|
|
722
|
-
|
|
723
|
-
const result = await this.redis.xreadgroup(
|
|
724
|
-
'GROUP', group, consumer,
|
|
725
|
-
'COUNT', count,
|
|
726
|
-
'BLOCK', block,
|
|
727
|
-
'STREAMS', stream, '>'
|
|
728
|
-
);
|
|
729
|
-
|
|
730
|
-
if (!result) return [];
|
|
731
|
-
|
|
732
|
-
return result[0][1].map(([id, fields]: [string, string[]]) => ({
|
|
733
|
-
id,
|
|
734
|
-
fields: this.parseFields(fields),
|
|
735
|
-
}));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
async acknowledge(stream: string, group: string, ...ids: string[]): Promise<number> {
|
|
739
|
-
return this.redis.xack(stream, group, ...ids);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async getPendingMessages(
|
|
743
|
-
stream: string,
|
|
744
|
-
group: string,
|
|
745
|
-
consumer?: string
|
|
746
|
-
): Promise<any> {
|
|
747
|
-
if (consumer) {
|
|
748
|
-
return this.redis.xpending(stream, group, '-', '+', 100, consumer);
|
|
749
|
-
}
|
|
750
|
-
return this.redis.xpending(stream, group);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
private parseFields(fields: string[]): Record<string, string> {
|
|
754
|
-
const result: Record<string, string> = {};
|
|
755
|
-
for (let i = 0; i < fields.length; i += 2) {
|
|
756
|
-
result[fields[i]] = fields[i + 1];
|
|
757
|
-
}
|
|
758
|
-
return result;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Stream consumer worker
|
|
763
|
-
async function startStreamWorker(
|
|
764
|
-
stream: string,
|
|
765
|
-
group: string,
|
|
766
|
-
consumer: string,
|
|
767
|
-
handler: (message: StreamMessage) => Promise<void>
|
|
768
|
-
) {
|
|
769
|
-
const streamService = new StreamService();
|
|
770
|
-
await streamService.createConsumerGroup(stream, group);
|
|
771
|
-
|
|
772
|
-
while (true) {
|
|
773
|
-
try {
|
|
774
|
-
const messages = await streamService.readGroup(stream, group, consumer);
|
|
775
|
-
|
|
776
|
-
for (const message of messages) {
|
|
777
|
-
try {
|
|
778
|
-
await handler(message);
|
|
779
|
-
await streamService.acknowledge(stream, group, message.id);
|
|
780
|
-
} catch (error) {
|
|
781
|
-
console.error('Error processing message:', error);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
} catch (error) {
|
|
785
|
-
console.error('Stream read error:', error);
|
|
786
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
787
|
-
}
|
|
788
|
-
}
|
|
63
|
+
const results = await pipeline.exec();
|
|
64
|
+
const count = results?.[2]?.[1] as number;
|
|
65
|
+
return count <= limit;
|
|
789
66
|
}
|
|
790
67
|
```
|
|
791
68
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
### Distributed Locking
|
|
69
|
+
### Distributed Lock
|
|
795
70
|
|
|
796
71
|
```typescript
|
|
797
|
-
async function acquireLock(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
): Promise<string | null> {
|
|
801
|
-
const redis = getRedis();
|
|
802
|
-
const lockId = randomUUID();
|
|
803
|
-
|
|
804
|
-
const acquired = await redis.set(
|
|
805
|
-
`lock:${key}`,
|
|
806
|
-
lockId,
|
|
807
|
-
'PX', ttl,
|
|
808
|
-
'NX'
|
|
809
|
-
);
|
|
810
|
-
|
|
72
|
+
async function acquireLock(key: string, ttlMs = 10000): Promise<string | null> {
|
|
73
|
+
const lockId = crypto.randomUUID();
|
|
74
|
+
const acquired = await redis.set(`lock:${key}`, lockId, 'PX', ttlMs, 'NX');
|
|
811
75
|
return acquired === 'OK' ? lockId : null;
|
|
812
76
|
}
|
|
813
77
|
|
|
814
78
|
async function releaseLock(key: string, lockId: string): Promise<boolean> {
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
818
|
-
return redis.call("del", KEYS[1])
|
|
819
|
-
else
|
|
820
|
-
return 0
|
|
821
|
-
end
|
|
822
|
-
`;
|
|
823
|
-
|
|
824
|
-
const result = await redis.eval(script, 1, `lock:${key}`, lockId);
|
|
825
|
-
return result === 1;
|
|
826
|
-
}
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
### Leaderboard
|
|
830
|
-
|
|
831
|
-
```typescript
|
|
832
|
-
class LeaderboardService {
|
|
833
|
-
private redis: Redis;
|
|
834
|
-
private key: string;
|
|
835
|
-
|
|
836
|
-
constructor(name: string) {
|
|
837
|
-
this.redis = getRedis();
|
|
838
|
-
this.key = `leaderboard:${name}`;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
async addScore(userId: string, score: number): Promise<void> {
|
|
842
|
-
await this.redis.zadd(this.key, score, userId);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
async getTopN(n: number): Promise<Array<{ userId: string; score: number; rank: number }>> {
|
|
846
|
-
const results = await this.redis.zrevrange(this.key, 0, n - 1, 'WITHSCORES');
|
|
847
|
-
const entries = [];
|
|
848
|
-
|
|
849
|
-
for (let i = 0; i < results.length; i += 2) {
|
|
850
|
-
entries.push({
|
|
851
|
-
userId: results[i],
|
|
852
|
-
score: parseFloat(results[i + 1]),
|
|
853
|
-
rank: i / 2 + 1,
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return entries;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
async getUserRank(userId: string): Promise<number | null> {
|
|
861
|
-
const rank = await this.redis.zrevrank(this.key, userId);
|
|
862
|
-
return rank !== null ? rank + 1 : null;
|
|
863
|
-
}
|
|
79
|
+
const script = `if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`;
|
|
80
|
+
return (await redis.eval(script, 1, `lock:${key}`, lockId)) === 1;
|
|
864
81
|
}
|
|
865
82
|
```
|
|
866
83
|
|
|
867
84
|
## Best Practices
|
|
868
85
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
- Use key prefixes for namespacing
|
|
879
|
-
- Implement circuit breakers
|
|
880
|
-
- Use appropriate data structures
|
|
881
|
-
|
|
882
|
-
### Don'ts
|
|
883
|
-
|
|
884
|
-
- Don't store large objects (>100KB)
|
|
885
|
-
- Don't use KEYS in production
|
|
886
|
-
- Don't skip key expiration
|
|
887
|
-
- Don't ignore memory limits
|
|
888
|
-
- Don't use single Redis for critical apps
|
|
889
|
-
- Don't block on long operations
|
|
890
|
-
- Don't store sensitive data unencrypted
|
|
891
|
-
- Don't ignore connection errors
|
|
892
|
-
- Don't use Redis as primary database
|
|
893
|
-
- Don't skip monitoring
|
|
894
|
-
|
|
895
|
-
## References
|
|
896
|
-
|
|
897
|
-
- [Redis Documentation](https://redis.io/documentation)
|
|
898
|
-
- [ioredis Documentation](https://github.com/luin/ioredis)
|
|
899
|
-
- [Redis Best Practices](https://redis.io/docs/management/optimization/)
|
|
900
|
-
- [Redis University](https://university.redis.com/)
|
|
901
|
-
- [Redis Patterns](https://redis.io/docs/manual/patterns/)
|
|
86
|
+
| Do | Avoid |
|
|
87
|
+
|----|-------|
|
|
88
|
+
| Set TTL on all cache keys | Storing objects larger than 100KB |
|
|
89
|
+
| Use pipelines for batch operations | Using `KEYS` command in production |
|
|
90
|
+
| Implement connection pooling | Ignoring memory limits and eviction |
|
|
91
|
+
| Use Lua scripts for atomic operations | Using Redis as primary database |
|
|
92
|
+
| Add key prefixes for namespacing | Blocking on long-running operations |
|
|
93
|
+
| Monitor memory with `INFO memory` | Storing sensitive data unencrypted |
|
|
94
|
+
| Set up Redis Sentinel for HA | Skipping connection error handling |
|