0

[Series ioredis - Bài 2] Lưu Trữ Dữ Liệu Phức Tạp (Hash) & Bứt Phá Hiệu Năng Với Pipeline

Chào mừng các bạn quay trở lại với series ioredis! Ở Bài 1, chúng ta đã kết nối thành công và thao tác với dữ liệu dạng String (chuỗi).

Nhưng ở môi trường thực tế, dữ liệu backend hiếm khi chỉ là một chuỗi đơn giản. Chúng ta thường xuyên phải làm việc với các object phức tạp (ví dụ: thông tin User, cấu hình hệ thống, giỏ hàng). Hơn thế nữa, khi lưu lượng truy cập (traffic) tăng cao, việc gửi từng lệnh đơn lẻ lên Redis sẽ trở thành nút thắt cổ chai (bottleneck) cho toàn hệ thống.

Trong Bài 2 này, chúng ta sẽ giải quyết cả hai bài toán trên bằng hai vũ khí sắc bén của Redis và ioredis: Hash và Pipeline.

1. Bài toán lưu trữ Object: Stringify hay dùng Hash?

Giả sử bạn có một object user như sau:

const user = {
  id: "1001",
  name: "Hiếu",
  role: "engineer",
  status: "active"
};

Cách 1: Ép kiểu về String (JSON.stringify)

Bạn hoàn toàn có thể biến object này thành chuỗi JSON và lưu bằng lệnh SET như ở Bài 1.

  • Ưu điểm: Nhanh, dễ code.
  • Nhược điểm: Nếu bạn chỉ muốn cập nhật trường status từ "active" sang "offline", bạn phải GET toàn bộ chuỗi JSON về, parse nó ra, sửa đổi, stringify lại rồi mới SET lên Redis. Rất tốn băng thông và CPU!

Cách 2: Sử dụng Redis Hash Hash trong Redis giống như một object phẳng (flat object) hoặc Map. Nó cho phép bạn đọc/ghi từng trường (field) riêng biệt bên trong một key lớn.

2. Thao tác với Hash bằng ioredis

Hãy cùng xem ioredis xử lý Hash mượt mà như thế nào với các lệnh HSET, HGET và HGETALL.

const Redis = require("ioredis");
const redis = new Redis();

async function handleHashData() {
  const userKey = "user:1001";

  // 1. Lưu object vào Hash (HSET)
  // ioredis hỗ trợ truyền thẳng một object JavaScript vào HSET rất tiện lợi
  await redis.hset(userKey, {
    name: "Hieu",
    role: "engineer",
    status: "active"
  });
  console.log("Đã lưu thông tin user vào Hash.");

  // 2. Lấy toàn bộ thông tin user (HGETALL)
  const userData = await redis.hgetall(userKey);
  console.log("Toàn bộ dữ liệu user:", userData); 
  // Kết quả: { name: 'Hieu', role: 'engineer', status: 'active' }

  // 3. Lấy một trường cụ thể (HGET)
  const role = await redis.hget(userKey, "role");
  console.log("Chức vụ của user:", role); // Kết quả: engineer

  // 4. Cập nhật một trường cụ thể mà không ảnh hưởng tới dữ liệu khác
  await redis.hset(userKey, "status", "offline");
  console.log("Đã cập nhật trạng thái thành offline.");
}

Lưu ý: Redis Hash chỉ lưu giá trị dưới dạng chuỗi. Nếu bạn có object lồng nhau (nested object), bạn vẫn cần phải tính đến việc thiết kế lại cấu trúc hoặc kết hợp với JSON stringify cho các field con.

3. Pipeline: Bứt phá hiệu năng khi xử lý hàng loạt

Trong kiến trúc mạng, một khái niệm rất quan trọng là RTT (Round Trip Time) - thời gian kể từ khi client gửi một request đi cho đến khi nhận được response về.

Nếu bạn có 1000 lệnh SET, việc chạy vòng lặp await redis.set(...) 1000 lần nghĩa là bạn đang chịu đựng RTT x 1000. Đây là một thảm họa về mặt hiệu năng (Network Latency).

Pipeline sinh ra để giải quyết việc này. Nó gom tất cả các lệnh lại, gửi đi cùng một lúc trong một network request, và nhận về một mảng kết quả tương ứng.

async function testPipeline() {
  // Tạo một pipeline
  const pipeline = redis.pipeline();

  // Đưa các lệnh vào pipeline (chú ý: không dùng await ở đây)
  pipeline.set("config:theme", "dark");
  pipeline.hset("user:2002", "name", "Alex");
  pipeline.hset("user:2002", "role", "admin");
  pipeline.incr("page:views"); // Tăng biến đếm lên 1

  console.log("Đang gửi pipeline đi...");

  // Thực thi pipeline (lúc này mới thực sự gửi network request)
  // Kết quả trả về là một mảng chứa kết quả của từng lệnh tương ứng
  const results = await pipeline.exec();
  
  console.log("Kết quả pipeline:", results);
  /* 
  Kết quả có dạng: 
  [
    [null, 'OK'], // [error, result] của lệnh set
    [null, 1],    // [error, result] của lệnh hset
    [null, 1],    // [error, result] của lệnh hset
    [null, 1]     // [error, result] của lệnh incr
  ]
  */
}

Khi nào nên dùng Pipeline?

  • Khi bạn cần import dữ liệu lớn (Bulk insert).
  • Khi bạn phải query nhiều thông tin từ nhiều key khác nhau để tổng hợp dữ liệu trả về cho API.
  • Bất cứ khi nào các lệnh Redis của bạn không phụ thuộc vào kết quả của lệnh trước đó.

Tổng kết Bài 2

Đến đây, bạn đã nắm trong tay hai kỹ thuật cực kỳ quan trọng:

  1. Dùng Hash để quản lý memory hiệu quả và thao tác với object linh hoạt hơn.
  2. Dùng Pipeline để giảm thiểu độ trễ mạng (Network Latency) và tăng Throughput cho hệ thống Backend.

Ở Bài 3, chúng ta sẽ rời khỏi các thao tác CRUD truyền thống để bước vào một thế giới kiến trúc thú vị hơn: Sử dụng Redis như một Message Broker. Chúng ta sẽ cùng tìm hiểu cơ chế Pub/Sub trong ioredis để xây dựng hệ thống real-time nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí