我正在尝试从Azure Service Bus排队项目,以便我可以批量处理它们。我知道Azure Service Bus有一个ReceiveBatch()但由于以下原因似乎有问题:
我决定只使用比浪费偷看更便宜的消息监听器,并且会给我更多控制权。
基本上我试图让一定数量的消息积累起来 然后立即处理它们。我使用计时器强制延迟,但我需要 能够在他们进来时对我的物品进行排队。
根据我的计时器要求,似乎阻止集合不是一个好选项,所以我尝试使用ConcurrentBag。
var batchingQueue = new ConcurrentBag<BrokeredMessage>();
myQueueClient.OnMessage((m) =>
{
Console.WriteLine("Queueing message");
batchingQueue.Add(m);
});
while (true)
{
var sw = WaitableStopwatch.StartNew();
BrokeredMessage msg;
while (batchingQueue.TryTake(out msg)) // <== Object is already disposed
{
...do this until I have a thousand ready to be written to DB in batch
Console.WriteLine("Completing message");
msg.Complete(); // <== ERRORS HERE
}
sw.Wait(MINIMUM_DELAY);
}
但是,只要我在OnMessage之外访问该消息 管道显示BrokeredMessage已被处置。
我认为这必须是OnMessage的一些自动行为,除了立即处理之外我不会对消息做任何事情,我不想做。
答案 0 :(得分:3)
使用BlockingCollection
非常容易。
var batchingQueue = new BlockingCollection<BrokeredMessage>();
myQueueClient.OnMessage((m) =>
{
Console.WriteLine("Queueing message");
batchingQueue.Add(m);
});
您的消费者话题:
foreach (var msg in batchingQueue.GetConsumingEnumerable())
{
Console.WriteLine("Completing message");
msg.Complete();
}
GetConsumingEnumerable返回一个迭代器,它使用队列中的项目,直到设置IsCompleted
属性并且队列为空。如果队列为空但IsCompleted
为False
,则会对下一个项目进行非忙等待。
要取消使用者线程(即关闭程序),您将停止向队列添加内容并让主线程调用batchingQueue.CompleteAdding
。消费者将清空队列,看到IsCompleted
属性为True
,然后退出。
此处使用BlockingCollection
优于ConcurrentBag
或ConcurrentQueue
,因为BlockingCollection
界面更易于使用。特别是,GetConsumingEnumerable
的使用使您无需担心检查计数或进行忙等待(轮询循环)。它只是有效。
另请注意,ConcurrentBag
有一些相当奇怪的删除行为。特别是,删除项目的顺序根据哪个线程删除项目而不同。创建包的线程以与其他线程不同的顺序删除项目。有关详细信息,请参阅Using the ConcurrentBag Collection。
您还没有说明为什么要在输入中批量处理项目。除非有一个压倒一切的性能原因,否则使用该批处理逻辑使代码复杂化似乎不是一个特别好的主意。
如果要对数据库进行批量写入,那么我建议使用简单的List<T>
来缓冲项目。如果必须在将项目写入数据库之前对其进行处理,请使用上面显示的技术来处理它们。然后,而是直接写入数据库,将项目添加到列表中。当列表获得1,000个项目或经过一定时间后,分配新列表并启动任务以将旧列表写入数据库。像这样:
// at class scope
// Flush every 5 minutes.
private readonly TimeSpan FlushDelay = TimeSpan.FromMinutes(5);
private const int MaxBufferItems = 1000;
// Create a timer for the buffer flush.
System.Threading.Timer _flushTimer = new System.Threading.Timer(TimedFlush, FlushDelay.TotalMilliseconds, Timeout.Infinite);
// A lock for the list. Unless you're getting hundreds of thousands
// of items per second, this will not be a performance problem.
object _listLock = new Object();
List<BrokeredMessage> _recordBuffer = new List<BrokeredMessage>();
然后,在您的消费者中:
foreach (var msg in batchingQueue.GetConsumingEnumerable())
{
// process the message
Console.WriteLine("Completing message");
msg.Complete();
lock (_listLock)
{
_recordBuffer.Add(msg);
if (_recordBuffer.Count >= MaxBufferItems)
{
// Stop the timer
_flushTimer.Change(Timeout.Infinite, Timeout.Infinite);
// Save the old list and allocate a new one
var myList = _recordBuffer;
_recordBuffer = new List<BrokeredMessage>();
// Start a task to write to the database
Task.Factory.StartNew(() => FlushBuffer(myList));
// Restart the timer
_flushTimer.Change(FlushDelay.TotalMilliseconds, Timeout.Infinite);
}
}
}
private void TimedFlush()
{
bool lockTaken = false;
List<BrokeredMessage> myList = null;
try
{
if (Monitor.TryEnter(_listLock, 0, out lockTaken))
{
// Save the old list and allocate a new one
myList = _recordBuffer;
_recordBuffer = new List<BrokeredMessage>();
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(_listLock);
}
}
if (myList != null)
{
FlushBuffer(myList);
}
// Restart the timer
_flushTimer.Change(FlushDelay.TotalMilliseconds, Timeout.Infinite);
}
这里的想法是,您将旧列表排除在外,分配新列表以便继续处理,然后将旧列表的项目写入数据库。锁是为了防止计时器和记录计数器相互踩踏。如果没有锁定,事情可能会在一段时间内正常运行,然后在不可预测的时间内发生奇怪的崩溃。
我喜欢这种设计,因为它消除了消费者的民意调查。我唯一不喜欢的是消费者必须知道计时器(即它必须停止然后重新启动计时器)。稍微考虑一下,我可以消除这个要求。但它的编写方式很有效。
答案 1 :(得分:2)
切换到 OnMessageAsync 解决了我的问题
_queueClient.OnMessageAsync(async receivedMessage =>
答案 2 :(得分:1)
我向微软询问了关于在MSDN处理问题的BrokeredMessage,这是回复:
非常基本的规则,我不确定这是否有记录。收到的消息需要在回调函数的生命周期内处理。在您的情况下,将在异步回调完成时处理消息,这就是您在另一个线程中使用ObjectDisposedException失败的完整尝试的原因。
我真的不知道进一步处理的排队消息如何帮助提高吞吐量。这肯定会给客户增加更多负担。尝试在异步回调中处理消息,这应该足够高效。
就我而言,这意味着我无法以我想要的方式使用ServiceBus,而且我必须重新思考我希望如何运作。开溜。
答案 3 :(得分:1)
开始使用Azure Service Bus服务时遇到了同样的问题。
我发现OnMessage方法总是处理BrokedMessage对象。 Jim Mischel提出的方法对我没有帮助(但阅读非常有趣 - 谢谢!)。
经过一番调查后,我发现整个方法都是错误的。让我解释一下你想要的正确方法。
示例:
var messageOptions = new OnMessageOptions {
AutoComplete = false,
AutoRenewTimeout = TimeSpan.FromMinutes( 5 ),
MaxConcurrentCalls = 1
};
var buffer = new Dictionary<string, Guid>();
// get message from queue
myQueueClient.OnMessage(
m => buffer.Add(key: m.GetBody<string>(), value: m.LockToken),
messageOptions // this option says to ServiceBus to "froze" message in he queue until we process it
);
foreach(var item in buffer){
try {
Console.WriteLine($"Process item: {item.Key}");
myQueueClient.Complete(item.Value);// you can also use method CompleteBatch(...) to improve performance
}
catch{
// "unfroze" message in ServiceBus. Message would be delivered to other listener
myQueueClient.Defer(item.Value);
}
}
答案 4 :(得分:0)
我的解决方案是获取消息SequenceNumber,然后推迟消息并将SequenceNumber添加到BlockingCollection。一旦BlockingCollection拾取了一个新项目,它就可以通过SequenceNumber接收延迟的消息,并将消息标记为已完成。如果由于某种原因BlockingCollection不处理SequenceNumber,它将按延迟保留在队列中,以便稍后在重新启动过程时可以将其提取。这样可以防止在BlockingCollection中仍然有项目的情况下异常终止该过程时导致消息丢失。
BlockingCollection<long> queueSequenceNumbers = new BlockingCollection<long>();
//This finds any deferred/unfinished messages on startup.
BrokeredMessage existingMessage = client.Peek();
while (existingMessage != null)
{
if (existingMessage.State == MessageState.Deferred)
{
queueSequenceNumbers.Add(existingMessage.SequenceNumber);
}
existingMessage = client.Peek();
}
//setup the message handler
Action<BrokeredMessage> processMessage = new Action<BrokeredMessage>((message) =>
{
try
{
//skip deferred messages if they are already in the queueSequenceNumbers collection.
if (message.State != MessageState.Deferred || (message.State == MessageState.Deferred && !queueSequenceNumbers.Any(x => x == message.SequenceNumber)))
{
message.Defer();
queueSequenceNumbers.Add(message.SequenceNumber);
}
}
catch (Exception ex)
{
// Indicates a problem, unlock message in queue
message.Abandon();
}
});
// Callback to handle newly received messages
client.OnMessage(processMessage, new OnMessageOptions() { AutoComplete = false, MaxConcurrentCalls = 1 });
//start the blocking loop to process messages as they are added to the collection
foreach (var queueSequenceNumber in queueSequenceNumbers.GetConsumingEnumerable())
{
var message = client.Receive(queueSequenceNumber);
//mark the message as complete so it's removed from the queue
message.Complete();
//do something with the message
}