Яков Сироткин (yakov_sirotkin) wrote,
Яков Сироткин
yakov_sirotkin

Неожиданные проблемы параллельной обработки данных

Допустим, что вы пишите программное обеспечение для банка и у вас настроен автоплатеж за телефон — если баланс меньше 500, то положить на счёт 500. И на всякий случай ещё один автоплатеж настроен так, что если баланс меньше 450, то тоже добавить 500. И вот внезапно вы обнаруживаете, что у вас на счёте 1400 рублей, потому что баланс внезапно опустился до 400 и сработали оба автоплатежа. Предположим, что сам платеж реализован как отдельная задача и такие задачи выполняются асинхронно. Так же будем считать, что перед началом платежа баланс проверяется. Таким образом, проблема явно связана с параллельной обработкой. Для высоконагруженной системы выполнять все задачи в одном потоке и даже на одной машине было бы странно, поэтому нет ничего удивительного, что второй автоплатеж проверил баланс до того, как первый перевёл деньги на счёт.

Этот пример хорош тем, что мы зависим от внешней системы, поэтому очевидно, что нельзя взять крутую базу данных, настроить суровый уровень изоляции транзакций и написать хранимую процедуру. В любом случае, суровый уровень изоляции транзакций неминуемо сильно скажется на производительности системы, так что в наше время это весьма сомнительный путь. Для пользователя суть проблемы связана с тем, что он не ожидает параллельного выполнения платежей, для него каждый платёж — это атомарная операция, которая происходит мгновенно. Очевидно, что было бы естественно заставить нашу систему делать все операции со счётом делать последовательно, чтобы предыдущая операция заканчивалась до того, как начнётся следующая. Возможно, мы даже хотим, чтобы все операции над всеми счетами данного пользователя выполнялись последовательно. Это довольно легко сделать средствами базы данных, например, брать блокировку можно при помощи команды похожей на

UPDATE ACCOUNTS SET LOCKED_UNTIL = SYSDATE + 1/24/60 WHERE ACCOUNT_ID = ? AND LOCKED_UNTIL < SYSDATE

Если обновиться одна строка — значит контроль получен и можно выполнять транзакцию. Если обновления не произошло — значит в это время может выполняться другая транзакция и нужно подождать. Вместо UPDATE можно делать INSERT и ловить ошибку. Так же можно использовать MERGE. Этот подход хорошо сочетается с асинхронной обработкой задач, когда мы делаем вторую попытку через 1 минуту после первой, третью — через 2 минуты после второй, четвертую — через 4 минуты после третьей и так далее. Если задаче не удалось получить блокировку, то мы просто будем считать попытку выполнения неудачной и в штатном режиме через определённое время попробуем ещё раз.

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

Теперь рассмотрим другой пример, пусть у нас есть сложный бизнес-объект, которые мы кладём в базу данных и который может быть изменен двумя разными способами. При этом каждое изменение по отдельности может быть допустимым, а сделанные одновременно они приводят систему к недопустимому состоянию. Допустим, что наш бизнес-объект — это процесс одобрения закупок. Система предполагает, что есть два назначенных менеджера, каждый из которых должен утвердить закупку, но есть ограничение — эти менеджеры должны быть из разных филиалов. Предположим, что в данный момент назначены менеджеры из Москвы и из Петербурга. Злоумышленник хочет поменять первого менеджера на Васю из Екатеринбурга, а второго — на Петю, тоже из Екатеринбурга. Если система позволяет менять менеджеров параллельно, то есть все шансы, что через несколько попыток злоумышленник добьётся своего, со всеми вытекающими последствиями. Таким образом, в данном случае необходимо обеспечить последовательное выполнение операций над бизнес-объектом.

Допустим теперь, что вы реализуете поддержку какой-то операции с каким-то бизнес-объектом. Через год кто-то другой добавляет поддержку ещё одной операции. Ещё через пару лет оказывается, что при одновременном выполнении эти операции могут привести к фатальным последствиям. Очевидно, что заставить программистов думать о таких причудливых вариантах и пытаться написать тесты против таких неприятностей — это принципиально другой уровень сложности по сравнению с типичной разработкой программного обеспечения. Гораздо проще сразу обеспечить последовательное выполнение всех операций над объектом. Обычно бизнес-объекты редактируются довольно редко и никакой потребности в их параллельной модификации нет.

Концепция у меня есть, а вот проекта, на котором её было бы целесообразно применить — нет. В моём прошлом большом проекте модификации бизнес-объектов осуществлялись во время http-запроса, а для предотвращения параллельной работы мы следили за временем последней модификации объекта. Если оказывалось, что кто-то успел поменять объект во время выполнения бизнес-логики, то мы просто показывали пользователю ошибку. Будут рад комментариям, особенно если вы можете поделиться опытом разработки систем подходящего масштаба.
Tags: code
Subscribe
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 10 comments