延迟写入SQL Server

时间:2012-03-01 18:15:50

标签: sql optimization azure appfabric

我正在开发一款应用,需要跟踪网页的观看情况。几乎就像SO那样。它是用于确定给定页面的受欢迎程度的值。

我担心每次需要记录新视图时写入数据库都会影响性能。我知道这种边界预优化,但我之前遇到过这个问题。无论如何,价值不需要是实时的;如果延迟10分钟左右就可以了。我在想缓存数据,每隔X分钟做一次大写就应该有所帮助。

我在Windows Azure上运行,因此我可以使用Appfabric缓存。我最初的计划是创建某种复合键(PostID:UserID),并用“pageview”标记键。 Appfabric允许您按标签获取所有密钥。因此,我可以让他们建立,并在我的表中进行一次批量插入而不是许多小写。该表看起来像这样,但可以改变。

int PageID | guid userID | DateTime ViewTimeStamp

网站仍会从数据库中获取价值,写入会被延迟,有意义吗?

just read Windows Azure Appfabric缓存不支持基于标记的搜索,因此它几乎否定了我的想法。

我的问题是,你将如何实现这一目标?我是Azure新手,所以我不确定我的选择是什么。有没有办法在没有基于标签的搜索的情况下使用缓存?我只是在寻找有关如何将这些写入延迟到SQL的建议。

3 个答案:

答案 0 :(得分:2)

您可能需要查看http://www.apathybutton.com(以及它链接到的Cloud Cover剧集),其中讨论了一种高度可扩展的计算方法。 (这可能对你的需求有点过分,但希望它能为你提供一些选择。)

答案 1 :(得分:1)

您可以将队列保留在内存中,并在计时器上排空队列,通过按页面计算总计来折叠排队项目并写入一个SQL批处理/往返行程。例如,使用TVP,您可以使用一个sproc调用来编写排队的总计。

当然,这并不能保证视图计数在内存和潜在写入时都会被写入,但页面计数不应该是关键数据,崩溃应该是罕见的。

答案 2 :(得分:0)

您可能希望了解Azure中“诊断”功能的工作原理。不是因为你会根据你正在做的事情使用诊断,而是因为它处理类似的问题并且可能提供一些灵感。我正准备实现一个数据审计功能,我想将其记录到表存储中,所以也希望将更新延迟并汇总在一起,我从诊断中获得了很多灵感。

现在,Azure中的Diagnostics的工作方式是每个角色都会启动一个小的后台“传输”线程。因此,无论何时编写任何跟踪,都会将其存储在本地内存的列表中,后台线程将(默认情况下)将所有请求捆绑在一起并每分钟将它们传输到表存储。

在您的场景中,我会让每个角色实例跟踪命中数,然后使用后台线程每分钟更新一次数据库。 我可能会在每个webrole上使用类似静态ConcurrentDictionary(或一个挂掉单例)的东西,每次命中都会递增页面标识符的计数器。您需要一些线程处理代码,以允许多个请求更新列表中的同一个计数器。或者,只需允许每个“匹配”将新记录添加到共享的线程安全列表中 然后,每分钟有一个后台线程,使用自上次以来每页点击次数增加数据库并将本地计数器重置为0或者如果您采用这种方法则清空共享列表(再次注意多线程)和锁定)。

重要的是要确保您的数据库更新是原子的;如果您从数据库中执行读取当前计数,请将其递增然后再将其写回,那么您可能会有两个不同的Web角色实例同时执行此操作,从而丢失一个更新。

编辑: 以下是

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;
    }
}