我使用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();
}
}
GameUnitOfWork
所做的是使用每个请求的会话(保存在HttpContext.Current.Items
中)并开始交易。
我遇到的问题是,当2个请求同时到达时,似乎交易并非孤立地发生。
这是一个有2名玩家的游戏。我有一种情况,即每个玩家几乎同时向服务器发送请求。
该事务应该在发送请求的播放器上将字段FinishedExecutingTurn
设置为true。如果两个玩家现在都设置为真,那么应该发生一些事情("做一些事情")。
如果每个事务都是孤立发生的,据我所知,其中一个事务应该首先发生,并将一个播放器上的FinishedExecutingTurn
设置为true,然后另一个请求中的事务应该将FinishedExecutingTurn
设置为true在第二个玩家并输入我的if语句("做一些东西")。
但是,有时它根本不会输入if语句,因为在两个请求中,FinishedExecutingTurn
最初都设置为false。
我的问题是,不应该首先发生一个事务并将字段设置为true,然后在另一个请求中,其中一个玩家应该已经设置为true?
答案 0 :(得分:1)
经过大量关于数据库和锁的并发性的阅读后,我终于找到了解决方案。我只是使用隔离级别" RepeatableRead":
来定义此事务transaction = session.BeginTransaction(IsolationLevel.RepeatableRead);
这实际上是我尝试的第一个解决方案之一,但我最初在使用它时遇到了死锁。后来,当我尝试仅将此隔离方法用于需要它的特定事务时,死锁似乎消失了。
什么" RepeatableRead"应该实现的是锁定被抓取的玩家'行直到第一个请求提交了事务。
但是,由于我之前没有锁定主题的经验,我希望收到本专题专家的其他答案。
答案 1 :(得分:1)
这不是NHibernate的责任,而是数据库的责任。
据我所知,其中一项交易应该先发生
没有。交易被隔离并不意味着它们被序列化。在大多数情况下,ReadCommitted
在默认隔离级别下,这只意味着一个事务无法读取另一个正在进行的事务所做的更改。根据数据库引擎,它将读取先前的值(例如,Oracle执行此操作)或被阻止(SQL-Server未启用快照隔离时)。即使在读取相同的数据行时,事务仍然可以同时发生。
所以你有两个玩家同时阅读其他玩家状态然后标记自己已经结束了他们的回合。即使数据库阻止读取修改后的数据而不是产生先前的值,也会发生这种情况,因为您的更新可能在读取后发生。 因此两者都可以读取其他玩家的状态,因为它没有结束。
您可以通过选择其他隔离级别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.
}
}
可以使用session.BeginTransaction(IsolationLevel.Serializable)
设置隔离级别。这样做会降低为许多并发请求提供服务的能力,因此只有在需要它的情况下才能做到这一点。如果您使用TransactionScope
,则其构造函数也会将IsolationLevel
作为参数。
您可以尝试在读取其他播放器状态之前写入当前播放器状态(在写入后使用session.Flush
,然后查询其他播放器状态)。这可能适用于使用共享锁进行读取的数据库,以及在ReadCommitted
隔离级别下进行写入的独占锁定。但也有,你将不得不处理死锁。对于不使用共享锁在ReadCommitted
下读取的数据库,它不起作用,而是生成最后一个提交的值。
注意:强>
也许你应该完全改变你的模式:记录每个玩家的动作,而不处理“最后”玩家请求中的“回合结束”操作。然后使用一些后台进程来处理所有玩家轮到他们的游戏。
这可以通过一些排队技术来完成,其中标记的玩家回合结束排队。轮询数据库可以工作但不是很好。