From 9d9a4ba556197a42d808dc5b34b62482d7f0cc8f Mon Sep 17 00:00:00 2001 From: Struchkov Mark Date: Sun, 27 Nov 2022 10:17:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20jdbc-transaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../transaction/JdbcSimpleExample.java | 15 ++-- .../transaction/RepeatableReadExample.java | 61 ---------------- .../problems/DirtyReadExample.java | 13 ++-- .../problems/LostUpdateExample.java | 71 +++++++++++++++++++ .../problems/NonRepeatableRead.java | 29 ++++++-- .../transaction/problems/PhantomRead.java | 17 +++-- 6 files changed, 121 insertions(+), 85 deletions(-) delete mode 100644 jdbc-transaction/src/main/java/dev/struchkov/example/transaction/RepeatableReadExample.java create mode 100644 jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/LostUpdateExample.java diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/JdbcSimpleExample.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/JdbcSimpleExample.java index 96aff06..d1c46f7 100644 --- a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/JdbcSimpleExample.java +++ b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/JdbcSimpleExample.java @@ -14,17 +14,16 @@ public class JdbcSimpleExample { public static void main(String[] args) { final JdbcSimpleExample jdbcSimpleExample = new JdbcSimpleExample(); -// jdbcExample.runNoTransaction(2L, 1L, 100L); - jdbcSimpleExample.runWithTransaction(2L, 1L, 100L); + jdbcSimpleExample.runNoTransaction(2L, 1L, 100L); +// jdbcSimpleExample.runWithTransaction(2L, 1L, 100L); } - @SneakyThrows private void runNoTransaction(Long personIdFrom, Long personIdTo, Long amount) { - final Connection connection = Repository.getConnection(); - - sendMoney(connection, personIdFrom, personIdTo, amount); - - connection.close(); + try (final Connection connection = Repository.getConnection()) { + sendMoney(connection, personIdFrom, personIdTo, amount); + } catch (SQLException e) { + System.err.println(e.getMessage()); + } } @SneakyThrows diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/RepeatableReadExample.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/RepeatableReadExample.java deleted file mode 100644 index c1fd0fc..0000000 --- a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/RepeatableReadExample.java +++ /dev/null @@ -1,61 +0,0 @@ -package dev.struchkov.example.transaction; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class RepeatableReadExample { - - public static final String READ = "SELECT person.balance FROM person WHERE id = ?"; - public static final String UPDATE = "UPDATE person SET balance = ? WHERE id = ?"; - - public static void main(String[] args) { - try { - final Connection connectionOne = Repository.getConnection(); - final Connection connectionTwo = Repository.getConnection(); - - connectionOne.setAutoCommit(false); - connectionTwo.setAutoCommit(false); - - final int transactionLevel = Connection.TRANSACTION_REPEATABLE_READ; - connectionOne.setTransactionIsolation(transactionLevel); - connectionTwo.setTransactionIsolation(transactionLevel); - - final PreparedStatement readOne = connectionOne.prepareStatement(READ); - readOne.setLong(1, 1); - - final PreparedStatement readTwo = connectionTwo.prepareStatement(READ); - readTwo.setLong(1, 1); - - final ResultSet resultSetOne = readOne.executeQuery(); - resultSetOne.next(); - final long balanceOne = resultSetOne.getLong(1); - - final ResultSet resultSetTwo = readTwo.executeQuery(); - resultSetTwo.next(); - final long balanceTwo = resultSetTwo.getLong(1); - - final PreparedStatement updateOne = connectionOne.prepareStatement(UPDATE); - updateOne.setLong(1, balanceOne + 10); - updateOne.setLong(2, 1); - updateOne.execute(); - - connectionOne.commit(); - connectionOne.close(); - - final PreparedStatement updateTwo = connectionTwo.prepareStatement(UPDATE); - updateTwo.setLong(1, balanceTwo + 5); - updateTwo.setLong(2, 1); - updateTwo.execute(); - - connectionTwo.commit(); - connectionTwo.close(); - - } catch (SQLException e) { - System.out.println(e); - } - - } - -} diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/DirtyReadExample.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/DirtyReadExample.java index 1797bb3..fa596ab 100644 --- a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/DirtyReadExample.java +++ b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/DirtyReadExample.java @@ -7,17 +7,22 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +/** + *

«Грязное» чтение (dirty reads) — в результатах запроса появляются промежуточные результаты параллельной транзакции, которая ещё не завершилась.

+ */ public class DirtyReadExample { + private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_UNCOMMITTED; + public static void main(String[] args) throws SQLException, InterruptedException { try ( final Connection connection = Repository.getConnectionH2(); final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); - statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1"); + statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1"); new OtherTransaction().start(); Thread.sleep(2000); @@ -34,11 +39,11 @@ public class DirtyReadExample { final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); final ResultSet resultSet = statement.executeQuery("SELECT * FROM person WHERE id = 1"); while (resultSet.next()) { - System.out.println(resultSet.getString("balance")); + System.out.println("Balance: " + resultSet.getString("balance")); } } catch (SQLException e) { System.out.println(e.getMessage()); diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/LostUpdateExample.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/LostUpdateExample.java new file mode 100644 index 0000000..f7bdbb4 --- /dev/null +++ b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/LostUpdateExample.java @@ -0,0 +1,71 @@ +package dev.struchkov.example.transaction.problems; + +import dev.struchkov.example.transaction.Repository; +import lombok.SneakyThrows; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + *

Потерянное обновление (lost update) — две параллельные транзакции меняют одни и те же данные, при этом итоговый результат обновления предсказать невозможно.

+ */ +public class LostUpdateExample { + + public static final String READ = "SELECT person.balance FROM person WHERE id = ?"; + public static final String UPDATE = "UPDATE person SET balance = ? WHERE id = ?"; + + @SneakyThrows + public static void main(String[] args) { + + // Начинаем две транзакции. + final Connection connectionOne = getNewConnection(); + final Connection connectionTwo = getNewConnection(); + + // Первая и вторая транзакция запрашивают баланс пользователя. + // balance = 1000 + final long balanceOne = getBalance(connectionOne); + final long balanceTwo = getBalance(connectionTwo); + + // Первая транзакция готовится обновить баланс пользователю. + final PreparedStatement updateOne = connectionOne.prepareStatement(UPDATE); + updateOne.setLong(1, balanceOne + 10); + updateOne.setLong(2, 1); + updateOne.execute(); + + // Первая транзакция фиксирует изменения и завершается. + // Значение balance в базе в этот момент = 1010. + connectionOne.commit(); + connectionOne.close(); + + // Но вторая транзакция ничего не знает про изменения в БД. + // Значение balanceTwo все еще равно 1000, к этому значению мы добавляем 5. + final PreparedStatement updateTwo = connectionTwo.prepareStatement(UPDATE); + updateTwo.setLong(1, balanceTwo + 5); + updateTwo.setLong(2, 1); + updateTwo.execute(); + + // Вторая транзакция фиксирует свои изменения и завершается. + // В итоге в БД остается значение 1005, а не 1015, как хотелось бы нам. + connectionTwo.commit(); + connectionTwo.close(); + } + + private static Connection getNewConnection() throws SQLException { + final Connection connection = Repository.getConnection(); + connection.setAutoCommit(false); + connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + return connection; + } + + private static long getBalance(Connection connectionOne) throws SQLException { + final PreparedStatement preparedStatement = connectionOne.prepareStatement(READ); + preparedStatement.setLong(1, 1); + final ResultSet resultSet = preparedStatement.executeQuery(); + resultSet.next(); + final long balanceOne = resultSet.getLong(1); + return balanceOne; + } + +} diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/NonRepeatableRead.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/NonRepeatableRead.java index b372917..8f7e888 100644 --- a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/NonRepeatableRead.java +++ b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/NonRepeatableRead.java @@ -7,19 +7,25 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +/** + *

Неповторяющееся чтение (non-repeatable reads) — запрос с одними и теми же условиями даёт неодинаковые результаты в рамках транзакции.

+ */ public class NonRepeatableRead { + private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED; + public static void main(String[] args) { - try( + try ( final Connection connection = Repository.getConnection(); final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); - final ResultSet resultSet = statement.executeQuery("SELECT * FROM person WHERE id = 1"); - while (resultSet.next()) { - System.out.println(resultSet.getString("balance")); + final ResultSet resultSetOne = statement.executeQuery("SELECT * FROM person WHERE id = 1"); + while (resultSetOne.next()) { + final String balance = resultSetOne.getString("balance"); + System.out.println("[one] Balance: " + balance); } new OtherTransaction().start(); @@ -27,7 +33,8 @@ public class NonRepeatableRead { final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1"); while (resultSetTwo.next()) { - System.out.println(resultSetTwo.getString("balance")); + final String balance = resultSetTwo.getString("balance"); + System.out.println("[one] Balance: " + balance); } } catch (SQLException | InterruptedException e) { @@ -43,10 +50,18 @@ public class NonRepeatableRead { final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1"); connection.commit(); + + final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1"); + while (resultSetTwo.next()) { + final String balance = resultSetTwo.getString("balance"); + System.out.println("[two] Balance: " + balance); + } + + connection.commit(); } catch (SQLException e) { System.out.println(e.getMessage()); } diff --git a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/PhantomRead.java b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/PhantomRead.java index 2829f80..6bfb647 100644 --- a/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/PhantomRead.java +++ b/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/PhantomRead.java @@ -7,19 +7,25 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +/** + *

Фантомное чтение (phantom reads) — в результатах повторяющегося запроса появляются и исчезают строки, которые в данный момент модифицирует параллельная транзакция.

+ */ public class PhantomRead { + private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED; + public static void main(String[] args) { try( final Connection connection = Repository.getConnection(); final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); final ResultSet resultSet = statement.executeQuery("SELECT count(*) FROM person"); while (resultSet.next()) { - System.out.println(resultSet.getInt(1)); + final int count = resultSet.getInt(1); + System.out.println("Count: " + count); } new OtherTransaction().start(); @@ -27,7 +33,8 @@ public class PhantomRead { final ResultSet resultSetTwo = statement.executeQuery("SELECT count(*) FROM person"); while (resultSetTwo.next()) { - System.out.println(resultSetTwo.getInt(1)); + final int count = resultSetTwo.getInt(1); + System.out.println("Count: " + count); } } catch (SQLException | InterruptedException e) { @@ -43,9 +50,9 @@ public class PhantomRead { final Statement statement = connection.createStatement() ) { connection.setAutoCommit(false); - connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + connection.setTransactionIsolation(ISOLATION_LEVEL); - statement.executeUpdate("INSERT INTO person(name, balance) values ('test', 100)"); + statement.executeUpdate("INSERT INTO person(id, balance) values (3, 1000)"); connection.commit(); } catch (SQLException e) { System.out.println(e.getMessage());