CLOVER🍀

That was when it all began.

Hazelcastのローカルトランザクションを試す

Hazelcast&Clojureネタ。

Clojure書くの、約1ヶ月ぶりでだいぶ忘れてました(笑)。一時期けっこう書いていたのに、書かなくなると慣れない言語は忘れるのが早いですね〜。

…とそれはさておき、今回はHazelcastのトランザクション機能を試してみます。

Hazelcastには、トランザクション機能があって、ローカルトランザクションとJTAとの統合があります。今回は、ローカルトランザクションを使用します。

Transaction Interface
http://www.hazelcast.com/docs/3.1/manual/single_html/#TransactionInterface

Hazelcastのバージョンは、微妙に上がって3.1.1です。

Hazelcastでトランザクション付きのデータ構造を使用するには、主に以下のクラスやインターフェースを使用します。

  • TransactionOptions
  • TransactionContext

また、利用できるデータ構造は、TransactionContextインターフェースの上位インターフェースである、TransactionalTaskContextに定義があります。

TransactionalTaskContext
http://www.hazelcast.com/docs/3.1/javadoc/com/hazelcast/transaction/TransactionalTaskContext.html

これを見ると、利用できるのは

  • List
  • Map
  • MultiMap
  • Queue
  • Set

のようです。ドキュメントではQueue、Map、Setを使っていますが、今回はMapのみとします。

Hazelcastのトランザクションの特徴は、こんな感じらしいです。

  • トランザクション分離レベルは、REPEATABLE_READ
  • Queueに対するpoll、offer操作は、オブジェクトがコピーされるので、コミット/ロールバックに対して安全
  • MapやSetに対しては最初にロックを取得し、コミット/ロールバック時にロックを破棄する

トランザクション分離レベルがREPEATABLE_READ、トランザクション開始時点でコミット済みのデータしか読み取らないはず…ですが、ファントムリード的なものを含めて今回は確認していません。

雛形コード

ま、それはそうと進めてみましょう。まずは雛形コードを。

(require '[leiningen.exec :as exec])

(exec/deps '[[com.hazelcast/hazelcast "3.1.1"]])

(ns hazelcast-local-transaction
  (:import (com.hazelcast.config Config)
           (com.hazelcast.core Hazelcast HazelcastInstance IMap TransactionalMap)
           (com.hazelcast.transaction TransactionContext TransactionNotActiveException)
           (com.hazelcast.transaction TransactionOptions TransactionOptions$TransactionType)))


(try
  ;; HazelcastInstanceを作成する
  (let [^Config config (Config.)
        ^HazelcastInstance hazelcast (Hazelcast/newHazelcastInstance config)
        ;; ローカルトランザクションを使用するように設定
        ^TransactionOptions options (-> (TransactionOptions.)
                                        (.setTransactionType (TransactionOptions$TransactionType/LOCAL)))
        ^TransactionContext context (. hazelcast newTransactionContext options)]

    ;; ここを埋めていく

    ;; HazelcastInstanceをシャットダウンする
    (.. hazelcast getLifecycleService shutdown))

  ;; 全Hazelcastインスタンスをシャットダウンする
  (finally (Hazelcast/shutdownAll)))

トランザクションを使用しない時と違うのは、TransactionContextを作成しているところですね。

        ;; ローカルトランザクションを使用するように設定
        ^TransactionOptions options (-> (TransactionOptions.)
                                        (.setTransactionType (TransactionOptions$TransactionType/LOCAL)))
        ^TransactionContext context (. hazelcast newTransactionContext options)]

TransactionOptionsは指定しなくても、一応TransactionContextを作成することができます。ちなみに、ドキュメントではnewしたTransactionOptionsをどこにも使っていません…。

以後、ここで生成したTransactionContextから、トランザクションの開始、コミット/ロールバックを行ったり、TransactionalMapやTransactionalSetなどを取得していくことになります。

コミット

では、早速TransactionalMapを取得してみましょう。

    (try
      ;; トランザクションを開始せず、TransactionalMapを取得しようとすると
      ;; 例外となる
      (. context getMap "transactional-map")
      (catch TransactionNotActiveException e (println e)))

と言いたいところですが、トランザクションを開始しないままTransactionalMapを取得しようとすると、TransactionNotActiveExceptionがスローされます。

というわけで、まずはトランザクションを開始します。

    ;; トランザクション開始
    (.. context beginTransaction)

これで、TransactionalMapが取得できるようになるので、取得後データを登録してみます。

    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (. transactional-map put "key1" "value1")
      (. transactional-map put "key2" "value2")
      (. transactional-map put "key3" "value3"))

Mapライクなメソッドを提供していますが、実はIMapでもなければ、Mapインターフェースを実装しているわけでもありません。

終了したら、コミット。

    ;; コミット
    (.. context commitTransaction)

基本的なコミットパターンでした。

では、値が入ったことを確認しましょう。ここでは、IMapを使用します。後述しますが、読み取りにはIMapを使った方が無難だと思います。

    ;; 値を見る時には、IMapで
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (assert (= (. transactional-map get "key1") "value1"))
      (assert (. transactional-map containsKey "key2"))
      (assert (= (. transactional-map get "key3") "value3")))

IMapの取得になるので、今度はHazelcastInstance#getMapを呼び出すことになります。

それに、値を見るためだけにトランザクションを開始するのも微妙ですしね…。なお、HazelcastInstance#getMapの引数として与える名前は、TransactionContext#getMapと同じ名前であることに注意してください。

ロールバック

続いて、ロールバックを試してみましょう。

    ;; ロールバックを試す
    (.. context beginTransaction)

    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (. transactional-map put "key4" "value4")
      (. transactional-map put "key5" "value5")
      (. transactional-map put "key6" "value6"))

    ;; ロールバック
    (.. context rollbackTransaction)

最後がTransactionContext#rollbackTransactionとなります。

で、値の確認ですが、やはりIMapを使用します。

    ;; IMapで確認しないと、TransactionalMapで見てしまうと値が見えてしまう模様
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (assert (nil? (. transactional-map get "key4")))
      (assert (false? (. transactional-map containsKey "key5")))
      (assert (nil? (. transactional-map get "key6"))))

コメントにも書いていますが、ここでだいぶハマりました。TransactionalMapで見ちゃうと、ロールバックしたつもりの値が見えちゃうんですよね…。

もちろん、このコードではロールバックしていることが確認できています。

最初に試したのは、こんなコードでした。

    ;; TransactionalMapで見るために、トランザクションを開始
    (.. context beginTransaction)

    ;; TransactionalMapで見ると、ロールバックしても値が見えてしまう模様…
    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (assert (= (. transactional-map get "key4") "value4"))
      (assert (true? (. transactional-map containsKey "key5")))
      (assert (= (. transactional-map get "key6") "value6")))

    ;; ロールバック
    (.. context rollbackTransaction)

assertの条件がIMapの時と逆になっていますが、ロールバックしたはずの値が見えていたり、キーが認識できたりしています。最後にロールバックしているのは、コミットしようとすると、処理の場所によってはエラーになるみたいで…このあたり、よくわかりません。

このコードの場合は、こんな感じでエラーになります。

    ;; コミットしようとすると、こんなエラーを見たりします…
    ;; (.. context commitTransaction)
    ;; java.lang.IllegalStateException: An operation[BasePutOperation{transactional-map}] can not be used for multiple invocations!

で、TransactionalMapのままだとロールバックしたはずの値が見えてしまうけんですが、最初まったく意味が分からずに諦めようかとも思ったのですが。そもそもテストってどうやってるんだっけ?というところから、テストコードを見るとTransactionalMapに登録してIMapで見ているところから、この結論になりました。

これは、想定通りの動作なのでしょうか?バグ?

ドキュメントにも、まったく書いていませんしねぇ…。

オートコミット的な

TransactionlMapと同じ名前で取得するIMapに対しても、更新可能らしいです。

    ;; ちなみに、同じ名前でIMapにputするのはOKらしいです
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (. transactional-map put "key7" "value7"))

    ;; TransactionalMapで見るために、トランザクションを開始
    (.. context beginTransaction)
    ;; TransactionalMapからも、確認可能
    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (assert (= (. transactional-map get "key7") "value7")))
    ;; ロールバック
    (.. context rollbackTransaction)

結果は、TransactionalMapからも参照することができます。

というわけで、微妙なところはありましたが、Hazelcastのトランザクションの確認でした。JTAとの統合は、なかなか大変そうなので、別の機会に。

最後に、書いたコードです。以下のコマンドで実行することが前提になっています。

$ lein exec hazelcast_local_transaction.clj

hazelcast_local_transaction.clj

(require '[leiningen.exec :as exec])

(exec/deps '[[com.hazelcast/hazelcast "3.1.1"]])

(ns hazelcast-local-transaction
  (:import (com.hazelcast.config Config)
           (com.hazelcast.core Hazelcast HazelcastInstance IMap TransactionalMap)
           (com.hazelcast.transaction TransactionContext TransactionNotActiveException)
           (com.hazelcast.transaction TransactionOptions TransactionOptions$TransactionType)))


(try
  ;; HazelcastInstanceを作成する
  (let [^Config config (Config.)
        ^HazelcastInstance hazelcast (Hazelcast/newHazelcastInstance config)
        ;; ローカルトランザクションを使用するように設定
        ^TransactionOptions options (-> (TransactionOptions.)
                                        (.setTransactionType (TransactionOptions$TransactionType/LOCAL)))
        ^TransactionContext context (. hazelcast newTransactionContext options)]
    (try
      ;; トランザクションを開始せず、TransactionalMapを取得しようとすると
      ;; 例外となる
      (. context getMap "transactional-map")
      (catch TransactionNotActiveException e (println e)))


    ;; トランザクション開始
    (.. context beginTransaction)

    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (. transactional-map put "key1" "value1")
      (. transactional-map put "key2" "value2")
      (. transactional-map put "key3" "value3"))

    ;; コミット
    (.. context commitTransaction)


    ;; 値を見る時には、IMapで
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (assert (= (. transactional-map get "key1") "value1"))
      (assert (. transactional-map containsKey "key2"))
      (assert (= (. transactional-map get "key3") "value3")))


    ;; ロールバックを試す
    (.. context beginTransaction)

    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (. transactional-map put "key4" "value4")
      (. transactional-map put "key5" "value5")
      (. transactional-map put "key6" "value6"))

    ;; ロールバック
    (.. context rollbackTransaction)


    ;; IMapで確認しないと、TransactionalMapで見てしまうと値が見えてしまう模様
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (assert (nil? (. transactional-map get "key4")))
      (assert (false? (. transactional-map containsKey "key5")))
      (assert (nil? (. transactional-map get "key6"))))


    ;; TransactionalMapで見るために、トランザクションを開始
    (.. context beginTransaction)

    ;; TransactionalMapで見ると、ロールバックしても値が見えてしまう模様…
    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (assert (= (. transactional-map get "key4") "value4"))
      (assert (true? (. transactional-map containsKey "key5")))
      (assert (= (. transactional-map get "key6") "value6")))

    ;; ロールバック
    (.. context rollbackTransaction)
    ;; コミットしようとすると、こんなエラーを見たりします…
    ;; (.. context commitTransaction)
    ;; java.lang.IllegalStateException: An operation[BasePutOperation{transactional-map}] can not be used for multiple invocations!


    ;; ちなみに、同じ名前でIMapにputするのはOKらしいです
    (let [^IMap transactional-map (. hazelcast getMap "transactional-map")]
      (. transactional-map put "key7" "value7"))

    ;; TransactionalMapで見るために、トランザクションを開始
    (.. context beginTransaction)
    ;; TransactionalMapからも、確認可能
    (let [^TransactionalMap transactional-map (. context getMap "transactional-map")]
      (assert (= (. transactional-map get "key7") "value7")))
    ;; ロールバック
    (.. context rollbackTransaction)


    ;; HazelcastInstanceをシャットダウンする
    (.. hazelcast getLifecycleService shutdown))

  ;; 全Hazelcastインスタンスをシャットダウンする
  (finally (Hazelcast/shutdownAll)))