由于项目需要,需要用到oscache的分布式集群,为了实现分布式环境下消息的通知,目前两种比较流行的做法是使用 JMS和JGROUPS,这两种方式都在底层实现了广播发布消息。 由于JGroups可以提供可靠的广播通信.所以我准备采用JGroups。
需要用的jar包有: oscache.jar 2.4.1(缓存组件),jgroups.jar2.8.GA(IP组播),commons-loggin.jar1.1(日志记录用的),concurrent-1.3.2.jar(线程同步用的)。尽量使用当前最高版本。
按照oscache的官方文档说明,docs/wiki/Documentation.html下的Tutorial第3点Clustering OSCache,提到了JavaGroups Configuration:
Just make sure you have jgroups-all.jar file in your classpath (for a webapp put it in WEB-INF/lib), and add the JavaGroups broadcasting listener to your oscache.properties file like this:
cache.event.listeners=com.opensymphony.oscache.plugins.clustersupport.JavaGroupsBroadcastingListener
In most cases, that's it! OSCache will now broadcast any cache flush events across the LAN. The jgroups-all.jar library is not included with the binary distribution due to its size, however you can obtain it either by downloading the full OSCache distribution, or by visiting the JavaGroups website.
If you want to run more than one OSCache cluster on the same LAN, you will need to use different multicast IP addresses. This allows the caches to exist in separate multicast groups and therefore not interfere with each other. The IP to use can be specified in your oscache.properties file by the cache.cluster.multicast.ip property. The default value is 231.12.21.132, however you can use any class D IP address. Class D address fall in the range 224.0.0.0 through 239.255.255.255.
If you need more control over the multicast configuration (eg setting network timeout or time-to-live values), you can use the cache.cluster.properties configuration property. Use this instead of the cache.cluster.multicast.ip property. The default value is:
UDP(mcast_addr=231.12.21.132;mcast_port=45566;ip_ttl=32;/
mcast_send_buf_size=150000;mcast_recv_buf_size=80000):/
PING(timeout=2000;num_initial_members=3):/
MERGE2(min_interval=5000;max_interval=10000):/
FD_SOCK:VERIFY_SUSPECT(timeout=1500):/
pbcast.NAKACK(gc_lag=50;retransmit_timeout=300,600,1200,2400,4800;max_xmit_size=8192):/
UNICAST(timeout=300,600,1200,2400):/
pbcast.STABLE(desired_avg_gossip=20000):/
FRAG(frag_size=8096;down_thread=false;up_thread=false):/
pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=true)
这些配置大家肯定都看过,而且看烂了,但是也的确是要这么配置的。
我的开发环境是Myeclipse8.0+Spring2.0+Oracle10g,其中还有些其他框架就不提了,主要是说说如何配置并实现oscache分布式集群。
首先要修改oscache.properties配置文件,我使用的是内存缓存,修改的地方不是很多,
cache.memory=true, cache.blocking=true(这个要打开,同步增加cache 需要用到这个块),cache.capacity=100000,后面的Clustering 方面的配置就按照上面的官方说明那么配就可以了。然后是LOG4J,这个大家应该都用到了的,接着配这个日志:
log4j.logger.com.opensymphony.oscache.base=INFO,A6
log4j.logger.com.opensymphony.oscache.plugins.clustersupport.AbstractBroadcastingListener=INFO,A6
log4j.logger.com.opensymphony.oscache.general.GeneralCacheAdministrator=INFO,A6
log4j.logger.com.opensymphony.oscache.plugins.clustersupport.ClusterNotification=INFO,A6
log4j.logger.com.opensymphony.oscache.plugins.clustersupport.JavaGroupsBroadcastingListener=INFO,A6
log4j.logger.org.jgroups.blocks.NotificationBus=INFO,A6
//说明:我主要是记录在使用到这几类的时候的一些后台日志,而且也是必须要记录这几个的,不然你怎么知道集群相关的信息还有cache有没有更新同步等。
log4j.appender.A6.Threshold = INFO
log4j.appender.A6=org.apache.log4j.DailyRollingFileAppender
log4j.appender.A6.File=/data/logs/mmbosslogs/oscache.log
log4j.appender.A6.DatePattern = '_'yyyy-MM-dd'.log'
log4j.appender.A6.layout=org.apache.log4j.PatternLayout
log4j.appender.A6.MaxFileSize = 1024KB
log4j.appender.A6.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
//日志记录配置自己按自己的路径去配吧
然后是配置commons-logging.properties:
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
这样就OK了。
在项目中的每个页面需要cache的地方配置好cache的key还有自动刷新的时间,默认是3600秒,key最好精确一点,比如在默认的URI后面再加上其他的ID参数等,我是这样配置的:
String cachekey=request.getRequestURI()+"|"+albumid+pageno;
int cacheTime = 3600;
<cache:cachetime="<%=cacheTime%>" key="<%=cachekey%>" >
在2个服务器上,把需要用到cache缓存的页面按照上面的配置都配置好,然后就可以部署并启动服务器了。
最开始我只实现了同步Flush,就是在一个节点服务器上清空某个页面的cache,然后通知其他集群节点也去Flush这个页面,也就是当这个页面再次被访问时,这个页面将会重新去数据库读数据,cache也将更新。我做了一个只用来Flush缓存的页面,在里面用到了
GeneralCacheAdministrator的flushAll()方法。2行代码就OK了。
补充:我在sys-service中加入了:
<beanid="cacheAdministrator"
class="com.opensymphony.oscache.general.GeneralCacheAdministrator"
destroy-method="destroy"/>
GeneralCacheAdministrator在com.opensymphony.oscache.general.GeneralCacheAdministrator中。
在这个时候我用,jgroups.包是2.2.8版本的。
这些准备工作做好以后,启动服务器,然后我就去访问一个页面, 当然这个页面有用到cache缓存。如果正常,就会在服务器后台打印如下信息:
-----------------------------------------------------------------
GMS: address=ThinkPad-48956, cluster=OSCacheBus, physical address=192.168.100.85:1351
-------------------------------------------------------------------上面的信息就说明JavaGroupsBroadcastingListener已初始化启动完成。
但是没有打印出这个信息,就说明出问题了。当然我第一次测试的时候并没有出现上面的信息,这时候我从日志记录里看到了报错信息,就是LOG4J里配置的osache.log,
ERROR AbstractCacheAdministrator:330 - Could not initialize listener 'com.opensymphony.oscache.plugins.clustersupport.JavaGroupsBroadcastingListener'. Listener ignored.
com.opensymphony.oscache.base.InitializationException: Initialization failed: ChannelException: failed loading class: java.lang.ClassNotFoundException: [Lorg.jgroups.Address;
[Lorg.jgroups.Address这个东东在网上搜了好久,有些老外说是JDK的BUG,不支持,原因是ClassLoader.loadClass is not supposed to support the array class name
syntax,但是jdk一直没有修复这个bug。其次网上还有兄弟说运行ClassConfigurator的main函数也有问题,这段代码以前是没有问题,肯定是环境的问题。
然后继续查找,发现是jg-magic-map.xml里的问题,网上说删除
就ok了。我在一些国外的网站上也查了,有些老外也是这么说的,把这个Lorg.jgroups.Address属性给他删除了就可以了,但是我测试发现,没用,你把这个删除后,它又来报错了:看看日志:
Could not initialize listener 'com.opensymphony.oscache.plugins.clustersupport.JavaGroupsBroadcastingListener'. Listener ignored.
com.opensymphony.oscache.base.InitializationException: Initialization failed: ChannelException: failed loading class: java.lang.ClassNotFoundException: [Ljava.lang.Object;
同样 [Ljava.lang.Object也在jg-magic-map.xml里,这个是JAVA的基础对象包,咋可能不认识。。。JavaGroupsBroadcastingListener还是不能正常初始化,郁闷了。。。
接着几天一直想着这个问题,网上慢慢的焦急的搜寻着答案。一直没搜到,后来直接去了JGROUPS的官网,看到有最新版2.8.GA,换个高级版本是不是就没问题了?
换上去了,然后我还是去访问有cache缓存的那个页面,果然换了这个高级版本的包还真没这个问题了。
-----------------------------------------------------------------
GMS: address=ThinkPad-48956, cluster=OSCacheBus, physical address=192.168.100.85:1351
--------------------------------------------------------------这个信息在服务器后台打印出来了,监听启动OK了,而且重新访问这个页面的时候,打开速度还是很快的,说明cache生效了。再
INFO JavaGroupsBroadcastingListener:117 - JavaGroups clustering support started successfully。
INFO AbstractBroadcastingListener:40 - AbstractBroadcastingListener registered
一切正常,而且监听也已注册,注册就是在知道有你这个监听节点,比如另一个服务器启动,也初始化正常,也注册了,这个服务器就知道集群中又多了一个节点。如下日志:
INFO JavaGroupsBroadcastingListener:189 - A new member at address 'ThinkPad-49675' has joined the cluster。
如果有一个注册过的服务器挂了或者是关闭了,就会看到如下日志信息:
INFO JavaGroupsBroadcastingListener:132 - JavaGroups shutting down...
INFO JavaGroupsBroadcastingListener:201 - Member at address 'ThinkPad-30262' left the cluster
INFO JavaGroupsBroadcastingListener:144 - JavaGroups shutdown complete.
下面接着测试下同步FLUSH,打开那个用来FLUSH缓存的页面(刷新范围是appliction级的,也就是全部的cache都清空),页面运行完成后这个服务器的所有cache都清空了,然后去看另一台服务器上的oscache日志:
INFO AbstractBroadcastingListener:174 - Cluster notification (type=4, data=Wed Jan 13 13:02:34 CST 2010) was received.
从com.opensymphony.oscache.plugins.clustersupport下的ClusterNotification类中可以得知类型4就是 public static final int FLUSH_CACHE = 4; Specifies a notification message indicating that an entire cache should be flushed.在原代码里可以看到的。
然后去打开本服务器的那个刚才加载过cache的页面,打开不快了,服务器后台在刷查询语句呢,这就对了,FLUSH功能实现了;然后打开另一个节点服务器也是打开刚才那个页面,同样的效果,也是重新读数据库,同步FLUSH缓存已经OK。
根据项目需要,同步FLUSH不能满足,需要做到cache同步增加,同步更新等功能,也就是说当集群所有节点都已经正常启动监听,其中某一个节点新增了一个cache,就会广播通知其他节点,并增加这个cache。
然而在测试过程中发现,一个节点 Cache.putInCache(key,content) 加入缓存后,
在另一个节点并没有收到任何数据同步信息。
于是翻看了osccache 的Cache 和AbstractBroadcastingListener源码发现;
在Cache的事件处理中,AbstractBroadcastingListener只是提供了 cacheflushXXX 相关的信息通知,
cacheEntryAdded,cacheEntryRemoved,cacheEntryUpdated都未做处理。
public void cacheFlushed(CachewideEvent event) {
if (!Cache.NESTED_EVENT.equals(event.getOrigin()) && !CLUSTER_ORIGIN.equals(event.getOrigin())) {
if (log.isDebugEnabled()) {
log.debug("cacheFushed called (" + event + ")");
}
//此处发送广播消息通知其他节点
sendNotification(new ClusterNotification(ClusterNotification.FLUSH_CACHE, event.getDate()));
}
}
// --------------------------------------------------------
// The remaining events are of no interest to this listener
// --------------------------------------------------------
public void cacheEntryAdded(CacheEntryEvent event) {
}
public void cacheEntryRemoved(CacheEntryEvent event) {
}
public void cacheEntryUpdated(CacheEntryEvent event) {
}
然来要自己来实现这个功能。。。那就实现吧。
要想实现这个几个功能,必须要看看这个几个类:
AbstractBroadcastingListener.java
//类说明:Implementation of a CacheEntryEventListener. It broadcasts the flush events
across a cluster to other listening caches. Note that this listener cannot be used in conjection with session caches.
ClusterNotification.java,
//类说明: A notification message that holds information about a cache event. This class is Serializable
to allow it to be sent across the network to other machines running in a cluster.
Cache.java,
//类说明:Provides an interface to the cache itself. Creating an instance of this class will create a cache that behaves according to its construction parameters. The public API provides methods to manage objects in the cache and configure any cache event listeners.
CacheEntry.java,
//类说明: A CacheEntry instance represents one entry in the cache. It holds the object that is being cached, along with a host of information about that entry such as the cache key, the time it was cached, whether the entry has been flushed or not and the groups it belongs to.
CacheEntryEvent.java
//类说明: CacheEntryEvent is the object created when an event occurs on a cache entry (Add, update, remove, flush). It contains the entry itself and its map.
还有jgroups里面很重要的几个类:
NotificationBus
//类说明:This class provides notification sending and handling capability.
* Producers can send notifications to all registered consumers.
* Provides hooks to implement shared group state, which allows an
* application programmer to maintain a local cache which is replicated
* by all instances. NotificationBus sits on
* top of a channel, however it creates its channel itself, so the
* application programmers do not have to provide their own channel.
(org.jgroups.util;)Util
//类说明:Collection of various utility routines that can not be assigned to other classes.
Util 类里有很多共用方法,例如在序列化的时候把对象转成流对象,反序列化的时候把流对象读成对象等,后面遇到的问题就和这个有关系。以上几个类是我遇到问题后,顺藤摸瓜着慢慢找问题的时候看到的,要想了解oscache,jgroups消息同步是怎样一个顺序流程,就必须了解这几个类,当中还有些和event事件监听有关系的这里就不一一提到,有空可以在SVN上把源代码拉下来看看就知道了。
还是来接着说实现要实现的功能,了解了上面几个类的功能和整体的流程后,我实现的代码如下:
public void cacheEntryAdded(CacheEntryEvent event)
{if(!CLUSTER_ORIGIN.equals(event.getOrigin()))
{sendNotification(new ClusterNotification(ClusterNotification.CLUSTER_ENTRY_ADD,new CacheEntryEvent(event.getMap(),event.getEntry(),CLUSTER_ORIGIN)));}
}
public void cacheEntryRemoved(CacheEntryEvent event)
{if(!CLUSTER_ORIGIN.equals(event.getOrigin()))
{sendNotification(new ClusterNotification(ClusterNotification.CLUSTER_ENTRY_DELETE,new CacheEntryEvent(event.getMap(),event.getEntry(),CLUSTER_ORIGIN)));}}
public void cacheEntryUpdated(CacheEntryEvent event)
{System.out.println("origin:"+event.getOrigin());if(!CLUSTER_ORIGIN.equals(event.getOrigin()))
{sendNotification(new ClusterNotification(ClusterNotification.CLUSTER_ENTRY_UPDATE,new CacheEntryEvent(event.getMap(),event.getEntry(),CLUSTER_ORIGIN)));}
}
代码写好后,接着就来调试了,我打开了2个myeclipse,端口一个是80一个8080,启动完成后,消息广播监听也都正常初始化,日志里也都看到2个节点已经加入到集群中。然后选择其中一个服务器,访问一个带有cache缓存的页面,如果能把这个cache同步到其他节点的话,去访问另一个服务器上的名字想同的页面时,应该很快打开的,可是我去打开的时候,还是很慢,在从数据库里读数据,而没有增加缓存成功。查看日志文件oscache.log,发现有报错: ERROR NotificationBus:292 - exception=java.lang.IllegalArgumentException: java.lang.NullPointerException,cache同步增加不成功,只能跟踪代码看看问题出在那。重复上面的操作,这时event监听器监听到了当前的操作,并且跳到cacheEntryAdded这个方法中,然后就调用sendNotification方法给其他节点发消息,这个方法sendNotification(ClusterNotification message);需要一个MESSAGE对象,而
new ClusterNotification(ClusterNotification.CLUSTER_ENTRY_ADD,new CacheEntryEvent(event.getMap(),event.getEntry(),CLUSTER_ORIGIN)));
就是构造一个message对象,
接着就去jgroups中去运行 sendNotification(Serializable n) { sendNotification(null, n); }这个方法,然后在sendNotification(Address dest, Serializable n) 这个方法中去构造一个对象流,代码如下:
public void sendNotification(Address dest, Serializable n) {
Message msg=null;
byte[] data=null;
Info info;
try {
if(n == null) return;
info=new Info(Info.NOTIFICATION, n);
data=Util.objectToByteBuffer(info);//在公共方法里来把这个对象转成bytebuffer
msg=new Message(dest, null, data);
if(channel == null) {
if(log.isErrorEnabled()) log.error("channel is null. Won't send notification");
return;
}
channel.send(msg);
}
catch(Throwable ex) {
if(log.isErrorEnabled()) log.error("error sending notification", ex);
}
}
在Util.objectToByteBuffer(info);中有这段代码,也就是按流程会走到这里
else { // will throw an exception if object is not serializable
out_stream.write(TYPE_SERIALIZABLE);
out=new ObjectOutputStream(out_stream);
((ObjectOutputStream)out).writeObject(obj);
注意out_stream.write(TYPE_SERIALIZABLE);在将INFO对象写到流对象的时候,在之前加了一个标示位TYPE_SERIALIZABLE,也就是说这个对象流里不完全是对象。这样一来就得看看后面在反序列化对象的时候是怎么操作的。
这个消息发出去后,在另一个myecilipse中进入了我设置的断点 public void receive(Message msg);里面有段 obj=msg.getObject();,就是调objectFromByteBuffer(byte[] buffer, int offset, int length)方法就是处理这个的,看看具体代码:
case TYPE_SERIALIZABLE: // the object is Externalizable or Serializable
in=in_stream; // changed Nov 29 2004 (bela)
ObjectInputStream inn= new ObjectInputStream(in);
retval=inn.readObject();
break;
在执行retval=inn.readObject();的时候进到了异常处理:
catch(Throwable ex) {
if(log.isErrorEnabled()) log.error("exception=" + ex);
}
一般情况下序列化里应该是一个完整的对象,但是这里不全是对象,是不是那个标示位的原因呢,我把代码改了下,让它把对象流写到我本地的一个文件中,然后我在写了个反序列化的类,发现反不出来,直接报错,接着我修改代码,在把对象读到对象流并写到本地文件的时候,没有把标示位写进去,然后我再反序列化,发现正常,可以得到info对象。
接下来修改objectFromByteBuffer方法:
case TYPE_SERIALIZABLE: // the object is Externalizable or Serializable
// changed Nov 29 2004 (bela)
byte[] result2=new byte[buffer.length-1];
System.arraycopy(buffer,1,result2,0,result2.length);
ByteArrayInputStream in_stream1=new ByteArrayInputStream(result2, offset, result2.length);
in=in_stream1;
ObjectInputStream inn= new ObjectInputStream(in);
// System.out.println(inn.readObject());
retval=inn.readObject();
// retval=(Info)inn.readObject();
break;
反序列化的时候把标示位给它去掉,然后再去读对象,改了后,程序在这里终于正常的走下去了。然后就到了case ClusterNotification.CLUSTER_ENTRY_ADD:
System.out.println("cluster cache add:"+message.getData());
if(message.getData()instanceof ClusterNotification)
{
CacheEntryEvent event=(CacheEntryEvent)message.getData();
cache.putInCache(event.getKey(),event.getEntry().getContent(),null,null,CLUSTER_ORIGIN);
}
break;
在执行cache.putInCache的时候会抛出一个异常,试了很多次,结果一样,后来我就把
配置文件中CACHE ALGORITHM 修改为
cache.algorithm=com.opensymphony.oscache.base.algorithm.UnlimitedCache
重复上面的操作,日志中显示
INFO AbstractBroadcastingListener:174 - Cluster notification (type=9, data=key=/MMBOSS/rdp/simple/v1.0/Search.jsp_GET__42Lrd1K4b5IDrON_wukcrQ==1) was received.
服务器后台信息显示:
cluster cache add:key=/MMBOSS/rdp/simple/v1.0/Search.jsp_GET__42Lrd1K4b5IDrON_wukcrQ==1
obj=type= NOTIFICATION, notification=type=9, data=key=//MMBOSS/rdp/simple/v1.0/alum.jsp
然后访问这个页面,一点就开,同步增加cache完成。
考虑到以后的集群环境,如果节点很多,每当更新一个栏目的时候,都去全部刷新,这个就有点浪费了,所以之前用的application级全部刷新就不是很方便使用了,需要更加精确。
接下来我想到,cache标签有个refresh属性,可以自己控制什么时候刷新,接下来我就修改了cache标签:
<cache:cachetime="<%=cacheTime%>" key="<%=cachekey%>" refresh='<%=getNeedRefresh(request.getParameter("needRefresh"))%>' >
getNeedRefresh()是我写的一个方法,主要是处理url中传进来的needRefresh参数,把这个string转换成boolean型,因为refresh属性中只有true和false,而且是boolean型。这样设定后,如果有刷新某个栏目,只需要在他的url地址中加入一个请求参数needRefresh=true就可以了,例如:
先打开这个栏目,控制台就出现如下信息:
origin:CLUSTER
obj=type=NOTIFICATION,notification=type=9, data=key=//MMBOSS/rdp/simple/v1.0/Search.jsp|2308
cluster cache add:key=//MMBOSS/rdp/simple/v1.0/Search.jsp|2308
消息显示有个新栏目的cache数据产生了,并且发消息通知cluster每个节点来增加这个数据为data的cache。origin是范围,type=9是消息类型,9是cache增加,我在ClusterNotification这个类中增加了几个消息类型:
public final static int ACTION_ADD_OBJ=5;
public final static int ACTION_UPDATE_OBJ=6;
public final static int ACTION_DELETE_OBJ=7;
public final static int ACTION_FLUSH_OBJ=8;
public final static int CLUSTER_ENTRY_ADD=9;
public final static int CLUSTER_ENTRY_UPDATE=10;
public final static int CLUSTER_ENTRY_DELETE=11;
接下来试下但个更新参数:
http://localhost:8080/MMBOSS/rdp/simple/v1.0/Search.jsp?groupcode=2308&needRefresh=true
当这个请求成功发送给服务器后,服务器就会去重新读这个栏目,并且重新去数据库查询此栏目的相关数据信息。控制台信息如下:
origin:CLUSTER
obj=type=NOTIFICATION,notification=type=10, data=key=//MMBOSS/rdp/simple/v1.0/Search.jsp|2308
cluster cache update:key=//MMBOSS/rdp/simple/v1.0/Search.jsp|2308
日志消息为:
INFO AbstractBroadcastingListener:174 - Cluster notification (type=10, data=key=//MMBOSS/rdp/simple/v1.0/Search.jsp|2308) was received.
通过双开myeclipse,重复上面的操作,得到的结果是,同步更新功能已实现。后面还将这个刷新控制做了一个专门的页面,把带有请求参数needRefresh=true的栏目URL地址在后台运行,在前台页面是看不到更新的过程,用户只需要刷新下页面,更新后的数据就展现出来了。
oscache的功能就剩下Group相关的应用没有实现,例如(cacheGroupAdded,cacheGroupEntryAdded等)由于时间问题就没继续研究下去。