C# NHibernate事务不是';t隔离
我在MVC控制器中使用NHibernate框架编写了以下代码: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
[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.
}
}