Базы данных. Конфликты параллельного доступа (Часть 1 — поиск проблемы)

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

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

    Если к базе данных обращаться из нескольких соединений и проводить изменения, то возникновение конфликтов — это лишь вопрос времени и везения.

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

    Первая часть данной статьи посвящена самой проблеме и способах решения. Вторая часть статьи будет описывать способы разрешения конфликтов в LINQ to SQL. Возможно будет и третья часть, если мне удастся уговорить коллегу описать способы решения конфликтов в Hibernate (но об этом будет известно уже позднее). Эти статьи будут занимать куда больше места, поэтому первая часть описана отдельно, хотя и является достаточно краткой.

    Оптимистичный способ решения

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

    Обнаружение конфликта

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

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

    Например, предположим, что вы хотите обновить объект Customer и назначить новые значения для полей CompanyName, ContactTitle, ContactName. И допустим, что вы хотим, чтобы для поиска конфликта участвовали поля CompanyName (всегда), ContactName (только при обновлении), а ContactTitle — не участвовал. В этом случае, запрос может быть следующим:

    UPDATE Customers
    SET CompanyName = 'Art Sanders Park',
    ContactName = 'Samuel Arthur Sanders',
    ContactTitle = 'President'
    WHERE CompanyName = 'Lonesome Pine Restaurant' AND
    ContactName = 'Fran Wilson' AND
    CustomerID = 'LONEP'


    В этом примере значения столбцов в условии where — первоначальные значения столбцов, которые были прочитаны из базы данных. Как вы можете заметить, поле ContactTitle не участвовало в поиске конфликтов, т.к. мы решили, что она для нас менее важно.

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

    Если кто-то обновил запись после того, как мы её прочитали, то наш запрос не изменит записей в базе данных. Для этого после запросы мы проверим @@ROWCOUNT и узнаем, была ли запись обновлена. И если нет — значит был конфликт параллельного доступа.

    После того, как конфликт был найден — необходимо его решить. Разрешение конфликтов может быть разное, но во второй части статьи мы подробно рассмотрим, как это делается в LINQ to SQL, и возможно мой коллега опишет, как это делается в Hibernate (для третьей части статьи).

    Пессимистичный способ решения

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

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

    Но как я уже говорил, этот способ менее подходит для приложений, в которых необходима возможность одновременной работы с данными из нескольких подключений, т.к. при установки блокировок, данные никто не сможет обновить, пока блокировка не будет снята.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 7
    • –1
      Эта проблема решается через диспетчер обращений, который являет собой надстройку над SQL с заданным алгоритмом поведения
      • –1
        данная статья описывает поиск проблемы, LINQ to SQL работает примерно таким образом, иначе как им узнать, была ли запись обновлена или нет?
      • +1
        References:
        Мартин Фаулер «Архитектура корпоративных программных приложений» (ozon)
        Глава 16. Типовые решения для обработки задач автономного параллелизма
        — Оптимистическая автономная блокировка (Optimistic Offline Lock)
        — Пессимистическая автономная блокировка (Pessimistic Offline Lock)
        — Блокировка с низкой степенью детализации (Coarse-Grained Lock)
        — Неявная блокировка (Implicit Lock)
        • 0
          Думаю, что эта статья о CAS будет уместна здесь: www.ibm.com/developerworks/java/library/j-jtp04186/index.html
          • 0
            При пессимистичном подходе к параллелизму нет конфликтов, которые нужно решать, потому что база данных блокирована вашей транзакцией, так что никто другой не сможет её модифицировать у вас за спиной.

            это не так, одна из транзакций упадет.

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

                Это зависит от внутренней реализации механизма транзакций и используемого уровня изоляции транзакций. В случае mvcc описанный подход не работает, он приведет либо к потере обновлений либо к откату транзакции. Можете почитать здесь https://devcenter.heroku.com/articles/postgresql-concurrency. Нужно использовать явную блокировку lock или select for update.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.