Чи можу я покластися на читання значень ідентичності 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 Persistent Engine NEventStore. NEventStore реалізує сховище лише для додатків, де кожна подія отримує новий порядковий номер контрольно-пропускного коду, що збільшується. Клієнти читають події з магазину подій, замовленого контрольною точкою для виконання будь-яких обчислень. Після того, як подія з контрольною точкою X буде оброблена, клієнти розглядають лише "новіші" події, тобто події з контрольною точкою X + 1 і вище. Тому важливо, щоб події ніколи не можна було пропустити, тому що вони ніколи не будуть розглянуті знову. Зараз я намагаюся визначити, чи Identityреалізація контрольно-пропускної бази відповідає цій вимозі. Це саме використовувані оператори SQL : схема , запит письменника ,Запит читача .)

Якщо я маю рацію і може скластись описана вище ситуація, я бачу лише два варіанти боротьби з ними, обидва з них незадовільні:

  • Побачивши значення послідовності контрольної точки X + 1 перед тим, як побачити X, відхиліть X + 1 і повторіть спробу пізніше. Однак, оскільки, Identityзвичайно, може виникнути прогалини (наприклад, коли транзакція відкочується назад), X ніколи не може відбутися.
  • Отже, такий же підхід, але прийняти проміжок через п ять мілісекунд. Однак яке значення n слід припустити?

Якісь кращі ідеї?


Ви намагалися використовувати послідовність замість ідентичності? Що стосується ідентичності, я не думаю, що ви можете надійно передбачити, яка вставка отримає певне значення ідентичності, але це не повинно бути проблемою з використанням послідовності. Звичайно, це змінює те, як ти зараз робиш справи.
Антуан Ернандес

@SoleDBAGuy Невже послідовність не зробить умови гонки, яку я описав вище, ще ймовірнішою? Я створюю нове значення послідовності X (замінюючи крок 2 вище), після чого вставляю рядок (кроки 3 і 4). Між 2 та 3, існує ймовірність, що хтось інший може створити наступне значення послідовності X + 1, здійснить його, і читач прочитає це значення X + 1, перш ніж я навіть прийму до вставки мого рядка зі значенням послідовності X.
Фабіан Шмід

Відповіді:


26

Чи вставляє рядок, чи існує вікно можливостей між генерацією нового значення Identity та блокуванням відповідного ключа рядка в кластерному індексі, де зовнішній спостерігач міг бачити нове значення Identity, вставлене за допомогою одночасної транзакції?

Так.

Розподіл значень ідентичності не залежить від містить користувальницької транзакції . Це одна з причин того, що значення ідентичності споживаються, навіть якщо транзакція повертається назад. Сама операція збільшення приросту захищена засувкою для запобігання корупції, але це ступінь захисту.

У конкретних обставинах вашої реалізації, розподіл особи (виклик до CMEDSeqGen::GenerateNewValue) проводиться до того, як транзакція користувача для вставки буде навіть активована (і так до того, як будуть зроблені будь-які блокування).

Запустивши дві вставки одночасно з відладчиком, приєднаним дозволити мені заморозити один потік відразу після того, як значення ідентифікації зросте та розподілено, я зміг відтворити сценарій, де:

  1. Сесія 1 набуває значення ідентичності (3)
  2. Сесія 2 набуває значення ідентичності (4)
  3. Сесія 2 виконує вставку та здійснює (тому рядок 4 повністю видно)
  4. Сесія 1 виконує вставку та здійснює (рядок 3)

Після кроку 3 запит із використанням рядкового числу під блокуванням прочитаного читання повертає наступне:

Знімок екрана

У вашій реалізації це призведе до неправильного пропуску Checkpoint ID 3.

Вікно невмілості порівняно невелике, але воно існує. Щоб надати більш реалістичний сценарій, ніж додавання налагоджувача: Виконання потоку запитів може дати планувальник після кроку 1 вище. Це дозволяє другому потоку виділити значення ідентичності, вставити та здійснити, перш ніж початковий потік почне відновити його вставку.

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

По суті, гарантій щодо цінностей особи не існує більше, ніж задокументовано :

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

Це насправді це.

Якщо потрібна сувора транзакційна обробка FIFO, у вас, швидше за все, немає іншого вибору, крім серіалізації вручну. Якщо у додатку менше вимог, у вас є більше варіантів. Питання не є на 100% зрозумілим у цьому плані. Тим не менш, ви можете знайти корисну інформацію в статті Ремуса Русану " Використання таблиць як черг" .


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
Хороший код, але ви перевіряєте лише наявність послідовних ідентифікаторів ( "// рядок запису, якщо рядок1 та рядок не є послідовними" ). Можливо, виникнуть прогалини, які надрукує ваш код. Це не означає, що ці прогалини будуть заповнені пізніше.
ypercubeᵀᴹ

1
Оскільки код не запускає сценарій, коли IDENTITYб створювати прогалини (наприклад, відкат транзакції), надруковані рядки дійсно показують "пропущені" значення (або, принаймні, це робилося, коли я бігав і перевіряв це на своїй машині). Дуже приємний зразок запитання!
Фабіан Шмід

5

Найкраще не сподіватися, що посвідчення особи будуть послідовними, оскільки існує багато сценаріїв, які можуть залишити прогалини. Краще розглядати ідентичність як абстрактне число і не надавати йому жодного ділового значення.

В основному, прогалини можуть статися, якщо ви відкатуєте операції INSERT (або явно видаляєте рядки), а дублікати можуть виникнути, якщо встановити властивість таблиці IDENTITY_INSERT на УВІМКНЕНО.

Прогалини можуть виникнути, коли:

  1. Записи видаляються.
  2. Під час спроби вставити новий запис сталася помилка (відкат)
  3. Оновлення / вставка з явним значенням (параметр ident_insert).
  4. Приріст значення перевищує 1.
  5. Угода повертається назад.

Властивість ідентичності стовпця ніколи не гарантувала:

• Унікальність

• Послідовні значення в межах транзакції. Якщо значення повинні бути послідовними, тоді транзакція повинна використовувати ексклюзивний замок на таблиці або використовувати рівень ізоляції СЕРІАЛІЗАЦІЙНО.

• Послідовні значення після перезавантаження сервера.

• Повторне використання значень.

Якщо ви не можете використовувати значення ідентичності через це, створіть окрему таблицю, що містить поточне значення, і керуйте доступом до таблиці та присвоєння числа за допомогою програми. Це може вплинути на продуктивність.

https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx


Я думаю, що прогалини не є моєю основною проблемою - головна моя проблема - це видимість цінностей. (Тобто, скажімо, значення ідентичності 7 не повинно бути помітним для впорядкування запитів за цим значенням до того, як має значення 6.)
Fabian Schmied

1
Я бачив, як значення ідентичності здійснюються так: 1, 2, 5, 3, 4.
stacylaray

Звичайно, це легко відтворити, наприклад, використовуючи сценарій відповіді Леннарта. Питання, з яким я стикаюся, полягає в тому, чи можу я спостерігати за тим порядком фіксації під час використання запиту з ORDER BY CheckpointSequenceпунктом (що трапляється як порядок кластерного індексу). Я думаю, що це зводиться до питання, чи генерування значення Identity так чи інакше пов'язане з блокуваннями, зробленими оператором INSERT, чи це просто дві непов'язані дії, що виконуються SQL Server одна за одною.
Фабіан Шмід

1
Що таке запит? Якщо ви використовуєте читання, здійснене, то у вашому прикладі порядок by відображатиме 1, 2, 3, 5, оскільки вони були скоєні, а 4 - ні, тобто брудне читання. Крім того, ваше пояснення NEventStore стверджує: "Тому важливо, щоб події ніколи не можна було пропустити, оскільки вони ніколи не будуть розглядатися знову".
stacylaray

Запит наведений вище ( gist.github.com/fschmied/47f716c32cb64b852f90 ) - він знаходиться на сторінці, але зводиться до простого SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence. Я не думаю, що цей запит буде читати повз заблокований рядок 4, чи не так? (У моїх експериментах він блокує, коли запит намагається придбати замок KEY для рядка 4.)
Fabian Schmied

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 KEY, придбаний безпосередньо перед блокуванням клавіші X для нового створюваного рядка. Цей короткочасний блокування діапазону запобігає одночасному вкладенню отримати інший замок RI_N KEY, оскільки блоки 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" на "без конфлікту" (не на "не сумісний") :)
Fabian Schmied

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)

Тестові сценарії

Наступні сценарії були виконані з використанням стандартних налаштувань клієнта SSMS SQL Server та стандартних налаштувань 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 буде відкрито на PAGE.

Сценарій для визначення блокування

Цей скрипт відображає блокування, що відбувається на об'єктах бази даних для двох транзакцій:

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) на DATABASE - призначений замок eXlusive (IX) на ОБ'ЄКТІ, призначений замок eXlusive (IX) на СТОРІНКІ, до якого він хоче вставити, та eXclusive замок (X) на KEY він вставлений, але ще не здійснений.

Через недопущені дані другий запит (SPID 58) має Спільний блокування (S) на рівні DATABASE, Блокуючий загальний (ІС) об'єкт на об'єкті, Замислений загальний (ІС) блокування на сторінці a Спільний (S ) заблокувати КЛЮЧ із статусом запиту ЗАЧЕКАЙТЕ.

Підсумок

Запит у першому вікні запиту виконується без скоєння. Оскільки другий запит може READ COMMITTEDмістити лише ті дані, які він чекає, доки не настане час очікування, або поки транзакція не буде здійснена в першому запиті.

Це з мого розуміння поведінка за замовчуванням Microsoft SQL Server.

Вам слід зауважити, що ідентифікатор дійсно є послідовним для наступного читання операторами SELECT, якщо перший оператор COMMIT.

Якщо перший вислів робить ROLLBACK, ви знайдете ідентифікатор, що відсутній у послідовності, але все ж із ідентифікатором у порядку зростання (за умови, що ви створили INDEX з опцією за замовчуванням або ASC у стовпці ідентифікатора).

Оновлення:

(Відверто) Так, ви можете розраховувати на правильне функціонування стовпця, поки не виникне проблема. На веб-сайті Microsoft існує лише один HOTFIX щодо SQL Server 2000 та стовпця особи .

Якщо ви не можете покластися на правильне оновлення стовпця особи, я думаю, що на веб-сайті Microsoft буде більше виправлень та виправлень.

Якщо у вас є договір підтримки Microsoft, ви завжди можете відкрити консультативну справу і попросити додаткову інформацію.


1
Дякую за аналіз, але моє запитання полягає в тому, чи є часове вікно між генерацією наступного Identityзначення та придбанням блокування KEY на ряду (куди можуть потрапити паралельні читання / записи). Я не думаю, що це виявляється неможливим вашими спостереженнями, оскільки не можна зупинити виконання запитів та проаналізувати блокування під час цього ультракороткого часового вікна.
Фабіан Шмід

Ні, ви не можете зупинити твердження, але моє (повільне) спостереження - це те, що відбувається швидко / нормально. Як тільки один SPID придбає замок для вставки даних, інший не зможе придбати той самий замок. Швидше оператор матиме перевагу в тому, що вже придбав замок та ідентифікатор послідовно. Наступне повідомлення отримає наступний ідентифікатор після звільнення блокування.
Джон aka hot2use

1
На звичайній основі ваші спостереження відповідають моїм власним (а також моїм очікуванням) - це добре знати. Цікаво, чи є виняткові ситуації, коли вони не втримаються.
Фабіан Шмід
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.