Блокировка таблицы для получения MAX в LINQ

У меня есть запрос в LINQ, я хочу получить MAX кода моей таблицы, увеличить его и вставить новую запись с новым кодом. точно так же, как функция IDENTITY SQL Server, но здесь мой столбец Code - это char (5), где могут быть алфавиты и числа.

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

моя команда:

var maxCode = db.Customers.Select(c=>c.Code).Max();
var anotherCustomer = db.Customers.Where(...).SingleOrDefault();
anotherCustomer.Code = GenerateNextCode(maxCode);
db.SubmitChanges();

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

using (var db = new DBModelDataContext())
{
    DbTransaction tran = null;
    try
    {
        db.Connection.Open();
        tran = db.Connection.BeginTransaction(IsolationLevel.Serializable);
        db.Transaction = tran;
        .
        .
        .
        .
        tran.Commit();
    }
    catch
    {
        tran.Rollback();
    }
    finally
    {
        db.Connection.Close();
    }
}

ошибка:

Транзакция (идентификатор процесса 60) заблокирована на ресурсах блокировки другим процессом и выбрана в качестве жертвы взаимоблокировки. Повторите транзакцию.

другие IsolationLevels генерируют эту ошибку:

Строка не найдена или изменена.

Пожалуйста, помогите мне, спасибо.

UPDATE2: у меня есть метод .NET, генерирующий новый буквенно-цифровой код. UPDATE3: Моя функция .NET генерирует такой код: 0000, 0001, 0002,..., 0009, 000a, 000b, 000c,..., 000z, 0010, 0011, 0012,..., 0019, 001a, 001b , 001з, ... ...


person Hossein Margani    schedule 26.05.2010    source источник
comment
Небольшой комментарий к вашему коду: вам не нужно откатывать транзакцию. База данных сделает это за вас, если она не зафиксирована явно. Просто используйте: using (var tran = BeginTran()) { tran.Commit(); }   -  person Steven    schedule 26.05.2010
comment
О, большое спасибо.   -  person Hossein Margani    schedule 27.05.2010
comment
Это распространенная проблема проектирования: использование бизнес-идентификатора для связывания таблиц. Используйте id int, чтобы связать объекты (таблицы) и код как собственность клиента. В будущем кто-то решит изменить алгоритм расчета этого Кода и что тогда делать? Во всяком случае, вы не сказали, как вы вычисляете этот код.   -  person garik    schedule 27.05.2010
comment
используйте функцию CLR! (операторов T-SQL недостаточно): см. третье решение   -  person garik    schedule 27.05.2010
comment
Нет, я не могу использовать функцию CLR, мой веб-сервер не поддерживает CLR!   -  person Hossein Margani    schedule 27.05.2010
comment
Не забудьте отметить понравившийся ответ.   -  person Steven    schedule 22.06.2010


Ответы (5)


Избегайте блокировки и постоянного доступа к одним и тем же ресурсам «медленного доступа»:

  • В начале ваше приложение (сервис) вычисляет следующий идентификатор (например, max + 1)
  • В некоторой переменной (вы должны заблокировать доступ ТОЛЬКО к этой переменной) зарезервируйте, например, 100 значений (это зависит от использования вашего идентификатора)
  • Используйте эти идентификаторы

Избегайте использования столбцов IDENTITY (при откате транзакции идентификатор все равно будет увеличиваться)

Используйте некоторую таблицу для хранения ключей (последних или следующих идентификаторов) для каждой таблицы (или для всех таблиц как вариант).

Удача.

Для вашего веб-приложения:

Как изменить и получить доступ к состоянию приложения:

Application.Lock();
Application["IDS"] = <some ids>
Application.UnLock();

Второе решение:

Используйте хранимую процедуру и код примерно такой:

declare @id int

update t set
    @id = id
    ,id = @id + 1 
from dbo.TableIdGenerator t where t.TableName = 'my table name that id I need'

select @id

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

Третье решение:

Используйте функцию CLR.

person garik    schedule 26.05.2010
comment
Спасибо, но у меня есть веб-приложение, и пользователи одновременно используют мое приложение. - person Hossein Margani; 26.05.2010
comment
1) Используйте событие Application_Start() (Global.asax) для резервирования и сохранения идентификаторов в состоянии приложения Application[IDS] = ‹бесплатные идентификаторы› 2) Доступ к этим данным из любого места вашего веб-приложения 3) Используйте блокировку для совместного чтения/записи данные из состояния приложения - person garik; 26.05.2010
comment
Мне нравится это решение. Однако в веб-ферме это не работает, потому что каждое приложение будет иметь свое собственное состояние. - person Steven; 26.05.2010
comment
Я думаю, что это не сработает, потому что asp.net имеет много потоков для моего веб-приложения, и каждый поток может запускать мой код. - person Hossein Margani; 26.05.2010
comment
@ Стивен: Ты прав. Состояние приложения не передается между веб-серверами в ферме. - person garik; 26.05.2010
comment
Это хорошо, и это хорошее решение для кода числового типа, но у меня есть метод, который генерирует код типа буквенно-цифровой, что насчет этого? смотрите обновление. - person Hossein Margani; 27.05.2010

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

Я предполагаю, что 5-символьное уникальное значение приращения является требованием, потому что, когда это не так, вам определенно следует просто использовать столбец IDENTITY. Однако, если предположить, что это не так, вот идея, которая может сработать.

Попробуйте создать метод, который позволяет выразить этот 5-символьный идентификатор как число. Как это сделать, зависит от того, какие символы разрешены в вашем идентификаторе символа и какие комбинации возможны, но вот несколько примеров: «00000» -> 0, «00009», -> 9, «0000z» -> 36, «00010». ' -> 37, '0001z' -> 71, 'zzzzz' -> 60466175. Когда вы нашли метод, используйте увеличивающийся первичный ключ для таблицы и используйте триггер, который вычисляет идентификатор символа после того, как вы вставили запись. Если триггер не подходит, вы также можете сделать это в .NET. Или вы можете не хранить это значение из 5 символов в своей базе данных, потому что оно вычисляется. Вы можете определить его в представлении или просто как свойство в объекте вашего домена.

person Steven    schedule 26.05.2010
comment
Большое Вам спасибо. Я создал эту функцию в .NET и работает правильно. но моя проблема в том, что это параллелизм. - person Hossein Margani; 26.05.2010
comment
Если вы позволите базе данных генерировать для вас первичные ключи, у вас не возникнет проблемы параллелизма. - person Steven; 26.05.2010

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

public void DoTheJob()
{
int tries = 0;
    try
    {
        using (var sr = new StreamReader(@"c:\lock.txt"))
        {
            try
            {
                // get the maximum code from my table
                // generate next code
                // update current record with the new code
            }
            catch (Exception ex)
            {
                Logger.WriteError(ex);
            }
            finally
            {
                sr.Close();
            }
        }
    }
    catch
    {
        Thread.Sleep(2000); // wait for lock for 2 second
    tries++;
    if (tries > 15)
        throw new Exception("Timeout, try again.");
    }
}

Скажите, пожалуйста, правильно ли это решение. Или используйте StreamWriter.

person Hossein Margani    schedule 27.05.2010

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

f(code) -> code

to

f(id) -> code

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

Но предполагая, что это действительно невозможно, какое-то быстрое решение - используйте бильярдный стол с предварительно сгенерированными кодами. Затем используйте просто идентификаторы (автоинкрементируемые) в вашей основной таблице. Недостаток: вам нужно использовать дополнительное соединение для извлечения данных. Лично мне это не нравится.

Другое решение, «нормальное»: сохранить более низкий уровень изоляции и просто обработать исключение (т.е. снова получить код, снова вычислить новый код и сохранить данные). Это довольно классическая ситуация, веб или не веб, не имеет значения.

Обратите внимание: вы получите ту же проблему при одновременном редактировании одних и тех же данных. Так что в каком-то смысле вы не можете избежать такого рода проблем.

ИЗМЕНИТЬ:

Итак, я правильно догадался, что эта функция просто f(id) -> code. Вы можете удалить столбец кода и использовать идентификатор автоинкремента. Затем добавьте представление, в котором код рассчитывается на лету. Использование представления как способа извлечения данных из таблицы всегда является хорошей идеей (подумайте об этом как о получателе свойства в C#). Если вы боитесь загрузки процессора ;-) вы можете вычислять код при вставке записей (используйте триггеры).

Конечно, проблемы с блокировкой записей полностью не устранены (по-прежнему могут происходить одновременные правки).

person greenoldman    schedule 26.05.2010
comment
ваше второе решение хорошее, но вы имеете в виду, что Serializable имеет более низкий уровень изоляции или читается незафиксированным? когда выдает ошибку? - person Hossein Margani; 27.05.2010
comment
Нет, мое первое решение (вверху) хорошее. Serializble — самый высокий транс. уровень чтения незафиксированных данных, также известный как грязное чтение, является самым низким. Выберите что-то среднее — MSSQLServer2005 поддерживает 4 транс. уровней, 2008 г. имеет еще два AFAIR. - person greenoldman; 28.05.2010

вот мой ответ, не совсем правильный, но без ошибок.

public static void WaitLock<T>() where T : class
{
    using (var db = GetDataContext())
    {
        var tableName = typeof(T).Name;
        var count = 0;
        while (true)
        {
            var recordsUpdated = db.ExecuteCommand("UPDATE LockedTable SET IsLocked = 1 WHERE TableName = '" + tableName + "' AND IsLocked = 0");
            if (recordsUpdated <= 0)
            {
                Thread.Sleep(2000);
                count++;
                if (count > 50)
                    throw new Exception("Timeout getting lock on table: " + tableName);
            }
            else
            {
                break;
            }
        }
    }
}


public static void ReleaseLock<T>() where T : class
{
    using (var db = GetDataContext())
    {
        var tableName = typeof(T).Name;
        db.ExecuteCommand("UPDATE LockedTable SET IsLocked = 0 WHERE TableName = '" + tableName + "' AND IsLocked = 1");
    }
}
public static void GetContactCode(int id)
{
    int tries = 0;
    try
    {
        WaitLock<Contact>();
        using (var db = GetDataContext())
        {
            try
            {
                var ct = // get contact
                var maxCode = // maximum code
                ct.Code = // generate next
                db.SubmitChanges();
            }
            catch
            {
            }
        }
    }
    catch
    {
        Thread.Sleep(2000);
        tries++;
        if (tries > 15)
            throw new Exception("Timeout, try again.");
    }
    finally
    {
        ReleaseLock<Contact>();
    }
}
person Hossein Margani    schedule 05.07.2010