Могу ли я рассчитывать на чтение значений идентификаторов SQL Server по порядку?

24

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 )

Мое рассуждение, это должно работать как-то так:

  1. Транзакция запускается (явно или неявно).
  2. Идентификационное значение (X) генерируется.
  3. Соответствующая блокировка строки берется в кластеризованном индексе на основе значения идентификатора (если не активируется повышение блокировки, в этом случае вся таблица блокируется).
  4. Строка вставлена.
  5. Транзакция фиксируется (возможно, довольно много времени спустя), поэтому блокировка снова снимается.

Я думаю, что между шагами 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 я должен принять?

Есть идеи получше?

Фабиан Шмид
источник
Вы пытались использовать Sequence вместо идентичности? С идентичностью, я не думаю, что вы можете надежно предсказать, какая вставка получит конкретное значение идентичности, но это не должно быть проблемой при использовании последовательности. Конечно, это меняет то, как ты сейчас делаешь.
Антуан Эрнандес
@SoleDBAGuy Разве последовательность не сделает условия гонки, которые я описал выше, еще более вероятными? Я создаю новое значение последовательности X (заменив шаг 2 выше), затем вставляю строку (шаги 3 и 4). Между 2 и 3 существует вероятность того, что кто-то другой может создать следующее значение последовательности X + 1, зафиксировать его, и читатель читает это значение X + 1, прежде чем я даже смогу вставить строку со значением последовательности X.
Фабиан Шмид

Ответы:

26

При вставке строки существует ли окно возможности между генерацией нового значения идентификатора и блокировкой соответствующего ключа строки в кластеризованном индексе, где внешний наблюдатель может видеть более новое значение идентификатора, вставленное параллельной транзакцией?

Да.

Распределение значений идентичности не зависит от содержащей пользовательской транзакции . Это одна из причин того, что значения идентификаторов используются, даже если транзакция откатывается. Сама операция приращения защищена защелкой, чтобы предотвратить повреждение, но это степень защиты.

В конкретных обстоятельствах вашей реализации распределение идентификаторов (вызов CMEDSeqGen::GenerateNewValue) выполняется еще до того, как пользовательская транзакция для вставки станет активной (и до того, как будут приняты какие-либо блокировки).

Запустив две вставки одновременно с отладчиком, подключенным, чтобы позволить мне заморозить один поток сразу после увеличения и выделения значения идентификатора, я смог воспроизвести сценарий, в котором:

  1. Сессия 1 приобретает значение личности (3)
  2. Сессия 2 приобретает значение личности (4)
  3. Сессия 2 выполняет вставку и фиксирует (поэтому строка 4 полностью видна)
  4. Сессия 1 выполняет вставку и фиксирует (строка 3)

После шага 3 запрос, использующий row_number при блокировке зафиксированного чтения, вернул следующее:

Скриншот

В вашей реализации это приведет к тому, что Checkpoint ID 3 будет пропущен неправильно.

Окно неудач относительно невелико, но оно существует. Чтобы дать более реалистичный сценарий, чем присоединение отладчика: Поток выполнения запроса может выдать планировщик после шага 1 выше. Это позволяет второму потоку выделить значение идентификатора, вставить и зафиксировать, прежде чем исходный поток возобновит выполнение своей вставки.

Для ясности, нет никаких блокировок или других объектов синхронизации, защищающих значение идентификатора после его выделения и до его использования. Например, после шага 1, указанного выше, параллельная транзакция может увидеть новое значение идентификатора, используя функции T-SQL, как IDENT_CURRENTдо того, как строка существует в таблице (даже незафиксированная).

По сути, нет никаких гарантий относительно значений идентичности, чем задокументировано :

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

Это действительно так.

Если требуется строгая транзакционная обработка FIFO, у вас, скорее всего, нет другого выбора, кроме как сериализовать вручную. Если у приложения меньше однозначных требований, у вас есть больше возможностей. В этом отношении вопрос не ясен на 100%. Тем не менее, вы можете найти некоторую полезную информацию в статье Ремуса Русану « Использование таблиц в качестве очередей» .

Пол Уайт говорит, что GoFundMonica
источник
7

Поскольку Пол Уайт ответил абсолютно правильно, существует возможность временно «пропустить» ряды идентификаторов. Вот лишь небольшой фрагмент кода, чтобы воспроизвести этот случай для себя.

Создайте базу данных и тестовый стол:

create database IdentityTest
go
use IdentityTest
go
create table dbo.IdentityTest (ID int identity, c1 char(10))
create clustered index CI_dbo_IdentityTest_ID on dbo.IdentityTest(ID)

Выполните одновременную вставку и выбор в этой таблице в консольной программе C #:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading;

namespace IdentityTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var insertThreads = new List<Thread>();
            var selectThreads = new List<Thread>();

            //start threads for infinite inserts
            for (var i = 0; i < 100; i++)
            {
                insertThreads.Add(new Thread(InfiniteInsert));
                insertThreads[i].Start();
            }

            //start threads for infinite selects
            for (var i = 0; i < 10; i++)
            {
                selectThreads.Add(new Thread(InfiniteSelectAndCheck));
                selectThreads[i].Start();
            }
        }

        private static void InfiniteSelectAndCheck()
        {
            //infinite loop
            while (true)
            {
                //read top 2 IDs
                var cmd = new SqlCommand("select top(2) ID from dbo.IdentityTest order by ID desc")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    var dr = cmd.ExecuteReader();

                    //read first row
                    dr.Read();
                    var row1 = int.Parse(dr["ID"].ToString());

                    //read second row
                    dr.Read();
                    var row2 = int.Parse(dr["ID"].ToString());

                    //write line if row1 and row are not consecutive
                    if (row1 - 1 != row2)
                    {
                        Console.WriteLine("row1=" + row1 + ", row2=" + row2);
                    }
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }

        private static void InfiniteInsert()
        {
            //infinite loop
            while (true)
            {
                var cmd = new SqlCommand("insert into dbo.IdentityTest (c1) values('a')")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    cmd.ExecuteNonQuery();
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }
    }
}

Эта консоль печатает строку для каждого случая, когда один из потоков чтения «пропускает» запись.

Стефан Кайнц
источник
1
Хороший код, но вы проверяете только последовательные идентификаторы ( "// пишите строку, если row1 и row не являются последовательными" ). Возможно, возникли пробелы, которые ваш код напечатает. Это не значит, что эти пробелы будут заполнены позже.
ypercubeᵀᴹ
1
Поскольку код не запускает сценарий, в котором IDENTITYмогут возникать пропуски (например, откат транзакции), напечатанные строки действительно показывают «пропущенные» значения (или, по крайней мере, они это делали, когда я запускал и проверял их на своем компьютере). Очень хороший репро образец!
Фабиан Шмид
5

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

В принципе, могут возникать пробелы, если вы откатываете операции INSERT (или явно удаляете строки), и могут возникать дубликаты, если для свойства таблицы IDENTITY_INSERT задано значение ON.

Пробелы могут возникать, когда:

  1. Записи удалены.
  2. Произошла ошибка при попытке вставить новую запись (откат)
  3. Обновление / вставка с явным значением (опция identity_insert).
  4. Инкрементное значение больше 1.
  5. Транзакция откатывается.

Свойство удостоверения в столбце никогда не гарантируется:

• уникальность

• Последовательные значения в рамках транзакции. Если значения должны быть последовательными, то транзакция должна использовать монопольную блокировку таблицы или уровень изоляции 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

stacylaray
источник
Я думаю, что пропуски не являются моей главной проблемой - моя главная проблема - повышение видимости ценностей. (То есть, скажем, значение идентичности 7 не должно наблюдаться при упорядочении запроса по этому значению, прежде чем значение идентичности 6 будет.)
Фабиан Шмид,
1
Я видел, как значения идентичности фиксируются как: 1, 2, 5, 3, 4.
stacylaray
Конечно, это легко воспроизвести, например, используя сценарий из ответа Леннарта. Вопрос, с которым я сталкиваюсь, заключается в том, могу ли я наблюдать этот порядок фиксации при использовании запроса с ORDER BY CheckpointSequenceпредложением (которое, как оказалось, является порядком кластеризованного индекса). Я думаю, что это сводится к тому, связан ли генерация значения Identity с блокировками, выполняемыми оператором INSERT, или это просто два несвязанных действия, выполняемых SQL Server одно за другим.
Фабиан Шмид,
1
Что за запрос? Если при использовании чтения зафиксировано, то в вашем примере order by покажет 1, 2, 3, 5, потому что они были зафиксированы, а 4 нет, то есть грязное чтение. Кроме того, ваше объяснение NEventStore гласит: «Поэтому крайне важно, чтобы события никогда не пропускались, поскольку они никогда не будут рассматриваться снова».
Stacylaray
Запрос приведен выше ( gist.github.com/fschmied/47f716c32cb64b852f90 ) - он разбит на страницы , но сводится к простому SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence. Я не думаю, что этот запрос будет читать мимо заблокированной строки 4, или это будет? (В моих экспериментах он блокируется, когда запрос пытается получить блокировку KEY для строки 4.)
Фабиан Шмид,
1

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

  1. T1: вставить в T ... - скажем, 5 вставить
  2. T2: вставить в T ... - скажем, 6 вставить
  3. T2: совершить
  4. Читатель видит 6, но не 5
  5. T1: коммит

В приведенном выше сценарии ваш LAST_READ_ID будет равен 6, поэтому 5 никогда не будет прочитан.

Леннарт
источник
Мои тесты, кажется, показывают, что этот сценарий не является проблемой, потому что Reader (шаг 4) будет блокировать (пока T1 не освободит свои блокировки), когда он попытается прочитать строку со значением 5. Я что-то упустил?
Фабиан Шмид
Вы можете быть правы, я не очень хорошо знаю механизм блокировки в SQL-сервере (поэтому я подозреваю, что в моем ответе).
Леннарт
Зависит от уровня изоляции читателя. Если я вижу оба, блокирую или вижу только 6.
Майкл Грин,
0

Запуск этого скрипта:

BEGIN TRAN;
INSERT INTO dbo.Example DEFAULT VALUES;
COMMIT;

Ниже приведены блокировки, которые я вижу полученными и снятыми как захваченные сеансом расширенного события:

name            timestamp                   associated_object_id    mode    object_id   resource_type   session_id  resource_description
lock_acquired   2016-03-29 06:37:28.9968693 1585440722              IX      1585440722  OBJECT          51          
lock_acquired   2016-03-29 06:37:28.9969268 7205759890195415040     IX      0           PAGE            51          1:1235
lock_acquired   2016-03-29 06:37:28.9969306 7205759890195415040     RI_NL   0           KEY             51          (ffffffffffff)
lock_acquired   2016-03-29 06:37:28.9969330 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969579 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969598 7205759890195415040     IX      0           PAGE            51          1:1235
lock_released   2016-03-29 06:37:28.9969607 1585440722              IX      1585440722  OBJECT          51      

Обратите внимание на блокировку ключа RI_N, полученную непосредственно перед блокировкой ключа X для новой создаваемой строки. Эта кратковременная блокировка диапазона будет препятствовать одновременной вставке получить другую блокировку KEY RI_N, поскольку блокировки RI_N несовместимы. Окно, которое вы упомянули между шагами 2 и 3, не имеет значения, поскольку блокировка диапазона получается перед блокировкой строки вновь сгенерированного ключа.

Пока вы SELECT...ORDER BYначинаете сканирование до нужных вновь вставленных строк, я ожидаю, что вы будете действовать на READ COMMITTEDуровне изоляции по умолчанию, если READ_COMMITTED_SNAPSHOTопция базы данных отключена.

Дэн Гусман
источник
1
Согласно technet.microsoft.com/en-us/library/… , две блокировки с RangeI_Nявляются совместимыми , то есть не блокируют друг друга (блокировка в основном там для блокировки на существующем сериализуемом считывателе).
Фабиан Шмид
@FabianSchmied, интересно. Эта тема конфликтует с матрицей совместимости блокировок в technet.microsoft.com/en-us/library/ms186396(v=sql.105).aspx , которая показывает, что блокировки не совместимы. Пример вставки в ссылке, которую вы упомянули, демонстрирует такое же поведение, как показано в трассировке в моем ответе (кратковременная блокировка диапазона вставки для проверки диапазона перед блокировкой монопольного ключа).
Дан Гузман
1
На самом деле, матрица говорит «N» для «нет конфликта» (не для «не совместим») :)
Фабиан Шмид
0

Из моего понимания SQL Server поведение по умолчанию для второго запроса не отображать никаких результатов, пока первый запрос не будет зафиксирован. Если первый запрос выполняет ROLLBACK вместо COMMIT, в вашем столбце будет отсутствующий идентификатор.

Базовая конфигурация

Таблица базы данных

Я создал таблицу базы данных со следующей структурой:

CREATE TABLE identity_rc_test (
    ID4VALUE INT IDENTITY (1,1), 
    TEXTVALUE NVARCHAR(20),
    CONSTRAINT PK_ID4_VALUE_CLUSTERED 
        PRIMARY KEY CLUSTERED (ID4VALUE, TEXTVALUE)
)

Уровень изоляции базы данных

Я проверил уровень изоляции моей базы данных с помощью следующего утверждения:

SELECT snapshot_isolation_state, 
       snapshot_isolation_state_desc, 
       is_read_committed_snapshot_on
FROM sys.databases WHERE NAME = 'mydatabase'

Который дал следующий результат для моей базы данных:

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(Это настройка по умолчанию для базы данных в SQL Server 2012)

Тестовые сценарии

Следующие сценарии были выполнены с использованием стандартных настроек клиента SQL Server SSMS и стандартных настроек SQL Server.

Настройки клиентских подключений

Клиент был настроен на использование уровня изоляции транзакции READ COMMITTEDв соответствии с параметрами запроса в SSMS.

Запрос 1

Следующий запрос был выполнен в окне запроса с SPID 57

SELECT * FROM dbo.identity_rc_test
BEGIN TRANSACTION [FIRST_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Nine')
/* Commit is commented out to prevent the INSERT from being commited
--COMMIT TRANSACTION [FIRST_QUERY]
--ROLLBACK TRANSACTION [FIRST_QUERY]
*/

Запрос 2

Следующий запрос был выполнен в окне запроса с SPID 58

BEGIN TRANSACTION [SECOND_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Ten')
COMMIT TRANSACTION [SECOND_QUERY]
SELECT * FROM dbo.identity_rc_test

Запрос не завершен и ожидает разблокировки eXclusive на странице.

Скрипт для определения блокировки

Этот скрипт отображает блокировку, возникающую в объектах базы данных для двух транзакций:

SELECT request_session_id, resource_type,
       resource_description, 
       resource_associated_entity_id,
       request_mode, request_status
FROM sys.dm_tran_locks
WHERE request_session_id IN (57, 58)

И вот результаты:

58  DATABASE                    0                   S   GRANT
57  DATABASE                    0                   S   GRANT
58  PAGE            1:79        72057594040549300   IS  GRANT
57  PAGE            1:79        72057594040549300   IX  GRANT
57  KEY         (a0aba7857f1b)  72057594040549300   X   GRANT
58  KEY         (a0aba7857f1b)  72057594040549300   S   WAIT
58  OBJECT                      245575913           IS  GRANT
57  OBJECT                      245575913           IX  GRANT

Результаты показывают, что первое окно запроса (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, вы всегда можете открыть консультативное дело и запросить дополнительную информацию.

Джон ака hot2use
источник
1
Спасибо за анализ, но мой вопрос заключается в том, есть ли промежуток времени между генерацией следующего Identityзначения и получением блокировки KEY в строке (куда могут попасть параллельные операции чтения / записи). Я не думаю, что это доказано невозможным вашими наблюдениями, потому что невозможно остановить выполнение запроса и проанализировать блокировки в течение этого сверхкороткого временного окна.
Фабиан Шмид
Нет, вы не можете остановить утверждения, но мое (медленное) наблюдение - это то, что происходит быстро / нормально. Как только один SPID получает блокировку для вставки данных, другой не сможет получить такую ​​же блокировку. Преимущество более быстрого оператора состоит в том, что он уже получил последовательность и идентификатор последовательно. Следующий оператор получит следующий идентификатор после снятия блокировки.
Джон aka hot2use
1
Как правило, ваши наблюдения совпадают с моими (а также моими ожиданиями) - это приятно знать. Интересно, есть ли исключительные ситуации, когда они не выдержат?
Фабиан Шмид