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

类构造器#region 类构造器25

/**//// <summary>26
/// 类默认构造器27
/// </summary>28
public OnlineUser()29

{30
}31

32

/**//// <summary>33
/// 类参数构造器34
/// </summary>35
/// <param name="uniqueID">用户 ID</param>36
/// <param name="userName">用户名称</param>37
public OnlineUser(int uniqueID, string userName)38

{39
this.UniqueID = uniqueID;40
this.UserName = userName;41
}42
#endregion43

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

类构造器#region 类构造器30

/**//// <summary>31
/// 类默认构造器32
/// </summary>33
internal OnlineUserRecorder()34

{35
this.m_db = new OnlineUserDB();36

37
// 初始化命令队列38
this.m_cmdQueueA = new Queue<OnlineUserCmdBase>();39
this.m_cmdQueueX = new Queue<OnlineUserCmdBase>();40
}41
#endregion42

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

类构造器#region 类构造器16

/**//// <summary>17
/// 类默认构造器18
/// </summary>19
public OnlineUserCmdBase()20

{21
}22

23

/**//// <summary>24
/// 类参数构造器25
/// </summary>26
/// <param name="db">在线用户数据库</param>27
/// <param name="currUser">当前用户</param>28
public OnlineUserCmdBase(OnlineUserDB db, OnlineUser currUser)29

{30
this.OnlineUserDB = db;31
this.CurrentUser = currUser;32
}33
#endregion34

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

类构造器#region 类构造器11

/**//// <summary>12
/// 类默认构造器13
/// </summary>14
public OnlineUserInsertCmd()15
: base()16

{17
}18

19

/**//// <summary>20
/// 类参数构造器21
/// </summary>22
/// <param name="db">在线用户数据库</param>23
/// <param name="currUser">当前插入的新用户</param>24
public OnlineUserInsertCmd(OnlineUserDB db, OnlineUser currUser)25
: base(db, currUser)26

{27
}28
#endregion29

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

类构造器#region 类构造器11

/**//// <summary>12
/// 类默认构造器13
/// </summary>14
public OnlineUserDeleteCmd()15
: base()16

{17
}18

19

/**//// <summary>20
/// 类参数构造器21
/// </summary>22
/// <param name="db">在线用户数据库</param>23
/// <param name="currUser">当前被删除的用户</param>24
public OnlineUserDeleteCmd(OnlineUserDB db, OnlineUser currUser)25
: base(db, currUser)26

{27
}28
#endregion29

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

类构造器#region 类构造器15

/**//// <summary>16
/// 类默认构造器17
/// </summary>18
public OnlineUserDB()19

{20
this.m_onlineUserList = new List<OnlineUser>();21
}22
#endregion23

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

用户删除条件断言#region 用户删除条件断言88
private class PredicateDelete89

{90
// 被删除的用户91
private OnlineUser m_delUser = null;92
// 是否为空条件93
private bool m_isNullCondation = true;94

95

类构造器#region 类构造器96

/**//// <summary>97
/// 类参数构造器98
/// </summary>99
/// <param name="delUser"></param>100
public PredicateDelete(OnlineUser delUser)101

{102
this.m_delUser = delUser;103

104
if (this.m_delUser == null)105
return;106

107
// 用户 ID108
this.m_isNullCondation &= this.m_delUser.UniqueID <= 0;109
// 名称110
this.m_isNullCondation &= String.IsNullOrEmpty(this.m_delUser.UserName);111
// SessionID112
this.m_isNullCondation &= String.IsNullOrEmpty(this.m_delUser.SessionID);113
}114
#endregion115

116

/**//// <summary>117
/// 判断用户 ID 是否等于指定值118
/// </summary>119
/// <param name="user"></param>120
/// <returns></returns>121
public bool Predicate(OnlineUser user)122

{123
if (this.m_isNullCondation)124
return false;125

126
if (user == null)127
return false;128

129
// 用户 ID 相同, ID > 0130
if (user.UniqueID > 0 && user.UniqueID == this.m_delUser.UniqueID)131
return true;132

133
// 用户名称相同, 并且不是空字符串134
if (!String.IsNullOrEmpty(user.UserName) && user.UserName == this.m_delUser.UserName)135
return true;136

137
// SessionID 相同, 并且不是空字符串138
if (user.SessionID == this.m_delUser.SessionID)139
return true;140

141
return false;142
}143
}144
#endregion145

146

用户超时条件断言#region 用户超时条件断言147
private class PredicateTimeOut148

{149
// 超时分钟数150
private int m_timeOutMinute;151

152

类构造器#region 类构造器153

/**//// <summary>154
/// 类参数构造器155
/// </summary>156
/// <param name="minute">超时分钟数</param>157
public PredicateTimeOut(int minute)158

{159
this.m_timeOutMinute = minute;160
}161
#endregion162

163

/**//// <summary>164
/// 判断用户活动时间是否小于指定值165
/// </summary>166
/// <param name="user"></param>167
/// <returns></returns>168
public bool Predicate(OnlineUser user)169

{170
if (user == null)171
return false;172

173
return user.LastActiveTime < DateTime.Now.AddMinutes(-this.m_timeOutMinute);174
}175
}176
#endregion177

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号