Lua入門:Redisユーザーのための実践ガイド
Redisに組み込みスクリプト言語が搭載されていると聞いたことはあるけれど、まだ試したことがない——そんな方は多いのではないでしょうか。この記事では、LuaのパワーをあなたのRedisサーバーで活かすために必要な基礎知識を、実例を交えながらわかりやすく解説します。
はじめてのLuaスクリプト
まずは、Redisと実際にやり取りせず、単に値を返すだけの最もシンプルなスクリプトから始めましょう。
local msg = "Hello, world!"
return msg
これ以上なくシンプルですね。1行目でメッセージをローカル変数に格納し、2行目でその値をRedisサーバーからクライアントへ返しています。このファイルをhello.luaという名前で保存し、次のように実行します。
redis-cli --eval hello.lua
接続できない場合
上記のredis-cliの例は、ローカル環境でRedisサーバーが動いていることを前提としています。RedisGreenのようなリモートサーバーを利用している場合は、ホスト名とポート番号を指定する必要があります。RedisGreenのダッシュボードにある「Connect」ボタンから、サーバーのログイン情報を簡単にコピーできます。
なお、「Could not connect to Redis at 127.0.0.1:6379: Connection refused」というエラーが出た場合は、サーバーが起動しているか、接続先の設定が正しいかを確認してください。
スクリプトを実行すると「Hello, world!」と表示されます。EVALの最初の引数はLuaスクリプト本体です(ここではcatコマンドでファイルから読み込んでいます)。2つ目の引数は、スクリプトがアクセスするRedisキーの数です。今回の「Hello World」スクリプトはどのキーにもアクセスしないため、0を指定しています。
キーと引数へのアクセス
次に、URL短縮サービスを構築することを想定してみましょう。URLが入力されるたびにそれを保存し、後でそのURLにアクセスできる一意の番号を返したいわけです。
そこでLuaスクリプトを使い、INCRでRedisから一意のIDを取得し、そのIDをキーとしてハッシュにURLを即座に保存します。
local link_id = redis.call("INCR", KEYS[1])
redis.call("HSET", KEYS[2], link_id, ARGV[1])
return link_id
ここで初めてcall()関数を使ってRedisにアクセスしています。call()の引数はRedisに送るコマンドそのもので、まずINCR <key>を実行し、続いてHSET <key> <field> <value>を実行します。これらのコマンドは順次実行され、スクリプトの実行中にRedisが他の処理を行うことはありません。そのため、非常に高速に動作します。
また、ここではKEYSとARGVという2つのLuaテーブルにアクセスしています。テーブルとは連想配列のことで、Luaにおける唯一のデータ構造化手段です。慣れ親しんだ言語の配列と同じものだと考えて差し支えありませんが、Lua初心者がつまずきやすい2つの特徴には注意が必要です。
テーブルは1始まりです。インデックスは1から始まるため、
mytableの最初の要素はmytable[1]、2番目はmytable[2]となります。テーブルはnil値を保持できません。
[ 1, nil, 3, 4 ]となるはずの操作結果は、実際には[ 1 ]になります。テーブルは最初のnil値で切り詰められるのです。
このスクリプトを実行する際は、KEYSとARGVの各テーブルに渡す値も指定する必要があります。生のRedisプロトコルでは、コマンドは次のようになります。
EVAL $incrset.lua 2 links:counter links:url https://malcolmgladwellbookgenerator.com/
EVALを呼び出すときは、スクリプトの後にアクセスするKEYSの数として2を指定し、続けてキーを列挙し、最後にARGVの値を渡します。
通常、アプリケーションでRedis Luaスクリプトを扱う場合、キーの数の指定はRedisクライアントライブラリが自動的に処理してくれます。上のコードブロックはあくまで参考用ですが、コマンドラインではもっと簡単に書けます。
redis-cli --eval incrset.lua links:counter links:urls , https://malcolmgladwellbookgenerator.com/
--evalオプションを使う場合、カンマ(,)がKEYS[]とARGV[]の項目を区切ります。
理解を深めるため、元のスクリプトをもう一度見てみましょう。今度はKEYSとARGVを展開した形です。
local link_id = redis.call("INCR", "links:counter")
redis.call("HSET", "links:urls", link_id, "https://malcolmgladwellbookgenerator.com")
return link_id
Redis向けのLuaスクリプトを書くときは、アクセスするすべてのキーを必ずKEYSテーブル経由で参照すべきです。ARGVテーブルはパラメータ受け渡し専用とし、ここでは保存したいURLの値を渡しています。
条件分岐:increxとhincrex
先ほどの例ではURL短縮サービスのリンクを保存しましたが、次に必要になるのは、各URLが何回アクセスされたかを追跡する仕組みです。そこで、Redisのハッシュ内にカウンターを持たせます。ユーザーがリンク識別子を持って訪れたら、その識別子が存在するかを確認し、存在すれば対応するカウンターをインクリメントします。
if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
return redis.call("HINCRBY", KEYS[1], ARGV[1], 1)
else
return nil
end
短縮リンクがクリックされるたびにこのスクリプトを実行すれば、リンクが再び共有されたことを記録できます。スクリプトはEVALで呼び出し、単一のキーとしてlinks:visitsを、引数としては前のスクリプトが返したリンク識別子を渡します。
ハッシュを使わなくても、ほぼ同じロジックで書けます。こちらは、標準的なRedisキーが存在する場合にのみインクリメントするスクリプトです。
if redis.call("EXISTS",KEYS[1]) == 1 then
return redis.call("INCR",KEYS[1])
else
return nil
end
SCRIPT LOADとEVALSHA
思い出してほしいのは、RedisがLuaスクリプトを実行中は他の処理を一切行えないという点です。理想的なスクリプトとは、Redisがもともと持つ小さなアトミックなデータ操作の語彙を、必要最小限のロジックで拡張するものです。LuaスクリプトのバグはRedisサーバー全体を停止させることもあり得るため、短くデバッグしやすい状態を保つのが賢明です。
とはいえ、スクリプトは通常とても短いものなので、実行のたびにスクリプト全文を送る必要はありません。実際のアプリケーションでは、起動時(またはデプロイ時)に各LuaスクリプトをRedisに登録しておき、以降は一意なSHA-1識別子で呼び出します。
redis-cli SCRIPT LOAD "return 'hello world'"
# "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis-cli EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
# "hello world"
本番アプリケーションでは、明示的にSCRIPT LOADを呼ぶ必要性は低いでしょう。というのも、EVALは渡されたスクリプトを暗黙的にロードするからです。アプリケーションはまず楽観的にEVALSHAを試み、スクリプトが見つからなかった場合のみEVALへフォールバックするのが一般的です。
Rubyプログラマーなら、RubyアプリでのLuaスクリプトのロードと管理を簡素化するShopifyのWolverineをチェックしてみてください。PHPプログラマーには、Predisがおすすめです。Luaスクリプトを通常のRedisコマンドのように呼び出せるようになります。こうしたツールやその他の手法でLuaとの連携を標準化している方は、ぜひ筆者まで教えてください。どんなツールがあるのか非常に興味があります。
いつLuaを使うべきか?
RedisのLuaサポートは、操作をまとめて一括実行するWATCH/MULTI/EXECブロックと機能が一部重複しています。では、どちらを選べばよいのでしょうか? MULTIブロック内の各操作は互いに独立している必要がありますが、Luaなら後続の操作が先行操作の結果に依存できます。さらに、Luaスクリプトを使えば、WATCH使用時に発生しうる、遅いクライアントが飢餓状態に陥るレースコンディションを回避することもできます。
RedisGreenでの経験から言えば、Luaを使っているアプリケーションの多くはMULTI/EXECも併用していますが、その逆は少ない傾向にあります。成功しているLuaスクリプトのほとんどはごく小さなもので、アプリが必要としているもののRedisの語彙には存在しない、たったひとつの機能を実装するにとどまっています。
利用可能なライブラリ
RedisのLuaインタプリタは、base、table、string、math、debug、cjson、cmsgpackという7つのライブラリをロードします。最初のいくつかは標準ライブラリで、どんな言語でも期待される基本的な操作が行えます。最後の2つは、RedisにJSONとMessagePackを理解させるためのものです。これは extremely 有用な機能でありながら、あまり使われているのを見かけないのが不思議でなりません。
公開APIを持つWebアプリケーションでは、JSONデータがそこら中に散らばっていることが多いものです。たとえば、通常のRedisキーに多数のJSON blobを保存していて、それをハッシュとして保存したかのように内部の特定の値にアクセスしたい——そんな場面があるかもしれません。RedisのJSONサポートを使えば、これは簡単に実現できます。
if redis.call("EXISTS", KEYS[1]) == 1 then
local payload = redis.call("GET", KEYS[1])
return cjson.decode(payload)[ARGV[1]]
else
return nil
end
ここでは、まずキーが存在するかを確認し、存在しなければすぐにnilを返します。その後、RedisからJSON値を取り出し、cjson.decode()でパースして、要求された値を返しています。
redis-cli set apple '{ "color": "red", "type": "fruit" }'
# OK
redis-cli --eval json-get.lua apple , type
# "fruit"
このスクリプトをRedisサーバーにロードすれば、Redisに保存されたJSON値をハッシュのように扱えるようになります。オブジェクトが適度に小さければ、アクセスのたびにパースが必要とはいえ、実際かなり高速に動作します。
パフォーマンスが求められるシステム向けの内部APIを開発しているなら、JSONよりもサイズが小さく高速なMessagePackを選ぶことになるでしょう。幸い、Redisでは(他の多くの場所と同様に)MessagePackはほぼJSONの置き換え(ドロップイン代替)として機能します。
if redis.call("EXISTS", KEYS[1]) == 1 then
local payload = redis.call("GET", KEYS[1])
return cmsgpack.unpack(payload)[ARGV[1]]
else
return nil
end
数値の扱い方
LuaとRedisは型システムが異なるため、値がRedis-Lua間の境界を越えるときにどう変化するかを理解しておくことが重要です。LuaからRedisクライアントへ数値が返されるとき、それは整数になります。小数点以下の桁はすべて切り捨てられます。
local indiana_pi = 3.2
return indiana_pi
このスクリプトを実行すると、Redisは整数の3を返します。円周率の興味深い部分は失われてしまうのです。単純に思えますが、スクリプトの途中でRedisとやり取りし始めると、話は少し厄介になります。例を見てみましょう。
local indiana_pi = 3.2
redis.call("SET", "pi", indiana_pi)
return redis.call("GET", "pi")
この結果得られる値は文字列の"3.2"です。なぜでしょうか? Redisには専用の数値型が存在しないからです。SETの時点でRedisは値を文字列として保存し、Luaが当初その値を浮動小数点数として扱っていたという事実は失われます。後で値を取り出しても、それは依然として文字列なのです。
GET/SETでアクセスされるRedisの値は、INCRやDECRといった数値操作が実行される場合を除き、文字列として考えるべきです。これらの特殊な数値操作は実際に整数の応答を返し(保存値を数学的な規則に従って操作します)、しかしRedisに保存されている値の「型」自体は依然として文字列なのです。
注意点まとめ
RedisでLuaを扱う際に、私たちがよく目にする典型的なミスは以下の通りです。
Luaのテーブルは1始まりであり、多くの人気言語とは異なります。KEYSテーブルの最初の要素は
KEYS[1]、2番目はKEYS[2]です。Luaではnil値がテーブルを終端させます。
[ 1, 2, nil, 3 ]は自動的に[1, 2]になってしまいます。テーブルにnil値を使わないようにしましょう。redis.callは例外形式のLuaエラーを発生させますが、redis.pcallはエラーを自動的にトラップし、検査可能なテーブルとして返します。Luaの数値はRedisへ送信される際に整数へ変換されます。小数点以下はすべて失われるため、浮動小数点数は返却前に文字列へ変換してください。
Luaスクリプトで使用するすべてのキーは必ず
KEYSテーブルで指定しましょう。そうしないと、将来のRedisバージョンでスクリプトが動作しなくなる可能性が高いです。LuaスクリプトはRedisにおける他の操作と同様、実行中は他の処理が一切行われません。スクリプトはRedisサーバーの語彙を拡張する手段と捉え、短く的確に保ちましょう。
さらに学ぶために
LuaとRedisに関する優れたリソースはネット上にたくさんあります。その中でも筆者がよく利用するものを紹介します。
- EVAL公式ドキュメント
- RedisGreenのLuaスクリプトライブラリ
- Luaリファレンスマニュアル
- Luaチュートリアル集
-
Androidユーザー必見!写真ライフがもっと楽しくなるおすすめ写真アプリ5選
スマートフォンは日常生活の中心となる万能デバイスです。仕事をこなしたり、久しぶりの友人と連絡を取ったりと、あらゆる場面で活躍します。そんなスマホで私たちが最も頻繁に行うことのひとつが「写真撮影」です。しかし、撮った写真のうち、本当に満足できるものはどれくらいあるでしょうか?ある調査によると、1人平均1日に322枚もの写真を撮影している一方で、撮影した写真をじっくり見返す時間はほとんどないというデータもあります。せっかく撮るなら、完璧で思い出に残る一枚にしたいですよね。そこで今回は、あなたの写真体験を格段に向上させるAndroid向け写真アプリを5つご紹介します。ステップ1:「Open Came
-
ディスククリーンアップ完全ガイド:Windows・Macユーザー必見のストレージ整理術
「ディスクの空き領域が不足しています」「起動ディスクがほぼ一杯です」——WindowsでもmacOSでも、誰もが一度は目にしたことのある警告メッセージではないでしょうか。家の隅々にホコリや不要物が溜まっていくように、パソコンの中にも時間とともに不要なファイルやプログラム、ジャンクデータが蓄積され、動作がどんどん遅くなってしまいます。 「ハードドライブが満杯だ」とパソコンに警告されたら、この記事を参考に、ディスククリーンアップを徹底的に行いましょう。 まずは、現在のディスク空き容量を確認しよう 作業を始める前に、自分のパソコンにどれだけのディスク容量があるのかを把握しておくことが大切です。残り容