Правильный дизайн, чтобы избежать тупиков Oracle? - PullRequest
8 голосов
/ 15 сентября 2011

Обычный совет, когда нужно избегать взаимоблокировок, - всегда блокировать ресурсы в одном и том же порядке.Но как бы вы реализовали это в отношении блокировок строк в сильно перегруженной базе данных Oracle?

Чтобы понять, что я имею в виду, рассмотрим следующий пример.Очень простой DAO для обработки банковских счетов:

@Component
public class AccountDao {

    @Resource
    private DataSource dataSource;

    public void withdraw(String account, int amount) {
        modifyBalance(account, -amount);
    }

    public void deposit(String account, int amount) {
        modifyBalance(account, amount);
    }

    private void modifyBalance(String account, int amount) {
        try {
            Connection connection = DataSourceUtils.getConnection(dataSource);
            PreparedStatement statement = connection
                    .prepareStatement("update account set balance = balance + ? where holder = ?");
            statement.setInt(1, amount);
            statement.setString(2, account);
            statement.execute();
        }
        catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Для выполнения перевода между двумя счетами существует некоторый класс InternalBankTransfer, который имеет метод перевода:

    public void transfer(String from, String to, int amount) {
        // start transaction
        accountDao.withDraw(from, amount);
        accountDao.deposit(to, amount);
        // commit transaction
    }

Обычно это работает нормально.Но давайте скажем, что у нас есть два человека, инициирующие переводы одновременно.Скажем, Анна хочет передать Бобу 100 баксов одновременно, а Боб хочет передать Энн 50 баксов.Таким образом, в одном потоке Энн вызывает transfer("Anne", "Bob", 100), а в другом Бобе вызывает transfer("Bob", "Anne", 50).Этот код восприимчив к мертвым блокировкам, если порядок выполнения следующий:

T1: accountDao.withDraw("Anne", 100);
T2: accountDao.withDraw("Bob", 50);
T1: accountDao.deposit("Bob", 100);
T2: accountDao.deposit("Anne", 50); // BAM! ORA-00060: deadlock detected while waiting for resource

Я признаю, что вообще не рассматривал это до того, как начал видеть мертвые блокировки в реальном приложении.Мое наивное мнение состояло в том, что изоляция транзакций как бы позаботилась об этом автоматически.Oracle говорит, что это связано с плохим дизайном приложений.Но что хорошего дизайна в этом случае?Нужно ли select for update все, что я планирую обновить?Что если это огромная транзакция, включающая обновления нескольких таблиц?Должен ли я спроектировать так, чтобы мертвые замки были невозможны, или просто минимизировать их и принять, что они являются фактом жизни?

Ответы [ 4 ]

4 голосов
/ 15 сентября 2011

Есть несколько проблем с дизайном выше.

Даже если вы спрашиваете о взаимоблокировках, я чувствую необходимость написать также о других проблемах, которые, по-моему, неверны, и они могут спасти вас от некоторых неприятностей в будущем.

В вашем дизайне первая проблема, которую я вижу, это разделение методов: Для того, чтобы внести изменения в баланс, у вас есть метод для вывода и способ для депозита. В каждом из них вы используете один и тот же метод "modifyBalance" для выполнения действия. и есть несколько проблем в том, как это делается:

1 - метод modifyBalance запрашивает соединение каждый раз, когда вызывается 2 - в соединении, скорее всего, будет включен режим автоматической фиксации, поскольку вы не отключили автоматическую фиксацию.

почему это проблематично? логика, которую вы делаете, должна быть единым целым. Предположим, вы забрали 50 у Боба, и это удалось. у вас есть автоматическая фиксация, и изменения являются окончательными. сейчас вы пытаетесь внести депозит Анне, и это не удается. согласно приведенному выше коду, Энн не получит 50, но Боб их уже потерял !!! так что в этом случае вам нужно снова позвонить на Deposit, чтобы вернуть Бобу, и вернуть ему 50, надеясь, что это не сработает, иначе ... бесконечная обработка. следовательно, эти действия должны быть в одной транзакции. либо снятие и внесение депозита завершаются успешно, и они совершаются, либо они терпят неудачу, и все откатывается.

это также проблематично, поскольку в режиме автоматической фиксации фиксация происходит после завершения оператора или следующего выполнения. если по какой-либо причине фиксация не произошла, то, поскольку вы не закрываете соединение (и это еще одна проблема, поскольку оно не возвращается в пул), и никакая фиксация не происходит, это может привести к взаимоблокировке, если на сервере будет выполнено другое обновление. строка заблокирована в первой транзакции.

поэтому я предлагаю вам сделать следующее: либо запросить соединение в вашем методе перевода, либо объединить методы изъятия и депозита в методе, изменить сам баланс.

Поскольку мне кажется, что вам понравилась идея иметь два метода такими, какие они есть, я продемонстрирую использование первого из упомянутых мной вариантов:)

public class AccountDao {

    @Resource
    private DataSource dataSource;

    public void withdraw(String account, int amount,Connection connection) throws SQLException{
        modifyBalance(account, -amount);
    }

    public void deposit(String account, int amount,Connection connection) throws SQLException{
        modifyBalance(account, amount);
    }

    private void modifyBalance(String account, int amount,Connection connection) throws SQLException {
            PreparedStatement statement = connection.prepareStatement("update account set balance = balance + ? where holder = ?");
            statement.setInt(1, amount);
            statement.setString(2, account);
            statement.execute();
   }

}   

и методом передачи становится:

public void transfer(String from, String to, int amount) {

        try {
            Connection connection = DataSourceUtils.getConnection(dataSource);
            connection.setAutoCommit(false);
            accountDao.withDraw(from, amount,connection);
            accountDao.deposit(to, amount,connection);
        }
        catch (SQLException e) {
            if (connection!=null)
              connection.rollback();
            throw new RuntimeException(e);
        }
        finally {
           if (connection!=null){
              connection.commit();
              connection.close();
           }
        }

    }

Теперь либо оба действия успешны, либо оба откатываются. Кроме того, когда обновление выполняется для строки, другие транзакции, пытающиеся обновить строку, будут ждать ее завершения, прежде чем они смогут продолжить. откат или фиксация обеспечивает снятие блокировки уровня строки.

Теперь вышесказанное является объяснением лучшего дизайна, чтобы сохранить логические действия и правильность данных. но это не решит проблемы блокировки !!!! Вот пример того, что может произойти:

предположим, что кто-то пытается выйти из Боба.

статус: строка bob заблокирована t1

в это время поток 2 выводится из anne

статус: строка anne заблокирована потоком 2

Теперь поток 1 хочет внести в Энн

status: поток 1 видит, что строка anne заблокирована, поэтому он сидит и ожидает снятия блокировки, чтобы он мог выполнить обновление: поток 1 фактически ожидает в потоке два, чтобы завершить обновление и зафиксировать или откатить блокировку выходит на свободу

теперь нить два хочет внести в боб

статус: строка bob заблокирована, поэтому нить 2 ждет своего освобождения

DEADLOCK !!!!!

два потока ждут друг друга.

так как мы решаем это? Пожалуйста, смотрите опубликованные ответы (я видел их при наборе этого) и, пожалуйста, не принимайте этот ответ, но принимайте тот, который вы фактически используете, чтобы предотвратить взаимные блокировки. Я просто хотел поговорить о других проблемах, как раньше, и извините за то, что я так долго.

4 голосов
/ 15 сентября 2011

Я думаю, что это факт жизни (и тот, который действительно должен происходить только с данными с высоким уровнем параллелизма и точек доступа).

Если вы хотите реализовать упорядочение блокировки, тогда да, вам нужно переписатькод для блокировки или обновления учетных записей в заранее определенном порядке (сначала Анна, затем Боб).Но это не будет возможно со сложными транзакциями.Если это происходит только с несколькими рядами горячих точек, возможно, вы можете использовать порядок блокировки только для них (и оставить все как есть) и покончить с этим.

Или используйте меньше гранулярных блокировок, но это убьет ваш параллелизм.

В вашем случае вы можете просто повторить прерванную транзакцию.И если это происходит слишком часто, кажется, что у вас есть проблемы с дизайном приложения.

Вот ссылка для протокола двухфазной фиксации для банковских переводов.Это из вики MongoDB, то есть от людей, которые вообще не имеют такой роскоши как блокировки строк и транзакций, но это можно реализовать и в СУБД, чтобы избежать конфликта блокировок.Это, конечно, было бы довольно радикальным изменением дизайна приложения.Сначала я попробовал бы все остальное (повторные попытки, грубые блокировки, искусственно уменьшенный уровень параллелизма, пакетная обработка).

2 голосов
/ 15 сентября 2011

Вы можете использовать SELECT FOR UPDATE NOWAIT в строке, прежде чем пытаться обновить ее. Если строка уже заблокирована, вы получите сообщение об ошибке (ORA-00054). Либо немного подождите и повторите попытку (*), либо сгенерируйте исключение.

Вы никогда не должны попадать в тупики, поскольку их так легко предотвратить.


(*) в этом случае вам придется повторить всю транзакцию (после отката), чтобы предотвратить ситуацию взаимоблокировки.

0 голосов
/ 15 сентября 2011

Предполагая, что снятие и внесение являются частью одной транзакции с базой данных, избежать взаимных блокировок должно быть относительно легко, просто работая с учетными записями по порядку. Если в вашем приложении реализованы переводы путем дебетования или зачисления меньшего номера счета, а затем дебетования или зачисления более высокого номера счета, вы никогда не сможете зайти в тупик, выполнив несколько одновременных переводов. С точки зрения предотвращения взаимных блокировок не имеет значения, какой порядок вы применяете (хотя это может иметь значение для производительности приложения), если вы последовательно применяете этот порядок.

...