• 在线用户统计与命令模式


    >>获取该文章的源码

      我阅读过几个论坛的在线用户统计代码,发现其中有两个问题,一个是需要借助数据库,另外一个是“锁”的粒度比较强!在线用户统计并不要求十分的精确(在这篇文章里,我不会讨论如何侦测到浏览器的关闭动作,而是讨论如何提高代码性能),那么借助数据库来完成这样的功能就显得很夸张!更重要的是对数据库进行读写操作(I/O操作),是要消耗性能的,而且还要在数据表里产生一条记录。为了一个不精确的功能需求消耗如此多的资源,的确不划算!另外一个办法是直接使用DataSet和ASP.NET缓存的方式来做统计,类似这样的代码我看过几个,自己也写过一个。但这样做也存在很大问题,最严重的地方还是“锁”的问题。在更新DataSet时需要通过lock关键字来将其锁定,但如果用户数量很大时,DataSet被加锁的次数过于频繁,所造成的坏结果千奇百怪。所以我不得不寻找一种更为有效的方法。

      在进行讨论之前,有必要先做一个介绍。在线用户信息应该包括:

    • SessionID 
    • 用户名称
    • 最后活动时间
    • 最后请求地址(Url地址)

    还可以包括IP地址或其他更详细的信息。这是在线用户信息的数据结构,在线用户统计的算法是:
    1. 将在线用户信息插入到集合,如果集合中已经存在相同用户名称的数据项,则更新该数据项;
    2. 根据最后活动时间倒排序;
    排序步骤虽然可以在列表显示在线用户信息的时候再做,但是那样会花费一点时间,不如在插入数据项以后马上排序,需要显示的时候直接显示。OnlineUser数据结构代码如下:


      1using System;
      2using System.Collections.Generic;
      3
      4namespace Net.AfritXia.Web.OnlineStat
      5{
      6    /// <summary>
      7    /// 在线用户类
      8    /// </summary>

      9    public class OnlineUser
     10    {
     11        // 用户 ID
     12        private int m_uniqueID;
     13        // 名称
     14        private string m_userName;
     15        // 最后活动时间
     16        private DateTime m_lastActiveTime;
     17        // 最后请求地址
     18        private string m_lastRequestURL;
     19        // SessionID
     20        private string m_sessionID;
     21        // IP 地址
     22        private string m_clientIP;
     23
     24        类构造器
     43
     44        /// <summary>
     45        /// 设置或获取用户 ID
     46        /// </summary>

     47        public int UniqueID
     48        {
     49            set
     50            {
     51                this.m_uniqueID = value;
     52            }

     53
     54            get
     55            {
     56                return this.m_uniqueID;
     57            }

     58        }

     59
     60        /// <summary>
     61        /// 设置或获取用户昵称
     62        /// </summary>

     63        public string UserName
     64        {
     65            set
     66            {
     67                this.m_userName = value;
     68            }

     69
     70            get
     71            {
     72                return this.m_userName;
     73            }

     74        }

     75
     76        /// <summary>
     77        /// 最后活动时间
     78        /// </summary>

     79        public DateTime ActiveTime
     80        {
     81            set
     82            {
     83                this.m_lastActiveTime = value;
     84            }

     85
     86            get
     87            {
     88                return this.m_lastActiveTime;
     89            }

     90        }

     91
     92        /// <summary>
     93        /// 最后请求地址
     94        /// </summary>

     95        public string RequestURL
     96        {
     97            set
     98            {
     99                this.m_lastRequestURL = value;
    100            }

    101
    102            get
    103            {
    104                return this.m_lastRequestURL;
    105            }

    106        }

    107
    108        /// <summary>
    109        /// 设置或获取 SessionID
    110        /// </summary>

    111        public string SessionID
    112        {
    113            set
    114            {
    115                this.m_sessionID = value;
    116            }

    117
    118            get
    119            {
    120                return this.m_sessionID;
    121            }

    122        }

    123
    124        /// <summary>
    125        /// 设置或获取 IP 地址
    126        /// </summary>

    127        public string ClientIP
    128        {
    129            set
    130            {
    131                this.m_clientIP = value;
    132            }

    133
    134            get
    135            {
    136                return this.m_clientIP;
    137            }

    138        }

    139    }

    140}

       对于在线用户列表数据集,我们只用一个List<OnlineUser>对象来表示就可以了,不过我现在是把它封装在OnlineUserRecorder类里。代码如下:

     1using System;
     2using System.Collections.Generic;
     3using System.Threading;
     4
     5namespace Net.AfritXia.Web.OnlineStat
     6{
     7    /// <summary>
     8    /// 在线用户记录器
     9    /// </summary>

    10    public class OnlineUserRecorder
    11    {
    12        // 在线用户列表
    13        private List<OnlineUser> m_onlineUserList = new List<OnlineUser>();
    14
    15        /// <summary>
    16        /// 保存在线用户
    17        /// </summary>
    18        /// <param name="onlineUser">在线用户信息</param>

    19        public void Persist(OnlineUser onlineUser)
    20        {
    21            if (onlineUser == null)
    22                return;
    23
    24            lock (typeof(OnlineUserRecorder))
    25            {
    26                // 添加在线用户到集合
    27                this.m_onlineUserList.Add(onlineUser);
    28     // 按最后活动时间排序
    29                this.m_onlineUserList.Sort(CompareByActiveTime);
    30            }

    31        }

    32
    33        /// <summary>
    34        /// 比较两个用户的活动时间
    35        /// </summary>
    36        /// <param name="x"></param>
    37        /// <param name="y"></param>
    38        /// <returns></returns>

    39        private static int CompareByActiveTime(OnlineUser x, OnlineUser y)
    40        {
    41            if (x == null)
    42                throw new NullReferenceException("X 值为空 ( X Is Null )");
    43
    44            if (y == null)
    45                throw new NullReferenceException("Y 值为空 ( Y Is Null )");
    46
    47            if (x.LastActiveTime > y.LastActiveTime)
    48                return -1;
    49
    50            if (x.LastActiveTime < y.LastActiveTime)
    51                return +1;
    52
    53            return 0;
    54        }

    55    }

    56}

       先不要管OnlineUserReader的代码是否正确,这还远远不是最终的代码。注意排序使用的是简化版的策略模式,这个并不主要。关键问题是在于lock代码段!代码执行到lock关键字时,需要判断锁定状态,如果正处于锁定状态,那么就会在此等待,直到被锁定的资源释放掉。想象一下,如果有两个用户先后请求一个页面,这个页面要需要执行这部分代码,那么就会有一个用户的请求先被处理,而另外一个用户只能原地等待。而恰好此时又来第三个用户,他也请求了这个页面,那么他也得等一会儿……第四个用户来了、第五个用户也来了……这样,用户就排起了长队。呵呵,等到第一个用户释放了被锁定的资源以后会发生什么情况呢?第二、三、四、五这几个用户会争夺资源!谁想抢到了,谁就先执行。也就是说第二个请求网页的用户,可能要等到最后才被执行。假如,第一个用户在执行代码的时候发生异常不能再继续执行,他也没有释放被锁定的资源,那么其他用户将无限期的等待下去……这就是死锁。

      比如你去一个裁缝店,叫那里的小裁缝给你做套西服。你先选定一块布料,然后小裁缝会用皮尺量出你的身高、腰围、臂长、腿长等数据。再然后呢?小裁缝马上扯下一块布裁裁剪剪,开启缝纫机来个现场制作对么?如果这时候又来了一个顾客怎么办?这位新来的顾客就一直等着?等到你要的西服都做好了,才去招呼这位新来的顾客吗?也许这位新来的顾客等一会儿就摔门走人了。对于这个裁缝店算是丢掉了一笔买卖。但事实并不是这样子的!小裁缝是将你的身高、腰围等信息记录在一个收据单上,你选择什么样的布料也记录在这单子上,最后会告诉你几天以后来取就可以了。如果这个时候又来了一个顾客,小裁缝也一样记录这位新顾客的身高腰围等信息,并告诉他几天以后来取……

      这就是小裁缝的智慧,即免除了顾客的等待时间,又为自己争取到了顾客量。你只需要选定一块布料,并且把自己的身高、腰围等信息留下就可以了。至于这个小裁缝是什么时候开工,手工过程是什么样的,你无需知道了。你只会记得到日子取回自己的西服。这就是命令模式!

      命令模式的特点是:

    • 命令的发出者和命令的执行者可能不是同一个人(不是同一个类或代码段,甚至不是在同一台服务器上);
    • 命令的发出者发出命令给执行者以后会立即返回,或者说发出命令的时间和执行命令的时间可能会有很大间隔,是不同步的;
    • 命令的发出者不知道也不想知道执行者的执行过程;


      你就是命令的发出者,小裁缝就是命令的执行者,那张记录你身高、腰围等信息的收据单,就是命令对象!

       对于统计在线用户,我们事将用户信息翻译成命令对象并记录到一个队列中。并不马上更新在下用户数据集合,而是延迟一段时间在更新。将用户更新信息积攒起来,等到一定时间后批量处理。这样就免除了对在线用户数据集的频繁加锁!更具体的说明如下:
    1. 创建两个命令队列,cmdQueueA和cmdQueueX。cmdQueueA专门负责接收并存储新的命令对象;cmdQueueX专门负责存储即将执行的命令对象;

     




    2. 在一定时间间隔之后,交换两个命令队列!系统(在下图中用红色曲线表示)将根据cmdQueueX中的命令对象,更新在线用户数据集合onlineUserList;在更新onlineUserList的时候需要加锁,所以使用两个命令队列可以保证命令的接收效率,也避免了对同一队列同时进行入出队操作;

     



    最终代码,首先是OnlineUserRecorder类,该类属于“控制器”:


      1using System;
      2using System.Collections.Generic;
      3using System.Threading;
      4
      5namespace Net.AfritXia.Web.OnlineStat
      6{
      7 /// <summary>
      8 /// 在线用户记录器
      9 /// </summary>

     10    public class OnlineUserRecorder
     11    {
     12        // 在线用户数据库
     13        private OnlineUserDB m_db = null;
     14        // 命令队列A, 用于接收命令
     15        private Queue<OnlineUserCmdBase> m_cmdQueueA = null;
     16        // 命令队列X, 用于执行命令
     17        private Queue<OnlineUserCmdBase> m_cmdQueueX = null;
     18        // 繁忙标志
     19        private bool m_isBusy = false;
     20        // 上次统计时间
     21        private DateTime m_lastStatisticTime = new DateTime(0);
     22        // 用户超时分钟数
     23        private int m_userTimeOutMinute = 20;
     24        // 统计时间间隔
     25        private int m_statisticEventInterval = 60;
     26        // 命令队列长度
     27        private int m_cmdQueueLength = 256;
     28
     29        类构造器
     42
     43        /// <summary>
     44        /// 设置或获取用户超时分钟数
     45        /// </summary>

     46        internal int UserTimeOutMinute
     47        {
     48            set
     49            {
     50                this.m_userTimeOutMinute = value;
     51            }

     52
     53            get
     54            {
     55                return this.m_userTimeOutMinute;
     56            }

     57        }

     58
     59        /// <summary>
     60        /// 设置或获取统计时间间隔(单位秒)
     61        /// </summary>

     62        internal int StatisticEventInterval
     63        {
     64            set
     65            {
     66                this.m_statisticEventInterval = value;
     67            }

     68
     69            get
     70            {
     71                return this.m_statisticEventInterval;
     72            }

     73        }

     74
     75        /// <summary>
     76        /// 设置或获取命令队列长度
     77        /// </summary>

     78        public int CmdQueueLength
     79        {
     80            set
     81            {
     82                this.m_cmdQueueLength = value;
     83            }

     84
     85            get
     86            {
     87                return this.m_cmdQueueLength;
     88            }

     89        }

     90
     91        /// <summary>
     92        /// 保存在线用户信息
     93        /// </summary>
     94        /// <param name="onlineUser"></param>

     95        public void Persist(OnlineUser onlineUser)
     96        {
     97            // 创建删除命令
     98            OnlineUserDeleteCmd delCmd = new OnlineUserDeleteCmd(this.m_db, onlineUser);
     99            // 创建插入命令
    100            OnlineUserInsertCmd insCmd = new OnlineUserInsertCmd(this.m_db, onlineUser);
    101
    102            if (this.m_cmdQueueA.Count > this.CmdQueueLength)
    103                return;
    104
    105            // 将命令添加到队列
    106            this.m_cmdQueueA.Enqueue(delCmd);
    107            this.m_cmdQueueA.Enqueue(insCmd);
    108
    109            // 处理命令队列
    110            this.BeginProcessCmdQueue();
    111        }

    112
    113        /// <summary>
    114        /// 删除在线用户信息
    115        /// </summary>
    116        /// <param name="onlineUser"></param>

    117        public void Delete(OnlineUser onlineUser)
    118        {
    119            // 创建删除命令
    120            OnlineUserDeleteCmd delCmd = new OnlineUserDeleteCmd(this.m_db, onlineUser);
    121
    122            // 将命令添加到队列
    123            this.m_cmdQueueA.Enqueue(delCmd);
    124
    125            // 处理命令队列
    126            this.BeginProcessCmdQueue();
    127        }

    128
    129        /// <summary>
    130        /// 获取在线用户列表
    131        /// </summary>
    132        /// <returns></returns>

    133        public IList<OnlineUser> GetUserList()
    134        {
    135            return this.m_db.Select();
    136        }

    137
    138        /// <summary>
    139        /// 获取在线用户数量
    140        /// </summary>
    141        /// <returns></returns>

    142        public int GetUserCount()
    143        {
    144            return this.m_db.Count();
    145        }

    146
    147        /// <summary>
    148        /// 异步方式处理命令队列
    149        /// </summary>

    150        private void BeginProcessCmdQueue()
    151        {
    152            if (this.m_isBusy)
    153                return;
    154
    155            // 未到可以统计的时间
    156            if (DateTime.Now - this.m_lastStatisticTime < TimeSpan.FromSeconds(this.StatisticEventInterval))
    157                return;
    158
    159            Thread t = null;
    160
    161            t = new Thread(new ThreadStart(this.ProcessCmdQueue));
    162            t.Start();
    163        }

    164
    165        /// <summary>
    166        /// 处理命令队列
    167        /// </summary>

    168        private void ProcessCmdQueue()
    169        {
    170            lock (this)
    171            {
    172                if (this.m_isBusy)
    173                    return;
    174
    175                // 未到可以统计的时间
    176                if (DateTime.Now - this.m_lastStatisticTime < TimeSpan.FromSeconds(this.StatisticEventInterval))
    177                    return;
    178
    179                this.m_isBusy = true;
    180
    181                // 声明临时队列, 用于交换
    182                Queue<OnlineUserCmdBase> tempQ = null;
    183
    184                // 交换两个命令队列
    185                tempQ = this.m_cmdQueueA;
    186                this.m_cmdQueueA = this.m_cmdQueueX;
    187                this.m_cmdQueueX = tempQ;
    188                tempQ = null;
    189
    190                while (this.m_cmdQueueX.Count > 0)
    191                {
    192                    // 获取命令
    193                    OnlineUserCmdBase cmd = this.m_cmdQueueX.Peek();
    194
    195                    if (cmd == null)
    196                        break;
    197
    198                    // 执行命令
    199                    cmd.Execute();
    200
    201                    // 从队列中移除命令
    202                    this.m_cmdQueueX.Dequeue();
    203                }

    204
    205    // 清除超时用户
    206    this.m_db.ClearTimeOut(this.UserTimeOutMinute);
    207    // 排序
    208    this.m_db.Sort();
    209
    210                this.m_lastStatisticTime = DateTime.Now;
    211                this.m_isBusy = false;
    212            }

    213        }

    214    }

    215}

      

    命令对象,包括OnlineUserCmdBase命令基础类、OnlineUserInsertCmd插入命令、OnlineUserDeleteCmd删除命令。

     1using System;
     2
     3namespace Net.AfritXia.Web.OnlineStat
     4{
     5    /// <summary>
     6    /// 在线用户命令基础类
     7    /// </summary>

     8    internal abstract class OnlineUserCmdBase
     9    {
    10        // 当前用户对象
    11        private OnlineUser m_currUser = null;
    12        // 在线用户数据库
    13        private OnlineUserDB m_db = null;
    14
    15        类构造器
    34
    35        /// <summary>
    36        /// 设置或获取当前用户
    37        /// </summary>

    38        public OnlineUser CurrentUser
    39        {
    40            set
    41            {
    42                this.m_currUser = value;
    43            }

    44
    45            get
    46            {
    47                return this.m_currUser;
    48            }

    49        }

    50
    51        /// <summary>
    52        /// 设置或获取在线用户数据库
    53        /// </summary>

    54        public OnlineUserDB OnlineUserDB
    55        {
    56            set
    57            {
    58                this.m_db = value;
    59            }

    60
    61            get
    62            {
    63                return this.m_db;
    64            }

    65        }

    66
    67        /// <summary>
    68        /// 执行命令
    69        /// </summary>

    70        public abstract void Execute();
    71    }

    72}

      

     1using System;
     2
     3namespace Net.AfritXia.Web.OnlineStat
     4{
     5    /// <summary>
     6    /// 插入命令
     7    /// </summary>

     8    internal class OnlineUserInsertCmd : OnlineUserCmdBase
     9    {
    10        类构造器
    29
    30        /// <summary>
    31        /// 执行命令
    32        /// </summary>

    33        public override void Execute()
    34        {
    35            this.OnlineUserDB.Insert(this.CurrentUser);
    36        }

    37    }

    38}


     1using System;
     2
     3namespace Net.AfritXia.Web.OnlineStat
     4{
     5    /// <summary>
     6    /// 删除命令
     7    /// </summary>

     8    internal class OnlineUserDeleteCmd : OnlineUserCmdBase
     9    {
    10        类构造器
    29
    30        /// <summary>
    31        /// 执行命令
    32        /// </summary>

    33        public override void Execute()
    34        {
    35            this.OnlineUserDB.Delete(this.CurrentUser);
    36        }

    37    }

    38}


    最后,OnlineUserList 被封装到OnlineUserDB类中,OnlineUserDB也属于“控制器”代码如下:

     

      1using System;
      2using System.Collections.Generic;
      3
      4namespace Net.AfritXia.Web.OnlineStat
      5{
      6    /// <summary>
      7    /// 在线用户数据库
      8    /// </summary>

      9    internal class OnlineUserDB
     10    {
     11        // 在线用户集合
     12        private List<OnlineUser> m_onlineUserList = null;
     13
     14        类构造器
     23
     24        /// <summary>
     25        /// 插入新用户
     26        /// </summary>
     27        /// <param name="newUser"></param>

     28        public void Insert(OnlineUser newUser)
     29        {
     30            lock (this)
     31            {
     32                this.m_onlineUserList.Add(newUser);
     33            }

     34        }

     35
     36        /// <summary>
     37        /// 删除用户
     38        /// </summary>
     39        /// <param name="delUser"></param>

     40        public void Delete(OnlineUser delUser)
     41        {
     42            lock (this)
     43            {
     44                this.m_onlineUserList.RemoveAll((new PredicateDelete(delUser)).Predicate);
     45            }

     46        }

     47
     48        /// <summary>
     49        /// 清除超时用户
     50        /// </summary>
     51        /// <param name="timeOutMinute">超时分钟数</param>

     52        public void ClearTimeOut(int timeOutMinute)
     53        {
     54            lock (this)
     55            {
     56                this.m_onlineUserList.RemoveAll((new PredicateTimeOut(timeOutMinute)).Predicate);
     57            }

     58        }

     59
     60  /// <summary>
     61  /// 排序在线用户列表
     62  /// </summary>

     63  public void Sort()
     64  {
     65   // 按活动时间进行排序
     66   this.m_onlineUserList.Sort(CompareByActiveTime);
     67  }

     68
     69        /// <summary>
     70        /// 获取所有用户
     71        /// </summary>
     72        /// <returns></returns>

     73        public IList<OnlineUser> Select()
     74        {
     75            return this.m_onlineUserList.ToArray();
     76        }

     77
     78        /// <summary>
     79        /// 获取在线用户数量
     80        /// </summary>
     81        /// <returns></returns>

     82        public int Count()
     83        {
     84            return this.m_onlineUserList.Count;
     85        }

     86
     87        用户删除条件断言
    145
    146        用户超时条件断言
    177
    178        /// <summary>
    179        /// 比较两个用户的活动时间
    180        /// </summary>
    181        /// <param name="x"></param>
    182        /// <param name="y"></param>
    183        /// <returns></returns>

    184        private static int CompareByActiveTime(OnlineUser x, OnlineUser y)
    185        {
    186            if (x == null)
    187                throw new NullReferenceException("X 值为空 ( X Is Null )");
    188
    189            if (y == null)
    190                throw new NullReferenceException("Y 值为空 ( Y Is Null )");
    191
    192            if (x.LastActiveTime > y.LastActiveTime)
    193                return -1;
    194
    195            if (x.LastActiveTime < y.LastActiveTime)
    196                return +1;
    197
    198            return 0;
    199        }

    200    }

    201}

     关于本文更详细的代码,请参见代码包WebUI项目中的DefaultLayout.master.cs文件。

     

  • 相关阅读:
    ASP.NET伪静态 UrlRewrite(Url重写) 实现和配置
    Using BitBucket for Source Control
    我去年写的数据访问层(EF篇)
    asp.net 导出Excel 设置格式
    各方面小知识点总结
    JavaWeb知识
    运行命令对电脑的操作(Windows + R)
    Android开发中用到的命令 —— 整理贴(转自别人,继续完善中...)
    解析XML文件(包含DOM4J,Xpath,SAX)第一部分
    SharePoint 2010 C# 获取People Or Group
  • 原文地址:https://www.cnblogs.com/afritxia2008/p/1231070.html
Copyright © 2020-2023  润新知