Задержка записи в SQL Server

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

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

Я работаю в Windows Azure, поэтому кэш Appfabric мне доступен. Мой первоначальный план состоял в том, чтобы создать какой-то составной ключ (PostID:UserID) и пометить ключ как «просмотр страницы». Appfabric позволяет получить все ключи по тегу. Таким образом, я мог позволить им накапливаться и выполнять одну массовую вставку в мою таблицу вместо множества мелких операций записи. Таблица выглядит так, но открыта для изменений.

int PageID | guid userID | DateTime ViewTimeStamp

Веб-сайт все равно получит значение из базы данных, запись просто будет отложена, есть ли смысл?

Я только что прочитал, что кэш Windows Azure Appfabric не поддерживает поиск по тегам. , так что это в значительной степени опровергает мою идею.

Мой вопрос в том, как бы вы этого добились? Я новичок в Azure, поэтому я не уверен, какие у меня есть варианты. Есть ли способ использовать кеш без поиска по тегам? Я просто ищу совета о том, как отложить эти записи в SQL.


person Community    schedule 01.03.2012    source источник
comment
Я не пытаюсь отслеживать посещения страниц, и это не ведение журнала. За тем, что определяет представление, стоят бизнес-правила. Как долго пользователь должен находиться на странице, как долго тот же пользователь посещал ее в последний раз и т. д.   -  person    schedule 01.03.2012


Ответы (3)


Возможно, вы захотите взглянуть на http://www.apathybutton.com (и эпизод Cloud Cover, на который он ссылается to), в котором говорится о хорошо масштабируемом способе подсчета вещей. (Это может быть излишним для ваших нужд, но, надеюсь, это даст вам некоторые варианты.)

person user94559    schedule 01.03.2012

Вы можете хранить очередь в памяти и по таймеру истощать очередь, сворачивать элементы в очереди, суммируя подсчеты по страницам и записывая в один пакет SQL/туда и обратно. Например, используя TVP, вы можете записать итоги в очереди одним вызовом sproc.

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

person bryanmac    schedule 03.03.2012

Возможно, вы захотите посмотреть, как работает функция «диагностики» в Azure. Не потому, что вы вообще будете использовать диагностику для того, что вы делаете, а потому, что она решает аналогичную проблему и может дать некоторое вдохновение. Я только собираюсь внедрить функцию аудита данных, и я хочу регистрировать это в хранилище таблиц, поэтому также хочу отложить и сгруппировать обновления, и я черпал много вдохновения от диагностики.

Теперь диагностика в Azure работает следующим образом: каждая роль запускает небольшой фоновый поток «передачи». Таким образом, всякий раз, когда вы записываете какие-либо трассировки, они сохраняются в списке в локальной памяти, и фоновый поток (по умолчанию) собирает все запросы и каждую минуту передает их в хранилище таблиц.

В вашем сценарии я бы позволил каждому экземпляру роли отслеживать количество попаданий, а затем использовать фоновый поток для обновления базы данных каждую минуту или около того. Я бы, вероятно, использовал что-то вроде статического ConcurrentDictionary (или одного, висящего на синглтоне) на каждой веб-ролике, при этом каждое обращение увеличивает счетчик для идентификатора страницы. Вам понадобится код обработки потоков, чтобы разрешить несколько запросов на обновление одного и того же счетчика в списке. В качестве альтернативы, просто разрешите каждому "попаданию" добавлять новую запись в общий потокобезопасный список.
Затем пусть фоновый поток раз в минуту увеличивает базу данных на количество обращений на страницу с момента последнего раза и сбрасывает локальный counter на 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;
    }
}    
person Frans    schedule 02.03.2012
comment
Кстати, кто-нибудь знает, почему пример кода не выглядит красиво? Что я делаю не так? - person Frans; 03.03.2012