例によって 素人の妄言 なので生真面目にとらないように。 あ、使ってるSQLite3バージョンは3.3.8です。OSはLinux kernel-2.6.22.6。
組込データベースとしては最も有名なSQLite3。 非常に軽量かつ高速、しかもライセンスが パブリックドメイン なので、 市販されているソフトウェアや組込機器にも多数使われている。
ネットワークやユーザの概念はないけれど、ローカルファイルをデータベースに仕立て挙げることができ、 トランザクションの概念 もしっかりあるため、「ぶっちゃけ今書いた内容なし!(rollback)」 できたりするわけだ。自分で生ファイルを管理するとデータの一貫性を維持するのにすごい気をつかう訳だが、 SQLite3を使うとSQL文脈で記述することができるのでマジ便利。
「database is locked(5)」 。少しSQLite3を使うとこのメッセージを頻繁に目にすることになる。 「5」ってのはsqlite3.hの定数で言うところの SQLITE_BUSY に相当する。 要するに他のプロセスが使ってるから使えないってこと...らしいのだが... 頻繁過ぎるので 調べてみた。
データベースのテーブルtest1。
create table test1 ( id integer primary key, text text );
書き込み側プログラムb.pl(PerlでDBIとDBD::SQLiteを使った)
#!/usr/bin/perl use strict; use DBI; use Time::HiRes qw( usleep ); my $dbh = DBI->connect( 'DBI:SQLite:dbname=/tmp/test.db', undef, undef, {RaiseError => 0, AutoCommit => 0} ); for (my $i = 0; $i < 1000; $i++) { my $teststring = "$i*******************"; my $sql = 'insert into test1 values(null, ?)'; my $sth = $dbh->prepare($sql); my $rv = $sth->execute($teststring); $dbh->commit(); $sth = ''; print "$i\n"; usleep(30); } $dbh->disconnect();
読み込み側プログラムa.pl。
#!/usr/bin/perl use strict; use DBI; use Time::HiRes qw( usleep ); my $dbh = DBI->connect( 'DBI:SQLite:dbname=/tmp/test.db', undef, undef, {RaiseError => 0, AutoCommit => 0} ); my $sql = 'select id from test1'; my $sth = $dbh->prepare($sql); for (my $i = 0; $i < 1000; $i++) { my $rv = $sth->execute(); while (my $ref = $sth->fetch()) { my ($id) = @$ref; } print "$i\n"; usleep(30); } $sth = ''; $dbh->disconnect();
「読み込みは普段フリー(高速)だが、書き込みすると読み込みが待たされる」 はずだったのだが..... 上記の例だと読み込みが動き出すと 書き込み側のcommit()が全くできなくなる ようだ。
試しにa.pl, b.pl両方に以下の行を足してみる。sqlite3.hのsqlite3_busy_timeout()関数に相当する。 SQLITE_BUSYになる(エラーで返る)までの待ち時間をミリ秒単位で設定できるものらしい。
$dbh->func(15, 'busy_timeout');
すると例のエラーがでるようになった。 しかしb.pl側で....
DBD::SQLite::db commit failed: database is locked(5) at dbdimp.c line 218 at b.pl line 20.
なんとか「複数のプロセスのselectの合間に、insert/updateを実行する」ようにならないだろうか。 これができないと電子掲示板みたいなアプリはほとんど全く動かなくなってしまう。
a.plのループ内にcommit()を追加した。
while (my $ref = $sth->fetch()) { my ($id) = @$ref; } $dbh->commit(); # ※これ追加
常識で考えればselect文のみであればDBには変更が加わらないのだからcommit()は全く不要...のはず(理解間違ってる?? )。 しかしなぜかうまくいく。 ソース本体を読んでないから断言はできないが、すごーーーーーーく 嫌な予感がするのは俺だけだろうか。 SQLite3は行ロックでもテーブルロックでもなくDBロック 、というのは 多分真実だとしても、その実装がセマフォではなく 本気でmutexのみ なのではなかろうか。 いやPerlのDBDが悪さをしているという可能性もあるけれども。テストプログラムをCで書いてみるべきかな....。
余談だがa.plのconnectをこうしてもうまくいく。
my $dbh = DBI->connect( 'DBI:SQLite:dbname=/tmp/test.db', undef, undef, {RaiseError => 0, AutoCommit => 1} # ※ AutoCommitをON );
なんだかな。本当かよー??という感じですが。
内部のロジックに関しては、私も素人なので、何とも言えませんが、sqliteは、読みの時も書きの時もデータベース全体をロックします。読みの時にロックするのは、Oracleでいうところの「読み込み一貫性」を確保するためではないかと思います。
なので、autocommitにすると、b.plのSELECT後のusleepでロックがいったん解放されるので、そういう動作になります。
ちなみに、DBロックを実現する方法ですが、flockだったように記憶しています
ああ、すみません、b.plじゃなくて、a.plですね。selectしてるのは
本来はselectのみでもトランザクションは必要です。
関連するデータ(A,B)を2回に分けて読み込んでいるとき、Aだけ読み込んだところで
他のプロセスが(A',B')に書き換えてしまうと、最終的に(A,B')という
矛盾したデータを読み込んでしまうことになります。
Oracleとかはすごく頑張ってselectのみの時は利用者側が明示的に
トランザクションを使わなくてもいいように見せていますが、
SQLiteはそこは利用者側が管理することで
軽くしているということですね。
あーーーー!!!!! そういうことですかーーーー!!!!
すごい助かりマッスル!!!!!!
.....えーと。結局SQLite3はユーザがexecuteやcommitで
リトライ処理を行うラッパーを書かない限り、
MySQLみたいな感覚で使っちゃダメってことですかね。
で、PHP5って確か標準DBMS(MS?)がSQLiteだったよーな気が
するんですがどうなってるんでしょうかね。
2プロセスでも問題おきるんだから、ましてやサーバなんかで
つかいものになる訳がないと思うのですが。
対策してあるのかなあ....
PHP5がどうなっているのかは分かりませんが、普通1トランザクションで
select1000回も発行したりしないので、問題にならないのではないかと。
(Perlでいう)AutoCommitがデフォルトでオンなのかもしれませんし。
sqlite の autocommit off ってのは、「自前でcommitするから常にトランザクション握ってね」宣言です。つまり最初の select が実行された瞬間にロックかかってるので、他のアプリから触れなくなるので正しい挙動です。
デフォルトの autocommit on のまま使うのが普通です。この場合は、明示的に begin すると、autocommit が自動的に off 状態になって、commit 成功するとまた on になります。
どっちにしても commit のエラーチェック&リトライは書くのが正しいです。
他でほんとにロックされてる可能性ありますので。
「DBの一貫性を保つにはトランザクションが必要」は大原則です。まあ Oracleは無茶してますが(苦笑)。
だから、sqlite では、autocommit on でも off でも、
読みだろうが書きだろうが常にトランザクションが開始されます。
autocommit on だと、1つの命令だけでそのトランザクションが
自動的に閉じますが、autocommit off の場合、または明示的に
beginした場合は、それが維持され、commit までの select/update
その他もろもろの一貫性が保証されるわけです。
sqliteのロックや autocommit 周りの詳細はこのへんをどーぞ
ttp://www.sqlite.org/lockingv3.html
該当リンク読みました。AutoCommitOFFだとconnect直後からbegin_workも
do('BEGIN IMMEDIATE;")もつかえなくなる問題も疑問氷解という感じです。
どうもすみませんありがとうございます。
結局のところ、AutoCommitをOFFにする場面とは、
他のプロセスがいじらない予想があり、かつ数百回insertや
updateを繰り返すような処理の場合、毎回ステートメントを実行するたびに
ロックをしなくて済む(自分で管理できる)ので高速化できる・・・と
いう感じですかね。
うわー完璧勘違いしてましたわ!死のう。
ああ、勘違いというのは「begin,commitはAUtoCommitOFFモードで使うものだ」という勝手な思い込みの部分です。死のう。