Как стать автором
Обновить

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

Время на прочтение4 мин
Количество просмотров15K
Уважаемые коллеги, в данной статье будем рассматривать не виды блокировок в 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 (для третьей части статьи).

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

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

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

Но как я уже говорил, этот способ менее подходит для приложений, в которых необходима возможность одновременной работы с данными из нескольких подключений, т.к. при установки блокировок, данные никто не сможет обновить, пока блокировка не будет снята.
Теги:
Хабы:
Всего голосов 7: ↑5 и ↓2+3
Комментарии7

Публикации

Истории

Ближайшие события