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

Redis Luaスクリプト完全解説:アプリのパフォーマンスを飛躍的に高める実践ガイド

著者:Andrei Chernikov

Redisは、プロセス間通信やデータストレージに利用される人気のインメモリデータグリッドです。「Luaスクリプトが実行できる」という話は聞いたことがあっても、その理由やメリットまではよく分からない、という方も多いのではないでしょうか。そんな方は、ぜひこの記事を読み進めてみてください。

前提条件

このガイドに沿って進めるには、システムにRedisがインストールされている必要があります。読みながらRedisのコマンドリファレンスを参照しておくと、理解が深まりやすくなります。

なぜLuaスクリプトが必要なのか?

結論から言えば、パフォーマンスの向上のためです。Redisで行う処理の多くは、複数のステップで構成されています。これらのステップをアプリケーション側の言語で実行する代わりに、Luaを使ってRedis内部でまとめて処理できます。

  • その結果、より高いパフォーマンスが期待できます。
  • さらに重要な点として、スクリプト内のすべての処理はアトミック(不可分)に実行されます。スクリプトの実行中には、他のRedisコマンドが割り込むことはありません。

たとえば筆者は、Redisに保存されたJSON文字列を書き換える処理にLuaスクリプトを使用しています。詳細については記事の後半で詳しく説明します。

でも、Luaなんて知らない…

ご心配なく。Luaはそれほど難しい言語ではありません。C系の言語(C、C++、Java、JavaScriptなど)を一つでも知っていれば、十分対応できます。また、本記事では動作するサンプルコードも多数紹介しています。

まずは基本の例から

redis-cli経由でスクリプトを実行するところから始めましょう。以下のコマンドで起動します。

redis-cli

続いて、次のコマンドを実行してみてください。

eval "redis.call('set', KEYS[1], ARGV[1])" 1 key:name value

EVALコマンドは、Redisに対して「後続のスクリプトを実行せよ」と指示します。"redis.call('set', KEYS[1], ARGV[1])"という文字列がスクリプト本体で、機能的にはRedisのsetコマンドと同一です。スクリプトテキストの後に3つのパラメータが続きます。

  1. 指定するキーの数
  2. キー名
  3. 第1引数

スクリプトへの引数は、KEYSARGVの2つのグループに分けられます。

スクリプト直後の数字で、必要なキーの数を指定します。この例では1です。この数値の直後に、キー名を順番に列挙します。これらはスクリプト内でKEYSテーブルとしてアクセスできます。今回の場合、インデックス1key:nameという単一の値が格納されています。

注意:Luaの配列(テーブル)のインデックスは0ではなく1から始まります。

キーの後ろには任意の数の引数を渡せます。これらはLua内でARGVテーブルとして参照可能です。この例では、valueという文字列を単一のARGV引数として渡しています。お察しのとおり、上記のコマンドはkey:nameというキーにvalueという値をセットします。

なお、スクリプトが使用するキーはKEYSで渡し、その他の引数はARGVで渡すのがベストプラクティスとされています。KEYSを0として、すべてのキーをARGVに含めるような使い方は避けましょう。

次に、スクリプトが正しく完了したか確認しましょう。Redisからキーを取得する別のスクリプトを実行して検証します。

eval "return redis.call('get', KEYS[1])" 1 key:name

出力が"value"になれば、前のスクリプトが正常に"key:name"というキーを設定できたことになります。

スクリプトの中身を詳しく解説

最初のスクリプトは、redis.call関数というたった1つのステートメントで構成されています。

redis.call('set', KEYS[1], ARGV[1])

redis.callを使うと、任意のRedisコマンドを実行できます。第1引数がコマンド名で、以降がそのパラメータです。setコマンドの場合は、keyvalueが引数となります。すべてのRedisコマンドがサポートされています。公式ドキュメントにも次のように記載されています。

「Redisは同じLuaインタープリタを使ってすべてのコマンドを実行する」

2つ目のスクリプトは、コマンドの実行に加えて値を返す点が異なります。

eval "return redis.call('get', KEYS[1])" 1 key:name

スクリプトが返した値は、呼び出し元のプロセスに送られます。今回は呼び出し元がredis-cliなので、ターミナル画面に結果が表示されます。

もう少し複雑な例は?

筆者はかつて、ハッシュマップから特定の順序で要素を取得するためにLuaスクリプトを活用しました。順序自体は、ソート済みセット(sorted set)に保存されたハッシュキーによって定義されています。

まず、redis-cliで以下のコマンドを実行してデータを準備しましょう。

hmset hkeys key:1 value:1 key:2 value:2 key:3 value:3 key:4 value:4 key:5 value:5 key:6 value:6
zadd order 1 key:3 2 key:1 3 key:2

これらのコマンドにより、キーhkeysにハッシュマップが作成され、キーorderにはhkeysから選ばれたキーが特定の順序で格納されたソート済みセットが作成されます。

詳細については、hmsetおよびzaddコマンドのリファレンスを参照するとよいでしょう。

それでは、次のスクリプトを実行してみます。

eval "local order = redis.call('zrange', KEYS[1], 0, -1); return redis.call('hmget',KEYS[2],unpack(order));" 2 order hkeys

次のような出力が得られるはずです。

"value:3"
"value:1"
"value:2"

これは、目的のキーの値を正しい順序で取得できたことを意味します。

毎回スクリプト全文を書く必要があるの?

いいえ! Redisでは、SCRIPT LOADコマンドを使ってスクリプトを事前にメモリへロードできます。

script load "return redis.call('get', KEYS[1])"

次のような出力が表示されます。

"4e6d8fc8bb01276962cce5371fa795a7763657ae"

これはスクリプトの一意なハッシュ値で、EVALSHAコマンドに渡すことでスクリプトを実行できます。

evalsha 4e6d8fc8bb01276962cce5371fa795a7763657ae 1 key:name

注意:実際にはSCRIPT LOADコマンドが返すSHA1ハッシュを使用してください。上記のハッシュはあくまで例です。

JSONの書き換えについての話は?

RedisにJSONオブジェクトを保存しているケースは少なくありません。それが良い設計かどうかはさておき、現場ではよく見られるパターンです。

このJSONオブジェクト内のキーを変更したい場合、通常は「Redisから取得→パース→変更→シリアライズ→再保存」という流れになります。しかし、このアプローチには2つの問題があります。

  1. 同時実行性(Concurrency):取得と保存の間に、別のプロセスが同じJSONを変更してしまう可能性があります。その場合、変更は失われてしまいます。
  2. パフォーマンス:変更頻度が高く、オブジェクトが大きい場合、アプリ全体のボトルネックになりかねません。このロジックをLuaで実装すれば、パフォーマンスを改善できます。

テスト用のJSON文字列をキーobjでRedisに登録してみましょう。

set obj '{"a":"foo","b":"bar"}'

続いて、次のスクリプトを実行します。

EVAL 'local obj = redis.call("get",KEYS[1]); local obj2 = string.gsub(obj,"(" .. ARGV[1] .. "\":)([^}]+)", "%1" .. ARGV[2]); return redis.call("set",KEYS[1],obj2);' 1 obj b bar2

実行後、キーobjには次のオブジェクトが格納されます。

{"a":"foo","b":"bar2"}

もちろん、このスクリプトをSCRIPT LOADでロードすることもできます。

SCRIPT LOAD 'local obj = redis.call("get",KEYS[1]); local obj2 = string.gsub(obj,"(" .. ARGV[1] .. "\":)([^}]+)", "%1" .. ARGV[2]); return redis.call("set",KEYS[1],obj2);'

ロード後は次のように実行します。

EVALSHA <your_script_sha> 1 obj b bar2

補足ポイント:

  • ..はLuaにおける文字列連結演算子です。
  • キーをマッチさせて値を置き換えるために、正規表現(RegEx)パターンを使用しています。正規表現に不慣れな場合は、別途入門ガイドを参照してください。
  • Luaの正規表現方言の特徴的な点として、バックリファレンスの指定にも、正規表現の特殊文字のエスケープにも、%を使用します。
  • "%ではなく\でエスケープしているのは、正規表現の特殊文字ではなく、Lua文字列の区切り文字をエスケープしているためです。

常にLuaスクリプトを使うべき?

いいえ。使用を推奨できるのは、パフォーマンス向上が実証できる場合だけです。必ず先にベンチマークを実施しましょう。

目的が原子性(atomicity)だけであれば、Redisのトランザクション(MULTI/EXEC)を検討した方がよいでしょう。

また、スクリプトは長くしすぎないことも重要です。スクリプトの実行中は他のすべての処理が完了を待つことになるため、実行時間が長いスクリプトは逆にボトルネックになり得ます。タイムアウト(デフォルト5秒)に達すると、スクリプトは強制停止されます。

最後に

Luaについてさらに学びたい方は、公式サイトlua.orgをチェックしてみてください。

また、筆者のGitHubにあるnode.jsライブラリ(src/luaフォルダ参照)には、Luaスクリプトの実用的なサンプルが含まれています。このライブラリを使えば、自分でLuaスクリプトを書かずにnode.jsからJSONオブジェクトを操作することも可能です。

最後までお読みいただきありがとうございました。ご質問やコメントをお待ちしています。Twitterでのフォローも歓迎です。

  1. Redisでキー名を変更する方法を解説 – RENAME / RENAMENXコマンドの使い方

    このチュートリアルでは、Redisデータストアに保存されたキーの名前を変更する方法について、RENAMEコマンドとRENAMENXコマンドを使って詳しく解説します。 RENAMEコマンドとは RENAMEコマンドは、既存のキーを古い名前から新しい名前に変更するためのコマンドです。もし新しい名前のキーがすでに存在している場合は、そのキーが上書きされ、古い名前のキーには暗黙的にDELコマンドが実行されて削除されます。新しい名前のキーが存在しない場合は、単純に古い名前が新しい名前に変更されます。 構文 redis host:port> RENAME <old name> <n

  2. Upstash KafkaとMongoDBコネクタで実現する低レイテンシセグメンテーションプラットフォームの構築

    はじめに セグメンテーションプラットフォームは、顧客や製品といった関連データを理解し、分類するうえで重要な役割を担います。 セグメンテーションとは、一定の基準に基づいて大きなグループを、より均質性の高い小さなサブグループへ分割することです。たとえばECサイトにおける顧客セグメンテーションでは、パーソナライズされたマーケティング施策の立案、ターゲットを絞ったプロモーションの実施、きめ細かなショッピング体験の提供などが可能になります。 目次 要件の把握 基本アーキテクチャ アーキテクチャ構成要素 設計上の課題 提案ソリューション まとめ 1. 要件の把握 EC向けの顧客セグメントを対象とし