web-dev-qa-db-ja.com

マルチスレッドを使用すると、重複キーが一意制約に違反する

HikariCPを使用して、50の固定スレッドプールと50の固定データベース接続プールでExecutorServiceを使用しています。各ワーカースレッドはパケット(「レポート」)を処理し、それが有効かどうか(各レポートに一意のunit_id、時間、緯度、経度が必要)を確認し、接続プールからデータベース接続を取得して、レポートをに挿入しますレポートの表。一意性制約はpostgresqlで作成され、「reports_uniqueness_index」と呼ばれます。大音量の場合、次のエラーが大量に発生します。

org.postgresql.util.PSQLException: ERROR: duplicate key value 
  violates unique constraint "reports_uniqueness_index"

ここに問題があると私は信じています。データベースを挿入する前に、同じunit_id、時間、緯度、経度のレポートがテーブルに既に存在するかどうかを確認するチェックを実行します。そうでない場合、レポートは有効であり、挿入を実行します。ただし、同時実行性を使用しているため、レポートが有効かどうかを同時にチェックする50のスレッドがあり、それらのいずれもまだ挿入されていないため、各スレッドは有効なレポートがあると見なし、いつ挿入するかを考えています。同じ瞬間、つまり、postgresqlがエラーを発生させます。

同時実行で遅延が発生しないソリューションが欲しいのですが。データベースの挿入は可能な限り迅速に行われる必要があるので、私は同期文や再入可能ロックの使用を避けようとしています。これはここの挿入です:

 private boolean save(){
        Connection conn=null;
        Statement stmt=null;
        int status=0;
        DbConnectionPool dbPool = DbConnectionPool.getInstance();
        String sql = = "INSERT INTO reports"
        sql += " (unit_id, time, time_secs, latitude, longitude, speed, created_at)";
        sql += " values (...)";
        try {
            conn = dbPool.getConnection();
            stmt = conn.createStatement();
            status = stmt.executeUpdate(sql);
        } catch (SQLException e) {
            return false;
        } finally {
            try {
                if (stmt != null)  
                {  
                    stmt.close();
                }  

                if (conn != null)  
                {  
                    conn.close();  
                }  
            } catch(SQLException e){}
        }

        if(status > 0){
            return true;            
        }

        return false;
}

私が考えた1つの解決策は、Classオブジェクト自体をロックオブジェクトとして使用することでした。

synchronized(Report.class) {
    status = stmt.executeUpdate(sql);
}

しかし、これは他のスレッドの挿入を遅らせます。より良い解決策はありますか?

5
JohnMerlino

テーブルに一意の制約を作成することで、データベースに「このテーブルに行を挿入しようとするたびに、この列の組み合わせが同じである既存の行がないことを確認してください。ああ、そうしてください。他の誰かが私と同時に挿入しようとした場合、私たちの1人だけが成功するように、それはアトミックな方法で行われます。」.

その後、ほとんどの場合、アプリケーションで同じロジックを複製しても意味がありません。プロセスが非効率になるだけです。 @horseの提案を受け取り、例外をキャッチする方がよいでしょう。

ただし、注意すべき点がいくつかあります。 1つは、トランザクション内でこれを行うと、エラー時にトランザクションが自動的にロールバックされることです。トランザクションでこれより前に何か他のことをした場合は、取り消されています。これを回避するには、挿入の前に [〜#〜] savepoint [〜#〜] を設定し、必要に応じてセーブポイントをRELEASEまたはROLLBACKする必要があります。

他の問題は、Javaコードによってキャッチおよび無視された場合でも、postgresqlログにこれらのエラーが含まれることです。意味のないノイズでログをフラッディングすると、潜んでいる可能性のある真の問題を隠すのに役立ちますそこにあるので、それは良いことではありません。

おそらくこれらの問題の両方を回避するより良い解決策は、レコードを挿入し、発生した例外を処理し、レコードが格納されているかどうかに関する指示をアプリケーションに返すストアドプロシージャを作成することです。

たとえば、次のようなテーブルがあるとします。

CREATE TABLE test(a INTEGER, b TEXT, UNIQUE(A,B));

次に、次のような関数を使用できます。

CREATE FUNCTION test_insert(pa INTEGER, pb TEXT) RETURNS INTEGER AS $$
BEGIN
    INSERT INTO test(a,b) VALUES (pa, pb);
    RETURN 1;
EXCEPTION
    WHEN unique_violation THEN
       RETURN -1;
END;
$$ LANGUAGE plpgsql;

次のように使用できます。

testdb=> SELECT test_insert(1,'foo');
 test_insert
-------------
           1
(1 row)

testdb=> SELECT test_insert(1,'foo');
 test_insert
-------------
          -1
(1 row)

制約に違反するレコードを挿入しようとした場合、例外はスローされず、ログにも記録されません。トランザクション内で実行されても、トランザクションは影響を受けません。関数は、レコードが挿入されたかどうかを確認するために使用できる値を返します。これは非常に基本的な例ですが、ポイントを説明する必要があります。

4
harmic