使用多线程时,重复键违反了唯一约束

Joh*_*ino 5 postgresql java

我正在使用 ExecutorService,其固定线程池为 50,固定数据库连接池为 50,使用 HikariCP。每个工作线程处理一个数据包(一个“报告”),检查它是否有效(其中每个报告必须有唯一的unit_id、时间、纬度和经度),从连接池中抓取一个db连接,然后将报告插入报告表。唯一性约束是用 postgresql 创建的,称为“reports_uniqueness_index”。当我的音量很大时,我会收到大量以下错误:

org.postgresql.util.PSQLException: ERROR: duplicate key value 
  violates unique constraint "reports_uniqueness_index"
Run Code Online (Sandbox Code Playgroud)

这就是我认为的问题所在。在插入数据库之前,我执行检查以确定表中是否已存在具有相同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;
}
Run Code Online (Sandbox Code Playgroud)

我想到的一种解决方案是使用 Class 对象本身作为锁定对象:

synchronized(Report.class) {
    status = stmt.executeUpdate(sql);
}
Run Code Online (Sandbox Code Playgroud)

但这会延迟其他线程的插入。有更好的解决方案吗?

小智 4

通过在表上创建唯一约束,您可以告诉数据库“每次我尝试在此表中插入行时,请检查是否存在具有相同列组合的现有行。哦,并确保您这样做它以一种原子的方式,这样如果其他人试图与我同时插入一个,我们中只有一个人会成功”。

完成此操作后,在大多数情况下,在应用程序中复制相同的逻辑就没有什么意义了。它只会降低整个过程的效率。你最好接受@horse的建议并捕获异常。

不过,有一些事情需要注意。一是如果您在事务中执行此操作,则事务将在出现错误时自动回滚。如果您在此之前在交易中执行了任何其他操作,则该操作将被撤消。为了避免这种情况,您需要在插入之前设置一个保存点,然后根据需要释放或回滚保存点。

另一个问题是 postgresql 日志仍然包含这些错误,即使它们被 Java 代码捕获并忽略。用无意义的噪音淹没你的日志有助于隐藏可能潜伏在那里的真正问题,所以这不是一件好事。

避免这两个问题的更好的解决方案可能是创建一个存储过程,该存储过程插入记录,处理发生的任何异常,并向应用程序返回有关记录是否已存储的指示。

例如,如果您有一个如下所示的表:

CREATE TABLE test(a INTEGER, b TEXT, UNIQUE(A,B));
Run Code Online (Sandbox Code Playgroud)

然后你可以有这样的函数:

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;
Run Code Online (Sandbox Code Playgroud)

你可以这样使用它:

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

testdb=> SELECT test_insert(1,'foo');
 test_insert
-------------
          -1
(1 row)
Run Code Online (Sandbox Code Playgroud)

如果尝试插入违反约束的记录,则不会引发或记录异常,并且如果在事务内执行,则事务不会受到影响。该函数返回一个值,您可以使用该值来查看记录是否已插入。这是一个非常基本的例子,但应该说明这一点。