在后台线程上排队并执行插入数据库

时间:2012-03-09 21:44:59

标签: asp.net database

我在Web场中有一个asp.net应用程序(3台服务器)。除了该应用程序,我还有一个模块,可以将网站的每个请求记录到数据库中。目前插件是同步的。我想更改它,以便将插入内容发送到队列,该队列将尽可能插入它们。

会更好吗?
  1. 尝试在后台线程上插入每个请求(也是 如果数据库出现问题,很多后台线程都会用完?)

  2. 在单个后台线程上启动进程内队列     从队列中读取并执行插入。

  3. 在每个服务器向其发送页面请求日志的数据库服务器上创建进程外队列。内置的MSMQ是否可以用于这样的事情? - 或者那会有点矫枉过正吗?

3 个答案:

答案 0 :(得分:2)

选项2听起来最好。选项1肯定会创建太多的后台线程,而选项3听起来比它需要的更复杂。

您可以尝试这样的事情。

鉴于此课程:

class LogEntry
{
    public string IpAddress { get; set; }
    public string UserAgent { get; set; }
    public DateTime TimeStamp { get; set; }
    public string Url { get; set; }
    //whatever else you need
}

使用此类执行日志记录:

class SiteLogger
{
    private static object loggerLock = new object();
    private static List<LogEntry> _pendingEntries = new List<LogEntry>();
    private static Thread savingThread;

    public static void AddEntry(LogEntry entry)
    {
        // lock when accessing the list to avoid threading issues
        lock (loggerLock)
        {
            _pendingEntries.Add(entry);
        }

        if (savingThread == null)
        {
            // this should only happen with the first entry
            savingThread = new Thread(SaveEntries);
            savingThread.Start();
        }
    }

    private static void SaveEntries()
    {
        while (true)
        {
            while (_pendingEntries.Count > 0)
            {
                // lock around each individual save, not the whole loop
                // so we don't force one web request to wait for
                // all pending entries to be saved.
                lock (loggerLock)
                {
                    // save an entry to the database, however the app does that
                    MyDatabase.SaveLogEntry(_pendingEntries[0]);
                    _pendingEntries.RemoveAt(0);
                }
            }

            Thread.Sleep(TimeSpan.FromSeconds(2));
            // 2 seconds is a bit of an arbitrary value.  Depending on traffic levels,
            // it might need to go up or down.
        }
    }
}

我使用一个简单的命令行测试应用程序运行它,没有任何数据库参与(通过睡眠模拟数据库调用10毫秒)它似乎工作得很好,但显然应该在进入生产环境之前进行更多测试。此外,如果请求速度快于将数据保存到数据库中,则会出现问题(这不太可能,但应该考虑)。

更新,2018年2月:现在看看这个,我意识到如果你不熟悉线程计时你可能会得到两个savingThread个实例(你应该假设你会这样) 。并且new Thread()现在是一种在C#中做这种事情的旧方法。我将把现代的,更加线程安全的实现作为练习留给读者。

答案 1 :(得分:2)

更现代的TPL方法(如接受答案中的2月&#39; 18所示)可能看起来像:

class SiteLogger
{
    private readonly Lazy<BlockingCollection<LogEntry>> _messageQueue = new Lazy<BlockingCollection<LogEntry>>(() =>
    {
        var collection = new BlockingCollection<LogEntry>();
        Task.Factory.StartNew(processMessages, TaskCreationOptions.LongRunning);
        return collection;

        void processMessages()
        {
            foreach (var entry in collection.GetConsumingEnumerable())
            {
                //Do whatever you need to do with the entry
            }
        }
    }, LazyThreadSafetyMode.ExecutionAndPublication);


    public void AddEntry(LogEntry logEntry) => _messageQueue.Value.TryAdd(logEntry);
}

其他最佳实践,如依赖注入,IoC等,以确保这是一个单独的并经过适当测试的建议,但最好留给任何一个主题的教程。

答案 2 :(得分:1)

选项2对我来说听起来不错。让您无需太多开销即可控制。

您也可以考虑(作为1的变体)使用ThreadPool.QueueUserWorkItem()而不是每个请求的新线程。 .Net管理工作项到线程的分配,这样就不会创建太多的线程。如果所有请求都挂起了很长一段时间,那么仍然有可能使ASP.Net的线程饿死但我认为ASP.Net使用一组不同于工作项线程的线程来避免这个问题。