Открыть сервис

Неповторяющееся чтение

Неповторяющееся чтение (англ. non-repeatable reads) — это один из феноменов (аномалий) параллельного доступа к данным в системах управления базами данных (СУБД), при котором в рамках одной транзакции один и тот же запрос на чтение данных возвращает разные результаты в разные моменты времени. Данная ситуация возникает, когда параллельная транзакция изменяет (команда UPDATE) или удаляет (команда DELETE) строки, которые были прочитаны первой транзакцией, и фиксирует (COMMIT) эти изменения до завершения первой транзакции. Неповторяющееся чтение является менее строгим уровнем изоляции, чем сериализуемость и повторяемое чтение, и более строгим, чем чтение незафиксированных данных и чтение зафиксированных данных.

Определение и сущность

Неповторяющееся чтение возникает в сценариях конкурентного доступа, когда две или более транзакции выполняются одновременно. В классификации аномалий транзакций, определённых в стандарте SQL (ANSI SQL-92), неповторяющееся чтение (Non-repeatable Read) занимает промежуточное положение. Аномалия проявляется, если при повторном выполнении одного и того же оператора SELECT внутри одной транзакции можно получить другой набор строк или изменённые значения в строках, которые уже были прочитаны ранее. Это принципиально отличается от «грязного чтения», при котором транзакция видит незафиксированные данные другой транзакции.

Стандарт SQL определяет три основных феномена, которые могут нарушать изоляцию транзакций:

Пример возникновения

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

Пусть транзакция T1 выполняет подсчёт средней цены товаров с ценой выше 1000 рублей:

  1. T1: SELECT AVG(цена) FROM Товары WHERE цена > 1000; — результат: 1500 рублей.
  2. Параллельно транзакция T2 изменяет цену одного из товаров с 1200 рублей на 2000 рублей (выполняет команду UPDATE) и фиксирует изменения (COMMIT).
  3. T1: Повторяет тот же запрос: SELECT AVG(цена) FROM Товары WHERE цена > 1000; — результат: 1600 рублей.

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

Уровни изоляции транзакций

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

READ UNCOMMITTED (Чтение незафиксированных данных)

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

READ COMMITTED (Чтение зафиксированных данных)

Уровень, при котором транзакция видит только те данные, которые были зафиксированы на момент выполнения запроса. Он предотвращает грязное чтение, но не исключает неповторяющееся чтение. Именно на этом уровне стандартный сценарий с изменением цены товара (см. выше) будет возможен. READ COMMITTED является уровнем по умолчанию для многих популярных СУБД, включая PostgreSQL и Oracle (в определённых режимах).

REPEATABLE READ (Повторяемое чтение)

При этом уровне изоляции транзакция гарантированно видит одни и те же данные для всех операций чтения, выполненных в её пределах. Если транзакция T1 прочитала какую-либо строку в первый раз, то при повторном чтении той же строки она не увидит изменений, внесённых другими транзакциями (даже если те были зафиксированы). Это предотвращает неповторяющееся чтение, но может допускать фантомное чтение. Например, если T1 прочитала строку с id=5 и получила цену 1200, то при повторном чтении она гарантированно получит ту же цену 1200, даже если T2 изменила её на 2000 и зафиксировала. Однако, если T2 вставит новую строку с id=10, соответствующую условиям запроса WHERE цена > 1000, то при повторном выполнении того же запроса T1 может увидеть новую строку (фантом). В некоторых СУБД (например, MySQL с движком InnoDB) уровень REPEATABLE READ также предотвращает и фантомное чтение за счёт блокировок диапазона.

SERIALIZABLE (Сериализуемость)

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

Механизмы предотвращения

Для предотвращения неповторяющегося чтения СУБД используют два основных подхода:

  1. Блокировки (Locking): На уровне изоляции REPEATABLE READ СУБД может накладывать блокировки на прочитанные строки. Это могут быть блокировки общего доступа (shared locks), которые не позволяют другим транзакциям изменять или удалять эти строки до завершения текущей транзакции. Недостаток — снижение параллелизма: другие транзакции, пытающиеся изменить заблокированные строки, вынуждены ждать.
  1. Многоверсионность (Multiversion Concurrency Control, MVCC): Более современный и эффективный подход, используемый в таких СУБД, как PostgreSQL, MySQL (InnoDB) и Oracle. При MVCC каждая транзакция работает со снимком (snapshot) данных на момент её начала (или на момент первого запроса). Изменения других транзакций не видны, пока они не зафиксированы. Даже после фиксации транзакция, работающая в режиме REPEATABLE READ, продолжает видеть свою старую версию данных, пока сама не завершится. Таким образом, повторное чтение всегда возвращает один и тот же результат.

Значение и практические последствия

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

Выбор уровня изоляции — это всегда компромисс между консистентностью и производительностью. Для аналитических систем (DWH, BI) часто выбирают REPEATABLE READ или SERIALIZABLE, чтобы гарантировать точность агрегированных данных. Для высоконагруженных операционных систем (OLTP), где скорость важнее идеальной точности, часто используется READ COMMITTED, допускающий неповторяющееся чтение.

Критика стандартных определений

Стандартное определение неповторяющегося чтения, данное в SQL-92, неоднократно подвергалось критике за двусмысленность. В частности, Ахри Беренсон и другие исследователи в работе «A Critique of ANSI SQL Isolation Levels» (1995) показали, что стандартные определения недостаточно строги и позволяют некоторым СУБД реализовывать уровни изоляции, которые формально предотвращают аномалии, но на практике приводят к новым, неучтённым аномалиям (например, «звёздный срыв» или «несогласованное чтение»). В ответ на это были предложены более чёткие формальные определения, основанные на историях выполнения транзакций (snapshot isolation).

Интересные факты

Источники

  1. Беренсон, А., Бернштейн, Ф., Грей, Дж., Мелтон, Д., О'Нил, Э., и О'Нил, П. (1995). A Critique of ANSI SQL Isolation Levels. ACM SIGMOD Record, 24(2), 1–10.
  2. Грей, Дж., и Риату, А. (1993). Transaction Processing: Concepts and Techniques. Morgan Kaufmann.
  3. Дата, К. Дж. (2009). Введение в системы баз данных (8-е изд.). Вильямс.
  4. ISO/IEC 9075-2:2016. Information technology — Database languages — SQL — Part 2: Foundation (SQL/Foundation).
  5. Молчанов, А. (2017). Конкурентный доступ к данным в СУБД. РГУ нефти и газа имени И.М. Губкина.

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →