【问题标题】:Delaying writes to SQL Server延迟写入 SQL Server
【发布时间】:2012-03-01 18:15:49
【问题描述】:

我正在开发一个应用程序,并且需要跟踪页面的浏览量。几乎就像 SO 是如何做到的。它是用于确定给定页面受欢迎程度的值。

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

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

int PageID | guid userID | DateTime ViewTimeStamp

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

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

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

【问题讨论】:

  • 我没有尝试跟踪页面点击,这也没有记录。定义视图的背后有业务规则。用户必须在页面上停留多长时间,自同一用户上次访问以来的时间等...

标签: sql optimization azure appfabric


【解决方案1】:

您可能想看看http://www.apathybutton.com(以及它链接到的 Cloud Cover 插曲),它讨论了一种高度可扩展的计数方式。 (这可能对您的需求有点过分,但希望它可以为您提供一些选择。)

【讨论】:

    【解决方案2】:

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

    这当然不能保证视图计数会被写入,因为它在内存中并且是潜在写入的,但页面计数不应该是关键数据,崩溃应该很少见。

    【讨论】:

      【解决方案3】:

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

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

      在您的场景中,我会让每个角色实例跟踪点击次数,然后使用后台线程每分钟左右更新一次数据库。 我可能会在每个 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;
          }
      }    
      

      【讨论】:

      • 顺便说一句,有谁知道为什么代码示例没有“美化”?我做错了什么?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-01
      • 1970-01-01
      相关资源
      最近更新 更多