Cách đây không lâu, chúng tôi đã có cơ hội hợp tác với Drizzle ORM.
Nhận thấy ORM TypeScript này được cộng đồng vô cùng yêu thích nên quyết định nói "có 😳" của chúng tôi là điều hiển nhiên:

Trong bài viết này, chúng ta sẽ xem cách tích hợp bộ nhớ đệm Upstash Redis x Drizzle cải thiện hiệu suất SQL cũng như cách chúng tôi sử dụng tập lệnh Lua và cấu trúc dữ liệu băm để tối ưu hóa việc tích hợp.
Thách thức:Hiệu suất SQL trong các ứng dụng hiện đại
Cơ sở dữ liệu SQL truyền thống có khả năng nhất quán và mô hình hóa các mối quan hệ phức tạp rất tốt, nhưng chúng có thể gặp khó khăn với:
- Độ trễ cao trong môi trường phân tán
- Hạn chế tổng hợp kết nối trong các chức năng không có máy chủ
- Chi phí truy vấn lặp lại cho dữ liệu được truy cập thường xuyên
- Nút thắt cổ chai khi mở rộng quy mô dưới tải đọc nặng
Giải pháp? Lớp bộ nhớ đệm hiểu được mối quan hệ dữ liệu của bạn và tự động quản lý việc vô hiệu hóa bộ nhớ đệm.
Cách thức hoạt động của bộ nhớ đệm Upstash x Drizzle
Cải thiện hiệu suất đọc:Cache-First với dự phòng
Khi bạn thực hiện một truy vấn với bộ đệm ẩn Drizzle được bật, trước tiên, quá trình tích hợp của chúng tôi sẽ kiểm tra Redis để tìm kết quả được lưu trong bộ nhớ đệm:
- Lỗi bộ nhớ đệm :Nếu không tìm thấy, truy vấn sẽ đọc từ cơ sở dữ liệu của bạn. Kết quả được lưu trữ trong Redis với siêu dữ liệu về các bảng phụ thuộc
- Lần truy cập bộ nhớ đệm :Các truy vấn giống hệt tiếp theo trả về ngay lập tức từ Redis mà không cần phải đọc từ cơ sở dữ liệu quan hệ
// This query checks Redis first and only reads from the database if needed
const users = await db.select().from(usersTable)
.where(eq(usersTable.status, 'active'))
.$withCache(); Vô hiệu hóa thông minh cho hoạt động ghi
Điều kỳ diệu xảy ra trong quá trình thao tác ghi. Khi bạn sửa đổi dữ liệu trong cơ sở dữ liệu quan hệ của mình, quá trình tích hợp của chúng tôi sẽ tự động:
- Xác định các yếu tố phụ thuộc :Xác định những truy vấn được lưu trong bộ nhớ đệm nào phụ thuộc vào các bảng đã sửa đổi
- Vô hiệu hóa hàng loạt :Xóa tất cả các mục bộ đệm bị ảnh hưởng
// This insert automatically invalidates all cached queries that depend on usersTable
await db.insert(usersTable).values({
email: 'new@user.com',
status: 'active'
}); Xây dựng bộ nhớ đệm đơn giản:Cách tiếp cận "ngây thơ"
Hãy bắt đầu với cách triển khai đơn giản nhất có thể để hiểu các vấn đề mà việc tích hợp bộ nhớ đệm của chúng tôi giải quyết. Khi lưu vào bộ nhớ đệm một kết quả truy vấn, chúng ta cần:
- Lưu trữ giá trị được lưu trong bộ nhớ cache
- Theo dõi bảng nào mà truy vấn này phụ thuộc vào việc vô hiệu
Bộ nhớ đệm đơn giản
// When adding an item to the cache
await redis.set(itemHash, cachedValue);
await Promise.all(
dependentTables.map((table) => redis.sadd(table, itemHash))
); Cách tiếp cận này lưu trữ kết quả được lưu trong bộ nhớ đệm dưới dạng cặp khóa-giá trị và theo dõi các phần phụ thuộc bằng cách thêm hàm băm mục vào các bộ được đặt tên theo từng bảng phụ thuộc.
Vô hiệu hóa bộ đệm đơn giản
// When invalidating based on table changes
const hashesToInvalidate = await redis.sunion(dependentTables);
await redis.del(...hashesToInvalidate); Tính năng này hoạt động bằng cách tìm tất cả các mục được lưu trong bộ nhớ đệm phụ thuộc vào các bảng đã sửa đổi, sau đó xóa chúng.
Các vấn đề với cách tiếp cận "ngây thơ" này
Mặc dù về mặt kỹ thuật nó hoạt động nhưng cách triển khai đơn giản này có hai vấn đề về hiệu suất.
Vấn đề 1:Nhiều chuyến khứ hồi
Quá trình vô hiệu hóa yêu cầu hai thao tác Redis riêng biệt :
- Đầu tiên, chúng tôi gọi
SUNIONđể lấy danh sách các phím cần xóa - Sau đó, chúng tôi gọi
DELvới kết quả từ bước 1
Điều này tạo ra sự phụ thuộc vào chuyến đi khứ hồi trong đó thao tác thứ hai phải đợi thao tác đầu tiên hoàn thành.
Vấn đề 2:Xóa hàng loạt chậm
DEL lệnh có thể trở thành nút cổ chai khi vô hiệu hóa nhiều khóa:
// This could potentially delete thousands of keys
await redis.del(...hashesToInvalidate);
Khi bạn có một bảng phổ biến như users được tham chiếu bởi hàng trăm truy vấn được lưu trong bộ nhớ đệm, một bản cập nhật duy nhất có thể kích hoạt việc xóa hàng trăm khóa Redis riêng lẻ. Với hàng trăm hoặc hàng nghìn khóa, quá trình này có thể trở nên quá chậm.
Giải pháp 1:Tập lệnh Lua
Upstash Redis hỗ trợ đầy đủ cho việc đánh giá tập lệnh Lua.
Tập lệnh Lua giải quyết vấn đề khứ hồi bằng cách thực thi nhiều lệnh Redis ở phía máy chủ:
-- Invalidation script that combines SUNION and DEL
local tables = KEYS -- table names passed as keys
local keysToDelete = {}
if #tables > 0 then
-- Get all hashes that depend on these tables
local hashesToInvalidate = redis.call('SUNION', unpack(tables))
-- Prepare for deletion
for _, hash in ipairs(hashesToInvalidate) do
keysToDelete[#keysToDelete + 1] = hash
end
-- Add table sets themselves to deletion list
for _, table in ipairs(tables) do
keysToDelete[#keysToDelete + 1] = table
end
-- Single atomic deletion
if #keysToDelete > 0 then
redis.call('DEL', unpack(keysToDelete))
end
end Lợi ích của tập lệnh Lua:
- Chuyến đi khứ hồi một lần :Tất cả các hoạt động xảy ra phía máy chủ
- Giảm độ trễ :Không có chi phí mạng giữa các hoạt động
- Tính nhất quán :Không có nguy cơ cập nhật một phần do sự cố mạng
Giải pháp 2:Lưu trữ dựa trên hàm băm để xóa hiệu quả
Ngay cả với tập lệnh Lua, việc xóa hàng trăm khóa riêng lẻ có thể chậm hơn chúng ta mong muốn. Băm Redis cung cấp giải pháp hiệu quả hơn nhiều:
Phương pháp tiếp cận dựa trên hàm băm
Thay vì lưu trữ từng truy vấn được lưu trong bộ nhớ đệm dưới dạng khóa Redis riêng biệt, chúng tôi nhóm các truy vấn tùy thuộc vào cùng một bảng thành các giá trị băm:
// Old approach: Each query gets its own key
await redis.set('query_hash_1', result1);
await redis.set('query_hash_2', result2);
await redis.set('query_hash_3', result3);
// New approach: Group queries by table dependencies
const compositeKey = 'users,posts'; // hash key for users and posts tables
await redis.hset(compositeKey, {
'query_hash_1': result1,
'query_hash_2': result2,
'query_hash_3': result3
}); Tại sao băm lại nhanh hơn nhiều
Khi vô hiệu hóa các truy vấn phụ thuộc vào users bảng:
// Old way: Delete many individual keys (slow)
await redis.del('query_hash_1', 'query_hash_2', /* ...hundreds more... */);
// New way: Delete entire hash table (fast)
await redis.del('__CT__users,posts'); Lợi thế về hiệu suất:
- Thao tác xóa một lần :Một
DELlệnh xóa hàng trăm truy vấn được lưu trong bộ nhớ đệm - Hiệu suất bộ nhớ :Redis có thể giải phóng toàn bộ bảng băm trong một thao tác
- Dọn dẹp nguyên tử :Tất cả các truy vấn liên quan đều bị vô hiệu cùng nhau
Để xem cuối cùng tập lệnh Lua trông như thế nào, bạn có thể kiểm tra quá trình triển khai trong kho Drizzle.
Thẻ bộ đệm để kiểm soát chi tiết
Ngoài tính năng vô hiệu hóa dựa trên bảng, Drizzle còn hỗ trợ các thẻ tùy chỉnh để kiểm soát bộ nhớ đệm chi tiết:
// Cache with a custom tag
const premiumUsers = await db.select().from(usersTable)
.where(eq(usersTable.plan, 'premium'))
.$withCache({ tag: 'premium_users' });
// Later, invalidate just this specific query
await db.$cache?.invalidate({ tags: 'premium_users' }); Vô hiệu hóa tự động và thủ công
Tự động vô hiệu hóa (mặc định):Các truy vấn bị vô hiệu khi các bảng phụ thuộc thay đổi, đảm bảo tính nhất quán của dữ liệu nhưng phải xóa bộ nhớ đệm tích cực hơn.
Vô hiệu hóa thủ công :Đối với các trường hợp có thể chấp nhận được tính nhất quán cuối cùng, bạn có thể tắt tính năng tự động vô hiệu hóa và kiểm soát thủ công thời điểm xóa bộ nhớ đệm:
// Won't be automatically invalidated - good for analytics data
const monthlyStats = await db.select()
.from(analyticsTable)
.$withCache({ autoInvalidate: false });
// Manually invalidate when needed (e.g., daily batch job)
await db.$cache?.invalidate({ tables: ['analyticsTable'] }); Các trường hợp sử dụng trong thế giới thực
Bây giờ chúng ta đã đề cập đến khía cạnh kỹ thuật của việc tích hợp, hãy xem những khái niệm này chuyển thành ứng dụng thực tế như thế nào.
Danh mục sản phẩm thương mại điện tử
// Cache product listings with automatic invalidation
const products = await db.select()
.from(productsTable)
.where(eq(productsTable.active, true))
.$withCache({ tag: 'active_products' });
// When inventory changes, cache is automatically invalidated
await db.update(productsTable)
.set({ stock: newStock })
.where(eq(productsTable.id, productId)); Quản lý nội dung
// Cache published articles with manual invalidation
const articles = await db.select()
.from(articlesTable)
.where(eq(articlesTable.status, 'published'))
.$withCache({
autoInvalidate: false,
tag: 'published_articles'
});
// Manually invalidate when content is updated
await db.$cache?.invalidate({ tags: 'published_articles' }); Kết luận
Việc tích hợp bộ nhớ đệm Upstash Redis &Drizzle có thể cải thiện đáng kể hiệu suất truy vấn SQL và giảm tải cơ sở dữ liệu với những thay đổi mã tối thiểu (khá).
Khi bật bộ đệm, bạn có thể mong đợi:
- Nhanh hơn đáng kể thời gian phản hồi truy vấn cho dữ liệu được lưu trong bộ nhớ đệm
- Giảm tải cơ sở dữ liệu và cải thiện khả năng mở rộng
Khả năng phân phối toàn cầu và kiến trúc serverless đầu tiên của Upstash Redis với mức giá trả theo mức sử dụng là nền tảng tuyệt vời cho các ứng dụng hiện đại.
Hoàn hảo cho nền tảng thương mại điện tử, trang tổng quan phân tích, hệ thống quản lý nội dung, v.v.
Đọc thêm
Bạn muốn lặn sâu hơn? Dưới đây là một số tài nguyên tuyệt vời mà tôi khuyên bạn nên xem:
- Hướng dẫn tích hợp Upstash Redis và Drizzle
- Tài liệu về bộ nhớ đệm mưa phùn
- Bắt đầu với Upstash Redis
- SDK giới hạn tốc độ tải lên (TypeScript) - Một SDK mạnh mẽ khác tận dụng tập lệnh Lua để có hiệu suất tối ưu
- SDK giới hạn tốc độ tải lên (Python) - Triển khai SDK giới hạn tốc độ bằng Python.