跪拜 Guibai
← Back to the summary

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:

  1. In-memory: All data resides in RAM, with read/write speeds at the millisecond or even microsecond level.
  2. Rich data structures: Not just String, but also Hash, List, Set, Sorted Set, and more.
  3. 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:

💡 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?

  1. Save memory: Redis data is all in memory; without expiration, memory will eventually explode.
  2. Ensure data timeliness: For example, verification codes expire in 5 minutes, sessions automatically expire after 30 minutes of inactivity.
  3. 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

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 EXPIRE to 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

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?


7. Summary

Through this article, you should have mastered the three core aspects of Redis:

  1. Data structures: The characteristics and applicable scenarios of String, Hash, and List.
  2. Expiration strategies: The role of TTL, how to set it, and Redis's cleanup mechanism.
  3. 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:

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.