在线用户统计与命令模式

>>获取该文章的源码

  我阅读过几个论坛的在线用户统计代码,发现其中有两个问题,一个是需要借助数据库,另外一个是“锁”的粒度比较强!在线用户统计并不要求十分的精确(在这篇文章里,我不会讨论如何侦测到浏览器的关闭动作,而是讨论如何提高代码性能),那么借助数据库来完成这样的功能就显得很夸张!更重要的是对数据库进行读写操作(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文件。

 

原文地址:https://www.cnblogs.com/afritxia2008/p/1231070.html