锁定表以在LINQ中获取MAX

时间:2010-05-26 06:48:36

标签: c# linq linq-to-sql transactions isolation-level

我在LINQ中有一个查询,我希望获得表格的MAX代码并增加它并使用新代码插入新记录。就像SQL Server的IDENTITY功能一样,但这里我的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();
    }
}

错误:

  

交易(流程ID 60)是   锁定资源死锁   另一个过程并被选为   死锁的受害者。重新运行   事务。

其他IsolationLevels会生成此错误:

  

未找到或更改行。

请帮帮我,谢谢。

UPDATE2:我有一个.NET方法生成新代码,这是一个字母数字。 UPDATE3:我的.NET函数生成如下代码:0000,0001,0002,...,0009,000a,000b,000c,...,000z,0010,0011,0012,...,0019,001a,001b ,001z,......

5 个答案:

答案 0 :(得分:1)

避免锁定和持续访问相同的“慢速访问”资源:

  • 在开始时,您的应用程序(服务)计算下一个id(例如,max + 1)
  • 在某个变量中(您应该仅锁定对此变量的访问权限)保留,例如100个值(取决于您的id的用法)
  • 使用这些ID

避免使用IDENTITY列(如果事务回滚,id仍将增加)

使用一些表来存储每个表的键(last或next id)(或作为variant的所有表)。

运气。

对于您的网络应用程序:

如何更改和访问应用程序状态:

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

更新操作是原子的,你可以增加id并返回当前的id。 不要忘记为每个表的id插入第一个也是唯一的记录。

第三个解决方案:

使用CLR功能。

答案 1 :(得分:0)

如果可能,请尝试重新考虑您的数据库设计。正如您已经注意到的那样,在锁定整个表时,必须使用隔离级别Serializable可能会很麻烦。

我假设5个字符的唯一递增值是一个要求,因为当它不是时,你绝对应该只使用IDENTITY列。但是,假设情况并非如此,这可能有用。

尝试创建一种方法,允许将该5个字符标识符表示为数字。如何执行此操作取决于您的字符标识符中允许使用哪些字符以及可能的组合,但以下是一些示例:'00000' - &gt; 0,'00009', - &gt; 9,'0000z' - &gt; 36,'00010' - &gt; 37,'0001z' - &gt; 71,'zzzzz' - &gt; 60466175.找到方法后,对表使用递增主键,并在插入记录后使用计算字符标识符的触发器。当触发器不合适时,您也可以在.NET中执行此操作。或者您可以选择不在数据库中存储该5个char值,因为它是计算的。您可以在视图中定义它,也可以只在域实体中定义它。

答案 2 :(得分:0)

查看函数GenerateNextCode非常有用,因为它可能是至关重要的信息。为什么?因为我不相信无法从

更改此功能
f(code) -> code

f(id) -> code

如果后者是真的,你可以重新设计你的桌子,整个概念会更容易。

但是假设它真的不可能,一些快速解决方案 - 使用带有预生成代码的池表。然后在主表中使用简单的ids(自动增量)。 缺点:您必须使用额外的连接来检索数据。我个人不喜欢它。

另一种解决方案,“正常”解决方案:保持较低的隔离级别并简单地处理异常(即再次获取代码,再次计算新代码并保存数据)。这是非常经典的情况,网络,没有网络,没关系。

请注意:在同时编辑相同数据时会遇到同样的问题。所以从某种意义上说,你无法避免这种问题。

编辑:

所以,我猜对了,这个函数只是f(id) - &gt;码。您可以删除代码列并使用autoincrement id。然后添加一个视图,其中代码是在飞行中计算的。使用视图作为从表中检索数据的方法总是一个好主意(把它想象为C#中的属性getter)。 如果你害怕CPU使用率;-)你可以在插入记录时计算代码(使用触发器)。

当然,锁定记录的问题不会完全删除(仍然可以进行并发编辑)。

答案 3 :(得分:0)

我有一个解决方案,但不完整,它减少了错误和问题:我有一个名为“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。

答案 4 :(得分:0)

这是我的答案,不完全正确,但没有错误。

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>();
    }
}