在线用户统计与命令模式
>>获取该文章的源码
我阅读过几个论坛的在线用户统计代码,发现其中有两个问题,一个是需要借助数据库,另外一个是“锁”的粒度比较强!在线用户统计并不要求十分的精确(在这篇文章里,我不会讨论如何侦测到浏览器的关闭动作,而是讨论如何提高代码性能),那么借助数据库来完成这样的功能就显得很夸张!更重要的是对数据库进行读写操作(I/O操作),是要消耗性能的,而且还要在数据表里产生一条记录。为了一个不精确的功能需求消耗如此多的资源,的确不划算!另外一个办法是直接使用DataSet和ASP.NET缓存的方式来做统计,类似这样的代码我看过几个,自己也写过一个。但这样做也存在很大问题,最严重的地方还是“锁”的问题。在更新DataSet时需要通过lock关键字来将其锁定,但如果用户数量很大时,DataSet被加锁的次数过于频繁,所造成的坏结果千奇百怪。所以我不得不寻找一种更为有效的方法。
在进行讨论之前,有必要先做一个介绍。在线用户信息应该包括:
- SessionID
- 用户名称
- 最后活动时间
- 最后请求地址(Url地址)
还可以包括IP地址或其他更详细的信息。这是在线用户信息的数据结构,在线用户统计的算法是:
1. 将在线用户信息插入到集合,如果集合中已经存在相同用户名称的数据项,则更新该数据项;
2. 根据最后活动时间倒排序;
排序步骤虽然可以在列表显示在线用户信息的时候再做,但是那样会花费一点时间,不如在插入数据项以后马上排序,需要显示的时候直接显示。OnlineUser数据结构代码如下:
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类里。代码如下:
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类,该类属于“控制器”:
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删除命令。
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}
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}
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也属于“控制器”代码如下:
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文件。