如何实现文章浏览量高并发计数?

个人博客新版上线 1 年以来,一直没有浏览量计数功能,该功能对于博客系统来说,必不可少。对于一篇文章来说,被多人同时浏览是很可能发生的事情,相应地,如果给文章添加浏览量计数功能,高并发是首要考虑的事情。那么,如何实现文章浏览量的高并发计数呢?

首先要清楚文章浏览量计数的主要流程:访客查看某篇文章后,系统记录该文章被查看,将文章浏览量加 1,更新该文章浏览量的数据。如果该文章同时被多人查看,那么就存在并发更新该文章浏览量的问题,这里不仅要确保浏览量准确,而且还要解决并发更新引起数据库的问题。

对于并发问题,我们很容易想到的就是使用队列来解决,另外,文章浏览量对于访客来说并不重要,队列的异步更新文章浏览量也满足要求。那么,问题又来了,我们应该使用什么队列?RabbitMQ? Kafka? RocketMQ?——NO!,这些对于一个小小的博客系统来说太重了。其实,.NET 自带了功能强大的队列工具类 —— ConcurrentQueue。

有了这个神器,对文章浏览量高并发计数的问题,可以进行转化,即每次访客查看某篇文章后,则将被查看文章 Id 压入队列,同时,建立一个存放文章 Id 及文章浏览量的内存容器,注意文章浏览量为增量值。当文章 Id 不断从队列弹出时,递增容器中各文章 Id 对应的浏览量。这样,高并发计数的问题初步解决了。但是,内存容器中各文章的浏览量如何持久化到数据库呢?

我目前的做法是使用定时器,定时将内存容器各文章浏览量,增量更新到数据库中各文章的流量。关于如何使用定时器,可参考文章《.Net Core 如何实现简易定时任务?》。关于浏览量计数,我构建了一个服务接口:IPostViewService,代码如下:

public interface IPostViewService 
{ 
   /// <summary> 
   /// 启动 
   /// </summary> 
   void Start(); 
   /// <summary> 
   /// 停止 
   /// </summary> 
   void Stop(); 
   /// <summary> 
   /// 添加文章访问 
   /// </summary> 
   /// <param name="postId"></param> 
   void Increase(int postId); 
    
   /// <summary> 
   /// 递增查看计数 
   /// </summary> 
   ConcurrentDictionary<int, int> IncreViews { get; } 
}

其中 Start() 和 Stop() 分别用于启动和停止文章浏览量计数,Increase为文章被查看后,浏览量递增加 1,ConcurrentDictionary 为上述的内存容器,用于记录各文章访问增量。

上述服务接口实现为:

public class DefaultPostViewService : IPostViewService 
{ 
   private static ConcurrentQueue<int> _viewQueue = new ConcurrentQueue<int>(); 
   private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); //线程取消标记 
   /// <summary> 
   /// 后台任务 
   /// </summary> 
   private static Task _task; 
   /// <summary> 
   /// 日志 
   /// </summary> 
   private static ILogger _logger; 
   /// <summary> 
   /// 增量浏览量 
   /// </summary> 
   public ConcurrentDictionary<int, int> IncreViews { get; private set; } 
   /// <summary> 
   /// 构造 
   /// </summary> 
   /// <param name="loggerFactory"></param> 
   public DefaultPostViewService(ILoggerFactory loggerFactory) 
   { 
       _logger = loggerFactory.CreateLogger(nameof(DefaultPostViewService)); 
   } 
   /// <summary> 
   /// 文章浏览处理委托 
   /// </summary> 
   private Action ViewHandler => () => 
       { 
           while (true) 
           { 
               try 
               { 
                   if (_cancellationTokenSource.Token.IsCancellationRequested) 
                   { 
                       _viewQueue = null; 
                       break; 
                   } 
                   else 
                   { 
                       if (!_viewQueue.IsEmpty) 
                       { 
                           if (_viewQueue.TryDequeue(out int postId)) 
                           { 
                               //处理文章浏览逻辑 
                               UpdatePostView(postId); 
                               Thread.Sleep(10); 
                           } 
                       } 
                       else 
                       { 
                           Thread.Sleep(TimeSpan.FromSeconds(30)); 
                       } 
                   } 
               } 
               catch (Exception ex) 
               { 
                   _logger.LogError(ex, "执行浏览处理异常"); 
               } 
           } 
       }; 
   /// <summary> 
   /// 更新文章浏览 
   /// </summary> 
   /// <param name="postId">文章Id</param> 
   private void UpdatePostView(int postId) 
   { 
       //没有浏览 
       if (IncreViews.ContainsKey(postId)) 
       { 
           var count = IncreViews[postId]; 
           IncreViews[postId] = count + 1; 
       } 
       else 
       { 
           IncreViews[postId] = 1; 
       } 
   } 
   public void Increase(int postId) 
   { 
       try 
       { 
           _viewQueue.Enqueue(postId); 
       } 
       catch (Exception ex) 
       { 
           _logger.LogError(ex, "向队列添加文章[{0}]访问记录失败", postId); 
       } 
   } 
   /// <summary> 
   /// 停止 
   /// </summary> 
   public void Stop() 
   { 
       _cancellationTokenSource.Cancel(); 
   } 
   /// <summary> 
   /// 启动 
   /// </summary> 
   public void Start() 
   { 
       if (_task == null) 
       { 
           _task = new Task(ViewHandler, _cancellationTokenSource.Token); 
       } 
       if (IncreViews == null) 
       { 
           IncreViews = new ConcurrentDictionary<int, int>(); 
       } 
       if (_task.Status != TaskStatus.Running) 
       { 
           _task.Start(); 
       } 
   } 
}

注意,接口和实现注册时,要使用时单例模式。

《如何实现文章浏览量高并发计数?》的相关评论

发表评论

必填项已用 * 标记,邮箱地址不会被公开。