トランザクションに関して

866 views
Skip to first unread message

daidai

unread,
Jan 27, 2015, 10:08:42 PM1/27/15
to dbf...@googlegroups.com
お世話になります。

現在ある会員システムの保守をしており、バグが発生しております。

ある登録を行う際に、
selectで検索(複数)して、条件に合致する情報がない場合に、
insertする処理に入ります。

ユーザー環境のネットが切れていたのか不明ですが、短時間(100ミリ秒満たない)にアクセスが来た際に
二つ目が一つ目のinsertの処理前に処理され重複して登録される事が発生しております。

初めはDBのカラムをユニークで対応しようかと考えてましたが、テーブルをまたいでおり、
難しいかと考えております。

トランザクション管理をきちんと行う事が必要かと今は考えております。

Dbfluteでの行い方、参考になる情報等御座いましたらご教示頂ければ幸いで御座います。

現状のイメージ
=========================
try{
         check() // selectでのチェックです。
         insert() // insertします。
}catch(Exception e){

      // check()がダメならメソッド内でエラーを生成してここにきます。
}

kubo

unread,
Jan 27, 2015, 11:35:38 PM1/27/15
to DBFluteユーザの集い
jfluteです

こんにちは、フィードバックありがとうございます。

> ユーザー環境のネットが切れていたのか不明ですが、短時間(100ミリ秒満たない)にアクセスが来た際に
> 二つ目が一つ目のinsertの処理前に処理され重複して登録される事が発生しております。

状況を整理させてください。

A と B がほぼ同時に同じデータをリクエストして、

1. A が select してそのデータがDBにないことを確認
2. B が select してそのデータがDBにないことを確認
3. A が そのデータを insert してDBに登録
4. B が そのデータを insert してDBに登録

本来は、事前にselectしてそのデータがないかどうかを確認しているので、
B は 先に A が登録データを検知して insert を取りやめるはずだけど、
AとBの事前selectがほぼ同時なのでAもBもinsertされてしまう、
というようなことでしょうか?


もし、そうであれば、
通常は、PK制約違反、もしくはユニーク制約違反で落ちるかと思います。
例えば、事前selectのwhere句に列挙している条件にユニーク制約が貼られていれば、
そういう状況でも EntityAlreadyExistsException が発生して重複登録にはなりません。

もし、DB設計的に「ユニーク制約を貼れる状況なのに貼られてない」
というのであれば、いまからでもユニーク制約を貼ることをおススメします。
ぼくは、ユニーク制約貼れるところには必ず貼りますね。
ちなみに、その場合は事前selectも不要ですね。
insertしてその例外が発生したのであれば、すでに同じデータが存在していると言えるので。

もし、どうしてもユニーク制約を貼れない、もしくは、もっと複雑な業務条件での
重複チェックなのであれば、insert()を二回実行してしまうのをなんとかして防がないといけません。


トランザクション管理というキーワードが出ていましたが、
トランザクションは発行されていますでしょうか?
(selectとinsertは同じトランザクションで実行していますでしょうか?)
まず、selectとinsertを同じトランザクションにしないと防ぎようがないかなと。

ただもちろん、同じトランザクションにしただけではダメで、
トランザクション分離レベルが普通のReadCommittedだと、
select同士は待ちにならないので(通常はなったら困るし)、
select for update で更新ロックを取るなどしないといけないかなと。
ただ、存在しないレコードに対して select for update で待つかどうか、
ちょっとパッとどうだったかな!? って感じなので、
マルチスレッドのテストを書いて試してみるといいかと思います。

その辺、DBMSによって挙動が変わるかと思います。
こういうとき、DBFluteのバージョンや利用しているDBMSの種類とバージョン、
トランザクションの設定などの情報を一緒に載せて頂けると、
ML閲覧者の方もコメントしやすいかなと思います。
> --
> このメールは Google グループのグループ「DBFluteユーザの集い」に登録しているユーザーに送られています。
> このグループから退会し、グループからのメールの配信を停止するには dbflute+u...@googlegroups.com
> にメールを送信してください。
> このグループに投稿するには dbf...@googlegroups.com にメールを送信してください。
> http://groups.google.com/group/dbflute からこのグループにアクセスしてください。
> その他のオプションについては https://groups.google.com/d/optout にアクセスしてください。

kubo

unread,
Jan 27, 2015, 11:38:50 PM1/27/15
to DBFluteユーザの集い
jfluteです。

> 初めはDBのカラムをユニークで対応しようかと考えてましたが、テーブルをまたいでおり、
> 難しいかと考えております。

すいません、ユニークは検討したけれどもできないということでしたね。
「テーブルをまたいでおり」っていうのがちょっとわからなかったのですが、
単純に一つのテーブルで事前selectしてinsertじゃなくて、
複数のテーブルのカラムを組み合わせてユニーク性を確認しないといけない
ような条件なのでしょうか。。。

kubo

unread,
Jan 28, 2015, 12:24:05 AM1/28/15
to DBFluteユーザの集い
jfluteです

ちょっとMySQLで試してみましたが、
(UTFluteのcannonball()を使って検証、MySQL-5.6でRepeatableRead)

cannonball(car -> {
car.projectA(dragon -> {
dragon.expectNormallyDone();
memberBhv.selectEntity(cb -> {
cb.query().setMemberId_Equal(99999);
cb.lockForUpdate();
});
}, 1);
car.projectA(dragon -> {
dragon.expectOvertime();
memberBhv.selectEntity(cb -> {
cb.query().setMemberId_Equal(99999);
cb.lockForUpdate();
});
}, 2);
}, new CannonballOption().threadCount(2));

存在しないレコード同士だとさすがに待ちませんね。


複数テーブルにまたがるユニークという話ですが、
中心となるテーブル (一番最初にinsertするテーブル!?) に、
そのユニーク性を表現するカラムを追加するってのも手かもしれません。
ちょっとDB構造を想像しているだけですが、
複数の構成要素を束ねたハッシュのようなカラムがあってもいいのかと。
こういう問題が起きるということは、それ以外の場面でもユニーク性を
追従するのが難しいのではないか!? ということなので。

daidai

unread,
Jan 28, 2015, 4:00:41 AM1/28/15
to dbf...@googlegroups.com
jflute様

お忙しい中、ご回答頂き誠にありがとう御座います。

これから色々調査させて頂きますが、とりあえず返信出来る限りで、
記載させて頂きます。

------------------------------------------------------------------------------------------------------------
本来は、事前にselectしてそのデータがないかどうかを確認しているので、
B は 先に A が登録データを検知して insert を取りやめるはずだけど、
AとBの事前selectがほぼ同時なのでAもBもinsertされてしまう、
というようなことでしょうか?
→おっしゃる通りです。

複数のテーブルのカラムを組み合わせてユニーク性を確認しないといけない
ような条件なのでしょうか。。。
→仰る通りです。
------------------------------------------------------------------------------------------------------------

またDBはpostgresです。

以外に関しては、一旦調査させて頂きます。

重ねてでは御座いますがお忙しい中、ご回答頂き誠にありがとうございます。

以上、何卒よろしくお願い致します。



2015年1月28日水曜日 14時24分05秒 UTC+9 jflute:

kubo

unread,
Jan 28, 2015, 11:44:37 PM1/28/15
to DBFluteユーザの集い
jfluteです

> またDBはpostgresです。
PostgreSQLで for update してみましたが結果は同じですね。
まあ、存在しないレコードでロックかけて止まっちゃったら、
それはそれで困るので当然かもですね。


結局のところ、どこかでAtomicなユニークチェックをしないといけなくて、
それがユニーク制約であればDBMSの中でできるということで、
それができなければアプリ側でsynchronizedなどでシリアルにする
しかないのかなぁという風には思います。

ただ、アクセス数が多いということなので、
それをやってしまうとちょっとスピードに影響が出る可能性があるので、
その辺、その業務との兼ね合いですね。

daidai

unread,
Feb 6, 2015, 12:46:54 AM2/6/15
to dbf...@googlegroups.com
 お世話になっております。

上記の件で御座いますが、DBの構造変更は回避しようと考えております。
資料が乏しい、コスパ等々でそういった判断になりました。

そこでストアドプロシージャ(plpgsql)で処理を一括で行い、
処理前に関係するテーブルを全て完全ロックをかけ、
一連の作業を行い、完了出来ればと現在考えております。

ただ現在、問題点が御座いまして、もし何かご助言頂ければ幸いで御座います。

ストアドプロシージャを呼び出して、処理を一括でやる想定でいました。
ProcedurePmbを生成して実行すると。。
この場合だと、ストアドプロシージャ内でBEGINやらCOMMITが出来ないので、
ProcedurePmb実行前にBEGINやらCOMMITが書けずロックが出来ず、厳しいです。

ならばと外出しSQLに一連を書いてみたらエラーが返ってきており、
おそらくストアドプロシージャは難しいのかと。

何か解決に近づける方法等御座いましたらご教示頂ければ幸いで御座います。

以上、宜しくお願い致します。



kubo

unread,
Feb 6, 2015, 6:28:51 AM2/6/15
to DBFluteユーザの集い
jfluteです

daidaiさん、こんばんは

> そこでストアドプロシージャ(plpgsql)で処理を一括で行い、
> 処理前に関係するテーブルを全て完全ロックをかけ、
> 一連の作業を行い、完了出来ればと現在考えております。

思い切った作戦ですね…
完全ロックってテーブルロック (LOCK TABLE) でしょうか?

もしそうだと、完全にシリアルな処理になると思いますが、
そもそもそれが許容されるあれば Java 側で synchronize で
そこの一連の処理を囲ってもいいのかなって思いました。

ちょっと、他の状況とかわからないの違うかもですが、
LOCK TABLE しちゃうと別のロジックのそのテーブルへの
アクセスに待ちが発生してしまわないかがちょっと心配です。

> ストアドプロシージャを呼び出して、処理を一括でやる想定でいました。
> ProcedurePmbを生成して実行すると。。
> この場合だと、ストアドプロシージャ内でBEGINやらCOMMITが出来ないので、
> ProcedurePmb実行前にBEGINやらCOMMITが書けずロックが出来ず、厳しいです。

すいません、これがちょっとよくわからなかったのですが...
o PostgreSQLのストアドプロシージャではBEGIN/COMMITができない前提がある?
o ProcedurePmb実行前というのは、Javaの呼び出し側ということ?
でしょうか?
要はトランザクション内でストアドプロシージャを呼び出して、
その中で LOCK TABLE して業務ロジックを実行したいということになりますかね?

Java側のトランザクションがストアドプロシージャにも継続すると思うので、
(PostgreSQLはわからないですが、Oracleとかそうだったような…)
普通のJava側のトランザクション内でProcedurePmbを実行してできないですかね?

> ならばと外出しSQLに一連を書いてみたらエラーが返ってきており、
ここの一連とは、業務ロジックでしょうか?
それともBEGIN/LOCK TABLE/COMMITのトランザクション・ロック制御でしょうか?
Reply all
Reply to author
Forward
0 new messages