2017-05-20 9 views
1

I MVC 컨트롤러에 다음 코드는 자 NHibernate 프레임 워크 사용이 :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(); 
     } 
    } 

세션 당 요청합니다 (HttpContext.Current.Items에 저장)를 사용하는 것입니다 무엇을 GameUnitOfWork을 트랜잭션을 시작하십시오.

문제는 두 개의 요청이 동시에 도착할 때 트랜잭션이 고립되어 발생하지 않는 것처럼 보입니다.

2 명의 플레이어가 참여하는 게임입니다. 각 플레이어가 거의 동시에 서버에 요청을 보내는 상황이 있습니다. 트랜잭션은 요청을 보낸 플레이어에서 FinishedExecutingTurn 필드를 true로 설정해야합니다. 두 플레이어가 모두 true로 설정되면 무언가가 발생해야합니다 ("물건을해라").

각 트랜잭션이 분리되어 발생하는 경우 트랜잭션 중 하나가 먼저 발생하여 한 플레이어의 FinishedExecutingTurn을 true로 설정해야합니다. 그러면 다른 요청의 트랜잭션이 두 번째 플레이어에서 FinishedExecutingTurn을 true로 설정하고 my if 문 ("do some stuff"). 그러나 양쪽 모두 요청시 FinishedExecutingTurn이 처음에는 false로 설정 되었기 때문에 가끔 if 문을 입력하지 않는 경우가 있습니다.

내 질문에, 반드시 하나의 트랜잭션이 먼저 발생하고 필드를 true로 설정 한 다음 다른 요청에서 플레이어 중 하나가 이미 true로 설정되어 있어야합니다.

답변

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 아래에서 읽기 위해 공유 잠금을 사용하지 않는 데이터베이스에서는 작동하지 않지만 대신 커밋 된 마지막 값을 생성합니다.

참고 :

어쩌면 대신 완전히 패턴을 변경해야합니다 : "마지막"선수의 요청에 작업 "턴의 끝을"처리하지 않고, 각 플레이어의 행동을 기록한다. 그런 다음 모든 플레이어가 자신의 차례를 끝낸 플레이를 처리하기 위해 백그라운드 프로세스를 사용하십시오.

이 작업은 대기열에있는 플레이어의 종료 플래그가있는 대기열 기술을 사용하여 수행 할 수 있습니다. 폴링 데이터베이스는 작동하지만 아주 좋지는 않습니다.

+0

답변을 주셔서 대단히 감사합니다. 이것은 정확히 내가 찾고 있었던 것입니다. 보시다시피, 나는 당신이 게시 한 것과 같은 시간에 내 자신의 질문에 대답했습니다. 내가 이해할 수없는 것은 단순히 "RepeatableRead"를 사용하여 교착 상태가 발생하지 않는다는 것입니다. (처음에는이 코드를 사용하고 있었지만이 트랜잭션은 "RepeatableRead"를 사용하고 모든 것이 올바르게 작동하고 예상대로 작동했습니다)? – Royar

+0

지금 당장 운이 좋지만 교착 상태가 올 것입니다. 오류 로그를 모니터링하거나 더 나은 방법으로 코드를 변경하여 지금 처리하십시오. 플레이어 상태 읽기 후에 브레이크 포인트로 디버깅하거나 스레드를 고정 (스레드 디버그 창 사용), 응용 프로그램을 다시 실행 (F5), 다른 플레이어의 회전 종료, 고정 된 스레드 해동 브레이크 포인트 히트, 그리고 모두 가자. –

+0

@Royar, 고려해야 할 사항 : SQL-Server 구현을 반복적으로 구현하기 위해 작성했습니다. 다른 데이터베이스 엔진은 사물을 잠그는 방식이 다를 수 있으므로 이전에 차단하여 교착 상태가 발생하지 않을 수도 있습니다. 그러나 적어도 반복 읽기의 의미를 존중해야하며, 이는 동시 트랜잭션이 다른 트랜잭션이 완료 될 때까지 다른 트랜잭션이 읽은 데이터를 갱신하는 것을 금지합니다. 이 점을 고려하면 문제가 발생하지 않도록 할 수 있습니다. –

1

데이터베이스와 잠금의 동시성에 대한 많은 책을 읽은 끝에 드디어 해결책을 찾았습니다.

transaction = session.BeginTransaction(IsolationLevel.RepeatableRead); 

이 실제로 내가 시도한 첫 번째 솔루션 중 하나 였지만을 사용할 때 처음 교착 상태를 가지고 : 나는 단순히 "REPEATABLEREAD"의 격리 수준이 트랜잭션을 정의했다. 나중에, 필자가 필요로하는이 특정 트랜잭션에만이 격리 방법을 사용하려고 시도했을 때 교착 상태가 사라진 것처럼 보였다.

"RepeatableRead"는 첫 번째 요청이 트랜잭션을 커밋 할 때까지 페치 된 플레이어의 행을 잠그는 것입니다.

그러나 자물쇠 주제에 대한 이전 경험이 없으므로이 주제에 대한 전문가의 답변을 받아 주시면 감사하겠습니다.

+0

어디서나 'RepeatableRead'를 사용하면 교착 상태가 자주 발생합니다. 일부 지점에서만이를 사용하면 트랜잭션이 상당히 줄어들지 만, 특히 이러한 격리 수준을 사용하는 트랜잭션에서는 여전히 발생할 수 있습니다. –