Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

SQL パフォーマンスの向上:Drizzle ORM クエリ用の Upstash Redis キャッシュ

少し前に、Drizzle ORM とコラボレーションする機会がありました。

この TypeScript ORM がコミュニティにいかに愛されているかを見て、私たちが「はい 😳」と言う決断をするのは簡単でした。

SQL パフォーマンスの向上:Drizzle ORM クエリ用の Upstash Redis キャッシュ

この記事では、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);

    これは、変更されたテーブルに依存するすべてのキャッシュされたアイテムを検索し、それらを削除することで機能します。

    この「単純な」アプローチの問題

    技術的には機能しますが、この単純な実装には 2 つのパフォーマンス上の問題があります。

    問題 1:複数の往復

    無効化プロセスには2 つの個別の Redis 操作が必要です。 :

    <オル>
  • まず、SUNION を呼び出します。 削除するキーのリストを取得します
  • 次に、DEL を呼び出します。 ステップ 1 の結果を使用して
  • これにより往復の依存関係が生じます。 ここで、2 番目のオペレーションは最初のオペレーションが完了するまで待機する必要があります。

    問題 2:一括削除が遅い

    DEL 多くのキーを無効にする場合、コマンドがボトルネックになる可能性があります:

    // This could potentially delete thousands of keys
    await redis.del(...hashesToInvalidate);

    users のような人気のあるテーブルがある場合 数百のキャッシュされたクエリによって参照される場合、1 回の更新で数百の個別の 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
    });

    ハッシュがはるかに速い理由

    users に依存するクエリを無効にする場合 テーブル:

    // 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');

    パフォーマンス上の利点:

    • 単一の削除操作 :DEL 1 つ コマンドは何百ものキャッシュされたクエリを削除します
    • メモリ効率 :Redis は 1 回の操作でハッシュ テーブル全体を解放できます
    • アトミッククリーンアップ :関連するすべてのクエリがまとめて無効化されます

    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 のグローバル ディストリビューションと従量課金制のサーバーレス ファースト アーキテクチャは、最新のアプリケーションの優れた基盤です。

    e コマース プラットフォーム、分析ダッシュボード、コンテンツ管理システムなどに最適です。

    さらに読む

    もっと深く潜ってみたいですか?以下に、チェックしておくことをお勧めする優れたリソースをいくつか示します。

    • Upstash Redis と Drizzle 統合ガイド
    • Drizzle キャッシュのドキュメント
    • Upstash Redis の使用を開始する
    • Upstash レート制限 SDK (TypeScript) - Lua スクリプトを活用して最適なパフォーマンスを実現するもう 1 つの強力な SDK
    • Upstash レート制限 SDK (Python) - Rate Limit SDK の Python 実装。

    1. Server‑Sent Events (SSE) を使用した Next.js でのリアルタイム LLM ストリーミング

      迅速に応答する AI を活用したアプリケーションは、瞬時に聞いたり見たりできるようにすることで、ユーザー エクスペリエンスを向上させます。ストリーミングを使用すると、クエリに即座に対応したり、パーソナライズされた推奨事項をリアルタイムで提供したりするチャットボットを作成できます。それはスピードだけではありません。ユーザーのニーズを迅速に満たし、その価値を高めることが重要です。 ストリーミングでは、データを 1 つの大きなブロックではなく、小さな連続したチャンクで送信します。チャットボットやレコメンデーション システムなどの AI アプリケーションのコンテキストでは、ストリーミングとは、応答全

    2. Redis HINCRBYFLOAT –ハッシュ値のフィールドに格納されている浮動小数点数をインクリメントする方法

      このチュートリアルでは、redisデータストアのキーに格納されているハッシュ値内のフィールドに格納されている浮動小数点数をインクリメントする方法について学習します。このために、コマンドを使用します– HINCRBYFLOAT redis-cliで。 このコマンドは、キーに格納されているハッシュ値の指定されたフィールドに格納されている浮動小数点数を指定された値だけインクリメントするために使用されます(インクリメント )。指定されたフィールドがハッシュ値に存在しない場合は、指定された増分で追加されます。 その値として。キーが存在しない場合は、指定されたフィールドを唯一のメンバーとして新