LuaスクリプトによるRedis操作の最適化

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を停止するため注意が必要です。

カスタムロジックを実装する際は、時間のかかる処理に注意してください。

タグ: redis lua scripting database Caching

8月2日 04:10 投稿