WebLogic层上的缓存信息可以显著地提高性能,并能减少数据检索所需的外部系统调用次数。当一个应用程序希望存储少量很少更改的信息时(比如国家或目录条目的列表),这一点尤为明显。如今的内存非常便宜,因此可以通过在Web/EJB层上缓存数据来改进应用程序体系结构。
这不是什么新思想;大多数开发人员已经这样做了。不论是从简单的java.util.Hashtable或java.util.Hashmap访问数据,还是使用复杂的LRU缓存机制(LinkedList的派生机制)访问数据,所需的系统开销都是最小的。真正的问题不是在于如何缓存数据或使用何种机制进行存储,而是在于如何在高度可用的群集化环境中保护数据的完整性和向集群成员通知更改。
例如,假设用户要构建销售服装的电子商务店面应用程序。在应用层上,可以在内存中存储项目名称、描述、SKU、价格和图像等产品信息。将数据存储在内存中的好处在于可以加快页面加载速度和减少数据库调用的次数。
现在,如果业务用户希望更改目录条目的价格或描述,那么会出现什么情况?如何向所有的集群成员通知这个“delta”更改?请记住,如果可能的话仍然希望将数据存储在内存中以获得更好的性能。同样,可能希望近乎实时地更新信息而不希望在数据没有更改的情况下重新加载所有数据。
本文阐释了一种简单的技术:实现一个向集群成员通知无效(删除)的“简便可重构”缓存系统。
例如,请查看以下代码:
static Hashtable cache = new Hashtable();
public Product getProduct(String productID) { Product product = (Product) cache.get(productID); if (product == null) { // get the product from the database product = ProductDAO.getProduct(productID); cache.put(productID, product); } return product; }
简单的java.util.Hashtable存储了由静态缓存中的产品标识符索引的产品列表。如果产品不存在,则从数据库中检索这些产品并简便地将其添加到缓存中。这个代码片段阐释了产品不存在时如何通过查看缓存并获取产品来检索产品。
如果某种特定产品的产品描述发生更改,那么会出现什么情况?请查看以下代码:
public void changeDescription(String productID, String newDescription) throws ProductNotFoundException { ProductDAO.changeDescription(productID, newDescription); Command command = new InvalidateCacheCommand(productID); ClusterNotifier.notify(command); }
ProductDAO.changeDescription方法后面的代码使用新描述更新数据库中的产品ID。ClusterNotifier.notify方法(参见清单2)将从本地缓存中删除产品并向集群成员发送一个无效事件以便从它的本地缓存中删除产品。之后,集群中的所有成员都将从内存中删除产品。如果用户在任何节点处请求这个产品,那么可以使用设定的新描述简便地进行初始化。请查看getProduct()示例中的以下代码片段:
if (product == null) { // get the product from the database product = ProductDAO.getProduct(productID); cache.put(productID, product); }
这将会在数据库中重新初始化产品并将信息插回缓存中。
这是怎么发生的呢?我们来考察一下。
深入考察类
注:本文使用的是WebLogic中未发布的私有API。WebLogic 7.x和8.x可以支持这些API,但是不能保证它们可以受到以后版本的支持。将WebLogic.jar 放入IDE的类路径中可以隐藏许多底层的细节。
Command接口(通用)
首先,我们来考察一下Command接口。此接口较为简单,只定义了一个“execute”方法。
public interface Command extends Serializable { public void execute(); }
Command接口用于执行需要在集群中完成的操作。注意该接口是序列化的。它包含了在抵达每个集群成员时执行任务的逻辑。这个接口很通用,可以执行任何操作。本文讨论如何使用此接口使各集群缓存中的键无效。
InvalidateCacheCommand具体实现(具体)
清单1中的类是Command接口的具体实现,用于使缓存中的键无效。注意execute方法清除了静态绑定缓存的条目。这是怎么发生的呢?
ClusterNotifier实现(特定的)
现在,我们来考察一下ClusterNotifier类(清单2)。要关注一个方法:
notify(Command command)
此方法是一个同步调用,接收一个用于通知所有集群成员的命令对象作为输入。
在内部,这个类利用WebLogic的多点传送工具在集群之间发送信息。WebLogic自动处理部分片段丢失、重新发送等操作。
本文中的所有类都必须位于系统类路径中,原因在于WebLogic底层实现位于系统类路径中并需要能够找到ClusterNotifier类和委托消息类。
结束语
多数人在考虑支持集群的缓存时,他们想到的是集群间真正同步和复制一致的缓存。可以为群集化缓存实现这个模型,但是该模型具有几个问题:比如数据完整性保护、事务处理和网络抖动(性能暗示)。
无效机制有几个好处。它只向集群成员通知delta更改以便从它的本地存储中删除条目。由于系统是可重构的,因此可以只随需应变地加载更新过的数据。
集群成员通知是分布式编程所必需的,尤其是本文所述的更新缓存中的信息的时候。本文的思想也适用于创建自定义Command对象,比如群集化的“事件通知”系统(分布式观察者/可观察)、异步回调或重复数据存储。
清单1
public class InvalidateCacheCommand implements Command {
private final static Map GLOBAL_CACHE = new HashMap();
private final String key;
public InvalidateCacheCommand(String key) {
if (key == null) {
throw new IllegalArgumentException("Key is null.");
}
this.key = key;
}
public void execute() {
System.out.println("Clearing cache for key: " + key);
GLOBAL_CACHE.remove(key);
}
}
清单2
// NOTE: This class uses undocumented private WebLogic APIs not guaranteed to be supported
by future versions of WebLogic. Use at your own risk.
public final class ClusterNotifier {
private static MulticastSession session;
private ClusterNotifier() {}
public static void notify(Command command) {
if (command == null) {
throw new IllegalArgumentException("Command is null.");
}
if (session == null) {
System.out.println("Not running in a cluster. Invoking command locally..");
command.execute();
}
else
{
System.out.println("Running in a cluster. Sending message to cluster
members..");
try
{
session.send(new DelegateMessage(command));
} catch (IOException io) {
io.printStackTrace();
System.err.println("An IO Exception occurred while sending the " +
"message to the cluster." + io.getMessage());
}
}
}
public static void main(String argv[]) {
Command command = new InvalidateCacheCommand("TestKey");
ClusterNotifier.notify(command);
}
private static synchronized MulticastSession getSession()
{
if(session == null)
{
ClusterServices cluster = ClusterService.getServices();
if(cluster == null)
return null;
session = cluster.createMulticastSession(null, -1, false);
}
return session;
}
public static class DelegateMessage implements GroupMessage {
private final Command command;
public DelegateMessage() {
this(null);
}
public DelegateMessage(Command command) {
this.command = command;
}
public void execute(HostID ignoredHostID) {
if (command != null) {
command.execute();
}
}
}
}
}