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

4
namespace Net.AfritXia.Web.OnlineStat5
{6
/// <summary>7
/// 在线用户类8
/// </summary>9
public class OnlineUser10
{11
// 用户 ID12
private int m_uniqueID;13
// 名称14
private string m_userName;15
// 最后活动时间16
private DateTime m_lastActiveTime;17
// 最后请求地址18
private string m_lastRequestURL;19
// SessionID20
private string m_sessionID;21
// IP 地址22
private string m_clientIP;23

24
类构造器43

44
/// <summary>45
/// 设置或获取用户 ID46
/// </summary>47
public int UniqueID48
{49
set50
{51
this.m_uniqueID = value;52
}53

54
get55
{56
return this.m_uniqueID;57
}58
}59

60
/// <summary>61
/// 设置或获取用户昵称62
/// </summary>63
public string UserName64
{65
set66
{67
this.m_userName = value;68
}69

70
get71
{72
return this.m_userName;73
}74
}75

76
/// <summary>77
/// 最后活动时间78
/// </summary>79
public DateTime ActiveTime80
{81
set82
{83
this.m_lastActiveTime = value;84
}85

86
get87
{88
return this.m_lastActiveTime;89
}90
}91

92
/// <summary>93
/// 最后请求地址94
/// </summary>95
public string RequestURL96
{97
set98
{99
this.m_lastRequestURL = value;100
}101

102
get103
{104
return this.m_lastRequestURL;105
}106
}107

108
/// <summary>109
/// 设置或获取 SessionID110
/// </summary>111
public string SessionID112
{113
set114
{115
this.m_sessionID = value;116
}117

118
get119
{120
return this.m_sessionID;121
}122
}123

124
/// <summary>125
/// 设置或获取 IP 地址126
/// </summary>127
public string ClientIP128
{129
set130
{131
this.m_clientIP = value;132
}133

134
get135
{136
return this.m_clientIP;137
}138
}139
}140
} 对于在线用户列表数据集,我们只用一个List<OnlineUser>对象来表示就可以了,不过我现在是把它封装在OnlineUserRecorder类里。代码如下:
using System;2
using System.Collections.Generic;3
using System.Threading;4

5
namespace Net.AfritXia.Web.OnlineStat6
{7
/// <summary>8
/// 在线用户记录器9
/// </summary>10
public class OnlineUserRecorder11
{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类,该类属于“控制器”:
using System;2
using System.Collections.Generic;3
using System.Threading;4

5
namespace Net.AfritXia.Web.OnlineStat6
{7
/// <summary>8
/// 在线用户记录器9
/// </summary>10
public class OnlineUserRecorder11
{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 UserTimeOutMinute47
{48
set49
{50
this.m_userTimeOutMinute = value;51
}52

53
get54
{55
return this.m_userTimeOutMinute;56
}57
}58

59
/// <summary>60
/// 设置或获取统计时间间隔(单位秒)61
/// </summary>62
internal int StatisticEventInterval63
{64
set65
{66
this.m_statisticEventInterval = value;67
}68

69
get70
{71
return this.m_statisticEventInterval;72
}73
}74

75
/// <summary>76
/// 设置或获取命令队列长度77
/// </summary>78
public int CmdQueueLength79
{80
set81
{82
this.m_cmdQueueLength = value;83
}84

85
get86
{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删除命令。
using System;2

3
namespace Net.AfritXia.Web.OnlineStat4
{5
/// <summary>6
/// 在线用户命令基础类7
/// </summary>8
internal abstract class OnlineUserCmdBase9
{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 CurrentUser39
{40
set41
{42
this.m_currUser = value;43
}44

45
get46
{47
return this.m_currUser;48
}49
}50

51
/// <summary>52
/// 设置或获取在线用户数据库53
/// </summary>54
public OnlineUserDB OnlineUserDB55
{56
set57
{58
this.m_db = value;59
}60

61
get62
{63
return this.m_db;64
}65
}66

67
/// <summary>68
/// 执行命令69
/// </summary>70
public abstract void Execute();71
}72
}
using System;2

3
namespace Net.AfritXia.Web.OnlineStat4
{5
/// <summary>6
/// 插入命令7
/// </summary>8
internal class OnlineUserInsertCmd : OnlineUserCmdBase9
{10
类构造器29

30
/// <summary>31
/// 执行命令32
/// </summary>33
public override void Execute()34
{35
this.OnlineUserDB.Insert(this.CurrentUser);36
}37
}38
}
using System;2

3
namespace Net.AfritXia.Web.OnlineStat4
{5
/// <summary>6
/// 删除命令7
/// </summary>8
internal class OnlineUserDeleteCmd : OnlineUserCmdBase9
{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也属于“控制器”代码如下:
using System;2
using System.Collections.Generic;3

4
namespace Net.AfritXia.Web.OnlineStat5
{6
/// <summary>7
/// 在线用户数据库8
/// </summary>9
internal class OnlineUserDB10
{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文件。


浙公网安备 33010602011771号