Luaは軽量なスクリプト言語であり、C言語で実装されています。データベースのストアドプロシージャと同様の役割を果たすことができます。
RedisでLuaスクリプトを使用する利点:
- 複数のコマンドを一度に送信し、ネットワークオーバーヘッドを削減
- スクリプト全体がアトミックに実行され、他のリクエストによる割り込みがない
- 複雑な処理をファイル化することでコードの再利用が可能
RedisでのLuaスクリプト実行
EVALコマンドを使用してLuaスクリプトを実行します。構文:
redis> EVAL lua-script key-count [key1 key2 ...] [arg1 arg2 ...]
- EVAL:Luaスクリプト実行コマンド
- lua-script:実行するLuaコード
- key-count:引数中のキーの数(キーがない場合は0)
- [key1 key2 ...]:Luaに渡されるキー配列
- [arg1 arg2 ...]:Luaに渡されるその他の引数
簡単な例として、文字列を返すスクリプト(引数なし):
redis> EVAL "return 'Hello World'" 0
実際には、Redisコマンドを実行するためにLuaスクリプトが使用されます。
RedisコマンドのLua内での呼び出し
基本構文
redis.call()関数を使用してRedisコマンドを実行します:
redis.call(command, key [, param1, param2...])
- command:実行するRedisコマンド(set, get, delなど)
- key:操作対象のキー
- param1, param2...:キーに対するパラメータ
例:snailキーに値を設定
EVAL "return redis.call('set', 'snail', '2573')" 0
パラメータを使用したバージョン:
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 snail xiaoming
スクリプトファイルの使用
実際の開発では、スクリプトをファイルに保存して実行します:
cd /usr/local/soft/redis-6.0.9/src
vim multiply.lua
multiply.luaの内容(値の設定と取得):
redis.call('set', 'test_key', 'lua_value')
return redis.call('get', 'test_key')
ファイルの実行:
redis-cli --eval multiply.lua 0
IPアクセス制限の実装例
要件:ユーザーごとにX秒間でY回のみアクセス許可
設計方針:
- String型のキーでIPアドレスを記録し、アクセス回数を値とする
- 初回アクセス時に期限(パラメータ1)を設定
- アクセス回数が上限(パラメータ2)を超えた場合に拒否
KEYS[1]:IPアドレス、ARGV[1]:期限時間、ARGV[2]:最大アクセス回数
rate_limit.lua:
-- アクセスカウンターのインクリメント
local count = redis.call('incr', KEYS[1])
-- 初回アクセス時の期限設定
if tonumber(count) == 1 then
redis.call('expire', KEYS[1], ARGV[1])
return 1
-- 回数超過チェック
elseif tonumber(count) > tonumber(ARGV[2]) then
return 0
else
return 1
end
テスト実行(6秒間で10回制限):
redis-cli --eval rate_limit.lua client:192.168.1.100 , 6 10
スクリプトのキャッシング
キャッシングの必要性
長いスクリプトを毎回送信するとネットワーク負荷が大きいため、SHA1ハッシュでキャッシュできます。
キャッシング方法
SCRIPT LOADコマンドでスクリプトを登録:
SCRIPT LOAD "return 'Hello Redis'"
EVALSHAコマンドでキャッシュ済みスクリプトを実行:
EVALSHA "470877a599ac74fbfda41caa908de682c5fc7d4b" 0
乗算処理の実装例
RedisにはINCRBYコマンドはあるが、乗算コマンドはありません。Luaで実装します:
SET multiplier 5
multiply_operation.lua:
-- 現在値の取得
local current = redis.call('get', KEYS[1])
-- キーが存在しない場合は0を設定
if current == false then
current = 0
else
current = tonumber(current)
end
-- 乗算計算と更新
current = current * tonumber(ARGV[1])
redis.call('set', KEYS[1], current)
return current
一行形式に変換:
local current = redis.call('get', KEYS[1]); if current == false then current = 0 else current = tonumber(current) end; current = current * tonumber(ARGV[1]); redis.call('set', KEYS[1], current); return current
SCRIPT LOAD実行:
SCRIPT LOAD "local current = redis.call('get', KEYS[1]); if current == false then current = 0 else current = tonumber(current) end; current = current * tonumber(ARGV[1]); redis.call('set', KEYS[1], current); return current"
返されたハッシュ値を使用して実行:
EVALSHA a1b2c3d4e5f6789012345678901234567890abcd 1 multiplier 3
スクリプトタイムアウト対策
Redisはシングルスレッドで動作するため、長時間実行されるLuaスクリプトは問題を引き起こします:
EVAL "while(true) do end" 0
デフォルトでは5秒でタイムアウト:
lua-time-limit 5000
タイムアウト後の対応:
- 他のクライアントは"BUSY"エラーを受け取る
- SCRIPT KILLでスクリプトを停止可能
ただし、データ変更を含むスクリプトは停止できません:
EVAL "redis.call('set','data','value') while true do end" 0
この場合、SCRIPT KILLは失敗し"UNKILLABLE"エラーを返します。これはアトミック性を維持するためです。
回避策としてSHUTDOWN NOSAVEコマンドがありますが、永続化せずにRedisを停止するため注意が必要です。
カスタムロジックを実装する際は、時間のかかる処理に注意してください。