我正在开发一款应用,需要跟踪网页的观看情况。几乎就像SO那样。它是用于确定给定页面的受欢迎程度的值。
我担心每次需要记录新视图时写入数据库都会影响性能。我知道这种边界预优化,但我之前遇到过这个问题。无论如何,价值不需要是实时的;如果延迟10分钟左右就可以了。我在想缓存数据,每隔X分钟做一次大写就应该有所帮助。
我在Windows Azure上运行,因此我可以使用Appfabric缓存。我最初的计划是创建某种复合键(PostID:UserID),并用“pageview”标记键。 Appfabric允许您按标签获取所有密钥。因此,我可以让他们建立,并在我的表中进行一次批量插入而不是许多小写。该表看起来像这样,但可以改变。
int PageID | guid userID | DateTime ViewTimeStamp
网站仍会从数据库中获取价值,写入会被延迟,有意义吗?
我just read Windows Azure Appfabric缓存不支持基于标记的搜索,因此它几乎否定了我的想法。
我的问题是,你将如何实现这一目标?我是Azure新手,所以我不确定我的选择是什么。有没有办法在没有基于标签的搜索的情况下使用缓存?我只是在寻找有关如何将这些写入延迟到SQL的建议。
答案 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;
}
}