锁定 Web API 控制器方法

Posted

技术标签:

【中文标题】锁定 Web API 控制器方法【英文标题】:Lock Web API controller method 【发布时间】:2017-11-23 22:25:51 【问题描述】:

我正在使用 C# 和 .Net Framework 4.7 开发一个 ASP.NET Web Api 应用程序。

我在控制器中有一个方法,我想一次只由一个线程执行。换句话说,如果有人调用这个方法,另一个调用必须等到该方法完成。

我发现这个SO answer 可以胜任这项工作。但在这里它使用一个队列,我不知道如何使用该队列。在那个答案中解释说我可以创建一个 Windows 服务来使用队列,但我不想在我的解决方案中添加另一个应用程序。

我想像这样在 Web Api 方法中添加一个锁:

[HttpPut]
[Route("api/Public/SendCommissioning/serial/withChildren")]
public HttpResponseMessage SendCommissioning(string serial, bool withChildren)

    lock
    
        string errorMsg = "Cannot set commissioning.";

        HttpResponseMessage response = null;
        bool serverFound = true;

        try
        
            [ ... ]
        
        catch (Exception ex)
        
            _log.Error(ex.Message);

            response = Request.CreateResponse(HttpStatusCode.InternalServerError);
            response.ReasonPhrase = errorMsg;
        

        return response;
    

但我认为这不是一个好的解决方案,因为如果运行该方法时出现问题,它可能会阻止许多待处理的调用,并且我将丢失所有待处理的调用,或者我错了,调用(线程) 将等到其他人结束。换句话说,我认为如果我使用 this 我可能会陷入僵局。

我正在尝试这个,因为我需要按照收到的顺序执行调用。查看此操作日志:

2017-06-20 09:17:43,306 DEBUG [12] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38441110778119919475, withChildren : False
2017-06-20 09:17:43,494 DEBUG [13] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38561140779115949572, withChildren : False
2017-06-20 09:17:43,683 DEBUG [5] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38551180775118959070, withChildren : False
2017-06-20 09:17:43,700 DEBUG [12] WebsiteAction - EXITING PublicController::SendCommissioning 
2017-06-20 09:17:43,722 DEBUG [5] WebsiteAction - EXITING PublicController::SendCommissioning 
2017-06-20 09:17:43,741 DEBUG [13] WebsiteAction - EXITING PublicController::SendCommissioning 

在其中任何一个结束之前我收到三个调用:线程[12], [13] and [5]。但是最后一个在第二个之前结束[12], [5] and [13]

我需要一种机制来不允许这样做。

我可以做些什么来确保呼叫将按照我拨打电话的相同顺序进行处理?

【问题讨论】:

I don't think this is a good solution because it could block a lot of pending calls. 是什么意思?这不正是你想要的吗?他们应该按照请求的顺序一次处理一个,并且只有在完成后才返回? @FrankerZ 我已经用这个解释更新了问题:'但我认为这不是一个好的解决方案,因为如果运行方法,我将丢失所有挂起的调用,或者我错了,调用(线程)将等到其他调用结束。换句话说,我认为如果我使用 this 我可能会陷入僵局。' 【参考方案1】:

您的锁定解决方案应该可以正常工作。如果请求失败,则释放锁,其他待处理的请求可以进入锁。不会发生死锁。

此解决方案的唯一问题是 Web 请求可能会持续很长时间(这可能会导致客户端超时)。

public class MyApi : ApiController

    public static readonly object LockObject = new object();

    [HttpPut]
    [Route("api/Public/SendCommissioning/serial/withChildren")]
    public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
    
        lock ( LockObject )
        
            //Do stuff
        
    

要解决挂起请求的问题,您应该使用队列,并轮询后端(或者如果您喜欢,请尝试 SignalR),直到您的工作完成。例如:

//This is a sample with Request/Result classes (Simply implement as you see fit)
public static class MyBackgroundWorker

    private static ConcurrentQueue<KeyValuePair<Guid, Request>> _queue = new ConcurrentQueue<KeyValuePair<Guid, Result>>()
    public static ConcurrentDictionary<Guid, Result> Results = new ConcurrentDictionary<Guid, Result>();

    static MyBackgroundWorker()
    
         var thread = new Thread(ProcessQueue);
         thread.Start();
    

    private static void ProcessQueue()
    
         KeyValuePair<Guid, Request> req;
         while(_queue.TryDequeue(out req))
         
             //Do processing here (Make sure to do it in a try/catch block)
             Results.TryAdd(req.Key, result);
         
    

    public static Guid AddItem(Request req)
    
        var guid = new Guid();
        _queue.Enqueue(new KeyValuePair(guid, req));
        return guid;
    



public class MyApi : ApiController

    [HttpPut]
    [Route("api/Public/SendCommissioning/serial/withChildren")]
    public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
    
        var guid = MyBackgroundWorker.AddItem(new Request(serial, withChildren));
        return guid;
    

    [HttpGet]
    [Route("api/Public/GetCommissioning/guid")]
    public HttpResponseMessage GetCommissioning(string guid)
    
        if ( MyBackgroundWorker.Results.TryRemove(new Guid(guid), out Result res) )
        
            return res;
        
        else
        
            //Return result not done
        
    

【讨论】:

@Simon_Weaver 你可以简单地使用 SemaphoreSlim 进行异步【参考方案2】:

我猜你可以锁定不同的层次,你的方法是一个。

我遇到了一个使用 redis 作为外部服务的系统(或网络应用程序)。在 redis 中,我们为请求保存了一个键,在你的情况下,我猜它可能是方法的名称。使用这种方法,我们首先有一个动作过滤器,它检查是否存在锁(与 redis 对话),然后阻止请求。

redis 的好处是它非常快,让我们指定一个超时时间,在该超时时间键将消失。这样可以防止永远被锁卡住。

【讨论】:

以上是关于锁定 Web API 控制器方法的主要内容,如果未能解决你的问题,请参考以下文章

更正 jsonp 请求的 web-api 控制器操作方法定义

Web API 控制器中的多个 HttpPost 方法

Web API 控制器中的多个 HttpPost 方法

Web API 控制器中多个 HttpPost 方法的共享逻辑

从 WEB API 控制器以异步方法返回 Void

如何在 Web API 2 控制器中放置多个 GET 方法?