クイックフィックス
Redisは、EVALSHA経由で提供されたSHA1ハッシュに一致するLuaスクリプトが見つからない場合にNOSCRIPTエラーをスローします。RedisはスクリプトをRAMに保存するため、サーバーの中断によりこのキャッシュが消去されることがあります。解決策は、アプリケーションでエラーをキャッチし、フルソースのEVALコマンドにフォールバックすることです。
# 堅牢な修正のための論理フロー
try:
return redis.evalsha(script_sha1, keys, args)
catch error "NOSCRIPT":
# スクリプトがキャッシュに存在しません。フルソースを送信します。
# これにより、次回のためにスクリプトが再キャッシュされます。
return redis.eval(script_content, keys, args)
根本原因:なぜスクリプトが消えるのか
Redisはスクリプトをキャッシュすることでパフォーマンスを最適化します。例えば、10KBのLuaスクリプトがあるとします。これを毎秒1,000回ネットワーク経由で送信すると、毎秒10MBの帯域幅を浪費することになります。Redisは、スクリプトを一度送信して40文字のSHA1ハッシュを取得し、以降の呼び出しにはEVALSHA <SHA1>を使用することで、この浪費を回避します。
しかし、このキャッシュは揮発性です。NOSCRIPTエラーは通常、以下の4つのシナリオで発生します:
- **サーバーの再起動:** RedisはスクリプトキャッシュをRDBやAOFファイルに永続化しません。再起動するとメモリはクリアされます。
- **クラスターのシャーディング:** これはよくある落とし穴です。Redis Clusterの各ノードは独自のスクリプトキャッシュを保持しています。クライアントが、まだスクリプトを受け取っていない新しいマスターノードに接続すると、エラーが発生します。
- **手動フラッシュ:** 管理者やメンテナンス用のcronジョブが`SCRIPT FLUSH`を実行し、キャッシュされたロジックをすべてクリアした可能性があります。
- **メモリ圧迫:** Redisが`maxmemory`制限に達すると、データを追い出す(エビクション)ことがあります。スクリプトメモリは別に処理されますが、極端な不安定さは予期しないキャッシュ動作につながる可能性があります。
恒久的な解決方法
1. 「Try-Catch-Reload」パターン(ベストプラクティス)
スクリプトが常に存在すると仮定しないでください。代わりに、スクリプトがない場合を想定してクライアントを設計します。以下はNode.js (ioredis) を使用した実装例です:
const script = "return redis.call('get', KEYS[1])";
const sha1 = "d0c0316d2d385f76263e699b664d50c76ca4c01d";
try {
await redis.evalsha(sha1, 1, "user:session");
} catch (err) {
if (err.message.includes("NOSCRIPT")) {
// EVALにフォールバックします。これにより実行と再キャッシュが同時に行われます。
await redis.eval(script, 1, "user:session");
} else {
throw err;
}
}
2. 起動時のプリロード
アプリケーションが主要なスクリプトに依存している場合は、ブートストラップ(起動)フェーズでそれらをロードしてください。これにより、最初のユーザーリクエストが来る前にキャッシュが「温まった」状態になります。
# CLI経由で手動でスクリプトをロードする
redis-cli SCRIPT LOAD "return redis.call('get', KEYS[1])"
# 出力: "d0c0316d2d385f76263e699b664d50c76ca4c01d"
本番環境のコードでは、.luaファイルをループしてそれぞれにSCRIPT LOADを実行します。サーバーがセッション中に再起動した場合の重要なセーフティネットとして、Try-Catchパターンも併用してください。
3. Redis Clusterへの対応
クラスター環境では、あるノードでのSCRIPT LOADは他のノードに同期されません。多くの高機能ライブラリ(redis-pyやioredisなど)は、複数ノードへのロードを自動的に処理する「Script」オブジェクトを提供しています。クラスター管理を簡素化するために、生のコマンドではなくこれらの抽象化機能を利用してください。
検証:キャッシュの確認
実際に実行することなく、スクリプトが現在キャッシュされているかどうかを確認できます。SHA1ハッシュを指定してSCRIPT EXISTSコマンドを使用します:
# 特定のSHA1を確認する
redis-cli SCRIPT EXISTS d0c0316d2d385f76263e699b664d50c76ca4c01d
# 結果:
# 1) (integer) 1 <-- キャッシュされており準備完了
# 1) (integer) 0 <-- 不足。NOSCRIPTの原因になります
プロのヒントと予防策
スクリプトの末尾にある目に見えないスペース1つや余分な改行1つで、SHA1ハッシュは完全に変わってしまいます。これは、コードは同じに見えるのにハッシュが一致しないという、非常にストレスの溜まるデバッグの原因になります。これを防ぐために、Luaブロックには常に一貫したフォーマットツールを使用することをお勧めします。
私は個人的に、スクリプトハッシュの検証にToolCraftのHash Generatorを使用しています。これは完全にブラウザ上で動作します。つまり、ロジックやキーがマシン外に送信されることがないため、デバッグ中も内部コードのセキュリティを保つことができます。
参考文献
- [Redis公式Lua APIガイド](https://redis.io/docs/manual/programmability/lua-api/)
- [EVALSHAコマンドリファレンス](https://redis.io/commands/evalsha/)
- [SCRIPT LOADドキュメント](https://redis.io/commands/script-load/)

