WEBサービスなどでは1レコードに複数プロセスがSELECTとUPDATEを連続して行うことがあります。 そのような連続アクセスの場合、トランザクション分離レベルやクエリーによって動作がどのようになるかを確認してみました。
テスト環境は以下のようになります。
各データーベースのテストスクリプトと結果の詳細は以下にあります。
テストスクリプトの概略は以下のようになります。 forkでプロセスを5個作成して、それぞれのプロセスが200まで連続して加算しています。 最後のプロセスのcountが1000になれば正常にカウントされたことになります。 1000に満たない場合はカウント漏れが発生していています。
TABLE_NAME = 'test_postgres'
PROCESS_MAX = 5
TEST_COUNT_MAX = 200
def pg_countup(max, title)
print("\n", title, "\n")
tp = MyPostgres.new(TABLE_NAME)
tp.db_init()
1.upto(PROCESS_MAX) do |index|
fork do
testdb = MyPostgres.new(TABLE_NAME, 'fork')
testdb.query(index, max) { |num, i, conn|
yield num, i, conn
}
end
end
ret = Process.waitall
end
def test_postgres()
tblname = TABLE_NAME
max = TEST_COUNT_MAX
title = "UPDATE #{tblname} SET count=count+1 WHERE id=1"
pg_countup(max, title) { |num, i, conn|
conn.exec("UPDATE #{tblname} SET count=count+1 WHERE id=1;")
}
・・・ 省略 ・・・
end
test_postgres()
PostgreSQLとMySQLでテストした結果をまとめました。
最後のプロセスのcountが1000になれば正常にカウントされたことになり、 以下の表では○で表しています。 1000に満たない場合はカウント漏れが発生していて、表では×(最終カウント数)で表示しています。 ×の場合の最終カウント数は実行するたびに変わりますでの、この表の最終カウント数は1実行例です。
| テスト項目 | PostgreSQL用のクエリー(※1) | PostgreSQL | MySQL |
|---|---|---|---|
| UPDATE | UPDATE test_postgres SET count=count+1 WHERE id=1; | ○ | ○ |
| READ UNCOMMITTED SELECT FOR UPDATE |
BEGIN; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT count AS current FROM test_postgres WHERE id=1 FOR UPDATE; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
○ | デッドロック |
| READ COMMITTED SELECT FOR UPDATE |
BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT count AS current FROM test_postgres WHERE id=1 FOR UPDATE; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
○ | デッドロック |
| REPEATABLE READ SELECT FOR UPDATE |
BEGIN; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT count AS current FROM test_postgres WHERE id=1 FOR UPDATE; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
エラー(※2) | ○ |
| SERIALIZABLE SELECT FOR UPDATE |
BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; SELECT count AS current FROM test_postgres WHERE id=1 FOR UPDATE; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
エラー(※2) | ○ |
| READ UNCOMMITTED SELECT |
BEGIN; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT count AS current FROM test_postgres WHERE id=1; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
×(239) | ×(618) |
| READ COMMITTED SELECT |
BEGIN; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT count AS current FROM test_postgres WHERE id=1; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
×(263) | ×(741) |
| REPEATABLE READ SELECT |
BEGIN; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT count AS current FROM test_postgres WHERE id=1; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
エラー(※2) | ×(622) |
| SERIALIZABLE SELECT |
BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; SELECT count AS current FROM test_postgres WHERE id=1; UPDATE test_postgres SET count=1+current WHERE id=1; COMMIT; |
エラー(※2) | デッドロック |
| SELECT UPDATE |
UPDATE test_postgres SET count=1+(SELECT count FROM test_postgres WHERE id=1) WHERE id=1; |
×(232) | エラー |
※1 クエリーはPostgreSQL用を表示しています。 今回のテストではdbiを使用していないので、MySQLとPostgreSQLで、トランザクションの開始とトランザクション分離レベルの設定の順番が異なります。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED BEGIN SELECT count AS current FROM test_innodb WHERE id=1 FOR UPDATE; UPDATE test_innodb SET count=1+current WHERE id=1; COMMIT