简体   繁体   English

NHibernate交易不是孤立的

[英]NHibernate transaction isn't isolated

I have the following code in a MVC controller, using the NHibernate framework: 我使用NHibernate框架在MVC控制器中有以下代码:

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

What GameUnitOfWork does is use a session-per-request (which is saved in the HttpContext.Current.Items ) and start a transaction. GameUnitOfWork所做的是使用每个请求会话(保存在HttpContext.Current.Items )并开始事务。

The problem I'm having is that when 2 requests arrive simultaneously, it seems like the transactions aren't happening in isolation. 我遇到的问题是,当2个请求同时到达时,事务似乎并不是孤立发生的。

It is a game with 2 players. 这是一个有2位玩家的游戏。 I have a situation in which each of the players sends a request to the server at approximately the same time. 我遇到的情况是每个玩家大约在同一时间将请求发送到服务器。 The transaction is supposed to set the field FinishedExecutingTurn to true on the player who sent the request. 该事务应该在发送请求的播放器上将字段FinishedExecutingTurn设置为true。 If both players are now set to true, something should happen ("do some stuff"). 如果两个玩家现在都设置为true,则应该发生一些事情(“做一些事情”)。

If each transaction happens in isolation, to my understanding one of the transactions should happen first and set FinishedExecutingTurn on one player to true, and then the transaction in the other request should set FinishedExecutingTurn to true on the second player and enter my if statement ("do some stuff"). 如果每笔交易都是独立发生的,据我所知,其中一项交易应首先发生,并将一个播放器上的FinishedExecutingTurn设置为true,然后在另一个请求中的交易将第二个播放器上的FinishedExecutingTurn设置为true并输入我的if语句(“做一些事情”)。 However, sometimes it doesn't enter the if statement at all, because FinishedExecutingTurn is initially set to false on both players, in both of the requests. 但是,有时它根本不输入if语句,因为在两个请求中, FinishedExecutingTurn最初在两个播放器上都设置为false。

My question is, shouldn't one transaction necessarily happen first and set the field to true, and then in the other request, one of the players should already be set to true? 我的问题是,是否不应该先进行一项交易并将该字段设置为true,然后在另一项请求中就已经将其中一个参与者设置为true?

After a lot of reading about concurrency in databases and locks, I finally found a solution. 经过大量有关数据库和锁并发性的阅读后,我终于找到了解决方案。 I simply defined this transaction with an isolation level of "RepeatableRead": 我仅使用隔离级别“ RepeatableRead”定义了该事务:

transaction = session.BeginTransaction(IsolationLevel.RepeatableRead);

This was actually one of the first solutions I tried, but I initially got deadlocks when using it. 这实际上是我尝试的第一个解决方案之一,但是最初使用它时出现了死锁。 Later, when I tried using this isolation method only for this specific transaction that required it, the deadlocks seemed to disappear. 后来,当我尝试仅对需要它的特定事务使用这种隔离方法时,僵局似乎消失了。

What "RepeatableRead" is supposed to achieve is locking the fetched players' rows until the first request has committed the transaction. “ RepeatableRead”应该达到的目的是锁定获取的玩家的行,直到第一个请求提交了事务。

However, as I have no previous experience in the topic of locks, I would appreciate receiving other answers from experts in this subject. 但是,由于我以前没有关于锁的经验,因此,我很高兴收到专家提供的其他答案。

This is not NHibernate responsibility but the database one. 这不是NHibernate的责任,而是数据库的责任。

to my understanding one of the transactions should happen first 据我了解,其中一项交易应首先发生

No. Transaction being isolated does not mean they are serialized. 不可以。事务被隔离并不意味着它们已被序列化。 Under default isolation level for most cases, ReadCommitted , it just means that one transaction can not read the changes another ongoing transaction has made. 在大多数情况下,默认隔离级别为ReadCommitted ,它仅表示一个事务无法读取另一正在进行的事务所做的更改。 Depending on the database engine, it will either read previous values (by example, Oracle does that) or be blocked instead (SQL-Server when snapshot isolation is not enabled). 根据数据库引擎的不同,它会读取以前的值(例如,Oracle会读取以前的值),或者将其阻止(当未启用快照隔离时使用SQL-Server)。 Transactions can still happen concurrently, even when reading the same rows of data. 即使读取相同的数据行,事务仍然可以同时发生。

So you have two players concurrently reading other player state then flagging themselves as having ended their turn. 因此,您有两个玩家同时读取其他玩家状态,然后将自己标记为结束了回合。 This can happen even when the database block reading modified data instead of yielding previous value, since your update can happen after the read. 即使数据库阻止读取修改的数据而不是产生先前的值,也可能发生这种情况,因为您的更新可能在读取之后发生。 So both can read other player state as not having ended its turn. 因此,双方都可以读到其他玩家状态未结束回合。

You can cause the write to be blocked by choosing another isolation level, RepeatableRead . 您可以通过选择另一个隔离级别RepeatableRead来阻止写入。 This will cause the two concurrent transactions to deadlock, one being cancelled (victim) and the other proceeded. 这将导致两个并发事务陷入僵局,其中一个被取消(受害者),而另一个进行。 The cancelled one should then be replayed, and since the non-victim would have either ended at that point or acquired an exclusive lock by writing the flag, the replayed transaction will then be able to read other player as having ended its turn (either immediately or by waiting other transaction end due to its exclusive lock forbidding the replayed one to put a shared lock on it, required for reading with this isolation level). 然后应该重播已取消的玩家,并且由于非受害者将在该点结束或通过写入标志获得排他锁,因此重播的交易将能够读取其他玩家,因为其回合结束了(立即或由于其独占锁而等待另一事务结束,从而禁止重播的事务在其上放置共享锁(使用此隔离级别进行读取时需要)。

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

Setting the isolation level can be done with session.BeginTransaction(IsolationLevel.Serializable) . 可以使用session.BeginTransaction(IsolationLevel.Serializable)来设置隔离级别。 Doing that reduces the ability of serving many concurrent requests, so better do that only for cases requiring it. 这样做会降低处理许多并发请求的能力,因此最好仅在需要它的情况下这样做。 If you use TransactionScope , their constructor take the IsolationLevel as argument too. 如果使用TransactionScope ,则它们的构造函数也将IsolationLevel作为参数。

You may try instead to write current player state before reading other player state (by using a session.Flush after writing, then querying the other player state). 您可以尝试在读取其他播放器状态之前写当前播放器状态(通过使用session.Flush 。写入后刷新,然后查询其他播放器状态)。 This could work for databases using shared locks for reading and exclusives lock for writing when under ReadCommitted isolation level. 对于在ReadCommitted隔离级别下使用共享锁进行读取而使用排他锁进行写入的数据库,这可能会起作用。 But there too, you will have to handle deadlocks. 但是在那儿,您也必须处理死锁。 And it will not work for databases which do not use shared locks for reading under ReadCommitted , but yield instead the last committed value. 对于在ReadCommitted下不使用共享锁进行读取的数据库,它会产生最后的提交值,这将不起作用。

Note: 注意:

Maybe should you change completely your pattern instead: record each player actions, without handling "end of turn" operations in the "last" player request. 也许您应该完全改变自己的模式:记录每个玩家的动作,而不在“最后一个”玩家请求中处理“回合结束”操作。 Then use some background process to handle plays where all players have ended their turn. 然后使用一些后台流程来处理所有玩家结束回合的游戏。

This can be done with some queuing technology, with the flagging of player end of turn being queued. 这可以通过某种排队技术来完成,将玩家回合结束的标记排队。 Polling database could work but is not very good. 轮询数据库可以工作,但不是很好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM