#c# #asp.net #.net #entity-framework-6
#c# #asp.net #.net #entity-framework-6
Вопрос:
Я разрабатываю игровой движок. Интерфейс angularjs взаимодействует с конечной точкой в проекте .NET WebAPI, запущенном в IIS. Этот веб-API представляет собой фасад, который просто выполняет команды против логики домена игры, которая находится в отдельном проекте. У меня есть единица работы, которая в настоящее время использует entity framework.
Итак, код angularjs вызывает конечную точку на веб-сайте webapi в IIS, которая создает команду для запуска в игре. Например: открыть банковский счет.
Эта команда загрузит экземпляр игры из хранилища данных, проверит завершение, а затем выполнит команду для открытия банковского счета. При выполнении команд происходит множество событий, которые изменяют данные экземпляра игры.
У меня есть много таких команд, которые все имеют один и тот же базовый класс, но только одна из них должна вызываться одновременно.
public abstract class GameInstanceCommandHandler<T>
: CommandHandlerBase<T, GameInstanceIDCommandResult>
where T : GameInstanceCommand
{
protected GameInstance GameInstance { get; private set; }
protected GameInstanceCommandHandler() { }
public GameInstanceCommandHandler(IUnitOfWork uow)
: base(uow)
{
}
public override GameInstanceIDCommandResult Execute(T command)
{
Check.Null(command, "command");
GameInstance = UnitOfWork.GetGameInstance(command.GameInstanceID);
if (GameInstance == null)
return GetResult(false, "Could not find game instance.");
if (GameInstance.IsComplete)
{
var completeResult = GetResult(GameInstance.ID);
completeResult.Messages.Add("Game is complete, you cannot update the game state.");
completeResult.Success = false;
completeResult.IsGameComplete = true;
return completeResu<
}
UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);
var result = ExecuteCommand(command);
UnitOfWork.Save();
return resu<
}
protected GameInstanceIDCommandResult GetResult(bool success, string message)
{
return new GameInstanceIDCommandResult(success, message);
}
protected GameInstanceIDCommandResult GetResult(Guid id)
{
return new GameInstanceIDCommandResult(id);
}
protected void LoadGameAndGameApps()
{
UnitOfWork.LoadReference(GameInstance, p => p.Game);
UnitOfWork.LoadCollection(GameInstance.Game, p => p.GameApps);
}
protected abstract GameInstanceIDCommandResult ExecuteCommand(T command);
}
Все базовые классы переопределяют абстрактную команду ExecuteCommand. Все работает нормально, я могу запустить свой движок с помощью консольного проекта, или в IIS, или где бы то ни было, все в порядке.
Проблема заключается в том, что несколько команд хотят изменить состояние экземпляра игры одновременно. Если я вызвал команду для расчета процентов на банковский счет в игре, в настоящее время 5 вызовов одной и той же команды произведут 5 вычислений.
Я хочу убедиться, что одновременно разрешено выполнять только одну команду для данного экземпляра игры. Поскольку это отдельная библиотека, созданная не только для запуска в процессе IIS, я знаю, что эта проблема должна быть решена в этом файле. Я думал об обновлении кода, чтобы соответствовать приведенному ниже:
public abstract class GameInstanceCommandHandler<T>
: CommandHandlerBase<T, GameInstanceIDCommandResult>
where T : GameInstanceCommand
{
private static readonly object @lock = new object();
private static volatile List<Guid> GameInstanceIDs = new List<Guid>();
protected GameInstance GameInstance { get; private set; }
protected GameInstanceCommandHandler() { }
public GameInstanceCommandHandler(IUnitOfWork uow)
: base(uow)
{
}
public override GameInstanceIDCommandResult Execute(T command)
{
Check.Null(command, "command");
lock(@lock)
{
// if game id is updating then return
if(GameInstanceIDs.Any(p => p == command.GameInstanceID))
return GetResult(false, "The game is already being updated.");
// (lock for update)
GameInstanceIDs.Add(command.GameInstanceID);
}
try
{
GameInstance = UnitOfWork.GetGameInstance(command.GameInstanceID);
if (GameInstance == null)
return GetResult(false, "Could not find game instance.");
if (GameInstance.IsComplete)
{
var completeResult = GetResult(GameInstance.ID);
completeResult.Messages.Add("Game is complete, you cannot update the game state.");
completeResult.Success = false;
completeResult.IsGameComplete = true;
return completeResu<
}
UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);
var result = ExecuteCommand(command);
UnitOfWork.Save();
return resu<
}
catch (Exception ex)
{ }
finally
{
lock (@lock)
{
GameInstanceIDs.Remove(command.GameInstanceID);
}
}
return GetResult(false, "There was an error.");
}
protected GameInstanceIDCommandResult GetResult(bool success, string message)
{
return new GameInstanceIDCommandResult(success, message);
}
protected GameInstanceIDCommandResult GetResult(Guid id)
{
return new GameInstanceIDCommandResult(id);
}
protected void LoadGameAndGameApps()
{
UnitOfWork.LoadReference(GameInstance, p => p.Game);
UnitOfWork.LoadCollection(GameInstance.Game, p => p.GameApps);
}
protected abstract GameInstanceIDCommandResult ExecuteCommand(T command);
}
Это полностью устраняет мою проблему, но есть ряд проблем.
- Это вызовет у меня головную боль, когда я запускаю ее в IIS?
- Если я хочу сбалансировать загрузку этой библиотеки на многих серверах приложений, это не сработает.
- Если существует миллион игровых экземпляров, список станет огромным, и производительность пострадает.
Другое исправление заключается в перемещении блокировки в базу данных:
public abstract class GameInstanceCommandHandler<T>
: CommandHandlerBase<T, GameInstanceIDCommandResult>
where T : GameInstanceCommand
{
private static readonly object @lock = new object();
protected GameInstance GameInstance { get; private set; }
protected GameInstanceCommandHandler() { }
public GameInstanceCommandHandler(IUnitOfWork uow)
: base(uow)
{
}
public override GameInstanceIDCommandResult Execute(T command)
{
Check.Null(command, "command");
lock(@lock)
{
GameInstance = UnitOfWork.GetGameInstance(command.GameInstanceID);
if (GameInstance == null)
return GetResult(false, "Could not find game instance.");
if(GameInstance.IsLocked)
return GetResult(false, "Game is locked by another command.");
// Lock the game in the database or datastore
GameInstance.Lock();
UnitOfWork.Save();
// Unlock only local copy
GameInstance.UnLock();
}
try
{
if (GameInstance.IsComplete)
{
var completeResult = GetResult(GameInstance.ID);
completeResult.Messages.Add("Game is complete, you cannot update the game state.");
completeResult.Success = false;
completeResult.IsGameComplete = true;
return completeResu<
}
UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);
var result = ExecuteCommand(command);
// this will unlock the gameinstance on the save
UnitOfWork.Save();
return resu<
}
catch (Exception ex)
{ }
return GetResult(false, "There was an error.");
}
protected GameInstanceIDCommandResult GetResult(bool success, string message)
{
return new GameInstanceIDCommandResult(success, message);
}
protected GameInstanceIDCommandResult GetResult(Guid id)
{
return new GameInstanceIDCommandResult(id);
}
protected void LoadGameAndGameApps()
{
UnitOfWork.LoadReference(GameInstance, p => p.Game);
UnitOfWork.LoadCollection(GameInstance.Game, p => p.GameApps);
}
protected abstract GameInstanceIDCommandResult ExecuteCommand(T command);
}
Возможно, я думаю об этом неправильно. Любая помощь была бы отличной.
Комментарии:
1. Вы должны установить GameInstance. Блокировка(); UnitOfWork.Save(); внутри блока try и GameInstance. Разблокировать(); внутри блока finally. Таким образом, в случае любого исключения при сохранении блокировка будет снята наверняка.
2. @SouvikGhosh Спасибо за это, да, я добавлю несколько попыток. Я обновил свой ответ.
Ответ №1:
Я решил, что лучший способ в настоящее время — создать очередь идентификаторов. Я пытаюсь немедленно ввести идентификатор экземпляра игры и, если вставка в порядке, продолжить. После выполнения работы удалите идентификатор из списка. Если вставить идентификатор не удается, значит, он уже существует.
Это исправление практически такое же, как блокировка, но я полагаюсь на хранилище уникальных значений ключа. Вероятно, я собираюсь создать интерфейс и класс, который обрабатывает это, чтобы в случае сбоя базы данных я мог сохранить сбои в файле или где-нибудь еще, чтобы убедиться, что идентификаторы будут удалены позже.
Я определенно за дополнительные предложения по этому поводу, поскольку у него действительно плохой запах кода.
public abstract class GameInstanceCommandHandler<T>
: CommandHandlerBase<T, GameInstanceIDCommandResult>
where T : GameInstanceCommand
{
protected GameInstance GameInstance { get; private set; }
protected GameInstanceCommandHandler() { }
public GameInstanceCommandHandler(IUnitOfWork uow)
: base(uow)
{
}
public override GameInstanceIDCommandResult Execute(T command)
{
Check.Null(command, "command");
try
{
AddCommandInstance(command.GameInstanceID);
}
catch(Exception ex)
{
return GetResult(false, "Only one command can be ran on an instance at a time.");
}
GameInstance = UnitOfWork.GameInstances.FirstOrDefault(p => p.ID == command.GameInstanceID);
if (GameInstance == null)
{
RemoveCommandInstance(command.GameInstanceID);
return GetResult(false, "Could not find game instance.");
}
if (GameInstance.IsComplete)
{
var completeResult = GetResult(GameInstance.ID);
completeResult.Messages.Add("Game is complete, you cannot update the game state.");
completeResult.Success = false;
completeResult.IsGameComplete = true;
RemoveCommandInstance(command.GameInstanceID);
return completeResu<
}
UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);
GameInstanceIDCommandResult result = null;
try
{
result = ExecuteCommand(command);
UnitOfWork.Save();
}
catch(Exception ex)
{
result = GetResult(false, ex.Message);
}
finally
{
RemoveCommandInstance(command.GameInstanceID);
}
return resu<
}
private void AddCommandInstance(Guid gameInstanceID)
{
UnitOfWork.Add(new CommandInstance() { ID = gameInstanceID });
UnitOfWork.Save();
}
private void RemoveCommandInstance(Guid gameInstanceID)
{
UnitOfWork.Remove(UnitOfWork.CommandInstances.First(p => p.ID == gameInstanceID));
UnitOfWork.Save();
}
protected GameInstanceIDCommandResult GetResult(bool success, string message)
{
return new GameInstanceIDCommandResult(success, message);
}
protected GameInstanceIDCommandResult GetResult(Guid id)
{
return new GameInstanceIDCommandResult(id);
}
protected void LoadGameAndGameApps()
{
UnitOfWork.LoadReference(GameInstance, p => p.Game);
UnitOfWork.LoadCollection(GameInstance.Game, p => p.GameApps);
}
protected abstract GameInstanceIDCommandResult ExecuteCommand(T command);
}
Комментарии:
1. О чем я думал — вы можете попробовать выполнить блок блокировки в отдельном потоке независимо. Это может повлиять на общие ресурсы, но есть способы позаботиться об этом. Таким образом, это позволит сохранить поток почты свободным, каждый раз новый поток будет работать независимо. Возможно, вам также придется создать пул потоков.