Skip to content
Thuan Bui
Go back

[Astro's Comment] Phần 2 - Tối ưu comment với KV Cache và D1 Index

Trong Phần 1, mình đã xây dựng nền tảng cơ bản cho hệ thống comment: database, API và UI. Hệ thống hiện tại đã hoạt động tốt, nhưng chưa thực sự tối ưu ở phần hiệu năng hoạt động.

Trong Phần 2 hôm nay, mình sẽ chia sẻ cách tối ưu hệ thống comment bằng hai kỹ thuật:

I. Vấn đề hiệu năng hiện tại

Theo thiết lập hiện tại, mỗi lần bạn đọc truy cập vào một bài viết, và kéo đến phần bình luận dưới cuối bài, hệ thống sẽ thực hiện một API request:

GET /api/comments/{slug}

Request này sẽ truy vấn trực tiếp vào D1 để lấy toàn bộ comment của bài viết hiện tại

SELECT id, author_name, author_url, content, createdAt, parent_id
FROM comments
WHERE post_slug = 'ten-bai-viet' AND status = 1
ORDER BY created_at DESC

Giả sử bài viết có 100 comment và một ngày có 10000 lượt xem, thì D1 sẽ phải xử lý 10000 query, mỗi query trả về 100 dòng dữ liệu. Tổng cộng là 1 triệu row reads/ngày (chiếm ~20% quota miễn phí của D1).

Điều quan trọng là: 10000 query này đều trả về cùng một kết quả — danh sách comment không thay đổi giữa các lần truy vấn.

Chúng ta không cần truy vấn database lặp đi lặp lại như vậy. Thay vào đó có thể tối ưu bằng cách cache kết quả ở lần truy vấn đầu tiên, các lần sau chỉ cần đọc từ cache. Vừa nhanh hơn vì không phải truy vấn vào database vừa không tốn quote row read của D1.

Cache chỉ cần làm mới khi có thay đổi (ví dụ: admin duyệt, xóa, trả lời comment). Nhờ đó, giảm tải cho database và tăng tốc độ phản hồi cho người dùng.

Giới hạn miễn phí của D1: Cloudflare D1 cung cấp 5 triệu row reads miễn phí mỗi ngày. Với đa số blog cá nhân, con số này là hoàn toàn dư dả. Nhưng nếu blog phát triển nhanh với nhiều bài viết và nhiều comment, quota miễn phí này sẽ nhanh chóng bị cạn kiệt nếu không được tối ưu.

II. Giải pháp: sư dụng Cloudflare KV Cache

Cloudflare KV là một hệ thống lưu trữ key-value toàn cầu, cực kỳ nhanh, được phân tán tới hàng trăm edge server trên thế giới. Đọc dữ liệu từ KV chỉ mất khoảng 1-2ms, nhanh hơn rất nhiều so với truy vấn D1 (10-20ms) và không tốn quota database.

Quy trình hoạt động sau khi áp dụng cache:

Bạn đọc truy cập bài viết
→ Trình duyệt gửi request GET /api/comments/ten-bai-viet
→ Kiểm tra KV có cache không?
→ Nếu CÓ: trả về ngay, không cần query D1
→ Nếu KHÔNG: query D1, lưu kết quả vào KV, trả về lại trình duyệt

Cache sẽ tồn tại cho đến khi có thay đổi liên quan đến comment (ví dụ: admin duyệt, xóa, trả lời comment). Khi đó, cache sẽ bị xóa và lần truy cập tiếp theo sẽ tự động tạo lại cache mới.

Giới hạn miễn phí của Cloudflare KV

Loại thao tácGiới hạn miễn phí mỗi ngày
Đọc (Reads)100,000
Ghi (Writes)1,000
Xóa (Deletes)1,000
Dung lượng1 GB

Với blog cá nhân, các giới hạn này là quá dư dả. Việc ghi/xóa KV chỉ diễn ra khi có comment mới được duyệt, xóa hoặc trả lời.

III. Tối ưu thêm: Tạo index cho bảng comments

Bên cạnh việc cache, mình tạo thêm index tổng hợp (composite index) cho bảng comments để tăng tốc độ truy vấn khi cache bị miss.

-- Index for API public: filter by post_slug
CREATE INDEX idx_comments_slug_status_created
ON comments (post_slug, status, created_at);

IV. Cài đặt Cloudflare KV

1. Tạo KV Namespace trên Cloudflare

Chạy lệnh sau để tạo namespace KV mới:

Terminal window
pnpm wrangler kv namespace create blog-thuanbui-comment-cache

Sau khi chạy, bạn sẽ nhận được ID của namespace. Hãy lưu lại để dùng ở bước tiếp theo.

2. Thêm KV namespace vào file cấu hình wrangler.jsonc

wrangler.jsonc
{
"d1_databases": [
{
"binding": "COMMENTS_DB",
"database_name": "blog-thuanbui-comments",
"database_id": "7ffe7eee-9d2b-4958-befd-2404d710d903",
},
],
"kv_namespaces": [
{
"binding": "COMMENTS_CACHE",
"id": "ae53fae9a47a4054811ffb98f6f34656", // ID từ bước 1
},
],
}

Lưu ý: binding là tên dùng trong code (env.COMMENTS_CACHE). id là ID thực trên Cloudflare. Tên namespace (blog-thuanbui-comment-cache) chỉ để dễ nhận diện trên dashboard.

3. Generate types từ wrangler config

Terminal window
pnpm wrangler types

Lệnh này sẽ cập nhật file worker-configuration.d.ts ở root project — cần thiết để Cloudflare Workers build thành công. Nếu file chưa có COMMENTS_CACHE trong Env, hãy thêm thủ công:

worker-configuration.d.ts
declare namespace Cloudflare {
interface Env {
COMMENTS_DB: D1Database;
COMMENTS_CACHE: KVNamespace; // thêm dòng này
}
}

4. Thêm index vào schema

Cập nhật src/db/schema.ts — thêm composite index cho bảng comments:

src/db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const comments = sqliteTable(
"comments",
{
// ... existing columns ...
},
table => [
index("idx_comments_slug_status_created").on(
table.postSlug,
table.status,
table.createdAt
),
]
);

Chạy lệnh generate để tạo file migration mới:

Terminal window
pnpm db:generate

Lệnh này sẽ tạo ra file migration mới trong thư mục migrations/ với index tương ứng.

migrations/0001_add_comments_indexes.sql
CREATE INDEX `idx_comments_slug_status_created` ON `comments` (`post_slug`, `status`, `created_at`);

V. Cập nhật Comment API

1. Thêm logic cache vào API GET comment

Mở file src/pages/api/comments/[slug].ts và cập nhật như sau:

src/pages/api/comments/[slug].ts
// ─── GET: Fetch comments for a post ───────────────────────────────────────
export async function GET({ params }: APIContext) {
const { slug } = params;
if (!slug) {
return new Response(JSON.stringify({ error: "Missing slug" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const cacheKey = `comments:${slug}`;
// Check KV cache first — permanent until invalidated on approve/delete
try {
const cached = await env.COMMENTS_CACHE.get(cacheKey);
if (cached) {
return new Response(cached, {
status: 200,
headers: {
"Content-Type": "application/json",
"X-Cache": "HIT",
},
});
}
} catch {
// KV unavailable (e.g. local dev without binding) — fall through to D1
}
try {
const db = drizzle(env.COMMENTS_DB, { schema: { comments } });
const result = await db
.select({ id, authorName, authorUrl, content, createdAt, parentId })
.from(comments)
.where(and(eq(comments.postSlug, slug), eq(comments.status, 1)))
.orderBy(desc(comments.createdAt));
return new Response(JSON.stringify({ comments: result }), {
status: 200,
});
const body = JSON.stringify({ comments: result });
await env.COMMENTS_CACHE.put(cacheKey, body).catch(() => {});
return new Response(body, {
status: 200,
headers: {
"Content-Type": "application/json",
"X-Cache": "MISS",
},
});
} catch (error) {
return new Response(JSON.stringify({ error: "Database error" }), {
status: 500,
});
}
}

Giải thích nhanh:

2. Xóa cache khi có thay đổi comment

Trong endpoint POST — khi email nằm trong danh sách đã được duyệt trước đó (trusted emails), comment sẽ được tự động được duyệt, cần xóa cache ngay để comment mới hiển thị ngay lập tức mà không cần refresh:

Trong src/pages/api/comments/[slug].ts, thêm vào sau phần insert comment:

src/pages/api/comments/[slug].ts
// ── Insert comment ───────────────────────────────────────────────────
await db
.insert(comments)
.values({
postSlug: slug,
// ...
})
.returning();
if (isTrusted) {
await env.COMMENTS_CACHE.delete(`comments:${slug}`).catch(() => {});
}

Ngoài ra, bất kỳ API endpoint nào làm thay đổi danh sách comment hiển thị (duyệt, xóa, trả lời) đều cần xóa cache tương ứng:

// Sau khi thay đổi comment của một bài viết
await env.COMMENTS_CACHE.delete(`comments:${postSlug}`);

Phần này sẽ được áp dụng khi thiết lập trang dashboard để duyệt / xoá / trả lời comment.

VI. Cập nhật database và kiểm tra trên local

1. Cập nhật database

Terminal window
pnpm db:migrate:local

2. Kiểm tra index đã tạo thành công

Chạy lẹện sau dể kiểm tra index

Terminal window
pnpm wrangler d1 execute blog-thuanbui-comments \
--local \
--command="SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='comments'"

Kết quả hiện ra như bên dưới là ổn

┌──────────────────────────────────────┐
│ name │
├──────────────────────────────────────┤
│ idx_comments_slug_status_created │
└──────────────────────────────────────┘

3. Khởi động dev server

Terminal window
pnpm run dev

4. Kiểm tra trong trình duyệt

  1. Mở một bài viết có comment, ví dụ: http://localhost:4321/cai-dat-wyze-bridge/
  2. Mở DevTools → tab Network
  3. Filter theo api/comments
  4. Click vào request, trong phần Header, kéo xuống phần Response Headers
  5. Kiểm tra header x-cache:
Lần truy cậpx-cacheÝ nghĩa
Lần đầu tiênMISSTruy vấn D1, lưu vào KV
Từ lần 2 (F5)HITĐọc từ KV, không truy vấn D1

5. Kiểm tra KV bằng dòng lệnh

Terminal window
# Xem danh sách keys
pnpm wrangler kv key list --local --binding=COMMENTS_CACHE
# Xem nội dung một key
pnpm wrangler kv key get "comments:cai-dat-wyze-bridge" \
--local --binding=COMMENTS_CACHE

VII. Áp dụng lên production

Sau khi thử nghiệm thành công trên local, mình sẽ áp dụng lên production (Cloudflare KV, D1)

1. Apply migration cho database

Terminal window
pnpm db:migrate:prod

Tiêp theo, commit code và push lên GitHub, Cloudflare Worker sẽ tự động build và deploy.

2. Kiểm tra cache trên production

Tương tự như local — mở DevTools trên production site, kiểm tra x-cache header trên request /api/comments/....

Ngoài ra có thể xem KV keys trên Cloudflare Dashboard: Storage & databasesWorkers KV → Chọn blog-thuanbui-comment-cache → KV Pairs

VIII. Tổng kết

Trong phần này, mình đã chia sẻ cách tối ưu hệ thống comment bằng hai kỹ thuật:

Nhờ áp dụng cache, hiệu năng hoạt động của blog đã đuợc cải thiện đáng kể

Cache sẽ tồn tại cho đến khi có thay đổi comment, đảm bảo danh sách luôn được cập nhật mới mà vẫn tối ưu hiệu năng.

Mình áp dụng comment cache cho blog từ ngày 10/04, có thể thấy ngay sau đó số lượng query read đã giảm hẳn so với trước đó

Trong Phần 3, mình sẽ chia sẻ cách xây dựng Admin Dashboard để quản lý comment (duyệt / xoá / trả lời). Hẹn gặp lại bạn trong phần tiếp theo!


Series:
Share this post on:

Previous Post
[Astro's Comment] Phần 3 - Xây dựng Admin Dashboard để quản lý comment
Next Post
[Astro's Comment] Phần 1 - Xây dựng hệ thống comment với Cloudflare D1 + Drizzle
Loading...
Loading comments...