Рефакторинг jdbc-transaction

This commit is contained in:
Struchkov Mark 2022-11-27 10:17:44 +03:00
parent b9dd311fb5
commit 9d9a4ba556
6 changed files with 121 additions and 85 deletions

View File

@ -14,17 +14,16 @@ public class JdbcSimpleExample {
public static void main(String[] args) { public static void main(String[] args) {
final JdbcSimpleExample jdbcSimpleExample = new JdbcSimpleExample(); final JdbcSimpleExample jdbcSimpleExample = new JdbcSimpleExample();
// jdbcExample.runNoTransaction(2L, 1L, 100L); jdbcSimpleExample.runNoTransaction(2L, 1L, 100L);
jdbcSimpleExample.runWithTransaction(2L, 1L, 100L); // jdbcSimpleExample.runWithTransaction(2L, 1L, 100L);
} }
@SneakyThrows
private void runNoTransaction(Long personIdFrom, Long personIdTo, Long amount) { private void runNoTransaction(Long personIdFrom, Long personIdTo, Long amount) {
final Connection connection = Repository.getConnection(); try (final Connection connection = Repository.getConnection()) {
sendMoney(connection, personIdFrom, personIdTo, amount);
sendMoney(connection, personIdFrom, personIdTo, amount); } catch (SQLException e) {
System.err.println(e.getMessage());
connection.close(); }
} }
@SneakyThrows @SneakyThrows

View File

@ -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);
}
}
}

View File

@ -7,17 +7,22 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
/**
* <p><b>«Грязное» чтение (dirty reads)</b> в результатах запроса появляются промежуточные результаты параллельной транзакции, которая ещё не завершилась.</p>
*/
public class DirtyReadExample { public class DirtyReadExample {
private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_UNCOMMITTED;
public static void main(String[] args) throws SQLException, InterruptedException { public static void main(String[] args) throws SQLException, InterruptedException {
try ( try (
final Connection connection = Repository.getConnectionH2(); final Connection connection = Repository.getConnectionH2();
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); 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(); new OtherTransaction().start();
Thread.sleep(2000); Thread.sleep(2000);
@ -34,11 +39,11 @@ public class DirtyReadExample {
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); connection.setTransactionIsolation(ISOLATION_LEVEL);
final ResultSet resultSet = statement.executeQuery("SELECT * FROM person WHERE id = 1"); final ResultSet resultSet = statement.executeQuery("SELECT * FROM person WHERE id = 1");
while (resultSet.next()) { while (resultSet.next()) {
System.out.println(resultSet.getString("balance")); System.out.println("Balance: " + resultSet.getString("balance"));
} }
} catch (SQLException e) { } catch (SQLException e) {
System.out.println(e.getMessage()); System.out.println(e.getMessage());

View File

@ -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;
/**
* <p><b>Потерянное обновление (lost update)</b> две параллельные транзакции меняют одни и те же данные, при этом итоговый результат обновления предсказать невозможно.</p>
*/
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;
}
}

View File

@ -7,19 +7,25 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
/**
* <p><b>Неповторяющееся чтение (non-repeatable reads)</b> запрос с одними и теми же условиями даёт неодинаковые результаты в рамках транзакции.</p>
*/
public class NonRepeatableRead { public class NonRepeatableRead {
private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED;
public static void main(String[] args) { public static void main(String[] args) {
try( try (
final Connection connection = Repository.getConnection(); final Connection connection = Repository.getConnection();
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); connection.setTransactionIsolation(ISOLATION_LEVEL);
final ResultSet resultSet = statement.executeQuery("SELECT * FROM person WHERE id = 1"); final ResultSet resultSetOne = statement.executeQuery("SELECT * FROM person WHERE id = 1");
while (resultSet.next()) { while (resultSetOne.next()) {
System.out.println(resultSet.getString("balance")); final String balance = resultSetOne.getString("balance");
System.out.println("[one] Balance: " + balance);
} }
new OtherTransaction().start(); new OtherTransaction().start();
@ -27,7 +33,8 @@ public class NonRepeatableRead {
final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1"); final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1");
while (resultSetTwo.next()) { 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) { } catch (SQLException | InterruptedException e) {
@ -43,10 +50,18 @@ public class NonRepeatableRead {
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); connection.setTransactionIsolation(ISOLATION_LEVEL);
statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1"); statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1");
connection.commit(); 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) { } catch (SQLException e) {
System.out.println(e.getMessage()); System.out.println(e.getMessage());
} }

View File

@ -7,19 +7,25 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
/**
* <p><b>Фантомное чтение (phantom reads)</b> в результатах повторяющегося запроса появляются и исчезают строки, которые в данный момент модифицирует параллельная транзакция.</p>
*/
public class PhantomRead { public class PhantomRead {
private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED;
public static void main(String[] args) { public static void main(String[] args) {
try( try(
final Connection connection = Repository.getConnection(); final Connection connection = Repository.getConnection();
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); connection.setTransactionIsolation(ISOLATION_LEVEL);
final ResultSet resultSet = statement.executeQuery("SELECT count(*) FROM person"); final ResultSet resultSet = statement.executeQuery("SELECT count(*) FROM person");
while (resultSet.next()) { while (resultSet.next()) {
System.out.println(resultSet.getInt(1)); final int count = resultSet.getInt(1);
System.out.println("Count: " + count);
} }
new OtherTransaction().start(); new OtherTransaction().start();
@ -27,7 +33,8 @@ public class PhantomRead {
final ResultSet resultSetTwo = statement.executeQuery("SELECT count(*) FROM person"); final ResultSet resultSetTwo = statement.executeQuery("SELECT count(*) FROM person");
while (resultSetTwo.next()) { while (resultSetTwo.next()) {
System.out.println(resultSetTwo.getInt(1)); final int count = resultSetTwo.getInt(1);
System.out.println("Count: " + count);
} }
} catch (SQLException | InterruptedException e) { } catch (SQLException | InterruptedException e) {
@ -43,9 +50,9 @@ public class PhantomRead {
final Statement statement = connection.createStatement() final Statement statement = connection.createStatement()
) { ) {
connection.setAutoCommit(false); 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(); connection.commit();
} catch (SQLException e) { } catch (SQLException e) {
System.out.println(e.getMessage()); System.out.println(e.getMessage());