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

本文关键字:使命 命令 同步 状态 更新 对象 | 更新日期: 2023-09-27 18:14:00

我正在开发一个游戏引擎。angularjs前端与运行在IIS上的。net WebAPI项目中的端点通信。这个web api是一个门面,它只是针对游戏的领域逻辑运行命令,这是一个单独的项目。我有一个工作单元目前正在使用实体框架。

因此,angularjs代码调用IIS上的webapi网站中的端点,创建一个针对游戏运行的命令。例如:open bank account.

该命令将从数据存储中加载游戏实例,检查完成情况,然后执行命令打开银行账户。在执行命令中有很多事件会改变游戏实例数据。

我有很多这样的命令,它们都有相同的基类,但是一次只应该调用一个。

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或任何地方运行我的引擎,这很好。

问题是当多个命令想要同时改变游戏实例状态时。如果我在游戏中调用一个命令来计算银行账户的利息,目前对同一命令的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. 如果有100万个游戏实例,那么列表将变得巨大,性能将受到影响。
另一种方法是将锁移到数据库中:
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);
}

也许我想错了。

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

我已经决定,目前最好的方法是有一个id队列。我尝试立即输入游戏实例id,如果插入是OK的,然后继续。完成工作后,从列表中删除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);
}