如何在更新对象状态时使命令同步?

时间:2016-10-20 04:40:57

标签: c# asp.net .net entity-framework-6

我正在开发一款游戏引擎。 angularjs前端与在IIS上运行的.NET WebAPI项目中的端点进行通信。这个web api是一个外观,只是针对游戏的域逻辑运行命令,该逻辑位于一个单独的项目中。我有一个目前正在使用实体框架的工作单元。

因此,angularjs代码调用IIS上的webapi网站中的端点,该端点创建一个针对游戏运行的命令。例如:开立银行账户。

此命令将从数据存储中加载游戏的实例,检查完成情况,然后执行命令以打开银行帐户。在执行命令中发生了许多改变游戏实例数据的事件。

我有很多这些命令都具有相同的基类,但一次只能调用一次。

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 completeResult;
        }

        UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);

        var result = ExecuteCommand(command);

        UnitOfWork.Save();

        return result;
    }

    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或任何地方,它没关系。

问题是当多个命令想要同时更改游戏实例状态时。如果我调用一个命令计算游戏中bankaccount的兴趣,那么目前对同一命令的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 completeResult;
            }

            UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);

            var result = ExecuteCommand(command);

            UnitOfWork.Save();
            return result;
        }
        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);
}

这完全解决了我的问题,但存在许多问题。

  1. 当我在IIS中运行它时,这会让我头疼吗?
  2. 如果我想在许多应用服务器上对此库进行负载均衡,那么这将无效。
  3. 如果有一百万个游戏实例,那么该列表将会变得庞大并且性能会受到影响。
  4. 另一个解决方法是将锁移动到数据库:

    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 completeResult;
                }
    
                UnitOfWork.LoadCollection(GameInstance, p => p.AppInstances);
    
                var result = ExecuteCommand(command);
                // this will unlock the gameinstance on the save
                UnitOfWork.Save();
                return result;
            }
            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 个答案:

答案 0 :(得分:0)

我已经决定目前最好的方法是拥有一个ID队列。我尝试立即输入游戏实例ID,如果插入正常则继续。完成工作后从列表中删除id。如果id插入失败,那么它已经存在。

此修复与锁几乎相同,但我依赖于唯一的键值存储。我可能会创建一个处理这个的接口和类,所以如果数据库失败,我可以将故障存储在文件或其他地方,以确保以后清除ID。

我肯定会对此提出更多建议,因为它的代码味道确实很糟糕。

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 completeResult;
        }

        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 result;
    }

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