1. 主键生成器
开发过数据库驱动信息系统的读者都知道,在一个关系数据库中,所有的数据都是存储在表里的,每一个表都有一个主键(Primary Key)。对大多数的用户输入数据来讲,主键需要由系统以序列号的方式产生,而不是由操作人员给出。
某些关系数据库引擎提供某种序列键生成机制。如SQL Server允许每一个表内可以有一个自动编号列。Oracle提供Sequence对象,可以提供序列键值。
但某些数据库引擎则没有相应的机制,如Sybase。这时就需要我们自己去生成主键序列号。通常的做法是使用一个表来存储所有的主键最大值。这个表包含两个列,一个列存放键名,另一个列存放键值,客户端使用SQL语句自行管理键值。
由于系统运行期间总是需要序列键,因此整个系统需要一个序列键管理对象,这个对象在运行期间存在。考虑到可以让一个序列键管理器负责管理分属于不同模块的多个序列键,因此这个序列键管理器需要让整个系统访问。
2. 单例模式
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
2.1 饿汉式单例
饿汉式单例是在Java里实现最为简单的单例类,其源代码如下所示:
public classEagerSingleton {privateEagerSingleton() {
}//静态工厂方法
public staticEagerSingleton getInstance() {returnm_instance;
}private static finalEagerSingleton m_instance= newEagerSingleton();
}
在这个类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用。这时单例类的唯一实例就被创建出来了。
Java语言中单例类最重要的特点就是类的构造函数是私有的,从而避免外界利用构造函数直接创建出任意多的实例。由于构造函数是私有的,因此此类不能被继承。
2.2 懒汉式单例
与饿汉式单例相同之处是,类的构造函数是私有的,不同的是懒汉式单例在第一次被引用时将自己实例化。如果加载器是静态的,那么懒汉式单例类被加载时不会将自己实例化。
public classLazySingleton {privateLazySingleton() {
}synchronized public staticLazySingleton getInstance() {if (m_instance == null) {
m_instance= newLazySingleton();
}returnm_instance;
}private static LazySingleton m_instance = null;
}
可以看到,在上面给出的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。同样由于构造函数是私有的,因此此类不能被继承。
饿汉式单例类在自己被加载时就将自己初始化,从资源利用效率角度来讲,比懒汉式单例类稍差一些。然而懒汉式单例类在将自己实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题。
2.3 单例类的状态
一个单例类是可以有状态的,一个有状态的单例对象一般也是可变的单例对象。有状态的单例对象常常当做状态库(repositary)使用。比如一个单例对象可以持有一个int类型的属性,用来给系统提供一个数值唯一的序列号码。
另一方面,单例类也可以没有状态,仅用作提供工具性函数的对象。一个没有状态的单例类也就是一个不变单例类。
3. 多例模式
单例模式很容易推广到任意且有有限多个实例的情况,这时候称它为多例模式。多例模式除了一个类可以有多个实例之外,特点与单例模式类似。
多例模式的实例数目并不需要有上限,由于没有上限的多例类对实例的数目是没有限制的,因此虽然这种多例类是单例模式的推广,但是这种多例类并不一定能够回到单例类。
由于事先不知道要创建多少个实例,因此,必然使用聚集管理所有的实例。
4. 单例模式的应用
我们回到那个主键生成器。读者应该已经意识到这个系统设计应当使用到单例模式。
这个设计由一个单例类KeyGenerator和一个存储某一个键的信息的KeyInfo对象组成。源代码如下:
public classKeyGenerator {private staticKeyGenerator keygen= newKeyGenerator();private static final int POOL_SIZE = 20;private HashMap keyList = new HashMap(10);privateKeyGenerator() {
}public staticKeyGenerator getInstance() {returnkeygen;
}public intgetNextKey(String keyName) {
KeyInfo keyinfo;if(keyList.containsKey(keyName)) {
keyinfo=(KeyInfo) keyList.get(keyName);
System.out.println("key found");
}else{
keyinfo= newKeyInfo(POOL_SIZE, keyName);
keyList.put(keyName, keyinfo);
System.out.println("new key created");
}returnkeyinfo.getNextKey();
}
}
可以看出KeyGenerator是一个单例类,它提供了私有构造函数和一个静态工厂方法向外界提供自己唯一的实例。
一个系统中往往不止一个主键,我们使用一个聚集keyList来存储不同序列键信息的KeyInfo对象。
下面是KeyInfo类的:
public classKeyInfo{
private intkeyMax;private intkeyMin;private intnextKey;private intpoolSize;privateString keyName;public KeyInfo(intpoolSize, String keyName) {this.poolSize =poolSize;this.keyName =keyName;
retrieveFromDB();
}public intgetKeyMax() {returnkeyMax;
}public intgetKeyMin() {returnkeyMin;
}public synchronized intgetNextKey() {if (nextKey >keyMax) {
retrieveFromDB();
}return nextKey++;
}private voidretrieveFromDB() {//step1:从数据库获取poolSize个新KeyValueString sql1= "UPDATE KeyTable SET keyValue = keyValue + " +poolSize+ " WHERE keyName = '" + keyName+ "'"; //step2:获取最新的KeyValue值
String sql:2 = "SELECT keyValue FROM KeyTable WHERE KeyName = '" + keyName + "'";
//在一个事务中执行上面的操作并提交//假设返回的keyValue值为1000
intkeyFromDB = 1000;
keyMax=keyFromDB;
keyMin= keyFromDB - poolSize + 1;
nextKey=keyMin;
}
}
使用KeyInfo的目的是不用每次都进行键值的查询。毕竟一个键只是一些序列号码,与其每接到一次请求就查询一次不如一次性的预先登记多个键值,然后连续多次地向客户端提供这些预定的键值。这就是键值的缓存机制。当KeyGenerator每次更新数据库中数据时,它都将键值增加,但不是加1而是更多,我们这个例子中增加的值是20。为了存储所有的与键有关的信息,使用KeyInfo。
这个KeyInfo除了存储与键有关的信息外,还提供一个retrieveFromDB()方法,向数据库查询键值。每次查询得到的20个键值会在随后提供给请求者,直到20个键值全部使用完毕,然后再向数据库预定后20个键值。KeyGenerator保持一个对KeyInfo对象的引用。客户端调用getNextKey()方法以得到下一个键的键值。
下面是一个示意性的客户端Client类的源代码:
public classClient {private staticKeyGenerator keygen;public static voidmain(String[] args) {
keygen=KeyGenerator.getInstance();for (int i = 0; i < 25; i++) {
System.out.println("key(" + (i + 1) + ")= " + keygen.getNextKey("PO_NUMBER"));
}
}
}
5.多例模式的应用
正如前面所谈到的,为了能够处理多系列键值的情况,除了可以将单例模式所封装的单一状态改为聚集状态之外,还可以采用多例模式,下面是KeyGenerator的源代码。可以看出,这是一个多例类,每一个KeyGenerator对象都持有一个特定的KeyInfo对象作为内蕴状态。客户端可以使用这个类的静态工厂方法得到所需的实例,而这个工厂方法会首先查看作登记用的keygens聚集。如果所要求的键名在聚集里面,就直接将这个键名所对应的实例返还给客户端,如果不存在就创建一个新的实例。
importjava.util.HashMap;public classKeyGenerator {private static HashMap kengens = new HashMap(10);private static final int POOL_SIZE = 20;privateKeyInfo keyinfo;privateKeyGenerator() {
}privateKeyGenerator(String keyName) {
keyinfo= newKeyInfo(POOL_SIZE, keyName);
}public static synchronizedKeyGenerator getInstance(String keyName) {
KeyGenerator keygen;if(kengens.containsKey(keyName)) {
keygen=(KeyGenerator) kengens.get(keyName);
}else{
keygen= newKeyGenerator(keyName);
}returnkeygen;
}public intgetNextKey() {returnkeyinfo.getNextKey();
}
}
KeyInfo与单例类中一样,下面是客户端代码:
public classClient {private staticKeyGenerator keygen;public static voidmain(String[] args) {
keygen= KeyGenerator.getInstance("PO_NUMBER");for (int i = 0; i < 25; i++) {
System.out.println("key(" + (i + 1) + ")= " +keygen.getNextKey());
}
}
}