Redis Data Structures and NestJS Integration for AI Chat Context
Redis from Beginner to Practice: Master Core Data Structures and NestJS Integration in One Article
From "in-memory cache" to "multi-turn conversation context," thoroughly master the core gameplay of Redis
Preface
Before encountering Redis, I simply understood it as "a super-large JavaScript Map placed on a server." It wasn't until I was building an AI multi-turn conversation application and came across the combination of Redis + Zustand that I truly realized — Redis is far more than a cache; it is a "Swiss Army knife" capable of solving countless backend challenges.
This article will start from scratch, helping you thoroughly understand Redis's core data structures, expiration strategies, and how to elegantly integrate Redis in NestJS, using it to implement multi-turn conversation context management. Whether you are a newcomer to Redis or a developer looking to implement it in real projects, this article can help you avoid detours.
1. What is Redis? Why is it so fast?
Redis (Remote Dictionary Server) is an in-memory key-value database. Its widespread use boils down to three core points:
- In-memory: All data resides in RAM, with read/write speeds at the millisecond or even microsecond level.
- Rich data structures: Not just
String, but alsoHash,List,Set,Sorted Set, and more. - Single-threaded + asynchronous I/O: Redis uses a single thread to process commands, avoiding lock contention from multi-threading, while leveraging I/O multiplexing to easily handle hundreds of thousands of QPS.
💡 One-sentence summary: Redis is an "insanely fast" in-memory database with various built-in, easy-to-use data structures, so you don't have to reinvent the wheel.
2. Core Data Structures: From String to Hash to List
Redis supports multiple data structures, but the most commonly used and essential to master are the following five.
1. String — The Most Basic Universal Type
String is the cornerstone of Redis, binary-safe, and can store any data such as JSON, images, and serialized objects.
Common commands:
SET key value # Set value
GET key # Get value
INCR key # Atomic increment by 1 (a counter's best friend)
SETEX key seconds value # Set value with expiration time
SETNX key value # Set only if key does not exist (basis for distributed locks)
Underlying principle: Redis does not use C's traditional strings but implements its own SDS (Simple Dynamic Strings), supporting O(1) length retrieval and space pre-allocation to reduce memory reallocation.
2. Hash — The Natural Container for Objects
Hash is similar to a Map or Object in programming languages, particularly suitable for storing objects because you can modify individual fields without serializing the entire object.
Common commands:
HSET user:1001 name "Tom" age 18 # Set multiple fields
HGET user:1001 name # Get a single field
HGETALL user:1001 # Get all fields and values
HINCRBY user:1001 age 1 # Atomically increment a field
HDEL user:1001 age # Delete a field
Typical scenarios: User information, product details, configuration items, and other structured data.
Underlying principle: When there are few fields, a compact listpack is used (saving memory); when fields increase, it automatically converts to a hash table, with O(1) query efficiency.
3. List — High-Performance Double-Ended Queue
List is an ordered sequence of strings, supporting insertion and removal of elements from both ends (left/right).
Common commands:
LPUSH queue "task1" # Insert from the left
RPUSH queue "task2" # Insert from the right
LPOP queue # Pop from the left
RPOP queue # Pop from the right
LRANGE queue 0 -1 # View all elements
LTRIM queue 0 99 # Keep only the first 100 (trim)
BLPOP queue 10 # Blocking pop (a message queue's best friend)
Typical scenarios: Message queues, latest activity feeds (like Weibo timelines), task queues.
3. Hash vs List: How to Choose?
This is a question that stumps many beginners. Let's clarify with a table:
| Dimension | Hash | List |
|---|---|---|
| Data Structure | Key-value pair collection (field → value) | Ordered sequence of strings |
| Access Method | Access by field name | Access by index or range |
| Read/Write Performance | Field-level read/write O(1) | Head/tail operations O(1), middle access O(n) |
| Applicable Scenarios | Storing objects, frequently updating partial fields | Queues, stacks, timelines, message streams |
Selection mnemonic:
- To store "multiple attributes of an object" and often modify a single attribute → Choose Hash
- To store an "ordered message list" and frequently append/trim → Choose List
💡 In practice, the two are often used together: Use Hash to store user metadata (systemPrompt, createdAt), and List to store the conversation message flow. This is the best practice of "leveraging each one's strengths."
4. TTL Expiration: Why Must It Be Set?
TTL (Time To Live) is Redis's automatic expiration mechanism, allowing you to set a "life countdown" for each key.
Why set TTL?
- Save memory: Redis data is all in memory; without expiration, memory will eventually explode.
- Ensure data timeliness: For example, verification codes expire in 5 minutes, sessions automatically expire after 30 minutes of inactivity.
- Fail-safe security: Even if a user doesn't actively log out, data will be automatically destroyed, protecting privacy.
How to set it?
EXPIRE session:user123 1800 # Automatically delete after 30 minutes
TTL session:user123 # Check remaining seconds (-2 means it has disappeared)
Redis's Expiration Cleanup Strategy
- Lazy deletion: Only when you access a key does Redis check if it has expired; if so, it deletes it.
- Periodic deletion: Redis actively scans a batch of expired keys at intervals (default 100ms) and cleans them up.
The combination of both ensures performance while preventing unlimited memory growth.
Best Practice Recommendations
| Data Type | Suggested TTL |
|---|---|
| Session data | 30 minutes ~ 24 hours |
| Verification codes | 5 ~ 10 minutes |
| API cache | Based on business needs (e.g., 1 minute ~ 1 hour) |
⚠️ Important: Every time a user is active, remember to use
EXPIREto reset the TTL, achieving "sliding expiration" — as long as the user keeps using it, the data lives forever.
5. Integrating Redis in NestJS (Based on ioredis)
To operate Redis in NestJS, the community most commonly uses the ioredis client.
Step 1: Install Dependencies
pnpm add ioredis
pnpm add -D @types/ioredis
Step 2: Create a Redis Provider
// redis.provider.ts
import { Provider } from '@nestjs/common';
import Redis from 'ioredis';
export type RedisClient = Redis;
export const RedisProvider: Provider = {
provide: 'REDIS_CLIENT',
useFactory: () => {
return new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
retryStrategy: (times) => Math.min(times * 50, 2000),
});
},
};
Step 3: Register as a Global Module
// app.module.ts
import { Module, Global } from '@nestjs/common';
import { RedisProvider } from './redis.provider';
@Global()
@Module({
providers: [RedisProvider],
exports: [RedisProvider],
})
export class RedisModule {}
Step 4: Use in a Service
import { Inject, Injectable } from '@nestjs/common';
import { RedisClient } from './redis.provider';
@Injectable()
export class ChatService {
constructor(@Inject('REDIS_CLIENT') private readonly redis: RedisClient) {}
// Save message (using List)
async addMessage(userId: string, role: string, content: string) {
const key = `chat:${userId}`;
const msg = JSON.stringify({ role, content, timestamp: Date.now() });
await this.redis
.multi()
.rpush(key, msg)
.ltrim(key, -20, -1) // Keep only the last 20
.expire(key, 60 * 30) // Expire in 30 minutes
.exec();
}
// Get history
async getHistory(userId: string): Promise<any[]> {
const key = `chat:${userId}`;
const messages = await this.redis.lrange(key, 0, -1);
// Refresh expiration (user is active, extend life)
await this.redis.expire(key, 60 * 30);
return messages.map(msg => JSON.parse(msg));
}
}
6. Practical Application: Implementing Multi-Turn Conversation Context with Redis
This is the ultimate real-world scenario of this article — using Redis to store the historical memory of AI multi-turn conversations.
Data Structure Design
- Key:
chat:{userId}(List type) - Stored Content: The most recent N rounds of conversation (e.g., 10 rounds), each stored as a JSON object
{role, content} - TTL: Automatically expires after 30 minutes of inactivity
Complete Flow
@Injectable()
export class AIChatService {
constructor(
@Inject('REDIS_CLIENT') private readonly redis: RedisClient,
private readonly llmService: LLMService,
) {}
async chat(userId: string, userMessage: string): Promise<string> {
const key = `chat:${userId}`;
// 1. Get conversation history
const history = await this.redis.lrange(key, 0, -1);
const historyParsed = history.map(msg => JSON.parse(msg));
// 2. Construct Prompt (last 10 rounds)
const context = historyParsed.slice(-10).map(turn =>
`${turn.role}: ${turn.content}`
).join('\n');
const prompt = `${context}\nuser: ${userMessage}`;
// 3. Call the large model
const reply = await this.llmService.generate(prompt);
// 4. Save this round of conversation (user message + AI reply)
const pipeline = this.redis.pipeline();
pipeline.rpush(key, JSON.stringify({ role: 'user', content: userMessage }));
pipeline.rpush(key, JSON.stringify({ role: 'assistant', content: reply }));
pipeline.ltrim(key, -20, -1); // Keep at most 20 messages (10 rounds)
pipeline.expire(key, 60 * 30); // Refresh expiration
await pipeline.exec();
return reply;
}
}
Why is this design efficient?
- Append operation O(1):
RPUSHinserts directly at the end, without involving reads/writes of historical data. - Automatic trimming:
LTRIMensures constant memory usage. - Atomic operations:
pipelineensures multiple commands are executed at once, avoiding concurrency issues. - Automatic expiration: Automatically cleaned up after 30 minutes of inactivity, not occupying extra memory.
7. Summary
Through this article, you should have mastered the three core aspects of Redis:
- Data structures: The characteristics and applicable scenarios of String, Hash, and List.
- Expiration strategies: The role of TTL, how to set it, and Redis's cleanup mechanism.
- NestJS integration: How to elegantly operate Redis in NestJS using
ioredis.
More importantly, you've seen a real production-level scenario — using Redis's List structure to store multi-turn conversation context, achieving high-performance, low-cost, auto-expiring session management.
The world of Redis goes far beyond this, with more data structures like Set, Sorted Set, Stream, and Bitmap waiting for you to explore. But by mastering the content of this article, you can already handle 80% of backend caching and session management scenarios.
Next steps:
- Try running the above code in your NestJS project
- Use the
redis-clicommand-line tool to observe data changes in real-time - Think about how to implement distributed rate limiting or distributed locks with Redis
If this article helped you, feel free to like, bookmark, and share! Any questions, see you in the comments 👇
This article was first published on Juejin. Please credit the source when reprinting.