Метод С# для блокировки таблицы SQL Server

У меня есть программа на С#, которая должна выполнять группу массовых обновлений (20k +) в таблице SQL Server. Поскольку другие пользователи могут обновлять эти записи по одному через веб-сайт интрасети, нам необходимо создать программу С# с возможностью блокировки таблицы. После того, как таблица заблокирована, чтобы другой пользователь не выполнял никаких изменений/поиска, нам необходимо предварительно запрограммировать запрошенные обновления/вставки.

Поскольку мы обрабатываем так много записей, мы не можем использовать TransactionScope (казалось, проще всего в первую очередь) из-за того, что наша транзакция завершается обработкой службы MSDTC. Нам нужно использовать другой метод.

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

Мой вопрос двоякий. Я правильно использую SqlTransaction? Если это так (или даже если нет), есть лучший способ для получения блокировки таблицы, которая позволяет текущей программе работать для поиска и предварительной печати обновлений?

Я хотел бы, чтобы таблица была заблокирована, а программа выполнила приведенный ниже код.

С#

SqlConnection dbConnection = new SqlConnection(dbConn);

dbConnection.Open();

using (SqlTransaction transaction = dbConnection.BeginTransaction(IsolationLevel.Serializable))
{
    //Instantiate validation object with zip and channel values
    _allRecords = GetRecords();
    validation = new Validation();
    validation.SetLists(_allRecords);

    while (_reader.Read())
    {
        try
        {
            record = new ZipCodeTerritory();
            _errorMsg = string.Empty;

            //Convert row to ZipCodeTerritory type
            record.ChannelCode = _reader[0].ToString();
            record.DrmTerrDesc = _reader[1].ToString();
            record.IndDistrnId = _reader[2].ToString();
            record.StateCode = _reader[3].ToString().Trim();
            record.ZipCode = _reader[4].ToString().Trim();
            record.LastUpdateId = _reader[7].ToString();
            record.ErrorCodes = _reader[8].ToString();
            record.Status = _reader[9].ToString();
            record.LastUpdateDate = DateTime.Now;

            //Handle DateTime types separetly
            DateTime value = new DateTime();
            if (DateTime.TryParse(_reader[5].ToString(), out value))
            {
                record.EndDate = Convert.ToDateTime(_reader[5].ToString());
            }
            else
            {
                _errorMsg += "Invalid End Date; ";
            }
            if (DateTime.TryParse(_reader[6].ToString(), out value))
            {
                record.EffectiveDate = Convert.ToDateTime(_reader[6].ToString());
            }
            else
            {
                _errorMsg += "Invalid Effective Date; ";
            }

            //Do not process if we're missing LastUpdateId
            if (string.IsNullOrEmpty(record.LastUpdateId))
            {
                _errorMsg += "Missing last update Id; ";
            }

            //Make sure primary key is valid
            if (_reader[10] != DBNull.Value)
            {
                int id = 0;
                if (int.TryParse(_reader[10].ToString(), out id))
                {
                    record.Id = id;
                }
                else
                {
                    _errorMsg += "Invalid Id; ";
                }
            }

            //Validate business rules if data is properly formatted
            if (string.IsNullOrWhiteSpace(_errorMsg))
            {
                _errorMsg = validation.ValidateZipCode(record);
            }

            //Skip record if any errors found
            if (!string.IsNullOrWhiteSpace(_errorMsg))
            {
                _issues++;

                //Convert to ZipCodeError type in case we have data/formatting errors
                _errors.Add(new ZipCodeError(_reader), _errorMsg);
                continue;
            }
            else if (flag)
            {
                //Separate updates to appropriate list
                SendToUpdates(record);
            }
        }
        catch (Exception ex)
        {
            _errors.Add(new ZipCodeError(_reader), "Job crashed reading this record, please review all columns.");
            _issues++;
        }
    }//End while


    //Updates occur in one of three methods below. If I step through the code,
    //and stop the program here, before I enter any of the methods, and then 
    //make updates to the same records via our intranet site the changes
    //made on the site go through. No table locking has occured at this point. 
    if (flag)
    {
        if (_insertList.Count > 0)
        {
            Updates.Insert(_insertList, _errors);
        }
        if (_updateList.Count > 0)
        {
            _updates = Updates.Update(_updateList, _errors);
            _issues += _updateList.Count - _updates;
        }
        if (_autotermList.Count > 0)
        {
            //_autotermed = Updates.Update(_autotermList, _errors);
            _autotermed = Updates.UpdateWithReporting(_autotermList, _errors);
            _issues += _autotermList.Count - _autotermed;
        }
    } 

    transaction.Commit();
}

Ответ 1

SQL на самом деле не обеспечивает способ исключительно блокировки таблицы: он предназначен для максимального использования одновременного использования при сохранении ACID.

Вы можете попробовать использовать эти подсказки в своих запросах:

  • TABLOCK

    Указывает, что полученная блокировка применяется на уровне таблицы. Тип блокировки, который будет зависеть от выполняемого утверждения. Например, оператор SELECT может приобрести общий замок. Указав TABLOCK, общий замок применяется к целую таблицу вместо строки или уровня страницы. Если HOLDLOCK также указан, блокировка таблицы сохраняется до конца транзакции.

  • TABLOCKX

    Указывает, что в таблице выполняется исключительная блокировка.

  • UPDLOCK

    Указывает, что блокировки обновлений должны быть взяты и удерживаться до завершения транзакции. UPDLOCK использует блокировки обновления для операций чтения только на уровне строк или страниц. Если UPDLOCK объединяется с TABLOCK, или блокировка на уровне таблицы выполняется для некоторых других разум, вместо этого будет использоваться исключающая (X) блокировка.

  • XLOCK

    Указывает, что исключительные блокировки должны быть взяты и удерживаться до тех пор, пока транзакция завершается. Если указано ROWLOCK, PAGLOCK или TABLOCK, применяются эксклюзивные блокировки к соответствующему уровню детализации.

  • HOLDLOCK/SERIALIZABLE

    Делает разделяемые блокировки более ограничительными, удерживая их до завершения транзакции, вместо того, чтобы освободить разделяемую блокировку, как только требуемая таблица или страница данных не будут дольше необходимо, была ли транзакция завершена или нет. Сканирование выполняется с той же семантикой, что и транзакция, выполняемая на SERIALIZABLE уровень изоляции. Для получения дополнительной информации об уровнях изоляции см. SET TRANSACTION УРОВЕНЬ ИЗОЛЯЦИИ (Transact-SQL).

В качестве альтернативы, вы можете попробовать установить SET TRANSACTION ISOLATION LEVEL SERIALIZABLE:

  • Заявления не могут читать данные, которые были изменены, но еще не были выполнены другими сделки.

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

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

Блокировки диапазона помещаются в диапазон значений ключей, которые соответствуют условиям поиска каждый оператор, выполненный в транзакции. Это блокирует другие транзакции от обновления или вставка любых строк, которые могут быть квалифицированы для любого из операторов, выполняемых текущая транзакция. Это означает, что если какое-либо из операторов транзакции выполненные во второй раз, они будут читать один и тот же набор строк. Блокировки диапазона удерживаются пока транзакция не завершится. Это наиболее ограничивает уровни изоляции потому что он блокирует целые диапазоны ключей и удерживает блокировки до тех пор, пока транзакция завершается. Поскольку concurrency ниже, используйте эту опцию только тогда, когда это необходимо. Эта опция имеет тот же эффект, что и установка HOLDLOCK во всех таблицах во всех операторах SELECT в транзакции.

Но почти наверняка эскалация блокировки вызовет блокировку, и ваши пользователи будут в значительной степени мертвы в воде (по моему опыту).

Итак...

Подождите, пока у вас появится окно обслуживания расписания. Установите базу данных в однопользовательском режиме, внесите изменения и верните их в режиме онлайн.

Ответ 2

Попробуйте: когда вы получаете записи из вашей таблицы (в функции GetRecords()?) используйте подсказку TABLOCKX:

    SELECT * FROM Table1 (TABLOCKX)

Он будет останавливать все остальные чтения и обновления за пределами вашей транзакции до тех пор, пока транзакция не будет выполнена или откат.

Ответ 3

Здесь все о Уровне изоляции. Измените уровень изоляции транзакций на ReadCommited (не просматривайте значение Enum Value в С#, но оно должно быть близко). Когда вы выполняете первое обновление/вставку в таблицу, SQL запустится, и никто не сможет прочитать данные, которые вы изменяете/добавляете, пока вы не совершаете транзакцию или не выполняете транзакцию при условии, что они не выполняют грязные чтения (используя NoLock на их SQL или установить уровень изоляции для Read Uncommited). Будьте осторожны, однако, в зависимости от того, как вы вставляете/обновляете данные, вы можете заблокировать всю таблицу в течение всей транзакции, хотя это может привести к ошибкам таймаута в клиент, когда они пытаются прочитать из этой таблицы, пока ваша транзакция открыта. Не видя SQL позади обновлений, хотя я не могу сказать, произойдет ли это здесь.

Ответ 4

Как заметил кто-то, транзакция, по-видимому, не используется после изъятия.

Из ограниченной информации, которую мы имеем о приложении/цели, трудно сказать, но из фрагмента кода мне кажется, что нам не нужна блокировка. Мы получаем некоторые данные из источника X (в данном случае _reader), а затем вставляем/обновляем в пункт назначения Y.

Вся проверка выполняется с исходными данными, чтобы убедиться в ее правильности, похоже, что мы не принимаем никаких решений или не заботимся о том, что в пункте назначения.

Если приведенное выше значение истинно, то лучшим подходом было бы загрузить все эти данные во временную таблицу (может быть реальной временной таблицей "#" или реальной таблицей, которую мы уничтожаем впоследствии, но цель та же), а затем в одном заявлении sql мы можем сделать массовую вставку/обновление из таблицы temp в наш пункт назначения. Предполагая, что схема db находится в приличной форме, 20 (или даже 30) тысяч записей должны произойти почти мгновенно, без необходимости ждать окна обслуживания или блокировать пользователей в течение продолжительных периодов времени.

Также, чтобы строго ответить на вопрос об использовании транзакции, ниже приведен простой пример того, как правильно использовать транзакцию, должно быть много других образцов и информации в Интернете

SqlConnection conn = new SqlConnection();
SqlCommand cmd1 = new SqlCommand();
SqlTransaction tran = conn.BeginTransaction();

...
cmd1.Transaction = tran;
...
tran.Commit();