现在的位置: 首页 > 综合 > 正文

http://www.cnblogs.com/onlytiancai/archive/2009/04/11/1433456.html

2012年10月08日 ⁄ 综合 ⁄ 共 5761字 ⁄ 字号 评论关闭
蛙蛙推荐:设计一个高效的网络服务器用户管理类 
蛙蛙推荐:设计一个高效的网络服务器用户管理类
摘要:

做一个有状态的网络服务端应用,一般需要维护一个在线用户列表,每次用户登录、注销都要修改这个列表,还得考虑超时清理的逻辑,对这个列表的操作大多时候需要用锁来进行线程同步,我们试图来用一种不需要线程同步的方法来做到这些事情。
分析:

1、我们可以预算好一个系统承受在线用户的上限,比如1w人,或者2w人,这样我们就可以初始化一个固定长度的集合,省得动态分配内存,增加GC压力。
2、一个固定长度的集合可以设置为只读的,一个只读的集合是不需要线程同步的。
3、集合里的每一个元素代表一个用户,因为应用初始化的时候已经为每个元素分配了内存,但这时候并没有人登录,所以这时候集合里的所有用户都是无效状态,当有人登录上来的时候随机找一个无效状态的用户,把他设置为有效状态,这表明有了一个在线用户,如果再有新的用户登录,也是这个逻辑,知道找不到任何一个无效状态的用户,说明系统已经达到最高在线用户,返回503应答。
4、集合是不用线程同步,但每个集合里的元素维持着用户的状态,用户的状态肯定会改变,我们也会用户状态进行读写操作,所以我们要在用户对象上暴露一个只读锁,更新锁和写锁。如果单纯的查看用户的一个状态值,加只读锁,如果单纯的改变一个用户的状态,我们用写锁,如果根据一定的条件更改用户的状态,我们就先用更新锁获取条件,然后再用写锁更改状态。
5、根据条件修改用户状态,为什么不直接从只读锁升级成写锁呢?我觉得应该是只读锁到写锁不是一个原子操作,而更新锁到写锁是个原子操作。为什么不直接用更新锁,不用写锁就更新用户状态呢?我觉得是因为进入更新锁的时候已经有些线程读取了用户状态,更新锁只是防止其它线程再来读取用户状态,如果没有等到已经读取了用户状态的线程释放读锁就改变用户状态的话,之前的读线程会引起脏读(幻影读?啥名词来着?),而更新锁后面加个写锁,就会等到其它线程都释放读线程才会改变用户状态,这就安全了。(这条是我自己总结的,未经考证,欢迎指教)
6、泛型类可以为每个类型生成不同的版本,所以静态成员也是不同的,这样我们就可以写一个泛型的单件模式了。
代码实现: 
public interface IWUser {
  DateTime Timestamp { get; set; }
  IDisposable ReadLock { get; }
  IDisposable UpdateLock { get; }
  IDisposable WriteLock { get; }
  int Index { get; set; }
  bool Invalid { get; set; }
  void Reset();
  bool Authentication(string credentials);
  string Credentials { get; }
}
public class WUserBase : IWUser
{
   
  [NonSerialized]
  ReaderWriterLockSlim _objLock = Locks.GetLockInstance(LockRecursionPolicy.NoRecursion); 

  public DateTime Timestamp { get; set; }
  public int Index { get; set; }
  public bool Invalid { get; set; }
  private string _credentials = string.Empty;

  public string Credentials {
  get { return _credentials; }
  }
  public IDisposable ReadLock {
  get { return new ReadOnlyLock(_objLock); }
  }
  public IDisposable UpdateLock {
  get { return new ReadLock(_objLock); }
  }
  public IDisposable WriteLock {
  get { return new WriteLock(_objLock); }
  }
   
  public virtual void Reset() {
  _credentials = Guid.NewGuid().ToString();
  }

  public bool Authentication(string credentials)
  {
  if(string.IsNullOrEmpty(_credentials)) return false;
  if (string.IsNullOrEmpty(credentials)) return false;
  if(_credentials != credentials) return false;
  return true;
  }

  public WUserBase()
  {
  Index = -1;
  Timestamp = DateTime.MinValue;
  Invalid = true;
  }
  public override string ToString() {
  return string.Format("{0}-{1}", Index, Timestamp);
  }
}
public class WUserManager<TUser> where TUser
  : IWUser, new() {
  private volatile static bool _initialized = false;
  private static WUserManager<TUser> _userManager = null;
  private Dictionary<int, TUser> _users = null;
  private int _maxUsers;
  private static ITracer _tracer = TracerFactory.GetTrace(typeof (WUserManager<TUser>));

  public static void Init(int maxUsers) {
  if (_initialized) throw new ApplicationException("alread inited");
  _userManager = new WUserManager<TUser>();
  _userManager._maxUsers = maxUsers;
  _userManager._users = new Dictionary<int, TUser>(maxUsers);
  for (int i = 0; i < maxUsers; i++) {
  TUser user = new TUser();
  user.Index = i;
  _userManager._users[i] = user;
  }
  Thread thread = new Thread(threadProce);
  thread.Name = "user manager clearner";
  thread.IsBackground = true;
  thread.Priority = ThreadPriority.Highest;
  thread.Start();
  _initialized = true;
  }

  static void threadProce(object State) {
  try {
  while (true)
  {
  //这里扫描整个用户列表,而不是当前登录用户,因为1分钟遍历一次
  //一般1分钟内应该能扫描完,如果只遍历当前登录用户,还得维护一个
  //当前登录用户的列表,要维护这个列表,肯定会使用锁,而降低性能。
  Dictionary<int,TUser>.ValueCollection users = _userManager._users.Values;
  foreach (TUser user in users) {
  using (user.ReadLock) {
  if (user.Invalid) continue;
  using (user.UpdateLock) {
  bool timeout = DateTime.Now - user.Timestamp > TimeSpan.FromMinutes(3);
  if (timeout) {
  using (user.WriteLock) {
  user.Invalid = true;
  }
  _tracer.Warn("one user timeout:{0}",user);
  }
  }
  }
  }
  Thread.Sleep(TimeSpan.FromMinutes(1));
  }
  }
  catch (Exception ex) {
  System.Diagnostics.Trace.TraceError(string.Format("user manager clear error:",ex));
  }
  }

  public static WUserManager<TUser> GetInstance() {
  if (!_initialized) throw new ApplicationException("no init");
  return _userManager;
  }

  public TUser GetUser(int index, string credentials) {
  if (index < 0 || index >= _maxUsers) return default(TUser);
  if(string.IsNullOrEmpty(credentials)) return default(TUser);
  TUser user = _users[index];
  using (user.UpdateLock) {
  if(!user.Authentication(credentials))
  throw new ApplicationException("Authentication failed");
  if (!user.Invalid) {
  using (user.WriteLock) {
  user.Timestamp = DateTime.Now; //刷新时间戳
  return user;
  }
  }
  }
  return default(TUser);
  }
  public TUser AddUser() {
  for (int i = 0; i < _maxUsers; i++) {
  TUser user = _users[i];
  using (user.UpdateLock) {
  if (!user.Invalid) continue;
  using (user.WriteLock) {
  user.Reset();
  user.Invalid = false;
  user.Timestamp = DateTime.Now;
  return user;
  }
  }
  }
  return default(TUser); //已达到服务在线人数上限
  }
}

单元测试:

 
WUserManager<WUserBase>.Init(10); //假设本系统的设计容量是10个用户同时在线
WUserManager<WUserBase> userManager = WUserManager<WUserBase>.GetInstance();

WUserBase user = userManager.AddUser(); // 模拟一个用户成功登录并为其维护状态
if(user == null) //如果返回null,表示系统已经达到同时在线人数上限
{
  OutPut("503 server busy");
  return;
}
OutPut(user.Timestamp.ToString());

//用户发送登录后的请求,从用户的请求信息里取出Index值和凭证信息
//然后根据这些信息从UserManager里取出用户状态,再进行操作
//加入凭证是为了防止恶意用户直接冒用Index值来模拟其它用户操作
//而凭证默认是一个guid,被猜测出来的机会很少,一次登录会话使用一个凭证且不重复
user = userManager.GetUser(user.Index,user.Credentials);
if (user == null) {
  OutPut("user timeout or invalid request");
  return;
}
OutPut(user.Timestamp.ToString());
参考链接:

并发数据结构 : .NET Framework 中提供的读写锁

http://www.cnblogs.com/lucifer1982/archive/2008/12/07/1349437.html

Thread Safe Dictionary in .NET with ReaderWriterLockSlim

http://devplanet.com/blogs/brianr/archive/2008/09/26/thread-safe-dictionary-in-net.aspx

----2009-04-11 8:10

想来想去,AddUser方法的效率不太高,如果突然短时间内上来的人很多,每个用户的登录都要遍历一边集合,如果集合大的话,可能会很慢,能想到的有两个办法
1、随机访问一个集合的元素,如果可用就用,如果不可用,再遍历集合找可用的,就看第一次随机到的命中率咋样了。
2、用一个指针来保存当前最新用户登录的索引,新用户上来了,直接+1计算出该用户的索引,但是这个指针达到maxusers后要返回0重新开始,这时候集合就不是连续的了,再只+1肯定不行了,难道还得设计一套类似GC的回收算法,感觉还是挺复杂的。

3、还有一个方案就是用一个int队列保存可用的索引,用户登录的时候从队列顶端取出一个索引来使用,但是用户登录注销的时候要写这个队列,需要线程同步,当然这个队列可以设计成用读写锁来同步,甚至用lock free算法,都有现成的,不必要用互斥锁。最后这个方案应该得经过压力测试才能看出是否比原始方案好,不能光凭直觉了。最后这个方案还能方便的得出在线用户数,用maxusers-队列长度就是了。

---------2009-04-11 9:30

IWUser写一个接口是为了可以让使用者从头到尾设计一个实现,写一个非封闭类WUserBase是为了让偷懒的人有一个默认的实现能用,而且还能继承它做扩展。我觉得一个框架应该尽量本着这个原则去设计。

抱歉!评论已关闭.