使用EF Code First实现锁定功能

3
我有一个包含汽车信息的表(我们称其为tbl_incoming_car)。该表有一个非唯一的列名为“customer_number”,用于显示目前进入系统的汽车数量。同一辆汽车可以多次进出,但仅注册一次。
因此,当新车进入时,我需要获取最后一辆车的编号,将其加1,并将其保存为新车的“customer_number”。
我知道最简单的方法是为汽车创建一个单独的表,在那里设置“customer_number”,并在其他表中注册入站和出站记录。但这只是一个愚蠢的例子,以揭示这种情况。因此,没有必要讨论该方法的不正确性,我已经知道了:)
正如我所说,每当新车进入系统时,我都必须获取最新添加的行,获取“customer_number”,增加它并将其保存为原子操作。其他应用程序实例可能会尝试执行相同的操作,因此数据库必须在“创建任务”期间保留对最后添加的行的请求。
我认为我可以通过将隔离级别设置为串行化来实现这一点,但我认为它不能防止读取最后一行,而是防止插入新行。因此,看来锁定是解决办法。我尝试在代码中使用静态对象作为监视器,并且它可以很好地工作,但当然它仅限于同一应用程序域,我需要在数据库级别上进行一些操作。
我认为EF中没有任何东西可以锁定数据库,因此,设置表锁定并稍后释放它的最佳方法是什么?
谢谢。
6个回答

3
到目前为止,这是我想出的最佳方法:
    public void SetTransactionLock(String resourceName)
    {
        Ensure.IsNotNull(resourceName, "resourceName");

        String command = String.Format(
        @"declare @result int;
          EXEC @result = sp_getapplock '{0}', 'Exclusive', 'Transaction', 10000 
          IF @result < 0
            RAISERROR('ERROR: cannot get the lock [{0}] in less than 10 seconds.', 16, 1);",resourceName);

        base.Database.ExecuteSqlCommand(command);
    }

    public void ReleaseTransactionLock(String resourceName)
    {
        Ensure.IsNotNull(resourceName, "resourceName");
        String command = String.Format("EXEC sp_releaseapplock '{0}';",resourceName);
        base.Database.ExecuteSqlCommand(command);
    }

由于Entity Framework(EF)没有内置的方法,因此我在我的数据层中添加了这两个方法,并使用它们来声明“关键部分”,在该部分中只允许一个并发操作。

您可以在try finally块中使用它。


1
你测试过了吗?如果在两者之间执行任何操作,我会在ReleaseTransactionLock上得到异常,原因是EF在每次查询之前都会运行sp_resetconnection,这将释放它获取的每个应用程序锁,因此锁实际上不再被保持。 - galets
我发布了自己的解决方案,请看看你是否喜欢它更多。 - galets
当从连接池中检索到连接以准备使用时,会调用sp_resetconnection。通常,在我的用例中,我一直保持连接直到释放,也就是说,我获取锁定,执行操作,保存并释放。所以它运行良好。 - vtortola
也许我们正在使用不同的版本……在我的情况下(EF 6.0),从DbContext连接不断被重置/重用,因此除非你克隆它,否则锁定不会保持。 - galets

1

此前在本线程提到的解决方案可能不适用于所有情况,因为 EF 在任何 SQL 操作之前都会运行 sp_resetconnection,因此当您在 DbContext 上运行涉及 SQL 的任何操作时,它实际上会释放您应该持有的锁。

我想到的解决方法是:在获取锁之前克隆 SqlConnection,并保留此连接,直到您准备释放它为止:

public class SqlAppLock {
    DbConnection connection;
    public SqlAppLock(DbContext context, string resourceName)
    {
        ... (test arguments for null, etc)
        connection = (DbConnection)(context.Database.Connection as ICloneable).Clone();
        connection.Open();

        var cmd = connection.CreateCommand();
        cmd.CommandText = "sp_getapplock";
        ... (set up parameters)
        cmd.ExecuteNonQuery();

        int result = (int)cmd.Parameters["@result"].Value;
        if (result < 0)
        {
            throw new ApplicationException("Could not acquire lock on resource");
        }
    }

然后进行释放,可以使用sp_releaseapplock来释放锁定,或者只需Dispose()连接即可


1

Serializable实际上解决了这个问题。Serializable表示事务的行为就像它们都获得了全局数据库X锁,就好像只有一个事务在同一时间执行。

对于插入操作也是如此。锁将被采取以防止在错误的位置进行插入。

但您可能无法实现死锁自由。尝试一下仍然值得。


我尝试过,但是遇到了死锁问题。我不太确定Serializable的工作原理。例如,如果我在INSERT之前执行SELECT以获取最后一行,那么未来可能出现在SELECT中的行是否被防止读取?因为应用程序会读取最后一行,处理值,然后保存新行。我的理解是,“Serializable”将防止应用程序插入行,但不会防止读取最后一行。 - vtortola

1
使用REPEATABLE READ隔离级别可以在执行SELECT命令的那一刻获取锁。但我不知道如何选择检索最后一行的SELECT命令,可能无法正常工作。您执行SELECT的方式将改变SQL锁定行/索引/表的方式。
最好的方法是执行存储过程。EF在数据库和应用程序之间进行了一些往返,这将增加锁定时间并影响应用程序性能。
也许您可以使用触发器在插入后进行更新。

对的,但我们试图在没有明确所有要做的事情之前不使用 SP(动态需求,你知道的 :P)。目前,我们正在进行大量的重构周期,而 SP 会妨碍这个过程。 - vtortola
EF不会给你太多控制SQL事务的选项。过去我只使用EF,但为了性能,我被迫打破规则。在整个应用程序中,我们只有大约5个存储过程,用于非常特定的性能密集型数据事务。 - Bruno Matuk

1

EF与C#的TransactionScope一起使用。

过去,我曾解决过类似的问题:

 using (var ts = new TransactionScope())
        {
            using (var db = new DbContext())
            {
                db.Set<Car>().Add(newCar);
                db.SaveChanges();
                // newCar.Id has value here, but record is locked for read until ts.Complete()

                newCar.CustomerNumber = db.Set<Car>().Max(car => car.CustomerNumber) + 1;
                db.SaveChanges();
            }
            ts.Complete();
        }

任何并发任务中的代码行 db.Set<Car>().Max(car => car.CustomerNumber) 都必须等待,因为它需要访问所有记录,您可以在 ts.Complete() 之前添加断点并尝试执行 Select max(CustomerNumber) from dbname.dbo.Cars 来检查此问题。当代码暂停在该行上时,查询将在恢复代码和 ts 完成后完成。

当然,这仅适用于您的示例足够描述实际情况的情况,但很容易适应您的需求。

有关更多信息,请参见 MSDN 上的 EF 中的事务


请注意,尽管存在事务,但我需要阻止其他线程读取数据,这不能通过简单的事务实现。使用“可串行化”隔离级别的事务将防止其他线程添加与给定谓词匹配的行,但不会阻止其他线程读取该行。 - vtortola
我不明白。默认的事务,就像这里的事务一样,会阻止其他线程读取正在添加的行(newCar),如果你只使用这种方法插入记录,那么它们将等待事务完成才能读取该记录并添加新记录。 - Goran Obradovic

0

EF支持SQL时间戳,也称为rowversion数据类型。

这使您可以使用乐观锁定执行更新操作。如果字段已正确声明,则框架会为您执行此操作。基本上:

UPDATE where ID=ID SET Customer_number to X+1 变成 Update where ID=ID and Rowversion = rowversion Set Customer_number =X+1

因此,如果自加载线程X以来该行发生了更改,则会出现错误。您可以重新加载并重试,或者根据您的情况采取任何适当的操作。

有关更多信息,请参见http://msdn.microsoft.com/en-us/data/jj592904

http://msdn.microsoft.com/en-us/library/aa0416cz.aspx

还有这篇Stack Overflow的帖子

如何确定实体数据在数据库中何时被更改是一个好的方法?


我在谈论插入,而不是更新。没有人会更新任何行,这只是插入行。 - vtortola
如何在插入时获取锁定?那么密钥的来源就是问题所在。DB生成的Key或Guid将起作用。您使用什么来生成“唯一”键?这听起来很奇怪。 - phil soady
使用悲观并发,因为我现在要发布。 - vtortola
好的,我现在明白你想做什么了,这应该可以实现。对于这个问题,我个人会尝试一种乐观锁定模式,而不是悲观锁定。例如,使用带有键“LastNumberCateegory”的表LastNumber,其中包含一个字段Lastnumbre和一个并发rowversion字段。 - phil soady
1
顺便提一下,从您的示例代码中并不清楚您是否打算将锁定更新解锁放在事务内。如果您选择使用sp_getapplock,则可以使用提交/回滚用法示例。http://msdn.microsoft.com/en-us/library/ms189823.aspx - phil soady

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接