摘要:
通过使用jdk 1.3中引入的rmi和proxy api,本篇文章讨论了一种允许一台或多台servlet服务器在一台或多台对话服务器上维护对话信息的技术,采用这种技术后,单一点故障就不会再出现了。
如果系统中有一台或多台servlet服务器,对话信息只存在于运行着jvm的一台servlet服务器上,而不会被传输给其他servlet服务器。如果该servlet服务器当机或因为维护而被关机,任何保存在对话中的信息都会丢失。如果一个系统中有多台servlet服务器,一个带有对话的用户需要访问对话中的任何信息,都需要被重新定向到同一台servlet服务器。曾经有专家建议采用关系数据库保存所有的对话信息,但这仍然存在单一点故障的危险,那就是运行关系数据库的服务器。而且如果数据库出了故障,所有的servlet服务器就都不能再访问对话信息了。另外,在数据库中保存可串行化的对象在有些数据库中是比较难以实现的。
多服务器对话管理的另一个可能的途径是利用javaspaces api来维护对话对象中的记录。当然,如果运行javaspaces的服务器由于维护或故障而被关机,也会丢失所有的对话信息,我们再一次遇到了单一点故障的问题。
要实现带有n个节点的分布式对话服务器,我们必须解决如下的三个问题:
━━如何建立一个库来存贮对话信息。
━━如何对分布式对话信息存贮库进行同步。
━━在一个对话信息存贮库脱离网络后,如何使该服务器从下一个对话信息存贮库中访问对话信息。
mnemosyne的简介
我们用来存贮对话信息的库是mnemosyne界面的执行。执行mnemosyne的对象负责管理对话信息存贮库中的所有对象,任何试图写、访问或删除库中对象的对象都必须调用mnemosyne的相应的方法来实现相应的操作。
一个对象要存贮在mnemosyne,就必须执行memory界面,该界面定义了equalsmemory()操作来探测二个内存对象是否相同,这就使 mnemosyne判断出应当把哪个对象返回给read要求或take要求。memory界面也可以进行串行化扩充,以便我们可以用rmi在网络上传输该对象。
mnemosyne使用三种界面表达其状态。
1、commoncontext界面存贮mnemosyne的全部信息。每个mnemosyne都有一个commoncontext对象的实例,以便read、write、take memory对象时在各个方法之间进行同步。在write或者take说memory对象时,commoncontext对象既定义“silent”方法也定义“loud”方法,当在不进行事件通知的情况下添加对象时,就会用到“silent”方法。例如,当mnemosyne对象接收到writeremoteevent(向一个远程mnemosyne对象写对象的告示。)事件后,如果它也希望向commoncontext对象写另一个对象,它就无需通知其他的远程commoncontext对象,最初的mnemosyne已经通知了它们,因此这种写是通过调用commoncontext对象的silentwrite()方法“静悄悄”地完成的。而“loud”方法则在有对象第一次被添加时将这一事件的详细信息通知所有被调用的监听对象。
2、transaction对象用于在read、write、take memory对象时进行分布式事务处理,这意味在mnemosyne对象上可能会有多步骤的操作。
3、transactioncontext界面管理分布式事务,使得系统可以中止或提交一个事务。
保持mnemosynes的同步是通过synchronize()和notify()这二个由 mnemosyne定义的方法完成的。synchronize()可以使一个本地的mnemosyne与其他mnemosyne的vector保持同步(这些mnemosyne可以是本地的或远程的,为简明起见,我们假定它们是远程的。)下面是synchronize()的一个例子:
| public void synchronize(vector mnemosynes) throws remoteexception, transactionexception { file:// matchallmemory对象是一个有效类 file://对任何二个memory对象进行比较 matchallmemory matchallmemory = new matchallmemory(); file:// 从primary中获得所有memory mnemosyne mnemosyne = (mnemosyne) mnemosynes.firstelement(); vector allmemories = mnemosyne.readall(matchallmemory,null); commoncontext.silentwriteall(allmemories); // 注册进行发送、接收事件 enumeration enumeration = mnemosynes.elements(); while(enumeration.hasmoreelements()) { mnemosyne nextmnemosyne = (mnemosyne) enumeration.nextelement(); file://注册接收通知 nextmnemosyne.addtakeremoteeventlistener(this, matchallmemory); nextmnemosyne.addwriteremoteeventlistener(this, matchallmemory); file:// 注册发送通知 addtakeremoteeventlistener(nextmnemosyne, matchallmemory); addwriteremoteeventlistener(nextmnemosyne, matchallmemory); } // ... } |
本地的mnemosyne对象读取vector中第一个mnemosyne对象的所有memory对象,并采用“silent”方法将它们写到其commoncontext对象中。然后,本地mnemosyne将自己作为takeremoteeventlistener和writeremotelistener添加到所有的远程mnemosyne中,这就意味着任何对远程mnemosynes的take或read操作都将调用本地mnemosyne的notify()方法。最后,本地mnemosyne将远程mnemosyne添加到其takeremoteeventlisteners和writeremotelisteners队列中,确保对本地mnemosyne的write或take操作都会通知远程mnemosyne。
当添加或删除一个memory对象时,经过同步的本地mnemosyne对象需要对所有的mnemosyne进行更新,可以通过notify()方法来完成这一任务。无论是发生write或take事件,mnemosyne都会针对发生的事件调用适当的监听者的notify()方法。在synchronize()方法中,我们把本地mnemosyne注册为所有远程mnemosyne的take和write事件的监听者,一旦远程mnemosyne上有take和write事件发生,就会调用本地 mnemosyne的notify()方法。然后,本地mnemosyne必须对事件作出反应。下面是mnemosyne如何与远程mnemosyne进行同步的例子:
| public void notify(remoteevent remoteevent) throws remoteexception { // 回写被写的内存,但无需通知所有的mnemosyne if(remoteevent instanceof writeremoteevent) { writeremoteevent wre = (writeremoteevent) remoteevent; commoncontext.silentwrite(wre.getmemory()); } file:// 取被写的memory,但无需通知所有的mnemosyne if(remoteevent instanceof takeremoteevent) { takeremoteevent tre = (takeremoteevent) remoteevent; commoncontext.silenttake(tre.getmemory()); } } |
现在已经创建了一个控制所有memory对象的mnemosyne,它自动与远程mnemosyne保持同步,如果任何一个远程mnemosynes得到或失去一个memory对象时,都可以使它保持最新的状态。
要通过mnemosyne管理http对话,servlet需要创建httpsession的实例(从httpservletrequest中使用getsession()),在实现memory对象的类中封装对话,并调用mnemosyne对象的write()方法把封装类写到一个mnemosyne中。
通过调用write()方法,封装着对话的memory对象沿着网络传送给mnemosyne,并通知远程机器。当对象被写到mnemosyne时,writeremoteevent被发送给在mnemosyne上注册的所有writeremoteeventlisteners,这样,所有其他的mnemosynes就能将新的对象作为mnemosynes添加到它们的对话信息存贮库中。
要对存贮的对话进行查询,servlet调用read()方法查找包含对话的memory对象,如果mnemosyne找到了要查找的对象,则该对象通过rmi返回到servlet服务器。
最后,要删除对话,servlet就会调用mnemosyne的take()方法,mnemosyne将象有read事件发生那样退还memory对象,同时从其存贮对象库中删除该memory对象。同时,向其所有takeremoteeventlisteners发送takeremoteevent事件,通知所有的远程mnemosynes该memory对象已经被删除了。
建立对话服务器
上面我们已经讨论了如何在多服务器上维护对话存贮库,下面我们将讨论如何建立对话服务器。在初始化过程中,对话服务器完成下列任务:
━━创建本地mnemosyne对象。
━━把本地mnemosyne绑定到rmi。
━━把本地mnemosyne与其他的远程mnemosyne进行同步。
首先,对话服务器将获得mnemosyne对象的一个实例,该实例被绑定到对话服务器的本地ip上。
| protected void bindmnemosyne() { file://得到mnemosyne mnemosyne mnemosyne = null; try { mnemosyne = mnemosynefactory.getmnemosyne(); } catch(remoteexception remoteexception) { system.out.println("internal error:"); system.out.println("can't create a mnemosyne"); system.exit(1); } // 把mnemosyne绑定到mnemosyneimpl try { string rmiurl = "//" + _localip + "/mnemosyneimpl"; naming.rebind(rmiurl, mnemosyne); } catch(arrayindexoutofboundsexception arrayindexoutofboundsexception) { throw new illegalargumentexception("localip is invalid"); } catch(malformedurlexception malformedurlexception) { throw new illegalargumentexception("localip is invalid"); } catch(remoteexception remoteexception) { system.out.println("internal error:"); system.out.println("can't rebind a mnemosyne to mnemosyneimpl"); system.exit(1); } } |
通过把本地mnemosyne上一系列代表rmi名字符号的url赋予远程对话服务器,就能引发同步操作,这些url存贮在一个被称作rmiurl的字符串数组中。在sessionserver的符号中,url是作为参数从命令行命令中获得的,但它可以来自其他渠道:
| protected void synchronizemnemosyne() { file://获得本地mnemosyne mnemosyne localmnemosyne = null; try { localmnemosyne = (mnemosyne) naming.lookup(_localip); } catch(exception exception) { system.out.println("internal error:"); system.out.println("can't lookup local mnemosyneimpl"); system.exit(1); } file://获得同步用的远程mnemosynes vector remotemnemosynes = new vector(); // _rmiurls对象是代表需要进行同步的远程服务器的字符串数组 for(int index = 1;index < _rmiurls.length;index++) { try { remotemnemosynes.add(naming.lookup(_rmiurls[index])); } catch(exception exception) { } } file:// 同步 try { if(remotemnemosynes.size() > 1) localmnemosyne.synchronize(remotemnemosynes); } catch(exception exception) { system.out.println("internal error:"); system.out.println("can't synchronize local mnemosyneimpl"); system.exit(1); } } |
远程访问mnemosyne
下面我们来讨论在servlet服务器上访问远程mnemosyne的方法。要在无需特定服务器在线的情况下加载一个包含对话信息的mnemosyne,需要创建一个failoverhandler的实例,failoverhandler利用jdk 1.3中的proxy api处理对话服务器当机的问题。failoverhandler把一个代表访问远程对话服务器的rmi url的字符串数组作为参数,然后,从proxy类中获取mnemosyne实例。下面的sessionmanager类中的initializemnemosyne()方法可以显示出这一切是如何完成的:
| public static void initializemnemosyne(string[] rmiurls) { // 设置当机服务器的处理程序 failoverhandler fh = new failoverhandler(null, rmiurls); // 得到mnemosyne. 的一个实例 _mnemosyne = (mnemosyne)proxy.newproxyinstance(mnemosyne.class.getclassloader(), new class[] { mnemosyne.class }, fh ); } |
如果用proxy类获取mnemosyne的实例,所有的方法调用必须通过failoverhandler的 invoke()方法进行。当有方法访问mnemosyne时,failoverhandler将试着调用该方法访问一个远程对象。如果方法调用失败(例如服务器关机),failoverhandler将从提供给构造器的url清单中再取得下一个url,这样就会无缝地转向下一个对话服务器。
| // 建立远程加载类的url清单 public failoverhandler(remote delegate, string[] delegateurls) { this.delegateurls = delegateurls; // 如果这个url无效,则获取下一个有效的url try { this.delegate = ((delegate == null)?getnextvaliddelegate():delegate); } catch (remoteexception ex) { // 如果发生远程意外错误,则该url不能使用,向调用者发送一个 //illegalargumentexception事件 throw new illegalargumentexception("remote urls could not " + "be found"); } } public object invoke(object proxy, method method, object[] arguments) throws throwable { while(true) { try { file:// 尝试对获得的最后一个url调用被调用的方法 return method.invoke(delegate, arguments); } catch(invocationtargetexception invocationtargetexception) { file://如果获得的url无效,则取下一个url try { throw invocationtargetexception.gettargetexception(); } catch(remoteexception remoteexception) { delegate = getnextvaliddelegate(); } } } } file://从构造器中的url清单中获得下一个url protected remote getnextvaliddelegate() throws remoteexception { for(int i = 0; i < delegateurls.length;i++) { try { return naming.lookup(delegateurls[i]); } catch(exception exception) { } } throw new remoteexception("all lookup failed"); } |
当使用failoverhandler对象时,从一个对话服务器向另一个对话服务器的转换对于调用mnemosyne的任何用户端机器都是透明的。
尽管我们已经能够访问对话服务器,而且可以避免单一点故障,我们还必须为httpsession建立一个封装对象,而sessionwrapper就是这样一个对象,而且,它还假定httpsession的执行也是串行化的。如果它不是串行化的,可以很方便地修改封装对象将对话的信息转移到一个哈希表中并在其他成员变量中保留其他信息(id、创作时间等信息。)。
| public interface sessionwrapper extends memory { /** * 得到httpsession的信息。 */ public httpsession getsession(); } public class sessionwrapperimpl implements sessionwrapper { /**识别该对话的关键字 */ protected string _id; /** 当前httpsession的信息。 */ protected httpsession _sess; /** * 建立id,但没有建立对话的其他信息,可用于通过read寻找一个对话。 */ public sessionwrapper(string id) { _id = id; } /** * 建立一个带对话的sessionwrapper。其id与对话的id相同。 */ public sessionwrapper(httpsession sess) { _sess = sess; _id = sess.getid(); } /** * 如果memory对象是sessionwrapper的一个实例,当前的sessionwrapper * 已经建立了与对象相同的id,则此方法返回的值为真。 */ public boolean equalsmemory(memory m) { return (m instanceof sessionwrapper && _id != null && _id.equals(((sessionwrapper)m)._id)); } /** * 得到httpsession的信息。 */ public httpsession getsession() { return _sess; } } |
sessionwrapper类执行了memory的界面,因此,httpsession对象的id可以与远程对话的id进行比较。
最后需要创建read()、write()和delete(),以对远程对话进行管理。我们向sessionmanager类添加三个静态类:
| /** * 从在初始化时建立的mnemosyne中得到httpsession信息。 */ public static httpsession getsession(string id) throws remoteexception { try { sessionwrapper result = (sessionwrapper)_mnemosyne.read(new sessionwrapper(id), null); return result.getsession(); } catch (transactionexception ex) { // 由于没有处理事物,因此不会有事务意外被放弃。 ex.printstacktrace(); } return null; } /** * 在初始化时指定的mnemosyne中保存对话信息。 */ public static void savesession(httpsession sess) throws remoteexception { try { _mnemosyne.write(new sessionwrapper(sess), null); } catch (transactionexception ex) { file://由于没有处理事物,因此不会有事务意外被放弃。 ex.printstacktrace(); } } /** * 从在初始化时指定的mnemosyne中删除一个对话。 */ public static void removesession(string id) throws remoteexception { try { _mnemosyne.take(new sessionwrapper(id), null); } catch (transactionexception ex) { // /由于没有处理事物,因此不会有事务意外被放弃。 ex.printstacktrace(); } } |
在servlet中,可以以如下方式管理对话:
| public void init(servletconfig conf) throws servletexception { // 调用一个方法得到指示对话服务器位置的rmi url清单 // 例如://server1.foo.com/mnemosyneimpl, //server2.foo.com/mnemosyneimpl,等 string[] urls = geturls(conf); // method to get the urls from properties for the session servers sessionmanager.initializemnemosyne(urls) } public void doget(httpservletrequest req, httpservletresponse rsp) throws ioexception { file:// 得到存贮在cookie中的对话,仅仅是为了得到其id。 httpsession basesess = req.getsession() file://根据得到的id,从mnemosyne中得到真正的对话 httpsession realsess = sessionmanager.getsession(base.getid()); sessionmanager.savesession(realsess); } |
结论
尽管这篇文章讨论了一个分布式对话管理的例子,但我们可以将这一技术用于管理必须容忍任一节点出错的分布式内存管理系统中。mnemosyne还可以用在成员不断加入和离开的p2p应用中。通过使用mnemosyne,任何一个成员都可以与系统进行快速同步,而无需要求为保持系统有效而必须保证某一结点不出故障。
闽公网安备 35060202000074号