在asp.net中实现观察者模式,或有更好的方法(续)

 写完这个Post后本来想把完整代码实现传上来,后来看到不少园友提出异议,看了大家的留言后我也一直在思索:我为什么这样做?当初我是怎样想到这个解决方案的?我在几个解决方案之间做了取舍了么?我这样做是不是矫枉过正了?经过这些思考有了现在的这个Post

首先我进一步谈一下需求:

这是一个Web Application,有很多客户端向服务器端提交数据(客户端是C++的,以http-post方式向服务器端提交二进制数据,服务器端解析这个二进制包,数据提交很频繁),管理员可以进入监视页面浏览这些数据,数据要即时的,客户端发来一条,管理员屏幕上要马上可以看到,允许多个管理员同时监视即时数据,所有管理员看到的数据都是一样的(目前是这样的,也许以后对管理员要分角色,各角色管理员看到的信息将不同)

由于数据提交非常频繁,客户要求不允许频繁的数据库操作,所以我将数据保存在一个IList的缓存里面,当这个IList的大小超过了我在配置文件里定义的大小的时候就将数据批量插入到数据库。

下面我将以我当初思考的思路为主线描述:

第一个版本:

//在程序里我写了一个静态类,这个静态类保存整个程序中共享的一些数据,相当于原来的//Application对象,但是静态成员是编译期类型检查的
public static ApplicationData
{
    
//这个队列用来保存客户端传递过来的数据,当队列达到一定长度的时候同步到数据库
    public static Queue<DataHead> OperateDataList = new Queue<DataHead>();
//这个List也是保存客户端传递过来的数据的,但它是为监视准备数据的,
//当一个监视页面的请求到来的时候将这个List的数据Response过去,然后Clear这个//List
    public static IList<DataHead> MonitorDataList = new List<DataHead>();
}
public class ReciveDataHandler : IHttpHandler
{
    
//……
    Public void ProcessRequest(HttpContext context)
    {
        
//解析从客户端传递过来的数据
        DataHead data = GetData(context);
        OperateDataList.Add(data);
        If(OperateDataList.Count 
> BufferSize)
        {
            
//将数据写入到数据库
            AddToBase();
}
        MonitorDataList.Add(data);
}
}
//监视页面从这里获取数据
public class MonitorHandler : IHttpHandler
{
    
//……
    Public void ProcessRequest(HttpContext context)
    {
        If(MonitorDataList.Count 
> 0)
        {
            
//将MonitorDataList里的数据Response出去
            OutPut();
            MonitorDataList.Clear();
}
}
}

 

说实话,我当初做出这个的时候觉得一点问题都没有,开始的时候客户测试也没有发现任何问题,终于有一天客户和我同时测试部署在同一IIS的时候,问题出现了:只有一个监视页面有数据。看到这个后我还百思不得其解,顺着程序的执行流程一步一步走下去,没有找出任何错误。后来做了下日志,原来MonitorDataList是一个全局共享的,一个在监视把数据Clear了后别人就无法获取数据了。不知道有没有人这样做过:有时候忘记了自己正在做一个web程序,而web程序是一个并发的,对一些共享资源的访问有着微妙的问题,如果没有记住这点,按照程序流程的执行步骤是找不出任何问题的。

怎么办?再一看这不是事件订阅所描述的场景么?所以就有了上一篇PostSolution。不过那个方案受到不少人质疑,其中金色海洋提出这样的方法:

Public class ReciveData : IHttpHandler
{
    
//……….
    
//将客户端传递过来的数据存入数据库
}

Public 
class MonitorHandler : IHttpHandler
{
    
//………
    
//为null的时候说明该管理员第一次监视
    If(Session[“id”] == null)
    {
        
//根据时间从服务器取出数据
        
//并将取出数据的最后一个id保存在session中
        Session[“id”] = id;
}
//不为null则说明该管理员已经开始监视了
Else
{
    
//根据session里保存的最后一个id,取出大于那个id的数据
    Session[“id”] = currentId;
}
}

 

看似这个方案不错,我尝试着将我的程序修改为这样,但是我将上面的代码编写完,我发现我不可以再进行下去了:上面的方案满足不了我的需求,客户明确要求了客户端提交的数据要先缓存然后缓存超过配置大小(这个大小还需要可以在配置文件里面配置,以便可以经过测试找出一个最合理的值),而这种Session记录的方案是依靠数据库来保存数据,这个Session[“id”]就相当于一个游标,这个游标指向的是数据库,那好,我们将Session[“id”]指向缓存数据,但是请注意缓存随时可能超过设置大小而被同步到数据库并被清空。

经过一番思考后我还是回到我自己的Solution上,不过我又有了新的看法了。不是要将数据先缓存么?看看这个缓存,实际上她也是个观察者,至于她执行怎样的缓存策略是她的事情,如是我又有了一个新类:

//这里的代码接上篇Post
using System;
using System.Collections.Generic;
using System.Text;
namespace ForyourSoft.NetTraffic.Framework
{
    
public sealed class DataBase
    {
        
private IList<string> _buffer = new List<string>();
        
private static DataBase _instance = null;
        
public static void Subscribe()
        {
            
if (_instance == null)
                _instance 
= new DataBase(Monitor.Current);
        }
        
public DataBase(Monitor monitor)
        {
     monitor.OnMessage 
+= new EventHandler<Monitor.MessageEventArgs>(monitor_OnMessage);
        }
        
void monitor_OnMessage(object sender, Monitor.MessageEventArgs e)
        {
            _buffer.Add(e.Message);
            
if (_buffer.Count >= Config.BufferSize)
            {
                
//将数据添加到数据库
            }
        }
    }
}

 

PS:由于系统中我们只需要这样唯一一个订阅者,所以我将其实现为一个单件,在Application_Start的时候调用DataBase.Subscribe()

现在系统是这样的结构:


 

可以设想以后还会有更多的订阅者。果然,昨天客户要求在下一个版本中管理员分角色,各个角色看到的数据不同的,只有超级管理员才可以监视所有数据,OMG,呵呵,不过还好,我只需要添加几个订阅者就可以轻松搞定。

后记:也许是我的文章标题没有起好,也许很多人得到模式恐惧症,提到模式总是要来考察一下你的case,不是那种Enterprise级别的用了pattern就是过火了。其实这篇文章的内容里没有一点模式的气息,只是用.netEvent实现观察者模式的思想,我想如果合适,今天模式的投资,明天你会有收获的。


 

posted @ 2008-05-06 02:00 横刀天笑 阅读(1565) 评论(13)  编辑 收藏 所属分类: 技术点滴设计模式初学者系列

  回复  引用  查看    
#1楼 2008-05-06 06:29 | 金色海洋(jyk)      
我记得我是这么写的

Session["aa"] = 当前记录的序号。

“当前记录的序号”,可以是数据库里的序号,也可以是你的那个缓存里的序号,也可以是得到数据的时间。

当然了我还不知道你的缓存里有没有序号?

不过这个也是有一个 Clear 的问题,在QQ里面已经说明了。


不好意思,又是只看前半句,没看后半句。
  回复  引用  查看    
#2楼 2008-05-06 06:39 | 金色海洋(jyk)      
你现在的clear是怎么处理的呀?
  回复  引用  查看    
#3楼 [楼主]2008-05-06 08:35 | 横刀天笑      
现在就不存在Clear的问题了,每个订阅者都有自己的消息队列
  回复  引用  查看    
#4楼 [楼主]2008-05-06 08:48 | 横刀天笑      
第一个:缓存是经常要同步到数据库的,同步后会被清空,所以这里不确定
第二个:你这个方法扩展性不强,扩展的时候要修改大量代码,其实这个Post里的DataBase类,是我那天和你聊天的时候加的,原来的代码一点都没有修改,我想如果以后有更多类型的订阅者我还是无须修改代码。
  回复  引用  查看    
#5楼 2008-05-06 09:05 | 玉开      
mark
  回复  引用  查看    
#6楼 2008-05-06 12:29 | 金色海洋(jyk)      
其实每个人都有自己的思维习惯和方式,

我习惯这么想,你习惯那么想,也都没什么的。

只是觉得对于你的这个项目,只有两个订阅者,

一个负责向数据库里添加数据;
一个负责像浏览器输出数据。

至于多用户,每个用户都会有不同的角色(权限),那么我想应该用 策略 的方式来应对,而不应该看成是多个订阅者。


>>每个订阅者都有自己的消息队列

同一条记录会复制好几份吗?

消息队列例记录的是数据本身还是数据的ID?


置于代码的修改量嘛,我想不管用哪种方法,都是差不多的。

  回复  引用  查看    
#7楼 [楼主]2008-05-06 13:08 | 横刀天笑      
@金色海洋(jyk)

用不用策略模式我没有深入的去考虑
消息本来也没有Id,因为多个客户端同时发,我无法给他一个递增的Id,有Id可能也是Guid的,无法比较大小。
代码修改量:请注意是代码修改量,不是代码量,我的solution在扩展的时候只需要添加而不需要修改原来的代码。

  回复  引用  查看    
#8楼 2008-05-06 18:00 | 镜涛      
Learn
  回复  引用  查看    
#9楼 2008-05-07 00:02 | 静静的黎明      
其实如果是我的话,我更喜欢自己实现一个session机制来保存Admin实例.

否则的话Session的失效,从Monitor的委托链表中删除失效的Admin是个问题。

如果Session失效了, Session到Admin的引用没有了,但Monitor的委托链还引用着Admin实例的方法,那个Admin实例应该还在的, 这样会导致Admin实例越来越多.
============

>>每个订阅者都有自己的消息队列

同一条记录会复制好几份吗?

消息队列例记录的是数据本身还是数据的ID?
===================
其实这个问题好回答, 其实不是一个记录复制几份,而是有几个Admin的消息队列会引用这个记录.我觉得这里又回到上一个问题,如果失效的Admin实例的消息队列还引用着这些记录, 这些消息记录就不会被回收

  回复  引用  查看    
#10楼 [楼主]2008-05-07 00:08 | 横刀天笑      
@静静的黎明

对,呵呵,这才是这个方案最大的毛病,也是我觉得很不完美的地方,所以我写出来想得到一些建议,如果抛弃Session失效的话,我觉得这个方案的扩展性真的不错。

不过如果Session失效了,即使不用该方案也很难找出更加稳定的方案来呀

  回复  引用  查看    
#11楼 2008-05-07 07:00 | 金色海洋(jyk)      
1、关于序号。
没有ID,我们可以用时间来代替,就是记录服务器收到消息的时间,用这个来表示数据,也可以来排序。

2、关于clear 。
好像lz说用我的方法,最大的问题就是clear数据。
我相出现问题的原因就是,当数据达到一定数量的时候会一次性的清除所有缓存的数据。

那么是不是可以改一下呢?clear的时候,只清除admin不再需要的数据,那么如何来区分呢?

我记得lz是在 浏览器里面使用ajax定时刷新的方式来获取数据,假设这个是就按间隔是 10s。
也就是说 15s之前的数据,一定是不再需要的。

那么在clear的时候我们就可以只把从当前时间算起 15s 之前的数据保存到数据库,然后清除这些数据。15s 以内的数据先保留。


3、角色

不同的人员可以看到不同类型的数据,那么这个可以用策略或者工厂模式,或者其他的方式来解决。



if (Session["aa"] == null)
{
//第一次访问页面,应该没有数据返回
Session["aa"] = 当前的时间。
}
else
{
//不是第一次访问,返回 从 Session["aa"] 开始 到 当前时间 之间的数据。
//具体提取数据的方法略

//一种实现方式
//第一个判断条件
//Session["aa"] <= n < 当前时间

//第二个判断条件,根据角色提取符合条件的数据

Session["aa"] = 当前时间。
}



//具体提取数据的方法略

这个 “方法略” ,就是说可以用很多种方法来实现。


不知道这个行不行。


ps:两种思维方式,两种不同的解决方案,每个人都熟悉自己的方法,用别人的方式来思维就会觉得很别扭。我也是一样的。

按照你的想法继续向我觉得挺繁琐,估计你也是这样的。

但是都会有巧妙的方法来解决问题。问题是在自己的思维方向里能不能想得到。


  回复  引用  查看    
#12楼 [楼主]2008-05-07 08:44 | 横刀天笑      
@金色海洋(jyk)

呵呵,我一看你回复的时间怎么都这么早啊,6:30就在办公室了?

  回复  引用  查看    
#13楼 2008-05-07 12:21 | 金色海洋(jyk)      
在家。
昨天晚上头痛,没有上网。

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      


相关链接: