トランザクション分離の問題または間違ったアプローチ?
-
02-07-2019 - |
質問
私は私の同僚の何人かをSQLの問題で助けていました。主に、すべての行をテーブルAからテーブルB(同じ列(名前とタイプ)を持つ両方のテーブル)に移動したかったのです。これはOracle 11gで行われましたが、実際には重要ではないと思います。
最初の単純な実装は次のようなものでした
BEGIN
INSERT INTO B SELECT * FROM A
DELETE FROM A
COMMIT;
END
懸念は、AからBへのコピー中にテーブルAに対してINSERTが実行され、「DELETE FROM A」 (または価値のあるものを切り捨てる)はデータの損失を引き起こします(Aの新しい挿入行が削除されます)。
もちろん、コピーした行のIDを一時テーブルに保存し、一時テーブルのIDSに一致するAの行のみを削除することをすぐにお勧めしました。
ただし、好奇心のために、INSERTとDELETEの間に待機コマンド(PL / SQL構文を覚えていない)を追加して、少しテストを行いました。別の接続から、行を挿入します DURING THE WAIT 。
そうすることで、データが失われることがわかりました。コンテキスト全体をSQL Serverで再現し、すべてトランザクションでラップしましたが、SQL Serverでも新しいデータは失われました。これにより、初期アプローチに体系的なエラー/欠陥があると思いました。
ただし、TRANSACTIONが新しいINSERTから(何らかの理由で)分離されていないという事実なのか、WAITコマンド中にINSERTが来たという事実なのかわかりません。
最終的には、提案された一時テーブルを使用して実装されましたが、「データ損失の理由」に対する答えを得ることができませんでした。理由を知っていますか?
解決
分離レベルによっては、テーブルからすべての行を選択しても、新しい挿入が妨げられることはなく、読み取った行がロックされるだけです。 SQL Serverでは、Serializableアイソレーションレベルを使用すると、選択クエリに含まれていた新しい行を防ぐことができます。
http://msdn.microsoft.com/en-us/library /ms173763.aspx -
シリアライズ可能 以下を指定します。
-
ステートメントは、変更されたが他のトランザクションによってまだコミットされていないデータを読み取ることはできません。
-
他のトランザクションは、現在のトランザクションが完了するまで、現在のトランザクションによって読み取られたデータを変更できません。
-
他のトランザクションは、現在のトランザクションが完了するまで、現在のトランザクション内のステートメントによって読み取られるキーの範囲に収まるキー値を持つ新しい行を挿入できません。
他のヒント
トランザクションの安定性について話すことはできませんが、別のアプローチは、2番目のステップが存在するソーステーブルから削除することです(ターゲットテーブルからIDを選択します)。
構文を容認します。このコードはテストしていませんが、アイデアを得ることができるはずです:
INSERT INTO B SELECT * FROM A;
DELETE FROM A WHERE EXISTS (SELECT B.<primarykey> FROM B WHERE B.<primarykey> = A.<primarykey>);
リレーショナルエンジンを使用して、新しいデータが削除されないようにします。また、トランザクションで2つのステップを実行する必要はありません。
更新:サブクエリの構文を修正
これは、Oracleで以下を使用して実現できます。
Alter session set isolation_level=serializable;
これは、EXECUTE IMMEDIATEを使用してPL / SQLで設定できます。
BEGIN
EXECUTE IMMEDIATE 'Alter session set isolation_level=serializable';
...
END;
これは、トランザクションの動作方法です。手元のタスクの正しい分離レベルを選択する必要があります。
同じトランザクションでINSERTとDELETEを実行しています。トランザクションが使用している分離モードについては言及していませんが、おそらく「コミットされた読み取り」です。つまり、DELETEコマンドでは、その間にコミットされたレコードが表示されます。この種のジョブでは、「スナップショット」タイプのトランザクションを使用することをお勧めします。INSERTとDELETEの両方が同じレコードのセットを知っているからです。
これが関連するかどうかはわかりませんが、SQL Serverの構文は
begin tran
....
commit
「開始」だけではありません
別のトランザクションからの挿入がトランザクションに影響しないように、トランザクション分離レベルを設定する必要があります。 Oracleでそれを行う方法がわかりません。
Oracleでは、デフォルトのトランザクション分離レベルは読み取りコミットです。これは基本的に、クエリが開始されたときにSCN(システム変更番号)に存在していた結果をOracleが返すことを意味します。トランザクション分離レベルをシリアライズ可能に設定すると、トランザクションの開始時にSCNがキャプチャされるため、トランザクションのすべてのクエリがそのSCNの時点でデータを返します。これにより、他のセッションやトランザクションが何をしていても一貫した結果が保証されます。一方、他のトランザクションが実行しているアクティビティが原因でトランザクションをシリアル化できないとOracleが判断する可能性があるため、その種のエラーを処理する必要があります。
AskTomディスカッションへのTonyのリンクは、これらすべてについてかなり詳細にリンクしています。これを強くお勧めします。
はい、ミラノ。トランザクション分離レベルを指定していません。私はそれがデフォルトの分離レベルだと思いますが、それがどれなのかわかりません。 Oracle 11gでもSQL Server 2005でもない。
さらに、WAITコマンド(2番目の接続)中に行われたINSERTは、トランザクション内では NOT でした。このデータの損失を防ぐためにすべきだったのですか?
これは、前述のように、デフォルトの読み取りコミットモードの標準的な動作です。 WAITコマンドは処理の遅延を引き起こすだけで、DBトランザクション処理へのリンクはありません。
問題を解決するには、次のいずれかを実行できます。
- 分離レベルをシリアライズ可能に設定すると、ORAエラーが発生する可能性があり、再試行で処理する必要があります。また、重大なパフォーマンスヒットが発生する可能性があります。
- 最初に一時テーブルを使用して値を保存する
- データが大きすぎてメモリに収まらない場合は、RETURNING句を使用してネストされたテーブルにBULK COLLECT INTOし、ネストされたテーブルに行が存在する場合にのみ削除できます。
別の方法として、スナップショット分離を使用して、失われた更新を検出できます:
I have written a sample code:-
First run this on Oracle DB:-
Create table AccountBalance
(
id integer Primary Key,
acctName varchar2(255) not null,
acctBalance integer not null,
bankName varchar2(255) not null
);
insert into AccountBalance values (1,'Test',50000,'Bank-a');
Now run the below code
package com.java.transaction.dirtyread;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DirtyReadExample {
/**
* @param args
* @throws ClassNotFoundException
* @throws SQLException
* @throws InterruptedException
*/
public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connectionPayment = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:xe", "hr",
"hr");
Connection connectionReader = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:xe", "hr",
"hr");
try {
connectionPayment.setAutoCommit(false);
connectionPayment.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
} catch (SQLException e) {
e.printStackTrace();
}
Thread pymtThread=new Thread(new PaymentRunImpl(connectionPayment));
Thread readerThread=new Thread(new ReaderRunImpl(connectionReader));
pymtThread.start();
Thread.sleep(2000);
readerThread.start();
}
}
package com.java.transaction.dirtyread;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ReaderRunImpl implements Runnable{
private Connection conn;
private static final String QUERY="Select acctBalance from AccountBalance where id=1";
public ReaderRunImpl(Connection conn){
this.conn=conn;
}
@Override
public void run() {
PreparedStatement stmt =null;
ResultSet rs =null;
try {
stmt = conn.prepareStatement(QUERY);
System.out.println("In Reader thread --->Statement Prepared");
rs = stmt.executeQuery();
System.out.println("In Reader thread --->executing");
while (rs.next()){
System.out.println("Balance is:" + rs.getDouble(1));
}
System.out.println("In Reader thread --->Statement Prepared");
Thread.sleep(5000);
stmt.close();
rs.close();
stmt = conn.prepareStatement(QUERY);
rs = stmt.executeQuery();
System.out.println("In Reader thread --->executing");
while (rs.next()){
System.out.println("Balance is:" + rs.getDouble(1));
}
stmt.close();
rs.close();
stmt = conn.prepareStatement(QUERY);
rs = stmt.executeQuery();
System.out.println("In Reader thread --->executing");
while (rs.next()){
System.out.println("Balance is:" + rs.getDouble(1));
}
} catch (SQLException | InterruptedException e) {
e.printStackTrace();
}finally{
try {
stmt.close();
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
package com.java.transaction.dirtyread;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class PaymentRunImpl implements Runnable{
private Connection conn;
private static final String QUERY1="Update AccountBalance set acctBalance=40000 where id=1";
private static final String QUERY2="Update AccountBalance set acctBalance=30000 where id=1";
private static final String QUERY3="Update AccountBalance set acctBalance=20000 where id=1";
private static final String QUERY4="Update AccountBalance set acctBalance=10000 where id=1";
public PaymentRunImpl(Connection conn){
this.conn=conn;
}
@Override
public void run() {
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(QUERY1);
stmt.execute();
System.out.println("In Payment thread --> executed");
Thread.sleep(3000);
stmt = conn.prepareStatement(QUERY2);
stmt.execute();
System.out.println("In Payment thread --> executed");
Thread.sleep(3000);
stmt = conn.prepareStatement(QUERY3);
stmt.execute();
System.out.println("In Payment thread --> executed");
stmt = conn.prepareStatement(QUERY4);
stmt.execute();
System.out.println("In Payment thread --> executed");
Thread.sleep(5000);
//case 1
conn.rollback();
System.out.println("In Payment thread --> rollback");
//case 2
//conn.commit();
// System.out.println("In Payment thread --> commit");
} catch (SQLException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
Output:-
In Payment thread --> executed
In Reader thread --->Statement Prepared
In Reader thread --->executing
Balance is:50000.0
In Reader thread --->Statement Prepared
In Payment thread --> executed
In Payment thread --> executed
In Payment thread --> executed
In Reader thread --->executing
Balance is:50000.0
In Reader thread --->executing
Balance is:50000.0
In Payment thread --> rollback
Uは、oracleの定義に従って新しい行を挿入することでテストできます。 幻像読み取りは、トランザクションAが特定の条件を満たす行のセットを取得し、その後トランザクションBが行を挿入または更新して、行がトランザクションAの条件を満たすようになり、トランザクションAが条件検索を繰り返したときに発生します。トランザクションAには、追加の行が表示されます。この行はファントムと呼ばれます。 TRANSACTION_SERIALIZABLEを使用したのと同様に、上記のシナリオを回避します。 Oracleに最も厳しいロックを設定します。 Oracleは、2種類のトランザクション分離レベルのみをサポートしています:-TRANSACTION_READ_COMMITTEDおよびTRANSACTION_SERIALIZABLE。