Как поддерживать состояние или очередь запросов в Web API

у меня есть ситуация, когда я должен получать запросы в методе веб-API, выстраивать эти запросы в очередь, а затем отправлять основную часть в базу данных (экземпляр Solr).

Я не совсем уверен, как поддерживать пакет запросов из нескольких источников. Пока я записываю данные каждого запроса в формате json в файл на диске, позже у меня будет служба windows, перейдите в папку "прочитать все файлы", обновите базу данных и удалите эти файлы.

вот что я делаю в моей Web API

public void Post(LogEntry value)
{
    value.EventID = Guid.NewGuid();
    value.ServerTime = DateTime.UtcNow;
    string json = JsonConvert.SerializeObject(value);
    using(StreamWriter sw = new StreamWriter(value.EventID.ToString()))
    {
        sw.Write(json);
    }
}

(здесь EventID is GUID)

этот процесс выглядит неправильно, должен быть способ поддерживать очередь запроса, но я не совсем уверен, как поддерживать очередь во время нескольких запросов.

причина, по которой я это делаю, заключается в том, что вставка пакетов в экземпляр solr быстрее, чем вставка одной записи через SolrNet. Я ожидаю получить не менее 100 запросов в секунду на веб-API. Я хочу создать пакет 1000 запрос и обновление экземпляра solr каждые 10 секунд. Пожалуйста, не думайте, что мне нужен код, просто нужно знать, какую стратегию следует принять для поддержания очереди запросов / государства.

6 ответов


вы можете использовать параллельную очередь, если вы используете .NET 4.0 или выше:

параллельная очередь (MSDN)

это потокобезопасный способ использования очереди, к которой затем можно получить доступ в нужное время.

Edit:

пример:

Это будет обертка для очереди:

public static class RequestQueue
{
    private static ConcurrentQueue<int> _queue;

    public static ConcurrentQueue<int> Queue
    {
        get
        {
            if (_queue == null)
            {
                _queue = new ConcurrentQueue<int>();
            }

            return _queue;
        }
    }

}

тогда вы можете настроить свой веб-api следующим образом (в этом примере хранятся целые числа краткость):

public class ValuesController : ApiController
{        
    public string Get()
    {
        var sb = new StringBuilder();
        foreach (var item in RequestQueue.Queue)
        {
            sb.Append(item.ToString());
        }

        return sb.ToString();
    }

    public void Post(int id)
    {
        RequestQueue.Queue.Enqueue(id);
    }        
}

Если вы используете этот пример, вы увидите, что очередь содержит значения для нескольких запросов. Но, поскольку он живет в памяти, эти элементы в очереди исчезнут, если пул приложений будет переработан (например).

Теперь вы можете создать проверку, когда очередь содержит 10 элементов, а затем сохранить их в БД, создавая другую очередь для хранения входящих значений.

вот так:

public static class RequestQueue
{
    private static ConcurrentQueue<int> _queue;

    public static ConcurrentQueue<int> Queue
    {
        get
        {
            if (_queue == null)
            {
                _queue = new ConcurrentQueue<int>();
            }

            if (_queue.Count >= 10)
            {
                SaveToDB(_queue);
                _queue = new ConcurrentQueue<int>();
            }

            return _queue;
        }
    }

    public static void SaveToDB(ConcurrentQueue<int> queue)
    {
        foreach (var item in queue)
        {
            SaveItemToDB(item);
        }
    }
}

вам нужно немного очистить это, но это настройка должна работать. Кроме того, вам может понадобиться некоторый механизм блокировки вокруг сброса очереди в БД и создания нового экземпляра. Я бы написал консольное приложение с несколькими потоками, которые обращаются к этой очереди, чтобы проверить его.


Это очень хороший сценарий, чтобы использовать MSMQ. Для каждого запроса просто отправьте элемент в очередь MSMQ. Либо в том же веб-приложении, либо в любом другом приложении просто прочитайте несколько элементов из очереди и опубликуйте их в solr. Независимо от сбоя вашего приложения или его рециркуляции, MSMQ будет безопасно хранить ваши данные, чтобы получить их позже.

MSMQ является надежным, надежным и масштабируемым. Это идеально подходит для вашей проблемы.


вы можете запрашивать запросы в очередь в памяти, и вы можете периодически отправлять их в базу данных с использованием Quartz .Сеть. Вы можете просто сделать это в Global.асакс.CS следующим образом:

public class RequestQueue
{
    private readonly Queue<HttpRequest> _requestHistory; 
    private RequestQueue()
    {
        _requestHistory = new Queue<HttpRequest>();
    }
    private static RequestQueue _singleton;

    public static RequestQueue Instance()
    {
        if (_singleton == null)
            _singleton = new RequestQueue();
        return _singleton;
    }

    public void Enqueue(HttpRequest request)
    {
        _requestHistory.Enqueue(request);
    }

    public void Flush()
    {
        while (_requestHistory.Count > 0)
        {
            var request = _requestHistory.Dequeue();
            try
            {
                //Write request To Db
            }
            catch (Exception)
            {
                _requestHistory.Enqueue(request);
            }
        }
    }
}

public class WebApiApplication : System.Web.HttpApplication
{
    public WebApiApplication()
    {
        base.BeginRequest += delegate
            {
                RequestQueue.Instance().Enqueue(HttpContext.Current.Request);
            };
    }

    private void InitializeQuartz()
    {
        ISchedulerFactory sf = new StdSchedulerFactory();
        IScheduler sched = sf.GetScheduler();

        DateTimeOffset runTime = DateBuilder.EvenMinuteDate(DateTime.UtcNow);
        DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 5);

        IJobDetail job = JobBuilder.Create<QueueConsumer>()
            .WithIdentity("job1", "group1")
            .Build();
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity("trigger1", "group1")
            .StartAt(runTime)
            .WithCronSchedule("5 0/1 * * * ?")
            .Build();

        sched.ScheduleJob(job, trigger);

        sched.Start();
    }

    public class QueueConsumer : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            RequestQueue.Instance().Flush();
        }
    }

    protected void Application_Start()
    {
        InitializeQuartz();

другое решение может содержать записи в очереди памяти, которая не находится в том же процессе, что и WebApi. Например: MemcacheQueue https://github.com/coderrr/memcache_queue

некоторые из этих реализаций очереди имеют возможности сохранения, и, следовательно, вы не потеряете данные в любом случае.


вы должны попытаться реализовать NServiceBus есть возможность планировать сообщения и отправлять сообщения в будущем, из документации служебной шины вы можете возможность планирования вы можете запланировать задачу или действие/лямбда, которые будут выполняться повторно в заданном интервале.

это означает, что вы можете иметь кэш memeory и записывать содержимое массива в свой solr/lucene impl каждые 10 минут, например, это что-то проще as:

Schedule.Every(TimeSpan.FromMinutes(10)).Action(() => { < task to be executed > })

в случае, если вам понадобится больше гибкости для установки schedueler, вы можете интегрировать его quartz.net

случай должен быть следующим:

  • WCF как служба windows и NServiceBus должны иметь один и тот же контекст или реализовать cacheManager, который может быть разделен между этими двумя различными частями вашей системы.
  • каждый раз, когда вы делаете запрос, вы вызываете свой wcf передать параметры и whitin ваш wcf вы добавляете в строка памяти в массив (это может быть строковый массив с тем же значением json, которое вы записываете на диск)
  • ServiceBus будет обрабатывать очереди для операций в массиве и избегать любых конфликтов для операций массива, например:

    • добавить элементы в массив
    • пустой массив
    • запись в базу данных

public class ThresholdBuffer<T>
{
    private ConcurrentBag<T> _buffer;

    private int _threshold;

    public ThresholdBuffer(int threshold)
    {
        _threshold = threshold;
        _buffer = new ConcurrentBag<T>();
    }

    public void Add(T item)
    {
        _buffer.Add(item);

        if(_buffer.Count >= _threshold)
        {
            Recycle();
        }
    }

    public void Recycle()
    {
        var value = Interlocked.Exchange<ConcurrentBag<T>>(ref _buffer, new ConcurrentBag<T>());
//flush value 
    }
}
  1. создать логику промывки
  2. At Application_Start (Глобальный.asax) событие создать ThresholdBuffer и храните его в приложении, статическом поле и т. д.
  3. вызов метода Add
  4. при ручном вызове Application_End Recycle

вы можете добавить логику блокировки в Recycle, чтобы предотвратить создание нескольких ConcurrentBag и промывку почти пустого мешка. Но, по моему мнению, это менее зло, чем замок.

обновление. Замок бесплатно без aditional ConcurrentBag creation

public class ThresholdBuffer<T>
{
    private ConcurrentBag<T> _buffer;

    private int _copacity;

    private int _threshold;

    public ThresholdBuffer(int threshold)
    {
        _threshold = threshold;
        _copacity = 0;
        _buffer = new ConcurrentBag<T>();
    }

    public void Add(T item)
    {
        _buffer.Add(item);
        if (Interlocked.Increment(ref _copacity) == _threshold)
        {
            Recycle();
        }
    }

    public void Recycle()
    {
        var value4flasshing = Interlocked.Exchange<ConcurrentBag<T>>(ref _buffer, new ConcurrentBag<T>());
        Thread.VolatileWrite(ref _copacity, 0);
    }
}

ps Вы можете использовать любой ConcurrentCollection вместо ConcurrentBag