Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Lost update when attempting to atomically increment a value using SELECT FOR UPDATE #4208

@simbag

Description

@simbag

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions