เมื่อไม่นานมานี้ เรามีโอกาสได้ร่วมงานกับ Drizzle ORM
เมื่อเห็นว่า TypeScript ORM นี้เป็นที่ชื่นชอบของชุมชนอย่างไร การตัดสินใจของเราที่จะพูดว่า "ใช่ 😂" ก็ไม่ใช่เรื่องง่าย:

ในบทความนี้ เราจะดูว่าการผสานรวมแคช Upstash Redis x Drizzle ปรับปรุงประสิทธิภาพของ SQL ได้อย่างไร และวิธีที่เราใช้สคริปต์ Lua และโครงสร้างข้อมูลแฮชเพื่อเพิ่มประสิทธิภาพการผสานรวม
ความท้าทาย:ประสิทธิภาพของ SQL ในแอปพลิเคชันสมัยใหม่
ฐานข้อมูล SQL แบบเดิมมีความสม่ำเสมอและสร้างแบบจำลองความสัมพันธ์ที่ซับซ้อนได้ดี แต่อาจประสบปัญหา:
- เวลาแฝงสูง ในสภาพแวดล้อมแบบกระจาย
- ข้อจำกัดการรวมการเชื่อมต่อ ในฟังก์ชันไร้เซิร์ฟเวอร์
- ค่าใช้จ่ายในการค้นหาซ้ำ สำหรับข้อมูลที่เข้าถึงบ่อย
- การปรับขนาดคอขวด ภายใต้ภาระการอ่านจำนวนมาก
วิธีแก้ปัญหา? เลเยอร์แคชที่เข้าใจความสัมพันธ์ของข้อมูลของคุณและจัดการการทำให้แคชใช้ไม่ได้โดยอัตโนมัติ
การทำงานของแคช Upstash x Drizzle
การปรับปรุงประสิทธิภาพการอ่าน:แคชต้องมาก่อนพร้อมทางเลือกสำรอง
เมื่อคุณดำเนินการค้นหาโดยเปิดใช้งานการแคช Drizzle การผสานรวมของเราจะตรวจสอบ Redis เพื่อหาผลลัพธ์ที่แคชไว้ก่อน:
- แคชพลาด :หากไม่พบ คิวรีจะอ่านจากฐานข้อมูลของคุณ ผลลัพธ์จะถูกจัดเก็บไว้ใน Redis พร้อมด้วยข้อมูลเมตาเกี่ยวกับตารางที่ขึ้นต่อกัน
- การเข้าถึงแคช :ข้อความค้นหาที่เหมือนกันตามมาจะส่งกลับทันทีจาก Redis โดยไม่ต้องอ่านจากฐานข้อมูลเชิงสัมพันธ์
// 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(); การยกเลิกอัจฉริยะสำหรับการดำเนินการเขียน
ความมหัศจรรย์นี้เกิดขึ้นระหว่างการดำเนินการเขียน เมื่อคุณแก้ไขข้อมูลในฐานข้อมูลเชิงสัมพันธ์ของคุณ การบูรณาการของเราโดยอัตโนมัติ:
- ระบุการขึ้นต่อกัน :กำหนดว่าแบบสอบถามแคชใดขึ้นอยู่กับตารางที่แก้ไข
- ชุดการใช้งานไม่ถูกต้อง :ลบรายการแคชที่ได้รับผลกระทบทั้งหมด
// This insert automatically invalidates all cached queries that depend on usersTable
await db.insert(usersTable).values({
email: 'new@user.com',
status: 'active'
}); การสร้างแคชอย่างง่าย:แนวทาง "ไร้เดียงสา"
เริ่มจากการใช้งานที่ง่ายที่สุดที่เป็นไปได้เพื่อทำความเข้าใจปัญหาที่การรวมแคชของเราจะแก้ไข เมื่อแคชผลลัพธ์แบบสอบถาม เราจำเป็นต้อง:
- จัดเก็บค่าแคช
- ติดตามว่าตารางใดที่แบบสอบถามนี้ขึ้นอยู่กับเพื่อให้เป็นโมฆะ
พื้นที่จัดเก็บแคชอย่างง่าย
// When adding an item to the cache
await redis.set(itemHash, cachedValue);
await Promise.all(
dependentTables.map((table) => redis.sadd(table, itemHash))
); วิธีการนี้จะจัดเก็บผลลัพธ์ที่แคชไว้เป็นคู่คีย์-ค่า และติดตามการขึ้นต่อกันโดยการเพิ่มแฮชของรายการไปยังชุดที่ตั้งชื่อตามตารางที่ขึ้นอยู่กับแต่ละตาราง
การทำให้แคชใช้ไม่ได้อย่างง่าย
// When invalidating based on table changes
const hashesToInvalidate = await redis.sunion(dependentTables);
await redis.del(...hashesToInvalidate); ซึ่งทำงานโดยการค้นหารายการแคชทั้งหมดที่ขึ้นอยู่กับตารางที่แก้ไข แล้วลบออก
ปัญหาของแนวทาง "ไร้เดียงสา" นี้
แม้ว่าในทางเทคนิคจะใช้งานได้ แต่การใช้งานที่ไร้เดียงสานี้มีปัญหาด้านประสิทธิภาพสองประการ
ปัญหาที่ 1:การเดินทางหลายรอบ
กระบวนการทำให้ใช้งานไม่ได้ต้องมี การดำเนินการ Redis สองรายการแยกกัน :
- ขั้นแรก เราเรียก
09เพื่อรับรายการคีย์ที่จะลบ - จากนั้นเราเรียก
12ด้วยผลลัพธ์จากขั้นตอนที่ 1
สิ่งนี้จะสร้างการขึ้นต่อกันแบบไปกลับ โดยที่การดำเนินการครั้งที่สองต้องรอจนกว่าการดำเนินการแรกจะเสร็จสิ้น
ปัญหาที่ 2:การลบมวลช้า
25รหัส> คำสั่งอาจกลายเป็นคอขวดเมื่อทำให้หลายคีย์ใช้ไม่ได้:
// This could potentially delete thousands of keys
await redis.del(...hashesToInvalidate);
เมื่อคุณมีตารางยอดนิยมเช่น 31 ที่ถูกอ้างอิงโดยการค้นหาที่แคชไว้หลายร้อยรายการ การอัปเดตเพียงครั้งเดียวสามารถกระตุ้นให้มีการลบคีย์ Redis หลายร้อยคีย์ ด้วยคีย์หลายร้อยหรือหลายพันคีย์ การดำเนินการนี้อาจช้าเกินไป
โซลูชันที่ 1:สคริปต์ Lua
Upstash Redis รองรับการประเมินสคริปต์ Lua อย่างเต็มรูปแบบ
สคริปต์ Lua แก้ปัญหาไปกลับโดยดำเนินการคำสั่ง Redis หลายคำสั่งบนฝั่งเซิร์ฟเวอร์:
-- 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 ประโยชน์ของสคริปต์ Lua: ป>
- ไป-กลับเดี่ยว :การดำเนินการทั้งหมดเกิดขึ้นฝั่งเซิร์ฟเวอร์
- เวลาแฝงลดลง :ไม่มีค่าใช้จ่ายเครือข่ายระหว่างการดำเนินงาน
- ความสม่ำเสมอ :ไม่มีความเสี่ยงในการอัปเดตบางส่วนเนื่องจากปัญหาเครือข่าย
โซลูชันที่ 2:พื้นที่เก็บข้อมูลแบบแฮชเพื่อการลบที่มีประสิทธิภาพ
แม้ว่าจะใช้สคริปต์ Lua การลบคีย์เดี่ยวหลายร้อยคีย์ก็อาจจะช้ากว่าที่เราต้องการ แฮช Redis มอบโซลูชันที่มีประสิทธิภาพมากกว่ามาก:
แนวทางที่ใช้แฮช
แทนที่จะจัดเก็บแต่ละคำค้นหาที่แคชไว้เป็นคีย์ Redis แยกต่างหาก เราจะจัดกลุ่มคำค้นหาตามตารางเดียวกันเป็นแฮช:
// 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
}); เหตุใดแฮชจึงเร็วกว่ามาก
เมื่อทำให้การสืบค้นที่ขึ้นอยู่กับ 44 เป็นโมฆะ ตาราง:
// 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'); ข้อดีด้านประสิทธิภาพ: ป>
- การดำเนินการลบครั้งเดียว :หนึ่ง
52คำสั่งลบข้อความค้นหาที่แคชไว้หลายร้อยรายการ - ประสิทธิภาพของหน่วยความจำ :Redis สามารถเพิ่มตารางแฮชทั้งหมดได้ในการดำเนินการครั้งเดียว
- การล้างข้อมูลแบบอะตอมมิก :ข้อความค้นหาที่เกี่ยวข้องทั้งหมดจะถูกยกเลิกพร้อมกัน
หากต้องการดูว่าสคริปต์ Lua มีลักษณะอย่างไรในตอนท้าย คุณสามารถตรวจสอบการใช้งานในพื้นที่เก็บข้อมูล Drizzle
แท็กแคชสำหรับการควบคุมแบบละเอียด
นอกเหนือจากการทำให้ใช้ไม่ได้ตามตารางแล้ว Drizzle ยังรองรับแท็กที่กำหนดเองสำหรับการควบคุมแคชแบบละเอียด:
// 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' }); อัตโนมัติเทียบกับการทำให้ใช้งานไม่ได้ด้วยตนเอง
การทำให้ใช้งานไม่ได้โดยอัตโนมัติ (ค่าเริ่มต้น):การสืบค้นจะไม่ถูกต้องเมื่อตารางที่ต้องพึ่งพามีการเปลี่ยนแปลง ทำให้มั่นใจได้ถึงความสอดคล้องของข้อมูลแต่มีการล้างแคชที่เข้มงวดมากขึ้น
การทำให้ใช้งานไม่ได้ด้วยตนเอง :สำหรับสถานการณ์ที่ยอมรับความสอดคล้องในที่สุด คุณสามารถปิดใช้งานการทำให้ใช้ไม่ได้อัตโนมัติและควบคุมด้วยตนเองว่าจะล้างแคชเมื่อใด:
// 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'] }); กรณีการใช้งานจริง
ตอนนี้เราได้พูดถึงด้านเทคนิคของการบูรณาการแล้ว มาดูกันว่าแนวคิดเหล่านี้แปลไปสู่การใช้งานจริงได้อย่างไร
แคตตาล็อกผลิตภัณฑ์อีคอมเมิร์ซ
// 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)); การจัดการเนื้อหา
// 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' }); บทสรุป
การรวมแคช Upstash Redis และ Drizzle สามารถปรับปรุงประสิทธิภาพการสืบค้น SQL ได้อย่างมาก และลดภาระของฐานข้อมูลด้วยการเปลี่ยนแปลงโค้ดเพียงเล็กน้อย (ค่อนข้าง)
เมื่อคุณเปิดใช้งานแคช คุณสามารถคาดหวัง:
- เร็วขึ้นอย่างมาก เวลาตอบสนองการค้นหาสำหรับข้อมูลที่แคชไว้
- โหลดฐานข้อมูลลดลง และความสามารถในการปรับขนาดที่ดีขึ้น
การจัดจำหน่ายทั่วโลกของ Upstash Redis และสถาปัตยกรรมแบบไร้เซิร์ฟเวอร์เป็นหลักพร้อมการกำหนดราคาแบบจ่ายตามการใช้งานเป็นรากฐานที่ดีสำหรับแอปพลิเคชันสมัยใหม่
เหมาะสำหรับแพลตฟอร์มอีคอมเมิร์ซ แดชบอร์ดการวิเคราะห์ ระบบการจัดการเนื้อหา และอื่นๆ อีกมากมาย
การอ่านเพิ่มเติม
ต้องการดำน้ำลึกกว่านี้ไหม? ต่อไปนี้เป็นแหล่งข้อมูลดีๆ บางส่วนที่ฉันแนะนำให้คุณลองดู:
- คู่มือบูรณาการ Upstash Redis และ Drizzle
- เอกสาร Drizzle Caching
- เริ่มต้นใช้งาน Upstash Redis
- SDK ขีดจำกัดอัตราขั้นสูง (TypeScript) - SDK อันทรงพลังอีกตัวหนึ่งที่ใช้ประโยชน์จากสคริปต์ Lua เพื่อประสิทธิภาพสูงสุด
- SDK ขีดจำกัดอัตราขั้นสูง (Python) - การใช้งาน Python ของ Rate Limit SDK