Redis & Caching là gì? Hướng dẫn sử dụng Redis trong Go

1. Caching là gì? Vì sao cần cache khi đã có database?

Caching (bộ nhớ đệm) là kỹ thuật lưu tạm dữ liệu thường được truy cập để có thể lấy lại nhanh chóng khi cần, thay vì phải thực hiện lại toàn bộ quá trình xử lý hoặc truy vấn nguồn dữ liệu ban đầu.

Trong một ứng dụng Backend, dữ liệu thường được lưu trong database như PostgreSQL hoặc MySQL. Khi client gửi request, server có thể truy vấn database, xử lý kết quả rồi trả response về cho client.

Ví dụ, một API cung cấp thông tin sản phẩm có thể nhận hàng trăm request để lấy cùng một sản phẩm. Nếu mỗi request đều truy vấn database, hệ thống phải lặp lại thao tác đọc dữ liệu dù kết quả có thể không thay đổi.

Caching giải quyết vấn đề này bằng cách lưu kết quả truy vấn vào một nơi có thể truy cập nhanh hơn. Những request tiếp theo có thể sử dụng dữ liệu đã lưu thay vì truy vấn database ngay từ đầu.

Ví dụ

Giả sử API cần lấy thông tin sản phẩm có ID là 10.

Không sử dụng cache:

  1. Client gửi request.
  2. Server truy vấn PostgreSQL.
  3. Database trả về thông tin sản phẩm.
  4. Server trả response cho client.

Mỗi request đều phải thực hiện lại quá trình truy vấn.

Có sử dụng cache:

  1. Client gửi request.
  2. Server kiểm tra cache.
  3. Nếu dữ liệu đã có trong cache, server lấy dữ liệu từ đó.
  4. Nếu dữ liệu chưa có, server truy vấn PostgreSQL, lưu kết quả vào cache rồi trả response.

Nhờ vậy, những request thường xuyên truy cập cùng một dữ liệu có thể được xử lý nhanh hơn và giảm số lần truy vấn database.

Vì sao không lấy dữ liệu trực tiếp từ database?

Database vẫn là nơi lưu trữ dữ liệu chính của ứng dụng. Tuy nhiên, truy vấn database có thể tiêu tốn tài nguyên như CPU, bộ nhớ, I/O và kết nối. Khi số lượng request tăng lên, những truy vấn lặp lại có thể làm tăng tải và thời gian phản hồi.

Cache giúp giảm một phần tải này bằng cách phục vụ những lần đọc dữ liệu lặp lại từ bộ nhớ đệm.

Tuy nhiên, cache không thay thế database. Dữ liệu trong cache có thể hết hạn, bị xóa hoặc không còn mới nhất. Vì vậy, ứng dụng vẫn cần database làm nguồn dữ liệu chính trong phần lớn các hệ thống thông thường.

2. Redis là gì? Redis khác gì với caching nói chung?

Redis là một hệ thống lưu trữ dữ liệu trong bộ nhớ, hỗ trợ nhiều cấu trúc dữ liệu như String, Hash, List, Set và Sorted Set. Redis thường được sử dụng làm cache, nhưng cũng có thể phục vụ những mục đích khác như quản lý session, bộ đếm và một số dạng hàng đợi.

Điểm quan trọng là Caching là kỹ thuật, còn Redis là một công cụ có thể được sử dụng để triển khai kỹ thuật đó.

Caching không bắt buộc phải sử dụng Redis. Một ứng dụng có thể lưu cache trực tiếp trong bộ nhớ của tiến trình, sử dụng cache tại trình duyệt hoặc dùng các hệ thống khác phù hợp với nhu cầu.

Redis được sử dụng phổ biến vì dữ liệu thường được truy cập trực tiếp từ bộ nhớ, hỗ trợ nhiều kiểu dữ liệu và có thể được chia sẻ giữa nhiều tiến trình hoặc nhiều instance của ứng dụng.

Redis nằm ở đâu trong kiến trúc Backend?

Một luồng xử lý thường gặp:

Client → Gin Handler → Service → Redis → PostgreSQL

Đây là cách mô tả đơn giản. Trên thực tế, service thường kiểm tra Redis trước; nếu cache miss thì mới truy vấn PostgreSQL. Khi cần ghi hoặc cập nhật dữ liệu, ứng dụng cũng có thể phải làm việc với cả database lẫn cache.

  • Gin Handler: tiếp nhận HTTP request và trả HTTP response.
  • Service: xử lý nghiệp vụ và quyết định khi nào cần đọc cache hoặc database.
  • Redis: lưu những dữ liệu có thể tái sử dụng nhằm giảm thời gian truy xuất.
  • PostgreSQL: lưu trữ dữ liệu chính của ứng dụng.

Redis và PostgreSQL đảm nhiệm những vai trò khác nhau. Redis thường được dùng để truy cập nhanh dữ liệu có thể tái tạo hoặc lấy lại, trong khi PostgreSQL phù hợp với việc lưu trữ dữ liệu có cấu trúc và quản lý tính nhất quán thông qua các cơ chế của hệ quản trị cơ sở dữ liệu.

3. Cache hoạt động như thế nào?

Để hiểu cơ chế caching, cần nắm hai khái niệm: Cache Hit và Cache Miss.

3.1. Cache Hit

Cache Hit xảy ra khi ứng dụng tìm thấy dữ liệu cần thiết trong cache.

Ví dụ, Redis đã lưu thông tin sản phẩm với key products:10. Khi client yêu cầu sản phẩm có ID là 10, server tìm thấy dữ liệu trong Redis và có thể trả về ngay mà không cần truy vấn PostgreSQL.

Lợi ích chính là giảm độ trễ và giảm tải cho database.

3.2. Cache Miss

Cache Miss xảy ra khi dữ liệu cần tìm không tồn tại trong cache, đã hết hạn hoặc không thể được sử dụng.

Trong trường hợp này, ứng dụng thường truy vấn database để lấy dữ liệu. Sau đó, nếu phù hợp, ứng dụng lưu kết quả vào cache để những request tiếp theo có thể sử dụng.

3.3. Luồng đọc dữ liệu với Cache-Aside

Một chiến lược phổ biến là Cache-Aside. Với chiến lược này, ứng dụng chủ động kiểm tra cache trước và chỉ truy vấn database khi cần.

Quy trình:

  1. Client gửi request lấy dữ liệu.
  2. Service tạo cache key tương ứng.
  3. Service kiểm tra dữ liệu trong Redis.
  4. Nếu có dữ liệu hợp lệ, service trả dữ liệu từ cache.
  5. Nếu không có dữ liệu, service truy vấn PostgreSQL.
  6. Service lưu kết quả vào Redis với thời hạn phù hợp.
  7. Service trả kết quả về cho client.

Ví dụ:

  • Key: products:10
  • Cache có dữ liệu: trả về dữ liệu từ Redis.
  • Cache không có dữ liệu: truy vấn PostgreSQL, sau đó lưu kết quả vào Redis.

Cache-Aside tương đối dễ triển khai vì ứng dụng kiểm soát việc đọc dữ liệu và quyết định thời điểm ghi cache. Tuy nhiên, chiến lược này cũng đòi hỏi ứng dụng xử lý đúng các tình huống cache hết hạn, dữ liệu thay đổi hoặc Redis gặp sự cố.

4. TTL là gì? Dữ liệu trong cache tồn tại bao lâu?

TTL (Time To Live) là khoảng thời gian một dữ liệu được phép tồn tại trước khi hết hạn.

Khi lưu dữ liệu vào Redis, ứng dụng có thể thiết lập TTL cho key. Khi hết thời hạn, Redis xem key đó là hết hạn và không còn cung cấp nó như một giá trị hợp lệ. Việc giải phóng bộ nhớ có thể diễn ra theo cơ chế hết hạn và thu hồi dữ liệu của Redis.

Ví dụ, thông tin sản phẩm được cache trong 5 phút. Nếu ứng dụng lưu dữ liệu lúc 10:00, key có thời hạn khoảng đến 10:05. Khi key hết hạn, request tiếp theo có thể gặp cache miss và phải lấy dữ liệu mới từ database.

Vì sao cần TTL?

TTL giúp giới hạn thời gian dữ liệu nằm trong cache, tránh việc dữ liệu cũ tồn tại vô thời hạn khi ứng dụng không chủ động xóa hoặc cập nhật cache.

Thời hạn phù hợp phụ thuộc vào loại dữ liệu:

  • Dữ liệu ít thay đổi: có thể dùng TTL dài hơn.
  • Dữ liệu thường xuyên thay đổi: nên dùng TTL ngắn hơn hoặc có cơ chế invalidation phù hợp.
  • Dữ liệu cần tính nhất quán cao: không nên chỉ dựa vào TTL; cần thiết kế cơ chế đọc và ghi phù hợp với yêu cầu nghiệp vụ.

TTL không bảo đảm dữ liệu luôn mới nhất. Nếu database thay đổi ngay sau khi cache được tạo, cache vẫn có thể chứa dữ liệu cũ cho đến khi hết hạn hoặc được cập nhật/xóa chủ động.

5. Cache Invalidation là gì?

Cache Invalidation là quá trình làm cho dữ liệu trong cache không còn được sử dụng khi dữ liệu nguồn thay đổi hoặc khi dữ liệu cache không còn phù hợp.

Đây là một trong những vấn đề quan trọng nhất khi triển khai caching. Đọc cache có thể giúp tăng hiệu năng, nhưng cập nhật cache đúng cách mới giúp hạn chế việc trả dữ liệu lỗi thời.

Giả sử Redis đang lưu sản phẩm có giá 500000. Sau đó, người quản trị cập nhật giá trong PostgreSQL thành 600000. Nếu Redis vẫn giữ giá cũ, API có thể tiếp tục trả về 500000 dù database đã có giá mới.

Có một số cách xử lý phổ biến.

5.1. Xóa cache sau khi cập nhật database

Ứng dụng cập nhật database trước, sau đó xóa cache tương ứng.

Ví dụ:

  1. Cập nhật giá sản phẩm trong PostgreSQL.
  2. Nếu cập nhật thành công, xóa key products:10 trong Redis.
  3. Request tiếp theo không tìm thấy key này, nên truy vấn database và lưu giá mới vào cache.

Đây là cách thường dùng trong mô hình Cache-Aside.

Tuy nhiên, nếu cập nhật database thành công nhưng Redis không thể xóa key do lỗi kết nối, cache cũ có thể tiếp tục được sử dụng cho đến khi hết hạn hoặc được xử lý bằng cơ chế khác. Vì vậy, hệ thống thực tế cần cân nhắc cách xử lý lỗi và khả năng thử lại.

5.2. Cập nhật cache sau khi cập nhật database

Một cách khác là cập nhật database rồi ghi giá trị mới vào Redis.

Cách này có thể giúp những request tiếp theo sử dụng dữ liệu mới ngay khi cập nhật cache thành công. Tuy nhiên, nếu database và Redis không được cập nhật đồng bộ, hai nơi vẫn có thể chứa dữ liệu khác nhau.

Ví dụ, database cập nhật thành công nhưng thao tác ghi Redis thất bại. Khi đó, ứng dụng cần có chiến lược xử lý để tránh tiếp tục trả dữ liệu cũ quá lâu.

5.3. Chỉ dựa vào TTL

Với những dữ liệu không yêu cầu cập nhật tức thời, ứng dụng có thể chấp nhận dữ liệu cache cũ trong một khoảng thời gian ngắn rồi để TTL hết hạn.

Cách này đơn giản, nhưng mức độ chấp nhận dữ liệu cũ phải phù hợp với nghiệp vụ. Chẳng hạn, cache danh sách bài viết có thể chấp nhận một độ trễ cập nhật nhất định, trong khi số dư tài khoản hoặc trạng thái thanh toán thường cần cách xử lý chặt chẽ hơn.

Không có một chiến lược invalidation phù hợp cho mọi loại dữ liệu. Việc lựa chọn phụ thuộc vào tần suất thay đổi, yêu cầu nhất quán và mức độ chấp nhận dữ liệu cũ của ứng dụng.

6. Áp dụng Redis trong Go

Để sử dụng Redis trong ứng dụng Go, có thể dùng thư viện go-redis.

6.1. Cài đặt thư viện

Chạy lệnh:

go get github.com/redis/go-redis/v9

Ngoài thư viện, cần có một Redis server đang chạy và ứng dụng Go phải kết nối được đến server đó.

Ví dụ dưới đây giả định Redis đang chạy trên máy cục bộ tại cổng 6379.

6.2. Kết nối Redis và lưu dữ liệu

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func main() {
	ctx, cancel := context.WithTimeout(
		context.Background(),
		3*time.Second,
	)
	defer cancel()

	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
	defer rdb.Close()

	if err := rdb.Ping(ctx).Err(); err != nil {
		panic(err)
	}

	err := rdb.Set(
		ctx,
		"products:count",
		"120",
		time.Minute,
	).Err()
	if err != nil {
		panic(err)
	}

	fmt.Println("Saved product count to Redis")
}

Trong ví dụ này:

  • redis.NewClient() tạo Redis client.
  • Addr chỉ định địa chỉ Redis server.
  • Ping() kiểm tra khả năng kết nối.
  • Set() lưu giá trị 120 với key products:count.
  • time.Minute thiết lập TTL một phút.
  • defer rdb.Close() đóng client khi chương trình kết thúc.

context.WithTimeout() giới hạn thời gian cho các thao tác sử dụng ctx trong ví dụ. Trong ứng dụng thực tế, nên cân nhắc cách xử lý lỗi kết nối thay vì dùng panic cho mọi trường hợp.

6.3. Đọc dữ liệu từ Redis

Có thể lấy dữ liệu đã lưu bằng Get():

value, err := rdb.Get(ctx, "products:count").Result()
if err != nil {
	if err == redis.Nil {
		fmt.Println("Cache miss: key does not exist")
		return
	}

	fmt.Println("Redis error:", err)
	return
}

fmt.Println("Product count:", value)

Trong đó:

  • Get() yêu cầu Redis lấy giá trị theo key.
  • Result() trả về giá trị và lỗi nếu có.
  • redis.Nil cho biết key không tồn tại, thường được xem là cache miss trong trường hợp này.
  • Các lỗi khác có thể liên quan đến kết nối hoặc quá trình thực hiện lệnh.

Lưu ý: ctx và rdb trong đoạn đọc là những biến đã được tạo ở ví dụ kết nối. Nếu muốn chạy đoạn mã độc lập, cần đưa nó vào một chương trình hoàn chỉnh và khởi tạo các biến tương ứng.

6.4. Xóa cache

Khi dữ liệu sản phẩm thay đổi, ứng dụng có thể xóa key cache tương ứng:

err := rdb.Del(ctx, "products:10").Err()
if err != nil {
	fmt.Println("Failed to invalidate cache:", err)
	return
}

Sau khi xóa thành công, request tiếp theo truy vấn products:10 sẽ không tìm thấy key trong Redis. Với chiến lược Cache-Aside, ứng dụng có thể lấy dữ liệu mới từ PostgreSQL rồi lưu lại vào cache.

Trong ứng dụng thực tế, những thao tác đọc, ghi và xóa cache nên được tổ chức trong service hoặc một thành phần chuyên trách, thay vì đặt toàn bộ logic Redis trực tiếp trong HTTP handler.

7. Ưu điểm, hạn chế và những vấn đề thường gặp

7.1. Ưu điểm của caching

Giảm độ trễ

Dữ liệu có thể được lấy từ cache nhanh hơn so với việc thực hiện lại truy vấn database hoặc tính toán tốn kém.

Giảm tải cho database

Những request lặp lại có thể được phục vụ từ cache, giúp giảm số truy vấn và tài nguyên tiêu thụ tại database.

Cải thiện khả năng phục vụ nhiều request

Khi cache hit cao và cache được thiết kế phù hợp, hệ thống có thể xử lý nhiều request đọc hơn với cùng mức tài nguyên database.

7.2. Hạn chế của caching

Dữ liệu có thể lỗi thời

Cache là bản lưu dữ liệu có thể không đồng bộ tức thời với nguồn dữ liệu chính. Ứng dụng phải xác định mức độ chấp nhận dữ liệu cũ và lựa chọn chiến lược cập nhật phù hợp.

Tăng độ phức tạp

Ngoài logic database, ứng dụng phải xử lý TTL, invalidation, cache miss, lỗi kết nối và các tình huống dữ liệu không nhất quán.

Tốn thêm bộ nhớ và chi phí vận hành

Redis cần tài nguyên bộ nhớ và công tác quản lý riêng. Nếu lưu quá nhiều dữ liệu hoặc đặt TTL không phù hợp, bộ nhớ có thể tăng cao.

Không phải mọi dữ liệu đều phù hợp để cache

Những dữ liệu hiếm khi được đọc lại hoặc thay đổi liên tục có thể không mang lại nhiều lợi ích khi cache. Việc thêm cache cũng không bảo đảm ứng dụng nhanh hơn nếu điểm nghẽn thực tế nằm ở nơi khác.

7.3. Cache Stampede

Cache Stampede xảy ra khi một key cache hết hạn hoặc bị mất, khiến nhiều request đồng thời cùng truy vấn database để lấy lại dữ liệu.

Ví dụ, một sản phẩm phổ biến có hàng nghìn request mỗi phút. Khi cache của sản phẩm hết hạn, nhiều request cùng lúc có thể gặp cache miss và cùng truy vấn PostgreSQL. Điều này tạo ra một đợt tải tăng đột biến đúng lúc hệ thống đang cần khôi phục cache.

Một số cách giảm nguy cơ này:

  • Dùng cơ chế khóa hoặc singleflight để phối hợp việc nạp lại cùng một key.
  • Thêm độ lệch ngẫu nhiên vào TTL để nhiều key không hết hạn cùng lúc.
  • Làm mới cache trước thời điểm hết hạn đối với dữ liệu quan trọng hoặc được truy cập nhiều.
  • Giới hạn tải và thời gian truy vấn database.

Không phải ứng dụng nào cũng cần triển khai tất cả các biện pháp trên. Nên lựa chọn dựa trên lưu lượng thực tế và mức độ nghiêm trọng khi nhiều request cùng tải lại một dữ liệu.

7.4. Cache Miss quá thường xuyên

Nếu dữ liệu liên tục không có trong cache, ứng dụng vẫn phải truy vấn database thường xuyên. Khi đó, chi phí vận hành cache có thể không mang lại lợi ích đáng kể.

Nguyên nhân có thể là TTL quá ngắn, key được thiết kế không nhất quán, dữ liệu có quá nhiều biến thể hoặc tập dữ liệu ít được truy cập lặp lại.

Để cải thiện, cần theo dõi tỷ lệ cache hit/miss, đánh giá mẫu truy cập và chọn dữ liệu có khả năng được tái sử dụng cao.

7.5. Quản lý bộ nhớ

Redis lưu dữ liệu trong bộ nhớ để hỗ trợ truy cập nhanh, vì vậy cần cân nhắc kích thước dữ liệu, số lượng key và chính sách hết hạn hoặc thu hồi dữ liệu.

Nếu bộ nhớ bị sử dụng quá mức, Redis có thể không thể lưu thêm key mới hoặc phải loại bỏ dữ liệu theo chính sách được cấu hình. Do đó, cần lựa chọn TTL, chính sách eviction và giới hạn tài nguyên phù hợp với ứng dụng.

Ngoài ra, không nên mặc định lưu mọi dữ liệu vào Redis. Cache nên ưu tiên những dữ liệu có giá trị tái sử dụng và có thể được tái tạo từ nguồn dữ liệu đáng tin cậy.

8. Kết luận — Khi nào nên sử dụng Redis và caching?

Caching là kỹ thuật giúp giảm thời gian truy xuất dữ liệu và hạn chế việc lặp lại những truy vấn hoặc phép tính tốn kém. Redis là một công cụ phổ biến để triển khai caching nhờ khả năng truy cập dữ liệu nhanh, hỗ trợ nhiều cấu trúc dữ liệu và cho phép nhiều tiến trình cùng sử dụng một kho dữ liệu.

Tuy nhiên, caching không phải giải pháp mặc định cho mọi vấn đề hiệu năng. Cache làm tăng độ phức tạp trong việc quản lý dữ liệu, xử lý hết hạn và bảo đảm tính nhất quán giữa cache với database.

Nên cân nhắc sử dụng Redis khi:

  • Một lượng lớn request thường xuyên đọc lại cùng dữ liệu.
  • Truy vấn database hoặc tính toán tạo ra chi phí đáng kể.
  • Dữ liệu có thể chấp nhận một mức độ trễ cập nhật phù hợp với nghiệp vụ.
  • Hệ thống cần một kho dữ liệu dùng chung cho nhiều instance ứng dụng.

Trước khi thêm cache, nên đo lường hiệu năng và xác định điểm nghẽn thực sự. Sau khi triển khai, cần theo dõi cache hit/miss, độ trễ, mức sử dụng bộ nhớ và cách hệ thống phản ứng khi Redis không khả dụng.

Điều quan trọng không phải là sử dụng Redis trong mọi ứng dụng, mà là hiểu dữ liệu nào nên được cache, cache trong bao lâu và làm thế nào để xử lý khi dữ liệu thay đổi.

Nguồn tham khảo

Bài viết khác

Database Migration là gì?

1. Database Migration là gì? Database Migration là quá trình quản lý và áp dụng những thay đổi đối với cấu trúc hoặc dữ liệu trong cơ sở dữ liệu theo từng phiên bản của ứng dụng. Trong quá trình phát triển phần mềm, yêu cầu nghiệp vụ thường xuyên thay đổi. Một bảng có […]

Severity vs Priority(Độ nghiêm trọng vs Mức độ ưu tiên)

1.Severity và Priority là gì? Phân biệt ra sao? Trong bức tranh toàn cảnh của Vòng đời kiểm thử phần mềm (Software Testing Lifecycle – STLC), việc phát hiện ra lỗi (Bug/Defect) mới chỉ đóng góp 50% vào thành công của quy trình đảm bảo chất lượng. 50% còn lại phụ thuộc hoàn toàn vào […]

Bug Lifecycle & Bug Report

1.Bug Lifecycle & Bug Report là gì? Trong quản lý chất lượng phần mềm, Bug Lifecycle (Vòng đời của lỗi) và Bug Report (Báo cáo lỗi) là hai khái niệm cực kỳ quan trọng giúp kiểm thử viên (Tester) và lập trình viên (Developer) trao đổi, theo dõi và sửa lỗi một cách chuyên nghiệp. […]

Bun ORM là gì?

Bun ORM là gì? Hướng dẫn sử dụng Bun ORM với Go và PostgreSQL 1. Bun ORM là gì? Bun là một thư viện ORM dành cho ngôn ngữ Go, được thiết kế theo hướng SQL-first. Thay vì cố gắng che giấu hoàn toàn SQL phía sau các phương thức trừu tượng, Bun cho phép […]

Positive vs Negative Testing(Kiểm thử tích cực và Kiểm thử tiêu cực)

1.Positive vs Negative Testing là gì? Trong ngành công nghệ phần mềm, chất lượng sản phẩm không bao giờ là một khái niệm đơn chiều. Một ứng dụng được lập trình hoàn hảo trên lý thuyết vẫn có thể sụp đổ hoàn toàn ngay trong ngày đầu tiên phát hành nếu nó chỉ được chuẩn […]

Eloquent ORM

Eloquent ORM là gì? Hướng dẫn sử dụng Eloquent trong Laravel 1. Eloquent ORM là gì? Eloquent ORM là hệ thống Object-Relational Mapping (ORM) được tích hợp trong Laravel, một framework phát triển ứng dụng Web bằng PHP. Eloquent cho phép lập trình viên tương tác với cơ sở dữ liệu thông qua các class […]

Leave a Reply

Your email address will not be published. Required fields are marked *