C# NHibernate事务不是';t隔离

C# NHibernate事务不是';t隔离,c#,asp.net-mvc-4,nhibernate,C#,Asp.net Mvc 4,Nhibernate,我在MVC控制器中使用NHibernate框架编写了以下代码: [HttpPost] public void FinishedExecutingTurn() { using (GameUnitOfWork unitOfWork = new GameUnitOfWork()) { int currentUid = int.Parse(User.Identity.Name); Game game

我在MVC控制器中使用NHibernate框架编写了以下代码:

    [HttpPost]
    public void FinishedExecutingTurn()
    {
        using (GameUnitOfWork unitOfWork = new GameUnitOfWork())
        {
            int currentUid = int.Parse(User.Identity.Name);
            Game game = unitOfWork.Games.GetActiveGameOfUser(currentUid);
            Player localPlayer = game.Players.First(p => p.User.Id == currentUid);
            localPlayer.FinishedExecutingTurn = true;

            if (game.Players.All(p => p.FinishedExecutingTurn))
            {
                //do some stuff
            }
            unitOfWork.Commit();
        }
    }
GameUnitOfWork
所做的是对每个请求使用会话(保存在
HttpContext.Current.Items
中)并启动事务

我遇到的问题是,当两个请求同时到达时,事务似乎不是孤立地发生的

这是一场有两名玩家的游戏。我遇到的情况是,每个玩家几乎同时向服务器发送请求。 事务应该将发送请求的玩家的
FinishedExecutingTurn
字段设置为true。如果两个玩家现在都设置为true,就会发生一些事情(“做一些事情”)

如果每个事务单独发生,据我所知,其中一个事务应该首先发生,并在一个播放器上将
FinishedExecutingTurn
设置为true,然后另一个请求中的事务应该在第二个播放器上将
FinishedExecutingTurn
设置为true,并输入我的If语句(“做一些事情”)。 但是,有时它根本不输入if语句,因为在两个请求中,两个播放器的
FinishedExecutingTurn
最初都设置为false


我的问题是,一个事务不一定首先发生并将字段设置为true,然后在另一个请求中,其中一个参与者应该已经设置为true吗?

在阅读了大量关于数据库和锁中并发性的内容后,我终于找到了一个解决方案。我只是用隔离级别“RepeatableRead”定义了这个事务:

这实际上是我尝试的第一个解决方案之一,但我最初在使用它时遇到了死锁。后来,当我尝试只对需要它的特定事务使用这种隔离方法时,死锁似乎消失了

“RepeatableRead”应该实现的是锁定获取的玩家行,直到第一个请求提交事务


但是,由于我以前没有锁方面的经验,我希望能从这方面的专家那里得到其他答案。

这不是NHibernate的责任,而是数据库的责任

据我所知,其中一项交易应该首先发生

否。被隔离的事务并不意味着它们被序列化。在大多数情况下,在默认隔离级别下,这只意味着一个事务无法读取另一个正在进行的事务所做的更改。根据数据库引擎的不同,它将读取以前的值(例如,Oracle会这样做)或被阻止(未启用快照隔离时为SQL Server)。即使在读取相同的数据行时,事务仍然可以并发发生

所以你有两个玩家同时读取另一个玩家的状态,然后标记他们自己已经结束了他们的回合。即使当数据库块读取修改的数据而不是产生以前的值时,也可能发生这种情况,因为更新可能发生在读取之后。 因此,双方都可以将其他玩家的状态解读为未结束回合

通过选择另一个隔离级别,
RepeatableRead
,可以阻止写入。这将导致两个并发事务死锁,一个被取消(受害者),另一个继续。然后应该重放被取消的一个,因为非受害者将在该点结束,或者通过写入标志获得独占锁,重放的交易将能够读取其他玩家已结束其回合(立即或通过等待另一个事务结束,因为它的独占锁禁止重放的事务结束在其上放置共享锁,这是使用此隔离级别读取所需的)

设置隔离级别可以通过
session.BeginTransaction(IsolationLevel.Serializable)
来完成。这样做会降低为许多并发请求提供服务的能力,因此最好只在需要的情况下才这样做。如果使用
TransactionScope
,它们的构造函数也会将
IsolationLevel
作为参数

您可以在读取其他玩家状态之前尝试写入当前玩家状态(在写入后使用
会话.Flush
,然后查询其他玩家状态)。这可能适用于在
ReadCommitted
隔离级别下使用共享锁进行读取和独占锁进行写入的数据库。但在隔离级别下,您也必须处理死锁。对于在
ReadCommitted
下不使用共享锁进行读取的数据库,这将不起作用,而是产生最后提交的v价值

注意:

也许你应该完全改变你的模式:记录每个玩家的动作,而不处理“最后一个”玩家请求中的“回合结束”操作。然后使用一些后台进程来处理所有玩家结束回合的游戏


这可以通过一些排队技术来实现,将玩家回合结束时的标记排队。轮询数据库可以工作,但不是很好。

在任何地方使用
RepeatableRead
都会导致死锁非常频繁。仅在某些点使用它可以大大减少死锁,但仍然可能发生,特别是在事务u中唱那种程度的孤立。非常感谢你的回答,这正是我想要的。正如你所看到的,我在你发帖的同时回答了我自己的问题。我不明白的是,我怎么能不通过简单地使用“RepeatableRead”来获得死锁(我最初是获得它们的,但后来只使用了这个事务)“RepeatableRead”和一切正常并按预期工作)?现在运气好,但死锁会出现。监控错误日志,或者更好的是,更改代码以立即处理它们。您还可以在读取播放机状态后使用断点进行调试,冻结线程(使用线程调试窗口),或
transaction = session.BeginTransaction(IsolationLevel.RepeatableRead);
while (true)
{
    try
    {
        using (GameUnitOfWork unitOfWork = new GameUnitOfWork())
        {
            int currentUid = int.Parse(User.Identity.Name);
            Game game = unitOfWork.Games.GetActiveGameOfUser(currentUid);
            Player localPlayer = game.Players.First(p => p.User.Id == currentUid);
            localPlayer.FinishedExecutingTurn = true;

            if (game.Players.All(p => p.FinishedExecutingTurn))
            {
                //do some stuff
            }
            unitOfWork.Commit();
        }
        return;
    }
    catch (GenericADOException ex)
    {
        // SQL-Server specific code for identifying deadlocks
        // Adapt according to your database errors.
        var sqlEx = ex.InnerException as SqlException;
        if (sqlEx == null || sqlEx.Number != 1205)
            throw;
        // Deadlock, just try again by letting the loop go on (eventually
        // log it).
        // Need to acquire a new session, previous one is dead, put some
        // code here for disposing your previous contextual session and 
        // put a new one instead.
    }
}