0

Bài 4: Hash & List - Ứng dụng lưu trữ Object và tạo Hàng đợi (Queue) đơn giản

Mục tiêu bài học:

  • Hiểu và thao tác với dữ liệu kiểu Hash để lưu trữ Object (thực thể/struct) tối ưu hơn so với việc lưu nguyên chuỗi JSON.

  • Sử dụng cấu trúc List để xây dựng hệ thống Hàng đợi (Message Queue) cơ bản với cơ chế Producer - Consumer.

1. Redis Hash: Lưu trữ Object hiệu quả

Nếu kiểu String lưu trữ một cặp Key-Value đơn giản, thì Hash lưu trữ một danh sách các cặp Field-Value bên trong một Key.

Tại sao không dùng String chứa JSON? Nếu bạn lưu một User Profile bằng JSON vào kiểu String, mỗi lần muốn cập nhật một trường nhỏ (ví dụ: avatar_url), bạn phải kéo toàn bộ chuỗi JSON về, parse, sửa đổi, serialize lại và ghi đè toàn bộ. Với Hash, bạn chỉ cần gửi lệnh cập nhật đúng một field đó (HSET user:1 avatar_url "new_url"), giúp tiết kiệm băng thông và tăng tốc độ xử lý.

Thao tác cơ bản với Hash:

Go

package main

import (
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func workWithHash(rdb *redis.Client) {
	userKey := "profile:100"

	// 1. Ghi dữ liệu vào Hash (HSET)
	// go-redis hỗ trợ truyền trực tiếp map hoặc struct (có thẻ tag `redis`)
	userData := map[string]interface{}{
		"name":  "Nguyen Van A",
		"email": "a@example.com",
		"age":   25,
		"role":  "admin",
	}
	
	// Lưu ý: HSet ghi đè các field đã có và thêm mới field chưa có
	err := rdb.HSet(ctx, userKey, userData).Err()
	if err != nil {
		fmt.Println("Lỗi khi lưu Hash:", err)
	}

	// 2. Lấy một field cụ thể (HGET)
	// Tối ưu network vì không cần kéo toàn bộ object
	email, _ := rdb.HGet(ctx, userKey, "email").Result()
	fmt.Println("Email của user:", email)

	// 3. Lấy toàn bộ Object (HGETALL)
	// Trả về map[string]string
	allFields, err := rdb.HGetAll(ctx, userKey).Result()
	if err == nil {
		fmt.Printf("Toàn bộ Profile: %v\n", allFields)
		// Output: Toàn bộ Profile: map[age:25 email:a@example.com name:Nguyen Van A role:admin]
	}

	// 4. Tăng giá trị của một field số học (HINCRBY)
	// Rất hữu ích để đếm số lượt xem, điểm số nội bộ của object
	newAge, _ := rdb.HIncrBy(ctx, userKey, "age", 1).Result()
	fmt.Printf("Tuổi sau khi tăng: %d\n", newAge)
}

2. Redis List: Xây dựng Message Queue đơn giản

Redis List là một danh sách liên kết (Linked List). Bạn có thể đẩy dữ liệu vào từ hai đầu (Trái/Phải) và lấy ra từ hai đầu. Đặc tính này biến List thành cấu trúc dữ liệu hoàn hảo cho Hàng đợi (Queue - FIFO) hoặc Ngăn xếp (Stack - LIFO).

Trong thực tế, mô hình Queue (Vào trước ra trước) được dùng phổ biến nhất để làm Background Jobs (như gửi email, xử lý ảnh).

  • Producer (Người sản xuất): Đẩy việc vào bên Trái hàng đợi (LPUSH).

  • Consumer (Người tiêu thụ): Kéo việc ra từ bên Phải hàng đợi (RPOP hoặc BRPOP).

Thao tác cơ bản với List:

Go

import (
	"time"
)

func producer(rdb *redis.Client, queueName string) {
	// Đẩy 3 task vào hàng đợi (LPUSH)
	tasks := []string{"send_email_1", "send_email_2", "process_image_1"}
	for _, task := range tasks {
		// LPUSH thêm vào đầu mảng.
		err := rdb.LPush(ctx, queueName, task).Err()
		if err == nil {
			fmt.Printf("Producer: Đã đẩy task [%s] vào %s\n", task, queueName)
		}
	}
}

func consumer(rdb *redis.Client, queueName string) {
	fmt.Println("Consumer: Bắt đầu lắng nghe hàng đợi...")
	for {
		// BRPOP: Blocking Right Pop
		// Thay vì dùng RPOP và dùng vòng lặp vô hạn gây tốn CPU (Polling),
		// BRPOP sẽ block (chờ) cho đến khi có dữ liệu mới hoặc hết thời gian timeout.
		// Số 0 nghĩa là block vô hạn (chờ tới khi nào có task thì thôi).
		
		result, err := rdb.BRPop(ctx, 0, queueName).Result()
		
		if err != nil {
			fmt.Println("Consumer lỗi:", err)
			time.Sleep(1 * time.Second) // Nghỉ một chút trước khi thử lại
			continue
		}

		// Result trả về mảng 2 phần tử: [tên_queue, giá_trị_lấy_ra]
		task := result[1]
		fmt.Printf("Consumer: Đang xử lý task [%s]...\n", task)
		
		// Giả lập thời gian xử lý
		time.Sleep(500 * time.Millisecond)
		fmt.Printf("Consumer: Đã hoàn thành task [%s]\n", task)
	}
}

Góc kinh nghiệm: Dùng Redis List với BRPOP rất tốt cho các hệ thống Queue đơn giản gọn nhẹ. Tuy nhiên, nếu service Consumer bị sập đang lúc xử lý task (sau khi đã pop ra khỏi Redis), task đó sẽ bị mất vĩnh viễn. Nếu bạn cần đảm bảo dữ liệu không bao giờ mất (At-least-once delivery) và hỗ trợ ACK, hãy đón chờ Bài 9: Redis Streams.


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í