EntityFramework 并处理重复的主要 key/concurrency/race 条件情况

EntityFramework and handling duplicate primary key/concurrency/race conditions situations

我写了一个被许多应用程序引用的库,它跟踪谁在线以及他们正在查看哪个应用程序和页面。

数据使用 EF6 存储在 Sql Server 2008 table 中,它跟踪用户名(主键)、应用程序、页面和时间戳。我只想存储每个人的最新请求,因此每个用户名只能存储一次。

从每个应用程序的 Global.asax 调用的库代码如下所示:

public static void Add(ApplicationType application, string username, string pageRequested)
{
    using (var db = new CommonDAL()) // EF context
    {
        var exists = db.ActiveUsers.Find(username);

        if (exists != null)
            db.ActiveUsers.Remove(exists);

         var activeUser = new ActiveUser() { ApplicationID = application.Value(), Username = username, PageRequested = pageRequested, TimeRequested = DateTime.Now };
         db.ActiveUsers.Add(activeUser);

         db.SaveChanges();
    }
}

我间歇性地收到错误 Violation of PRIMARY KEY constraint 'PK_tblActiveUser_Username'. Cannot insert duplicate key in object 'dbo.tblActiveUser'. The duplicate key value is (xxxxxxxx)

我只能猜测发生的是请求 A 进来,删除了现有的用户名。然后请求 B(来自同一用户)进来,试图删除用户名,但发现什么都不存在。然后请求 A 添加用户名。然后请求 B 尝试添加用户名。当 Web 服务器向客户端发送 401 状态时,似乎经常触发该错误,这再次指向触发此错误的短时间内的多个请求。

我在使用单元测试模拟这种竞争条件时遇到了麻烦,因为我之前没有做过太多的异步编程,但我试图创建具有延迟的异步测试来模拟多个同时发生的缓慢请求。我尝试使用 using (var transaction = new TransactionScope())using (var transaction = db.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted)) 来锁定请求,以便请求 A 可以在请求 B 开始之前完成,但无法验证其中任何一个都解决了问题,因为我无法可靠地模拟这种情况。

1)防止异常的正确方法是什么(最近的请求是最终存储的请求)?

2) 编写单元测试以证明其有效的正确方法是什么?

因为你只想存储最新的项目,你可以使用最后更新获胜并避免关于谁可以先插入的竞争条件,数据库处理锁和最后调用更新(这是最近的) 是 table.

中的内容

如果您 运行 遇到边缘情况下的并发问题,即全新用户同时有 2 个请求并避免 "infinite" 循环,则类似以下内容应该可以处理任何主键错误错误(直到出现堆栈溢出异常为止)。

public static void Add(ApplicationType application, 
                       string username, 
                       string pageRequested, 
                       int recursionCount = 0)
{
    using (var db = new CommonDAL()) // EF context
    {
        var exists = db.ActiveUsers.Find(username);

        if (exists != null)
        {
            exists.propa = "someVal";
        }
        else
        {

            var activeUser = new ActiveUser 
            { 
                ApplicationID = application.Value(), 
                Username = username, 
                PageRequested = pageRequested, 
                TimeRequested = DateTime.Now 
            };

            db.ActiveUsers.Add(activeUser);
        }
        try
        {
            db.SaveChanges();
        }
        catch(<Primary Key Violation>)
        {
            if(recursionCount < x)
            {
                Add(application, username, pageRequested, recursionCount++)
            }
            else
            {
                throw;
            }
        }
    }
}

至于对此进行单元测试,除非您插入人为延迟或可以强制两个线程同时 运行,否则将非常困难。有时,根据问题,竞争条件的时间在毫秒范围内。任务可能无法工作,因为它们不能保证同时 运行,你将它们扔到后台线程池,它们可以时 运行。旧式线程可能有效,但我不知道如何强制执行它,因为读取和删除和创建之间的时间很可能在 5 毫秒或更短的范围内。