Я создал API, который переводит деньги между двумя аккаунтами. У нас есть fromAccountNumber, toAccountNumber, которые являются полями String и имеют значение BigDecimal Данные счета хранятся в базе данных H2 (в памяти).
Для достижения параллелизма я использовал оператор Select .. for update (этот запрос: GET_BY_ID_QUERY_FOR_TRANSACTION) при извлечении записей из БД.
public void transferMoney(String sender, String receiver, BigDecimal amount) throws TransferException {
Connection conn = null;
try {
conn = H2DataBase.getConnection();
checkIfAccountNumbersAreValid(sender, receiver);
AccountDetailsDTO fromAccountDto = getAccountByNumber(conn, sender);
AccountDetailsDTO toAccountDto = getAccountByNumber(conn, receiver);
if (null != fromAccountDto && null != toAccountDto) {
transfer(conn, fromAccountDto, toAccountDto, amount);
} else {
rollback(conn);
throw new TransferException("Transfer Failed. Please check the account numbers.",
Response.Status.BAD_REQUEST);
}
} catch (SQLException e) {
throw new TransferException("Transfer Failed !", Response.Status.INTERNAL_SERVER_ERROR);
} catch (ValidationException e) {
throw new TransferException(e.getMessage(), e.getStatus());
} finally {
closeConnection(conn);
}
}
private void transfer(Connection conn, AccountDetailsDTO fromAccountDto, AccountDetailsDTO toAccountDto,
BigDecimal transactionAmount) throws TransferException {
PreparedStatement preparedStatementInsertToTransaction = null;
PreparedStatement preparedStatementUpdateFromAccount = null;
PreparedStatement preparedStatementUpdateToAccount = null;
try {
checkToAndFromAreDifferent(fromAccountDto, toAccountDto);
checkAccountBalanceBeforeTransfer(fromAccountDto.getAccountBalance(), transactionAmount);
fromAccountDto.setAccountBalance(fromAccountDto.getAccountBalance().subtract(transactionAmount));
toAccountDto.setAccountBalance(toAccountDto.getAccountBalance().add(transactionAmount));
preparedStatementInsertToTransaction = conn
.prepareStatement(DBQueryUtility.INSERT_INTO_TRANSACTION_TABLE_QUERY);
preparedStatementInsertToTransaction.setString(1, fromAccountDto.getAccountNumber());
preparedStatementInsertToTransaction.setString(2, toAccountDto.getAccountNumber());
preparedStatementInsertToTransaction.setBigDecimal(3, transactionAmount);
preparedStatementInsertToTransaction.executeUpdate();
preparedStatementUpdateFromAccount = conn.prepareStatement(DBQueryUtility.UPDATE_BANK_ACCOUNT_TABLE_QUERY);
preparedStatementUpdateFromAccount.setBigDecimal(1, fromAccountDto.getAccountBalance());
preparedStatementUpdateFromAccount.setString(2, fromAccountDto.getAccountNumber());
preparedStatementUpdateFromAccount.executeUpdate();
preparedStatementUpdateToAccount = conn.prepareStatement(DBQueryUtility.UPDATE_BANK_ACCOUNT_TABLE_QUERY);
preparedStatementUpdateToAccount.setBigDecimal(1, toAccountDto.getAccountBalance());
preparedStatementUpdateToAccount.setString(2, toAccountDto.getAccountNumber());
preparedStatementUpdateToAccount.executeUpdate();
conn.commit();
} catch (ValidationException e) {
rollback(conn);
throw new TransferException(e.getMessage(), Response.Status.BAD_REQUEST);
} catch (RuntimeException | SQLException e) {
rollback(conn);
throw new TransferException("Transfer Failed", Response.Status.INTERNAL_SERVER_ERROR);
} finally {
closeConnection(conn);
closePreparedStatement(preparedStatementInsertToTransaction, preparedStatementUpdateFromAccount,
preparedStatementUpdateToAccount);
}
}
public void checkIfAccountNumbersAreValid(String sender, String receiver) throws ValidationException {
if (null == sender || null == receiver || sender.isEmpty() || receiver.isEmpty()) {
throw new ValidationException("Account details cannot be null or empty !", Response.Status.BAD_REQUEST);
}
}
@Override
public void checkToAndFromAreDifferent(AccountDetailsDTO fromAccountDto, AccountDetailsDTO toAccountDto)
throws ValidationException {
if (toAccountDto.getAccountNumber().equals(fromAccountDto.getAccountNumber())) {
throw new ValidationException("To and From accounts cannot be same !", Response.Status.BAD_REQUEST);
}
}
@Override
public void checkAccountBalanceBeforeTransfer(BigDecimal accountBalance, BigDecimal amount)
throws ValidationException {
if (accountBalance.compareTo(BigDecimal.ZERO) < 0 || accountBalance.compareTo(amount) < 0) {
throw new ValidationException("Low account balance. Cannot initiate transfer !",
Response.Status.BAD_REQUEST);
}
}
public AccountDetailsDTO getAccountByNumber(Connection conn, String accountNumber) throws TransferException {
AccountDetailsDTO detailsDTO = null;
try {
PreparedStatement preparedStatement = conn.prepareStatement(DBQueryUtility.GET_BY_ID_QUERY_FOR_TRANSACTION);
preparedStatement.setString(1, accountNumber);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
detailsDTO = DBQueryUtility.extractAccountDtoFromResultSet(rs);
}
} catch (SQLException e) {
throw new TransferException("Get by account number failed ! Try again.",
Response.Status.INTERNAL_SERVER_ERROR);
}
return detailsDTO;
}
НО это пессимистическая блокировка, которая не идеальна. Мне нужно сделать ОПТИМАЛЬНУЮ блокировку здесь для лучшей производительности.
Я проверил в интернете, как я могу это реализовать, но не получил ответа, специфичного для моего варианта использования.
Везде, где они используют JPA и его аннотации.
Я не должен использовать какой-либо фреймворк!
Как получить и реализовать оптимистическую блокировку для следующего кода?