-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Closed
Description
Version 2.3.232
The following query sequence can lead to a lost update:
begin;
1 SELECT counter FROM Counter WHERE id = 1;
2 SELECT id FROM Counter WHERE id = 1 FOR UPDATE;
3 SELECT counter FROM Counter WHERE id = 1;
4 UPDATE Counter SET counter = ? WHERE id = 1;
commit;Important: Queries 1 and 3 must use the same PreparedStatement
This code is being executed by 1000 threads in parallel:
Connection conn = dataSource.getConnection();
try {
String selectQuery = "SELECT counter FROM Counter WHERE id = 1";
String lockRowQuery = "SELECT id FROM Counter WHERE id = 1 FOR UPDATE";
PreparedStatement selectCounterStmt = conn.prepareStatement(selectQuery);
selectCounterStmt.executeQuery(); // Select counter before lock
PreparedStatement lockStmt = conn.prepareStatement(lockRowQuery);
lockStmt.executeQuery(); // Lock row
ResultSet resultAfterLock = selectCounterStmt.executeQuery(); //Select locked row. Same PreparedStatement as used before locking
if (resultAfterLock.next()) {
int currentCounter = resultAfterLock.getInt("counter");
String updateCounterQuery = "UPDATE Counter SET counter = ? WHERE id = 1";
PreparedStatement updateStmt = conn.prepareStatement(updateCounterQuery);
if (!concurrentSet.add(currentCounter)) {
System.out.println("LOST UPDATE! value: " + currentCounter); // lost update warning, if concurrentSet already contains current value
}
updateStmt.setInt(1, currentCounter + 1); // Update counter++
updateStmt.executeUpdate();
} else {
throw new RuntimeException("Entity not found!");
}
conn.commit();
selectCounterStmt.close();
lockStmt.close();
return 0;
} catch (SQLException e) {
System.out.println(e.getMessage());
conn.rollback();
return -1;
} finally {
conn.close();
}Full code to reproduce:
public class Main {
public static void main(String[] args) throws SQLException {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:h2:mem:testdb");
config.setUsername("sa");
config.setPassword("");
config.setMaximumPoolSize(500);
config.setAutoCommit(false);
Set<Integer> concurrentSet = ConcurrentHashMap.newKeySet();
HikariDataSource dataSource = new HikariDataSource(config);
try (Connection conn = dataSource.getConnection()) {
String createTableQuery = "CREATE TABLE IF NOT EXISTS Counter (id INT PRIMARY KEY, counter INT)";
String insertInitialValueQuery = "INSERT INTO Counter(id, counter) VALUES (1, 0)";
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate(createTableQuery);
stmt.executeUpdate(insertInitialValueQuery);
conn.commit();
}
}
int threadsCount = 1000;
int taskCount = 100000;
ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
Callable<Object> callable = getCallable(dataSource, concurrentSet);
ArrayList<Callable<Object>> callables = new ArrayList<>();
for (int i = 0; i < taskCount; i++) {
callables.add(callable);
}
try {
executorService.invokeAll(callables);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
System.out.println("Unique values count: " + concurrentSet.size());
dataSource.close();
executorService.shutdownNow();
}
private static Callable<Object> getCallable(HikariDataSource dataSource,
Set<Integer> concurrentSet) {
return () -> {
Connection conn = dataSource.getConnection();
try {
String selectQuery = "SELECT counter FROM Counter WHERE id = 1";
String lockRowQuery = "SELECT id FROM Counter WHERE id = 1 FOR UPDATE";
PreparedStatement selectCounterStmt = conn.prepareStatement(selectQuery);
selectCounterStmt.executeQuery(); // Select counter before lock
PreparedStatement lockStmt = conn.prepareStatement(lockRowQuery);
lockStmt.executeQuery(); // Lock row
ResultSet resultAfterLock = selectCounterStmt.executeQuery(); //Select locked row. Same PreparedStatement as used before locking
if (resultAfterLock.next()) {
int currentCounter = resultAfterLock.getInt("counter");
String updateCounterQuery = "UPDATE Counter SET counter = ? WHERE id = 1";
PreparedStatement updateStmt = conn.prepareStatement(updateCounterQuery);
if (!concurrentSet.add(currentCounter)) {
System.out.println("LOST UPDATE! value: " + currentCounter); // lost update warning, if concurrentSet already contains current value
}
updateStmt.setInt(1, currentCounter + 1); // Update counter++
updateStmt.executeUpdate();
} else {
throw new RuntimeException("Entity not found!");
}
conn.commit();
selectCounterStmt.close();
lockStmt.close();
return 0;
} catch (SQLException e) {
System.out.println(e.getMessage());
conn.rollback();
return -1;
} finally {
conn.close();
}
};
}
}Metadata
Metadata
Assignees
Labels
No labels