Возможно, вы захотите взглянуть на то, как работает функция «диагностики» в Azure.Не потому, что вы будете использовать диагностику для всего, что делаете, а потому, что она имеет дело с аналогичной проблемой и может вдохновить вас.Я только собираюсь реализовать функцию аудита данных, и я хочу записать ее в хранилище таблиц, а также хочу отложить и собрать обновления вместе, и я получил много вдохновения от диагностики.
Теперь, путьДиагностика в Azure работает так: каждая роль запускает небольшую фоновую «передачу» потока.Таким образом, всякий раз, когда вы пишете какие-либо трассировки, они сохраняются в списке в локальной памяти, и фоновый поток (по умолчанию) объединяет все запросы и каждую минуту передает их в хранилище таблиц.
В вашем сценарииЯ бы позволил каждому экземпляру роли отслеживать количество попаданий, а затем использовать фоновый поток для обновления базы данных каждую минуту или около того.Я бы, вероятно, использовал что-то вроде статического ConcurrentDictionary (или один, висящий на синглтоне) на каждой веб-роли с каждым нажатием, увеличивающим счетчик для идентификатора страницы.Вам нужно иметь некоторый код обработки потоков, чтобы несколько запросов могли обновить один и тот же счетчик в списке.В качестве альтернативы, просто разрешите каждому «удару» добавлять новую запись в общий потокобезопасный список.
Затем создайте фоновый поток раз в минуту, увеличивая базу данных на количество обращений на страницу с момента последнего раза и сбрасывая локальный поток.Счетчик 0 или очистить общий список, если вы используете этот подход (опять же, будьте осторожны с многопоточностью и блокировкой).
Важно, чтобы обновление базы данных было атомарным;Если вы выполняете текущее считывание из базы данных, увеличиваете его, а затем записываете обратно, у вас могут быть два разных экземпляра веб-роли, выполняющих это одновременно и теряющих одно обновление.
РЕДАКТИРОВАТЬ: Здесьэто быстрый пример того, как вы могли бы пойти по этому поводу.
using System.Collections.Concurrent;
using System.Data.SqlClient;
using System.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main(string[] args)
{
// You would put this in your Application_start for the web role
Thread hitTransfer = new Thread(() => HitCounter.Run(new TimeSpan(0, 0, 1))); // You'd probably want the transfer to happen once a minute rather than once a second
hitTransfer.Start();
//Testing code - this just simulates various web threads being hit and adding hits to the counter
RunTestWorkerThreads(5);
Thread.Sleep(5000);
// You would put the following line in your Application shutdown
HitCounter.StopRunning(); // You could do some cleverer stuff with aborting threads, joining the thread etc but you probably won't need to
Console.WriteLine("Finished...");
Console.ReadKey();
}
private static void RunTestWorkerThreads(int workerCount)
{
Thread[] workerThreads = new Thread[workerCount];
for (int i = 0; i < workerCount; i++)
{
workerThreads[i] = new Thread(
(tagname) =>
{
Random rnd = new Random();
for (int j = 0; j < 300; j++)
{
HitCounter.LogHit(tagname.ToString());
Thread.Sleep(rnd.Next(0, 5));
}
});
workerThreads[i].Start("TAG" + i);
}
foreach (var t in workerThreads)
{
t.Join();
}
Console.WriteLine("All threads finished...");
}
}
public static class HitCounter
{
private static System.Collections.Concurrent.ConcurrentQueue<string> hits;
private static object transferlock = new object();
private static volatile bool stopRunning = false;
static HitCounter()
{
hits = new ConcurrentQueue<string>();
}
public static void LogHit(string tag)
{
hits.Enqueue(tag);
}
public static void Run(TimeSpan transferInterval)
{
while (!stopRunning)
{
Transfer();
Thread.Sleep(transferInterval);
}
}
public static void StopRunning()
{
stopRunning = true;
Transfer();
}
private static void Transfer()
{
lock(transferlock)
{
var tags = GetPendingTags();
var hitCounts = from tag in tags
group tag by tag
into g
select new KeyValuePair<string, int>(g.Key, g.Count());
WriteHits(hitCounts);
}
}
private static void WriteHits(IEnumerable<KeyValuePair<string, int>> hitCounts)
{
// NOTE: I don't usually use sql commands directly and have not tested the below
// The idea is that the update should be atomic so even though you have multiple
// web servers all issuing similar update commands, potentially at the same time,
// they should all commit. I do urge you to test this part as I cannot promise this code
// will work as-is
//using (SqlConnection con = new SqlConnection("xyz"))
//{
// foreach (var hitCount in hitCounts.OrderBy(h => h.Key))
// {
// var cmd = con.CreateCommand();
// cmd.CommandText = "update hits set count = count + @count where tag = @tag";
// cmd.Parameters.AddWithValue("@count", hitCount.Value);
// cmd.Parameters.AddWithValue("@tag", hitCount.Key);
// cmd.ExecuteNonQuery();
// }
//}
Console.WriteLine("Writing....");
foreach (var hitCount in hitCounts.OrderBy(h => h.Key))
{
Console.WriteLine(String.Format("{0}\t{1}", hitCount.Key, hitCount.Value));
}
}
private static IEnumerable<string> GetPendingTags()
{
List<string> hitlist = new List<string>();
var currentCount = hits.Count();
for (int i = 0; i < currentCount; i++)
{
string tag = null;
if (hits.TryDequeue(out tag))
{
hitlist.Add(tag);
}
}
return hitlist;
}
}