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

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

0 голосов
/ 01 марта 2012

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

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

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

int PageID | guid userID | DateTime ViewTimeStamp

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

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

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

Ответы [ 3 ]

2 голосов
/ 01 марта 2012

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

1 голос
/ 03 марта 2012

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

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

0 голосов
/ 03 марта 2012

Возможно, вы захотите взглянуть на то, как работает функция «диагностики» в 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;
    }
}    
...