【问题标题】:ASP.NET Web-api core: Handling client connections problems and find conflictsASP.NET Web-api core:处理客户端连接问题并发现冲突
【发布时间】:2018-10-18 15:48:35
【问题描述】:

背景

我有一个 web api 服务器(asp.net core v2.1),它提供一些基本的操作,比如管理服务器上的实体。这是界面:

[HttpPost]
[Route("create")]
public async Task<ActionResult<NewEntityResponse>> Create(CreateEntityModel model)
{
    // 1) Validate the request.
    // 2) Create a new row on the database
    // 3) Return the new entity in response.
}

以这种方式运行此 REST 方法的用户:

POST https://example.com/create

Content-Type: application/json

{
    "firstName": "Michael", 
    "lastName": "Jorden"
}

得到这样的回应:

Status 200

{
    "id": "123456" // The newly created entity id
}

问题

当发送数千个这样的请求时,在某些时候它会因为网络连接而失败。当连接失败时,它会导致我们进入两种不同的情况:

  • 网络调用在前往服务器的途中结束 - 在这种情况下,服务器不知道此请求。因此,没有创建实体。用户只需再次发送相同的消息。
  • 网络调用已从服务器发送回客户端,但从未丰富目的地 - 在这种情况下,请求已完全满足,但客户端不知道这一点。预期的解决方案是再次发送相同的请求。在这种情况下,它将两次创建相同的实体 - 这就是问题所在。

请求的解决方案

我想为 web-api 创建一个通用解决方案,它“remmeber”命令它已经完成。如果他两次收到相同的请求,则返回 HTTP 状态码Conflict

到目前为止我在哪里

我想给客户端添加一个选项来为请求添加一个唯一的id,这样:

POST https://example.com/create?call-id=XXX

向我的服务器添加一个新的过滤器,检查密钥 XXX 是否已经完成。如果是,请返回Conflict。否则 - 继续。

添加另一个服务器过滤器来检查方法的响应并将其标记为“已完成”以供进一步检查。

这个解决方案在并发调用上的问题。如果我的方法需要 5 秒才能返回并且客户端在 1 秒后再次发送相同的消息 - 它将创建两个具有相同数据的实体。

问题:

  1. 您认为这是解决此问题的好方法吗?
  2. 您是否熟悉执行此操作的即用型解决方案?
  3. 如何解决我的“并发”问题?
  4. 任何其他提示都会很棒!

谢谢。

【问题讨论】:

  • 好问题,但是,听起来您正在发明一种新的 WS-transation。
  • 并不是说这是完整或最佳的解决方案。但是您是否研究过 dbcontext 池?

标签: c# asp.net asp.net-web-api asp.net-core


【解决方案1】:

进行 REST 操作 idempotent 不是最简单的解决方案吗?

我的意思是:调用应该检查资源是否已经存在,如果不存在则创建一个新资源,如果存在则返回现有资源?

【讨论】:

  • 我也建议这样做,您可以轻松添加“客户参考”,例如一个 guid 或一些哈希,当客户端重试相同的请求(使用相同的 ID)时,服务器知道它已经这样做了。这些 ID 可以存储在 redis 中以便快速访问。
  • 它在某些情况下是一个选项,但并非总是如此。在这个策略中,用户应该知道元素的 id。就我而言,用户在创建对象之前不知道 ID...
  • @No1Lives4Ever 并非如此,服务器应该检测重复项(基于资源的内容,而不是 ID)并拒绝创建新资源,而是返回现有资源。坚持您的示例,如果发布了“Michael Jorden”,则服务器应检查该人是否使用该名称存在,如果存在,则返回现有而不是创建。与人脱离可能并不容易,因为可能存在多个同名的人,但我假设发布的信息不仅仅是名字/姓氏:)
  • 想了一会儿。我同意你的解决方案。你说服了我。谢谢。
【解决方案2】:

好的,我只是想出如何使它正确。所以,我自己实现了,分享给大家。

为了在不同服务器之间同步所有请求,我使用 Redis 作为缓存服务。如果您只有一台服务器,则可以改用Dictionary&lt;string, string&gt;

这个过滤器做:

  1. 在处理请求之前 - 向 Redis 添加一个新的空值键。
  2. 服务器处理请求后 - 将服务器响应存储在 Redis 中。当用户再次请求相同的请求时,将使用此数据。

    公共类 ConflictsFilter : ActionFilterAttribute { 常量字符串 CONFLICT_KEY_NAME = "冲突检查器"; 静态只读 TimeSpan EXPIRE_AFTER = TimeSpan.FromMinutes(30);

    private static bool ShouldCheck(ActionDescriptor actionDescription, IQueryCollection queries)
    {
        return queries.ContainsKey(CONFLICT_KEY_NAME);
    }
    
    private string BuildKey(string uid, string requestId)
    {
        return $"{uid}_{requestId}";
    }
    
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query))
        {
            using (var client = RedisConnectionPool.ConnectionPool.GetClient())
            {
                string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
                string existing = client.Get<string>(key);
                if (existing != null)
                {
                    var conflict = new ContentResult();
                    conflict.Content = existing;
                    conflict.ContentType = "application/json";
                    conflict.StatusCode = 409;
    
                    context.Result = conflict;
                    return;
                }
                else
                {
                    client.Set(key, string.Empty, EXPIRE_AFTER);
                }
            }
        }
    
        base.OnActionExecuting(context);
    }
    
    public override void OnResultExecuted(ResultExecutedContext context)
    {
        base.OnResultExecuted(context);
        if (ShouldCheck(context.ActionDescriptor, context.HttpContext.Request.Query) && context.HttpContext.Response.StatusCode == 200)
        {
            string key = BuildKey(context.HttpContext.User.GetId(), context.HttpContext.Request.Query[CONFLICT_KEY_NAME]);
            using (var client = RedisConnectionPool.ConnectionPool.GetClient())
            {
                var responseBody = string.Empty;
                if (context.Result is ObjectResult)
                {
                    ObjectResult result = context.Result as ObjectResult;
                    responseBody = JsonConvert.SerializeObject(result.Value);
                }
    
                if (responseBody != string.Empty)
                    client.Set(key, responseBody, EXPIRE_AFTER);
            }
        }
    }
    

    }

仅当查询?conflict-checker=XXX 存在时才会执行代码。

此代码在 MIT 许可下提供给您。

享受旅程:)

【讨论】:

    猜你喜欢
    • 2021-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-23
    • 2016-10-12
    • 2013-02-19
    • 2019-10-24
    • 2021-08-20
    相关资源
    最近更新 更多