1

我在 LINQ 中有一个查询,我想获取我的表的 MAX 代码并增加它并用新代码插入新记录。就像 SQL Server 的 IDENTITY 功能,但这里我的代码列是 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 的 Transaction,执行两三次后发生错误:

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, ... ...

4

5 回答 5

1

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

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

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

使用某个表来存储每个表(或所有表作为变体)的键(最后一个或下一个 id)。

运气。

对于您的网络应用程序:

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

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 插入第一条也是唯一一条记录。

第三种解决方案:

使用 CLR 函数。

于 2010-05-26T07:23:38.060 回答
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。

于 2010-05-27T10:59:22.433 回答
0

If possible, try to rethink your database design. As you already noticed, having to use isolation level Serializable, when locking a whole table, can be troublesome.

I assume that the 5 character unique incrementing value is a requirement, because when it is not, you should definitely simply use an IDENTITY column. However, assuming this is not the case, here is an idea that might work.

Try to create a method that allows to express that 5 char identifier as a number. How to do this depends on which characters are allowed in your char identifier and which combinations are possible, but here are some examples: '00000' -> 0, '00009', -> 9, '0000z' -> 36, '00010' -> 37, '0001z' -> 71, 'zzzzz' -> 60466175. When you've found a method, use a incrementing primary key for the table and use a trigger that calculates the char identifier after you inserted a record. When a trigger is not appropriate, you can also do this in .NET. Or you can choose not to store that 5 char value in your database, because it is calculated. You can define it in a view or simply as property in your domain entity.

于 2010-05-26T08:01:38.010 回答
0

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

f(code) -> code

f(id) -> code

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

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

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

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

编辑:

所以,我猜对了,这个函数就是 f(id) -> 代码。您可以删除代码列并使用自动增量 ID。然后添加一个动态计算代码的视图。使用视图作为从表中检索数据的一种方式始终是一个好主意(将其视为 C# 中的属性获取器)。如果您担心 CPU 使用率 ;-) 您可以在插入记录时计算代码(使用触发器)。

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

于 2010-05-26T09:08:10.800 回答
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>();
    }
}
于 2010-07-05T08:42:31.300 回答