トランザクションの分離レベルを実装する

データベースの勉強に、最初はかわいかったけどだいぶゴツくなったデータベースを作っています。
http://d.hatena.ne.jp/nowokay/20120817#1345197962


前回で、なんとなくトランザクションがちゃんと動いたんですけど、 id:kiwanami に「隔離レベルとかの検討とか。」と言われて、そのときはちょっと難しいから後回しかなーと思ったんだけど、案外簡単に実装できそうだったので、ちょっとやってみました。


トランザクションの分離レベルというのは、他のトランザクション内での変更がどのように見えるかというレベルです。
次のような4つのレベルがあります。

READ UNCOMMITTED 未コミットの操作も見えちゃう
READ COMMITTED コミットした操作は見えちゃう
REPEATABLE READ コミットした追加・削除は見えちゃう。更新は見えない
SERIALIZABLE トランザクション開始時のデータしか見えない

今回は、REPEATABLE READ以外を実装しました。

実装

まず分離レベルを定義します。

public enum IsolationLevel{
    READ_UNCOMMITTED,
    READ_COMMITTED,
    SERIALIZABLE
}


トランザクションオブジェクト生成時に分離レベルを指定するようにします。

public static class Transaction{
    /** トランザクションID */
    long txid;

    /** 分離レベル */
    IsolationLevel level;

    public Transaction(long txid, IsolationLevel level) {
        this.txid = txid;
        this.level = level;
    }


で、コンテキストでトランザクション開始するときに分離レベルを指定するようにします。省略時にはSERIALIZABLEです。

public void begin(){
    begin(IsolationLevel.SERIALIZABLE);
}

public void begin(IsolationLevel level){
    if(hasTransaction()){
        throw new IllegalStateException("already begin.");
    }
    ++currentTxid;
    tx = new Transaction(currentTxid, level);
}


そしたら、タプルの有効判定のときに、READ_UNCOMMITTEDなら全部有効、READ_COMMITTEDはコミット済みなら有効とします。

public boolean isAvailableTaple(TableTaple taple){
    if(level == IsolationLevel.READ_UNCOMMITTED){
        //READ_UNCOMMITTEDでは未コミットのデータも有効
        return true;
    }
    if(taple.isCommited()){
        if(level == IsolationLevel.READ_COMMITTED){
            //READ_COMMITTEDではコミット済みなら有効
            return true;
        }else{
            //SERIALIZABLEでは、トランザクション開始以前にコミットされたものだけ有効
            return taple.commitTx < txid;
        }
    }else{
        return taple.createTx == txid;
    }
}


あとは、変更履歴を取得するときに、READ_UNCOMMITTEDのときは取得なし、

public List<Taple> getModifiedTaples(Transaction tx){
    if(tx.level == IsolationLevel.READ_UNCOMMITTED){
        //READ_UNCOMMITTEDでは履歴からデータを返さない
        return Collections.EMPTY_LIST;
    }

READ_COMMITTEDならコミットデータは取得しないようにします。

if(mt.isCommited()){
    if(tx.level == IsolationLevel.READ_COMMITTED){
        //READ_COMMITTEDのときはコミット済みデータの履歴を使わない
        break;
    }
    if(mt.commitedTx < tx.txid){
        //すでにトランザクション前にコミットされているなら、変更履歴のデータを使わない
        break;
    }
}


これで、トランザクションの分離レベルが実装できました。
思いのほか簡単。正直、変更より、この解説のほうが時間がかかってます。

確認

まず、この時点でID5以上の商品はこのようになっています。

System.out.println(ctx.from("shohin").between("shohin_id", 5, 10));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id |shohin.price|
 |5|わかめ|null|250|
 |6|しいたけ|4|190|


さて、ここで、ID7の商品を追加して、ID6の商品を変更して、ID5の商品を削除してみます。

ctx.begin();
ctx.into("shohin").insert(7, "サニーレタス", 2, 230);
ctx.from("shohin").equalsTo("shohin_id", 6).update(new SubstOperation("price", 130));
ctx.from("shohin").equalsTo("shohin_id", 5).delete();


当然、同トランザクションからは変更結果が見えます。

System.out.println(ctx.from("shohin").between("shohin_id", 5, 10));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |6|しいたけ|4|130|
 |7|サニーレタス|2|230|


SERIALIZABLEトランザクションからは変更は見えません。

ctx2.begin(IsolationLevel.SERIALIZABLE);
System.out.println(ctx2.from("shohin").between("shohin_id", 5, 10));
ctx2.commit();
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |5|わかめ|null|250|
 |6|しいたけ|4|190|


未コミットなのでREAD_COMMITEDトランザクションからも変更は見えません。

ctx2.begin(IsolationLevel.READ_COMMITTED);
System.out.println(ctx2.from("shohin").between("shohin_id", 5, 10));
ctx2.commit();
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |5|わかめ|null|250|
 |6|しいたけ|4|190|


READ_UNCOMMITEDトランザクションからは変更が見えています。

ctx2.begin(IsolationLevel.READ_UNCOMMITTED);
System.out.println(ctx2.from("shohin").between("shohin_id", 5, 10));
ctx2.commit();
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |6|しいたけ|4|130|
 |7|サニーレタス|2|230|


ここで、READ_UNCOMMITEDトランザクションからはこの変更にさらに変更を加えることができます。
ただ、そうすると、今は実装していませんが、元変更を行ったトランザクションがabortを行ったときに、矛盾したデータができてしまう可能性があります。READ_UNCOMMITEDというのは、トランザクションの分離性がないということなので、このような不整合がおきます。


さて、ここでコミット前にトランザクションを開始しておきます。

ctx2.begin(IsolationLevel.SERIALIZABLE);
ctx3.begin(IsolationLevel.READ_COMMITTED);
Context ctx4 = new Context();
ctx4.begin(IsolationLevel.READ_UNCOMMITTED);


そうして、変更トランザクションをコミットします。

ctx.commit();


トランザクション開始後のコミットは、SERIALIZABLEトランザクションからは見えません。

System.out.println(ctx2.from("shohin").between("shohin_id", 5, 10));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |5|わかめ|null|250|
 |6|しいたけ|4|190|


READ_COMMITEDトランザクションでは、トランザクション開始後のコミットも見えます。

System.out.println(ctx3.from("shohin").between("shohin_id", 5, 10));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |6|しいたけ|4|130|
 |7|サニーレタス|2|230|


READ_UNCOMMITEDトランザクションからももちろん見えます。

System.out.println(ctx4.from("shohin").between("shohin_id", 5, 10));        
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |6|しいたけ|4|130|
 |7|サニーレタス|2|230|


ということで、トランザクションの分離レベルが実装できました。

ソース

ソースはこちら。実行サンプルも含めて2000行が近くなってきました。
https://gist.github.com/3380142/8c4827a7bf7f180f45c9e09aff106c553934dbb7