2012年2月1日水曜日

#JAVA - 文字列を元にシャーディングする際に便利なクラスを書いてみた

GAEでアクセス先をシャーディングする際に、IDなどから振り分けを行えるクラスを書いてみた。
入力文字列に基づいて指定したビット範囲の数値を返すので、それを利用してアクセス先を振り分けることが出来る。

使い方としては、以下の用に接頭語を付けてキーを作成する。

private static Key createShardKey(String inPrefix, String inKeyName) {
    int bitSize = 8;
    long hashNum = Digest.getHashNum(inKeyName, bitSize);
    String key = inPrefix + String.format("_%05x", hashNum);

    return Datastore.createKey(Hoge.class, key);
}

データの読み込みはキーの名前を文字列に格納し、クエリで取得する。

(ここでは検索用にキーとは別に文字列型のフィールドを用意していますが、キーに対して前方一致はが出来ればな良いのにな~、と思いました。
少しでも不要なインデックスを減らしたいので。)

Datastore.query(META)
            .filter(META.keyName.startsWith("Prefix"))
            .asList();


処理の内容としては、文字列をハッシュ変換し、その先頭から数値を取り出しています。
取り出す数値の範囲は出来るだけ偏りが無いように、ビットで指定します。
なので2, 4, 6, ...と2の倍数までの値となります。

package auction.service.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public class Digest {


    /**
     * 指定したビット範囲内のハッシュを取得
     *
     */
    public static long getHashNum(String str, int bit) {

        byte[] hash = digest(str);
        long num = 0;

        /* ハッシュ値の先頭8byteをLongへ変換 */
        for (int cnt_i = 0; cnt_i < 8; cnt_i++) {
            num = (num << 8) | (hash[cnt_i] & 0xff);
        }

        /* 指定ビットでマスク */
        long mask = getMask(bit);

        num = num & mask;

        return num;
    }

    /**
     * ハッシュ文字列を取得
     *
     */
    public static String getHash(String str) {

        byte[] hash = digest(str);

        return hexToString(hash);
    }


    /**
    * 文字列をハッシュ化した結果を表示
    *
    */
   public static void printDigest(String str) {

       byte[] hash = digest(str);

       System.out.println("文字列:" + str);
       System.out.println(hash.length);
       System.out.println(hexToString(hash));
   }

   /**
    * メッセージダイジェスト (固定長のハッシュ値) を取得
    *
    */
   private static byte[] digest( String inText ) {

       MessageDigest dig = null;

       try {
           dig = MessageDigest.getInstance("SHA-256");

       } catch (NoSuchAlgorithmException e) {
           e.printStackTrace();
           return null;
       }

       dig.update(inText.getBytes());

       return dig.digest();
   }

   /**
    * ビットマスクを取得
    *
    */
   private static long getMask(int bit) {

       long mask = 0L;

       for (int cnt_i = 0; cnt_i < bit; cnt_i++) {
           mask = (mask << 1) + 1L;
       }

       return mask;
   }

   /**
    * バイト列を文字列へ変換
    *
    */
    private static String hexToString(byte[] bin) {

        StringBuilder sb = new StringBuilder();
        int size = bin.length;

        for (int i = 0; i < size; i++) {
            String hex = String.format("%02x", bin[i]);

            sb.append(hex);
        }

        return sb.toString();
    }

}