2015年1月24日土曜日

JavaからLuaスクリプトを実行してRedisにアクセスしてみた

概要

RedisはデフォルトでLua言語を使ったデータ抽出ができます
JavaのRedis用ライブラリJedisを使えばJavaからもLuaスクリプトを使うことができるのでその方法を紹介します

環境

  • Windows7 64bit
  • Redis 2.6.14
  • Java 1.8.0_25
  • Eclipse 4.4.1 Luna
  • Maven 3.2.1
  • Jedis 2.6.0

手順

今回はRedis内のHASH情報を取得するLuaスクリプトを紹介します

必要な環境の準備

ここでは詳細に構築方法は紹介しませんが環境はWindows7上のEclipseでソースコードを書きプロジェクトはMavenプロジェクトで作りました
Redisはfor Windowsをインストールしています(インストール方法はこちら
特につまる所はないと思いますが、Eclipse+Mavenは必須ではないのでJavaを開発、動く環境があればOKです

Luaスクリプトの実行

まずはJavaを使わずにLuaスクリプトだけでRedisにアクセスできるか試してみます
作業前にredis-serverを起動しておいてください

RedisでLuaスクリプトを実行するためにはevalという仕組みを使います
redis-cliを使ってRedisにアクセスしてください
そしたら以下のコマンドを実行するとHello Worldが表示できると思います

redis 127.0.0.1:6379> eval "return 'Hello World'" 0
"Hello World"

evalコマンドの説明を簡単にすると

  • 第一引数が実行するスクリプト
  • 第二引数がkeyの数
  • 第三引数がkey
  • 第四引数がargv

になります
keyとargvが不要な場合は0を指定するだけでOKです
keyとargvはそれぞれで複数指定することができます
ちょっとわかりづらいので具体的なkeyとargvの使い方です

redis 127.0.0.1:6379> hset myhash "key1" "value1"
(integer) 1
redis 127.0.0.1:6379> eval "return redis.call('HGET', KEYS[1], ARGV[1])" 1 "myhash" "key1"
"value1"

複数のkeyを渡したい場合や複数のargvを渡されなければいけないコマンド(HGETやZADDなど)に使う感じです

redis.callを呼び出すことで実際にRedisのコマンドを実行することができます
第一引数に実行したいコマンドを文字列で渡します
第二、第三引数以降でそのコマンドを実行するための引数を指定します
また同じようなメソッドでredis.pcallというものがあり違いはエラーが発生したときにそこで終了するかしないかです、callは終了してpcallはエラーを捕捉しつつ次の処理に進みます

Luaスクリプトの作成

とりあえずredis-cliを使ってLuaを実行できるようになりました
次はLuaスクリプトファイルを作成してRedisにアクセスしてみます
実際にJavaから使う場合もスクリプトファイルに記載した内容をJavaに記述するのでスクリプトファイルに落として損はないと思います
また、Luaスクリプトでの処理が複雑な場合にはスクリプトファイルにしたほうが書きやすいし管理もしやすいです

では、先ほどと同様にHashデータにアクセスするためのスクリプトを作成していきます
せっかくスクリプトファイルにするのでちょっとプログラムっぽい感じにしてみます

local keys = redis.call('keys', KEYS[1])
local hash_info_map = {}
for i,key in ipairs(keys) do
    local hash_info = {}
    local key1 = redis.call('HGET', key, "key1")
    local key2 = redis.call('HGET', key, "key2")
    hash_info[1] = key1
    hash_info[2] = key2
    hash_info_map[i] = hash_info
end
return hash_info_map

指定されたKEYSを含むkey情報を持ってきてそのkeyに対してそれぞれHGETをしてHashの値を取得します
Hash構造なので一旦リストにデータを保存してからそのリスト情報を更にリストに保存したデータを返却します

では実行してみます
今度はredis-cliで事前に会話モードに入る必要はありません
redis-cliのオプションに「–eval」を指定してその引数にLuaスクリプトファイル名を指定します

$ ./redis-cli.exe --eval test.lua 'myhash*'
1) 1) "value1"
   2) "value"
2) 1) "value2"
   2) "value"
3) 1) "value3"
   2) "value"
4) 1) "value4"
   2) "value"

上記の場合はmyhashで始まるHashデータの各key情報が持つ値を取得して返してます

ちなみに事前に入れたデータは以下のとおりです

hset myhash key1 value1
hset myhash2 key1 value2
hset myhash3 key1 value3
hset myhash4 key1 value4
hset myhash key2 value
hset myhash2 key2 value
hset myhash3 key2 value
hset myhash4 key2 value

これでスクリプトファイルからも実行できるようになりました
最後にJavaから操作してみます

Javaへの組み込み

環境準備で説明したようにJedisのライブラリを使用するので事前に使用できるようにしておいてください

いきなりですがソースコードです
処理内容はスクリプトファイルで実施した内容と同じです

package test;

import java.util.List;

import redis.clients.jedis.Jedis;

public class LuaTest {

    private final static String SCRIPT = "" +
            "local keys = redis.call('keys', KEYS[1])" +
            "local hash_info_map = {}" +
            "for i,key in ipairs(keys) do" +
            "    local hash_info = {}" +
            "    local key1 = redis.call('HGET', key, 'key1')" +
            "    local key2 = redis.call('HGET', key, 'key2')" +
            "    hash_info[1] = key1" +
            "    hash_info[2] = key2" +
            "    hash_info_map[i] = hash_info " +
            "end " +
            "return hash_info_map";

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        List<List<String>> o = (List<List<String>>) jedis.eval(SCRIPT, 1, "myhash*");
        for (List<String> attr: o) {
            System.out.println(attr);
        }
    }

}

実行結果は以下のとおり

[value1, value]
[value2, value]
[value3, value]
[value4, value]

スクリプトファイルの内容をそのまま貼り付けたけなので特に説明する必要もないかもしれないですがポイントだけ

スクリプトの内容はString型で宣言すればOKです
サンプルのソースでは+を文字列で連携して見やすくしていますが全部1行に書いても問題ないです
ただ、Lua文法としては成立していなければいけません
ソースを良く見るとわかりますが、for文の始まりと終わり、endの前の1文の前にスペースが入っています
要は単純な文字列なのでJavaとしては文字がつながってしまうので文法エラーになる感じです
複数行にまたいでJavaに書くときは文末にスペースを入れておくのが無難です

あとはコールするメソッドはJedisクラスのオブジェクトに対してevalメソッドをコールします
これもインタフェース的にはスクリプトを直接実行したときと同じで「スクリプト」「keyの数」「key」「value」です
valueがある場合は、引数を4つで呼び出せるevalメソッドがあるのでそれを呼べばOKです
keyやvalueが複数ある場合は配列で渡せばOKです

また、evalの返り値は適切な型にCastしてあげる必要があります
今回はList内にListを突っ込んでいるのでList<List<String>>で受け取っています
単純のkey-valueの場合はString型でSET型の場合はListで受け取れます
間違った型で受け取ろうとするとCastExceptionが発生するのでわかると思います
あとはとりあえずObjectで受け取ってinstanceofで分岐することも可能だと思います

最後に

紹介は以上です
RedisはもともとLuaでアクセスできる仕組みを持っているので簡単にアクセスすることができました

そもそもなんでLuaを使うのかというポイントですが例えば処理対象のkeyが大量にありそのkeyをJava側で持ってきてからfor文を回すとJVMは遅いので非常に時間がかかったりします
そんなときにLuaスクリプトを書いて処理をRedis側に任せることで高速に処理できるようになる
のではないかと思っています
必ず効果が出るかどうかはやってみないとわかりませんが少なくともJVMよりかはRedis上での処理のほうが早いので検討する価値はあると思います

0 件のコメント:

コメントを投稿