MVCCトランザクションの動きが実装できた

データベースの勉強にかわいいデータベースを実装してたつもりが、だいぶゴツくなってきました。
http://d.hatena.ne.jp/nowokay/20120817#1345197962


前回、insertだけトランザクションが効いていたので、今回はupdate/deleteでもトランザクションが効くようにします。
insertでは、新しく追加したタプルにトランザクションIDを付加して、そのコミット以前に始まったトランザクションからはそのタプルを隠すことで、トランザクションの隔離を行っていました。
update/deleteでは、古いデータを保存しておく必要があります。

実装

そこで、次のようなクラスを用意して、古いデータを保持できるようにします。実際にはこれを継承してUpdatedTapleとDeletedTapleを定義しています。

public static abstract class ModifiedTaple{
    TableTaple oldTaple;
    long modifyTx;
    long commitedTx;

    public ModifiedTaple(long modifyTx, TableTaple oldTaple) {
        this.oldTaple = oldTaple;
        this.modifyTx = modifyTx;
    }
    public boolean isCommited(){
        return commitedTx != 0;
    }
    public void commit(long curTxid){
        commitedTx = curTxid;
    }
}


これを各テーブルで次のようにして保持します。Mapのキーはoidで、同じ行についての変更をまとめます。

Map<Long, List<ModifiedTaple>> modifiedTaples = new HashMap<>();


そうすると、データ抽出時に次のようにしてトランザクションIDにあったデータが抽出できます。

public List<Taple> getModifiedTaples(Transaction tx){
    List<Taple> result = new ArrayList<>();
    for(List<ModifiedTaple> list : modifiedTaples.values()){
        //一番新しいものを使うので逆順に走査
        for(Iterator<ModifiedTaple> di = ((LinkedList<ModifiedTaple>)list).descendingIterator();
                di.hasNext(); ){
            ModifiedTaple mt = di.next();
            if(mt.modifyTx == tx.txid){
                //同じトランザクションで変更されているなら
                //変更履歴のデータを使わない
                break;
            }
            if(mt.isCommited() && mt.commitedTx < tx.txid){
                //すでにトランザクション前にコミットされているなら
                //変更履歴のデータを使わない
                break;
            }
            if(tx.isAvailableTaple(mt.oldTaple)){
                result.add(mt.oldTaple);
                break;
            }
        }
    }
    return result;
}


あとは、更新削除でmodifiedTaplesに登録するようにすれば完了です。

更新確認

ということで、動作確認してみます。
まずトランザクションctxを開始してshohin_id:8の商品を200円に変更します。元は250円でした。

ctx.begin();
ctx.from("shohin").equalsTo("shohin_id", 8).update(new SubstOperation("price", 200));

当該トランザクションctxからは変更が見えます。

System.out.println(ctx.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|200|

他のトランザクションctx2からは見えず、250円のままです。

System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|250|


このとき、他のトランザクションctx2で変更するとエラーになります。

try{
    ctx2.from("shohin").equalsTo("shohin_id", 8).update(new SubstOperation("price", 150));
    System.out.println("エラーが出るはず");
}catch(IllegalStateException ex){
    System.out.println("変更の衝突");
}
変更の衝突


他のトランザクションctx2からの削除でもエラーになります。

try{
    ctx2.from("shohin").equalsTo("shohin_id", 8).delete();
    System.out.println("エラーが出るはず");
}catch(IllegalStateException ex){
    System.out.println("削除の衝突");
}
削除の衝突


もちろん、同一トランザクションでは変更できます。

ctx.from("shohin").equalsTo("shohin_id", 8).update(new SubstOperation("price", 180));
System.out.println(ctx.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|180|


トランザクションでは相変わらずもとの値 250円です。

System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|250|


トランザクションをctxコミット前にはじめていると、やはり変更は見えません。

ctx2.begin();
ctx.commit();
System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
ctx2.commit();
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|250|


もちろんctxコミット後のトランザクションからは見えます。

System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|180|


また、ctxコミット後なら変更できるようになります。

ctx2.from("shohin").equalsTo("shohin_id", 8).update(new SubstOperation("price", 220));
System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|220|


ここで、一連の処理の前にctx3トランザクションを始めていました。

Context ctx3 = new Context();
ctx3.begin();


そうすると、このctx3トランザクションからは相変わらず古い値が見えています。

System.out.println(ctx3.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|250|


ということで、トランザクションを開始すると、それ以降の変更が隔離されていることがわかります。

削除確認

さて、削除も簡単に確認してみます。
トランザクションctxを開始してデータを削除します。

ctx.begin();
ctx.from("shohin").equalsTo("shohin_id", 8).delete();


もちろん、当該トランザクションctxからは見えません。

System.out.println(ctx.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|


他のトランザクションctx2からはまだ見えます

System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|220|


そして、あいかわらずcommitしていない古いトランザクションからは、最初の値250が見えます。

System.out.println(ctx3.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|
 |8|レタス|2|250|


コミットすると、他のトランザクションからも見えなくなります。

ctx.commit();
System.out.println(ctx2.from("shohin").equalsTo("shohin_id", 8));
 |shohin.shohin_id|shohin.shohin_name|shohin.kubun_id|shohin.price|


削除もうまくトランザクションが効いているようです。

トランザクションについて残りの処理

実は、まだ、インデックスを使ったgroup byとorder byでは、トランザクションの処理を行っていません。ちょっとめんどいので。
それと、abortの処理は単にトランザクションを未コミットのまま放置する感じになっているので、ちゃんとabort処理を行う必要があります。
また、いまの状態だと変更履歴はどんどんたまっていく一方なので、トランザクションが終わったら不要な変更履歴を消去する必要があります。


そしてもうひとつ、タプルをTapleとずっとスペルミスしたままなので、一段落したら修正します。
このスペルミスには、こういう深い理由があるようですw
https://twitter.com/shinsan68k/status/237451173106446337

参考

MVCCについては、この本の説明をヒントにしました。

RDBMS解剖学 よくわかるリレーショナルデータベースの仕組み (DB Magazine Selection)

RDBMS解剖学 よくわかるリレーショナルデータベースの仕組み (DB Magazine Selection)

データベースの仕組みについて詳しい説明を読む前に見るといい感じです。

ソース

ソースはこれです。1600行を超えちゃいましたね。
https://gist.github.com/3380142/7243d9017eedb8c18db7954131d6c738ab315a98