Estou usando o ExecutorService com um pool de threads fixo de 50 e um pool de conexão de banco de dados fixo de 50, usando HikariCP. Cada thread de trabalho processa um pacote (um "relatório"), verifica se ele é válido (onde cada relatório deve ter um unit_id, hora, latitude e longitude exclusivos), obtém uma conexão db do pool de conexões e, em seguida, insere o relatório no a tabela de relatórios. A restrição de exclusividade é criada com postgresql e chamada de "reports_uniqueness_index". Quando eu tenho alto volume, recebo uma tonelada do seguinte erro:
org.postgresql.util.PSQLException: ERROR: duplicate key value
violates unique constraint "reports_uniqueness_index"
Aqui está o que eu acredito que seja o problema. Antes da inserção do banco de dados, realizo uma verificação para determinar se já existe um relatório na tabela com o mesmo unit_id, hora, latitude e longitude. Caso contrário, o relatório é válido e eu realizo a inserção. Porém, acho que por estar usando simultaneidade tenho 50 threads cada verificando ao mesmo tempo se o relatório é válido e como nenhum deles foi inserido ainda, cada thread pensa que tem um relatório válido e quando vai inseri-los em mesmo momento, é quando o postgresql gera o erro.
Gostaria de uma solução que não gerasse latência com a simultaneidade. Tenho tentado evitar o uso de instrução sincronizada ou um bloqueio reentrante porque as inserções de banco de dados precisam ocorrer o mais rápido possível. Esta é a inserção aqui:
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;
}
Uma solução que pensei foi usar o próprio objeto Class como um objeto de bloqueio:
synchronized(Report.class) {
status = stmt.executeUpdate(sql);
}
Mas isso atrasará a inserção de outros segmentos. Existe uma solução melhor?
Ao criar uma restrição única em sua tabela, você está dizendo ao banco de dados "cada vez que tento inserir uma linha nesta tabela, verifique se não há nenhuma linha existente com esta combinação de colunas igual. Ah, e certifique-se de fazer de forma atômica, de modo que, se alguém tentar inserir um ao mesmo tempo que eu, apenas um de nós conseguirá".
Depois de fazer isso, na maioria dos casos, não há muito sentido em replicar a mesma lógica em seu aplicativo. Isso só tornará o processo menos eficiente. Seria melhor você aceitar a sugestão de @horse e pegar a exceção.
No entanto, há algumas coisas a serem observadas. Uma é que, se você fizer isso dentro de uma transação, a transação será revertida automaticamente no erro. Se você fez qualquer outra coisa antes disso na transação, ela será desfeita. Para evitar isso, você precisa definir um SAVEPOINT antes da inserção e, em seguida, RELEASE ou ROLLBACK o ponto de salvamento conforme apropriado.
O outro problema é que o log do postgresql ainda conterá esses erros, mesmo que sejam detectados e ignorados pelo código Java. Inundar seus logs com ruído sem sentido ajuda a esconder problemas verdadeiros que podem estar escondidos lá, então não é uma coisa boa.
Possivelmente, uma solução melhor, que evita esses dois problemas, é criar um procedimento armazenado que insere o registro, manipula todas as exceções que ocorrem e retorna uma indicação ao seu aplicativo se o registro foi armazenado ou não.
Por exemplo, se você tivesse uma tabela assim:
Então você poderia ter uma função como esta:
e você poderia usar assim:
Se for feita uma tentativa de inserir um registro que viole a restrição, nenhuma exceção é lançada ou registrada e, se executada dentro de uma transação, a transação não é afetada. A função retorna um valor que você pode usar para ver se o registro foi inserido ou não. É um exemplo muito básico, mas deve ilustrar o ponto.