蛙蛙推荐:设计一个高效的网络服务器用户管理类

蛙蛙推荐:设计一个高效的网络服务器用户管理类

摘要:

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

分析:


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

代码实现: 

public   interface  IWUser < TContext >  {
    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 ; }
    TContext Context { 
get set ; }
}
public   class  WUserBase < TContext >  : IWUser < TContext >
{

    [NonSerialized]
    ReaderWriterLockSlim _objLock 
=  Locks.GetLockInstance(LockRecursionPolicy.NoRecursion);

    
public  DateTime Timestamp {  get set ; }
    
public   int  Index {  get set ; }
    
public   bool  Invalid {  get set ; }
    
public  TContext Context {  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();
        Context 
=   default (TContext);
    }

    
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
< string > 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);  // 已达到服务在线人数上限
    }

    
public   bool  RemoveUser( int  index,  string  credentials) {
        
if  (index  <   0   ||  index  >=  _maxUsers)  return   false ;
        
if  ( string .IsNullOrEmpty(credentials))  return   false ;
        TUser user 
=  _users[index];
        
if (user  ==   null return   false ;
        
using  (user.WriteLock)
        {
            user.Invalid 
=   true ;
        }
        
return   true ;
    }
}
单元测试:

 

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

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

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

if (userManager.RemoveUser(user.Index, user.Credentials))
{
    OutPut(
" I am Logout! " );
}
else
{
    OutPut(
" I am Logout faild! " );
}
参考链接:


并发数据结构 : .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是为了让偷懒的人有一个默认的实现能用,而且还能继承它做扩展。我觉得一个框架应该尽量本着这个原则去设计。

--------2009-04-11 22:16

1、加了注销的方法RemoveUser

2、接口改成泛型接口,基类改成泛型基类以支持保存用户状态

3、修改了部分单元测试代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值