Как поддерживать состояние или очередь запросов в 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 или выше:
это потокобезопасный способ использования очереди, к которой затем можно получить доступ в нужное время.
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
}
}
- создать логику промывки
- At Application_Start (Глобальный.asax) событие создать ThresholdBuffer и храните его в приложении, статическом поле и т. д.
- вызов метода Add
- при ручном вызове 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