TL; DR: вопрос ниже сводится к следующему: при вставке строки существует ли окно возможности между генерацией нового Identity
значения и блокировкой соответствующего ключа строки в кластеризованном индексе, где внешний наблюдатель может видеть более новую Identity
значение, вставленное параллельной транзакцией? (В SQL Server.)
Подробная версия
У меня есть таблица SQL Server с Identity
именем столбца CheckpointSequence
, который является ключом кластеризованного индекса таблицы (который также имеет ряд дополнительных некластеризованных индексов). Строки вставляются в таблицу несколькими параллельными процессами и потоками (на уровне изоляции READ COMMITTED
и без IDENTITY_INSERT
). В то же время существуют процессы, периодически читающие строки из кластеризованного индекса, упорядоченные по этому CheckpointSequence
столбцу (также на уровне изоляции READ COMMITTED
, с READ COMMITTED SNAPSHOT
отключенной опцией).
В настоящее время я полагаюсь на то, что процессы чтения никогда не могут «пропустить» контрольную точку. Мой вопрос: могу ли я рассчитывать на это имущество? А если нет, что я мог сделать, чтобы это стало правдой?
Пример: когда вставляются строки со значениями идентичности 1, 2, 3, 4 и 5, читатель не должен видеть строку со значением 5, прежде чем увидит строку со значением 4. Тесты показывают, что запрос, который содержит ORDER BY CheckpointSequence
предложение ( и WHERE CheckpointSequence > -1
пункт), надежно блокирует всякий раз, когда строка 4 должна быть прочитана, но еще не зафиксирована, даже если строка 5 уже зафиксирована.
Я полагаю, что, по крайней мере, теоретически, здесь может быть состояние гонки, которое может привести к нарушению этого предположения. К сожалению, документация о Identity
многом не говорит о том, как Identity
работает в контексте нескольких одновременных транзакций, она только говорит: «Каждое новое значение генерируется на основе текущего семени и приращения». и «Каждое новое значение для конкретной транзакции отличается от других одновременных транзакций в таблице». ( MSDN )
Мое рассуждение, это должно работать как-то так:
- Транзакция запускается (явно или неявно).
- Идентификационное значение (X) генерируется.
- Соответствующая блокировка строки берется в кластеризованном индексе на основе значения идентификатора (если не активируется повышение блокировки, в этом случае вся таблица блокируется).
- Строка вставлена.
- Транзакция фиксируется (возможно, довольно много времени спустя), поэтому блокировка снова снимается.
Я думаю, что между шагами 2 и 3, есть очень маленькое окно, где
- параллельный сеанс может сгенерировать следующее значение идентификатора (X + 1) и выполнить все оставшиеся шаги,
- тем самым позволяя читателю, приходящему точно в этот момент времени, прочитать значение X + 1, пропуская значение X.
Конечно, вероятность этого кажется крайне низкой; но все же - это могло случиться. Или это может?
(Если вас интересует контекст: это реализация механизма сохранения SQL от NEventStore . NEventStore реализует хранилище событий только для добавления, где каждое событие получает новый порядковый номер восходящей контрольной точки. Клиенты читают события из хранилища событий, упорядоченного по контрольной точке для выполнения всех видов вычислений. После обработки события с контрольной точкой X клиенты рассматривают только «более новые» события, т. е. события с контрольной точкой X + 1 и выше. Поэтому крайне важно, чтобы события никогда не пропускались, поскольку они никогда не будут рассматриваться снова. В настоящее время я пытаюсь определить, соответствует ли Identity
реализация контрольной точки на основе этого требования. Это именно те операторы SQL, которые используются : Схема , запрос Writer ,Запрос читателя .)
Если я прав и описанная выше ситуация может возникнуть, я вижу только два варианта решения этих проблем, оба из которых неудовлетворительны:
- Если вы видите значение последовательности контрольных точек X + 1 до того, как увидите X, отклоните X + 1 и повторите попытку позже. Однако, поскольку,
Identity
конечно , могут возникать пробелы (например, при откате транзакции), X может никогда не прийти. - Таким образом, тот же подход, но принять разрыв через n миллисекунд. Тем не менее, какое значение n я должен принять?
Есть идеи получше?
источник
Ответы:
Да.
Распределение значений идентичности не зависит от содержащей пользовательской транзакции . Это одна из причин того, что значения идентификаторов используются, даже если транзакция откатывается. Сама операция приращения защищена защелкой, чтобы предотвратить повреждение, но это степень защиты.
В конкретных обстоятельствах вашей реализации распределение идентификаторов (вызов
CMEDSeqGen::GenerateNewValue
) выполняется еще до того, как пользовательская транзакция для вставки станет активной (и до того, как будут приняты какие-либо блокировки).Запустив две вставки одновременно с отладчиком, подключенным, чтобы позволить мне заморозить один поток сразу после увеличения и выделения значения идентификатора, я смог воспроизвести сценарий, в котором:
После шага 3 запрос, использующий row_number при блокировке зафиксированного чтения, вернул следующее:
В вашей реализации это приведет к тому, что Checkpoint ID 3 будет пропущен неправильно.
Окно неудач относительно невелико, но оно существует. Чтобы дать более реалистичный сценарий, чем присоединение отладчика: Поток выполнения запроса может выдать планировщик после шага 1 выше. Это позволяет второму потоку выделить значение идентификатора, вставить и зафиксировать, прежде чем исходный поток возобновит выполнение своей вставки.
Для ясности, нет никаких блокировок или других объектов синхронизации, защищающих значение идентификатора после его выделения и до его использования. Например, после шага 1, указанного выше, параллельная транзакция может увидеть новое значение идентификатора, используя функции T-SQL, как
IDENT_CURRENT
до того, как строка существует в таблице (даже незафиксированная).По сути, нет никаких гарантий относительно значений идентичности, чем задокументировано :
Это действительно так.
Если требуется строгая транзакционная обработка FIFO, у вас, скорее всего, нет другого выбора, кроме как сериализовать вручную. Если у приложения меньше однозначных требований, у вас есть больше возможностей. В этом отношении вопрос не ясен на 100%. Тем не менее, вы можете найти некоторую полезную информацию в статье Ремуса Русану « Использование таблиц в качестве очередей» .
источник
Поскольку Пол Уайт ответил абсолютно правильно, существует возможность временно «пропустить» ряды идентификаторов. Вот лишь небольшой фрагмент кода, чтобы воспроизвести этот случай для себя.
Создайте базу данных и тестовый стол:
Выполните одновременную вставку и выбор в этой таблице в консольной программе C #:
Эта консоль печатает строку для каждого случая, когда один из потоков чтения «пропускает» запись.
источник
IDENTITY
могут возникать пропуски (например, откат транзакции), напечатанные строки действительно показывают «пропущенные» значения (или, по крайней мере, они это делали, когда я запускал и проверял их на своем компьютере). Очень хороший репро образец!Лучше не ожидать, что идентификаторы будут последовательными, потому что есть много сценариев, которые могут оставить пробелы. Лучше рассматривать идентичность как абстрактное число и не придавать ему никакого делового значения.
В принципе, могут возникать пробелы, если вы откатываете операции INSERT (или явно удаляете строки), и могут возникать дубликаты, если для свойства таблицы IDENTITY_INSERT задано значение ON.
Пробелы могут возникать, когда:
Свойство удостоверения в столбце никогда не гарантируется:
• уникальность
• Последовательные значения в рамках транзакции. Если значения должны быть последовательными, то транзакция должна использовать монопольную блокировку таблицы или уровень изоляции SERIALIZABLE.
• Последовательные значения после перезапуска сервера.
• Повторное использование значений.
Если вы не можете использовать значения идентификаторов из-за этого, создайте отдельную таблицу, содержащую текущее значение, и управляйте доступом к таблице и назначением номеров в вашем приложении. Это может повлиять на производительность.
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx
источник
ORDER BY CheckpointSequence
предложением (которое, как оказалось, является порядком кластеризованного индекса). Я думаю, что это сводится к тому, связан ли генерация значения Identity с блокировками, выполняемыми оператором INSERT, или это просто два несвязанных действия, выполняемых SQL Server одно за другим.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. Я не думаю, что этот запрос будет читать мимо заблокированной строки 4, или это будет? (В моих экспериментах он блокируется, когда запрос пытается получить блокировку KEY для строки 4.)Я подозреваю, что это иногда может привести к проблемам, проблемам, которые становятся хуже, когда сервер находится под большой нагрузкой. Рассмотрим две транзакции:
В приведенном выше сценарии ваш LAST_READ_ID будет равен 6, поэтому 5 никогда не будет прочитан.
источник
Запуск этого скрипта:
Ниже приведены блокировки, которые я вижу полученными и снятыми как захваченные сеансом расширенного события:
Обратите внимание на блокировку ключа RI_N, полученную непосредственно перед блокировкой ключа X для новой создаваемой строки. Эта кратковременная блокировка диапазона будет препятствовать одновременной вставке получить другую блокировку KEY RI_N, поскольку блокировки RI_N несовместимы. Окно, которое вы упомянули между шагами 2 и 3, не имеет значения, поскольку блокировка диапазона получается перед блокировкой строки вновь сгенерированного ключа.
Пока вы
SELECT...ORDER BY
начинаете сканирование до нужных вновь вставленных строк, я ожидаю, что вы будете действовать наREAD COMMITTED
уровне изоляции по умолчанию, еслиREAD_COMMITTED_SNAPSHOT
опция базы данных отключена.источник
RangeI_N
являются совместимыми , то есть не блокируют друг друга (блокировка в основном там для блокировки на существующем сериализуемом считывателе).Из моего понимания SQL Server поведение по умолчанию для второго запроса не отображать никаких результатов, пока первый запрос не будет зафиксирован. Если первый запрос выполняет ROLLBACK вместо COMMIT, в вашем столбце будет отсутствующий идентификатор.
Базовая конфигурация
Таблица базы данных
Я создал таблицу базы данных со следующей структурой:
Уровень изоляции базы данных
Я проверил уровень изоляции моей базы данных с помощью следующего утверждения:
Который дал следующий результат для моей базы данных:
(Это настройка по умолчанию для базы данных в SQL Server 2012)
Тестовые сценарии
Следующие сценарии были выполнены с использованием стандартных настроек клиента SQL Server SSMS и стандартных настроек SQL Server.
Настройки клиентских подключений
Клиент был настроен на использование уровня изоляции транзакции
READ COMMITTED
в соответствии с параметрами запроса в SSMS.Запрос 1
Следующий запрос был выполнен в окне запроса с SPID 57
Запрос 2
Следующий запрос был выполнен в окне запроса с SPID 58
Запрос не завершен и ожидает разблокировки eXclusive на странице.
Скрипт для определения блокировки
Этот скрипт отображает блокировку, возникающую в объектах базы данных для двух транзакций:
И вот результаты:
Результаты показывают, что первое окно запроса (SPID 57) имеет общую блокировку (S) на базе данных, блокировку Intended eXlusive (IX) на OBJECT, блокировку Intended eXlusive (IX) на PAGE, в которую он хочет вставить, и eXclusive замок (X) на КЛЮЧЕ он вставил, но еще не зафиксировал.
Из-за неподтвержденных данных второй запрос (SPID 58) имеет общую блокировку (S) на уровне базы данных, блокировку намеренного совместного использования (IS) на объекте, блокировку намеренного совместного использования (IS) на странице совместно используемой (S) ) Блокировка на ключ с запросом статуса WAIT.
Резюме
Запрос в первом окне запроса выполняется без фиксации. Поскольку второй запрос может обрабатывать только
READ COMMITTED
данные, он ожидает либо до истечения времени ожидания, либо до тех пор, пока транзакция не будет зафиксирована в первом запросе.Это из моего понимания поведения по умолчанию Microsoft SQL Server.
Вы должны заметить, что идентификатор действительно в последовательности для последующего чтения с помощью операторов SELECT, если первый оператор COMMITs.
Если первый оператор выполняет ROLLBACK, вы найдете отсутствующий идентификатор в последовательности, но все еще с идентификатором в порядке возрастания (при условии, что вы создали INDEX с параметром по умолчанию или ASC в столбце идентификатора).
Обновить:
(Прямо) Да, вы можете положиться на то, что столбец идентификаторов будет работать правильно, пока не возникнет проблема. Существует только один HOTFIX относительно SQL Server 2000 и столбца идентификаторов на веб-сайте Microsoft.
Если вы не можете полагаться на корректное обновление столбца идентификаторов, я думаю, что на веб-сайте Microsoft будет больше исправлений или исправлений.
Если у вас есть контракт на поддержку Microsoft, вы всегда можете открыть консультативное дело и запросить дополнительную информацию.
источник
Identity
значения и получением блокировки KEY в строке (куда могут попасть параллельные операции чтения / записи). Я не думаю, что это доказано невозможным вашими наблюдениями, потому что невозможно остановить выполнение запроса и проанализировать блокировки в течение этого сверхкороткого временного окна.