在一个关系数据库中,所有的数据都是存储在表里,而每一个表都有一个主键(Primary Key)。对大多数的用户输入数据来讲,主键需要由系统以序列号方式产生。比如一个餐馆的贩卖系统需要一个序列号给每天开出去的卖单编号,这个序列号码就应当存放到数据库里面。每当发出序列号码的时候,都应当从数据库读取这个号码,并更新这个号码。


 


为了保证在任何情况下键值都不会出现重复,应当使用预定式键值存储办法。在请求一个键值时,首先将数据库中的键值更新为下一个可用值,然后将旧值提供给客户端。这样万一出现运行中断的话,最多就是这个键值被浪费掉。


 


与此相对的是记录式键值存储办法。也就是说,键值首先被返还给客户端,然后记录到数据库中去。这样做缺点明显,因此不要使用这种登记式的存储办法。


 


预定式的存储办法可以每一次预定多个键值(也即一个键值区间),而不是每一次仅仅预定一个值。由于这些值都是一些序列数值,因此,所谓一次预定多个值,不过就是每次更新键值时将键值增加一个大于1的数目。


 


这个序列键管理器可以设计成一个单例类。


 


下面从一个最简单的情况出发,逐渐将问题的复杂性提高,直到给出具有实用价值的解决方案为止。


 


方案一:没有数据库的情况






package 
     com.javapatterns.keygen.ver1;

 
    public 
      
    class 
     KeyGenerator  
    ... 
    {
    private static KeyGenerator keygen = new KeyGenerator();
    private int key = 1000;

    private KeyGenerator() ...{}

    public static KeyGenerator getInstance() ...{
        return keygen;
    }

    public synchronized int getNextKey() ...{
        return key++;
    }
} 
 
 

       
package      com.javapatterns.keygen.ver1;

     public 
      
    class 
     Client  
    ... 
    {
    private static KeyGenerator keygen;

    public static void main(String[] args) ...{
        keygen = KeyGenerator.getInstance();
        System.out.println("key = " + keygen.getNextKey());
        System.out.println("key = " + keygen.getNextKey());
        System.out.println("key = " + keygen.getNextKey());
    }
}

这一设计基本上实现了向客户端提供键值的功能,但是也有明显的缺点。由于没有数据库的存储,一旦系统重新启动,KeyGenerator都会重新初始化,这就会造成键值的重复。为了避免这一点,就必须将每次的键值存储起来,以便一旦系统和重启时,可以将这个键值取出,并在这个值的基础上重新开始。


 


方案二:有数据库的情况


 




java MetaObjectHandler 主键ID 设置null java主键生成_java

package      com.javapatterns.keygen.ver2;

     public       
    class 
     KeyGenerator  
    ... 
    {
    private static KeyGenerator keygen = new KeyGenerator();

    private KeyGenerator() ...{}

    public static KeyGenerator getInstance() ...{
        return keygen;
    }

    public synchronized int getNextKey() ...{
        return getNextKeyFromDB();
    }

    private int getNextKeyFromDB() ...{
        String sql1 = "UPDATE KeyTable SET keyValue = keyValue + 1 ";
        String sql2 = "SELECT keyValue FROM KeyTable";
        //execute the update SQL
        //run the SELECT query
        //这里只是示意性地返回一个数值
        return 1000;
    }
}


Client类的代码与方案一类似,不再重复。


 


在接到客户端的请求时,这个KeyGenerator每次都向数据库查询键值,将新的键值登记到表里,然后将查询的结果返还给客户端。上面的代码中,只给出了SQL语句,为了将注意力集中在系统设计上而并没有给出执行这两行语句的JDBC代码。


 


方案三:键值的缓存方案


 


每一次都进行键值的查询,有必要吗?毕竟一个键的值只是一些序列号码,与其每接到一次请求就查询一次,然后向客户端提供这一个值,不如在一次查询中一次性地预先登记多个键值,然后连续多次地向客户端提供这些预订的键值。这样一来,不是节省了大部分不必要的数据库查询操作吗?


 


这就是键值的缓存机制。当KeyGenerator每次更新数据库中的键值时,它都将键值增加。与方案二不同之处是,键值的增加值不是1而是更多。下面的例子中,键值的增加值是20.为了存储所有的与键有关的信息,特地引进一个KeyInfo类,这个类除了存储与键有关的信息外,还提供了一个retrieveFromDB()方法,向数据库查询键值。


 




java MetaObjectHandler 主键ID 设置null java主键生成_java

package      com.javapatterns.keygen.ver3;

     public           class 
     KeyGenerator  
    ... 
    {
    private static KeyGenerator keygen = new KeyGenerator();
    private static final int POOL_SIZE = 20;
    private KeyInfo key ;

    private KeyGenerator() ...{
        key = new KeyInfo(POOL_SIZE);
    }

    public static KeyGenerator getInstance() ...{
        return keygen;
    }

    public int getNextKey() ...{
        return key.getNextKey();
    }
} 

       
package      com.javapatterns.keygen.ver3;

     class      KeyInfo      ...     {
    private int keyMax;
    private int keyMin;
    private int nextKey;
    private int poolSize;

    public KeyInfo(int poolSize) ...{
        this.poolSize = poolSize;
        retrieveFromDB();
    }

    public int getKeyMax() ...{
        return keyMax;
    }

    public int getKeyMin() ...{
        return keyMin;
    }

    public synchronized int getNextKey() ...{
        if (nextKey > keyMax) ...{
            retrieveFromDB();
        }
        return nextKey++;
    }

    private void retrieveFromDB() ...{
        String sql1 = "UPDATE KeyTable SET keyValue = keyValue + "
            + poolSize + " WHERE keyName = 'PO_NUMBER'";
        String sql2 = "SELECT keyValue FROM KeyTable WHERE KeyName = 'PO_NUMBER'";
        // execute the above queries in a transaction and commit it
        // assume the value returned is 1000
        int keyFromDB = 1000;
        keyMax = keyFromDB;
        keyMin = keyFromDB - poolSize + 1;
        nextKey = keyMin;
    }
} 

       
package      com.javapatterns.keygen.ver3;

     public           class      Client      ... 
    {
    private static KeyGenerator keygen;

    public static void main(String[] args) ...{
        keygen = KeyGenerator.getInstance();
        for (int i = 0 ; i < 25 ; i++) ...{
            System.out.println("key(" + (i+1)
                + ")= " + keygen.getNextKey());
        }
    }
}


现在,这个键值生成器已经具有如下的功能:在整个系统是唯一的,能将生成的键值存储到数据库中,以便在系统重新启动时也能够继续键值的生成,而不会造成键值上的重复。


 


这本来已经足够好了,但是还有一点值得设计师考虑改进的是,一般的系统都不会只有一个键值,而是有多个键值需要生成。怎么让上面的设计适用于任意多个键值的情况呢?


 


首先,由于KeyGenerator是单例类,因此,给出多个KeyGenerator的实例并无可能,除非将之推广为多例类。


 


其次,虽然KeyGenerator是单例类,但KeyGenerator仍然可以在内部使用一个聚集管理多个键值。换言之,可以使用一个本身是单例对象的聚集对象,配合上合适的接口达到目的。


 


方案四:有缓存的多序列键生成器


 


此方案是对方案三的改进,引进了一个聚集来存储不同序列键信息的KeyInfo对象。


 


java MetaObjectHandler 主键ID 设置null java主键生成_java

package       com.javapatterns.keygen.ver4;

      import       java.util.HashMap;

      public             class       KeyGenerator       ...      {
    private static KeyGenerator keygen = new KeyGenerator();
    private static final int POOL_SIZE = 20;
    private HashMap keyList = new HashMap(10);

    private KeyGenerator() ...{}

    public static KeyGenerator getInstance() ...{
        return keygen;
    }

    public int getNextKey(String keyName) ...{
        KeyInfo keyinfo;
        if ( keyList.containsKey(keyName) ) ...{
            keyinfo = (KeyInfo) keyList.get(keyName);
            System.out.println("key found");
        } else ...{
            keyinfo = new KeyInfo(POOL_SIZE, keyName);
            keyList.put(keyName, keyinfo);
            System.out.println("new key created");
        }
        return keyinfo.getNextKey();
    }
}  
 
        
package       com.javapatterns.keygen.ver4;

      class       KeyInfo       ...      {
    private int keyMax;
    private int keyMin;
    private int nextKey;
    private int poolSize;
    private String keyName;

    public KeyInfo(int poolSize, String keyName) ...{
        this.poolSize = poolSize;
        this.keyName = keyName;
        retrieveFromDB();
    }

    public int getKeyMax() ...{
        return keyMax;
    }

    public int getKeyMin() ...{
        return keyMin;
    }

    public synchronized int getNextKey() ...{
        if (nextKey > keyMax) ...{
            retrieveFromDB();
        }
        return nextKey++;
    }

    private void retrieveFromDB() ...{
        String sql1 = "UPDATE KeyTable SET keyValue = keyValue + "
            + poolSize + " WHERE keyName = '"
            + keyName + "'";
        String sql2 = "SELECT keyValue FROM KeyTable WHERE KeyName = '"
            + keyName + "'";
        // execute the above queries in a transaction and commit it
        // assume the value returned is 1000
        int keyFromDB = 1000;
        keyMax = keyFromDB;
        keyMin = keyFromDB - poolSize + 1;
        nextKey = keyMin;
    }
}


从上面的代码可以看出,每当getNextKey()被调用时,这个方法都会根据缓冲区的大小和已经用过的键值来判断是否需要更新缓冲区。当缓冲区被更新后,KeyInfo会持有已经向数据库预定过的20个序列号码,并不断向调用者顺序提供这20个号码。等这20个序列号用完之后,KeyInfo对象就会向数据库预定后20个新号码。


当然,如果系统被重新启动,而缓冲区中的号码并没有用完的话,这些没有完的号码就不会再次被使用了。系统重新启动之后,KeyInfo对象会重新向数据库预定下面的20个号码,并向外界提供这20个号码。


 


java MetaObjectHandler 主键ID 设置null java主键生成_java

package       com.javapatterns.keygen.ver4;

      public             class       Client       ...      {
    private static KeyGenerator keygen;

    public static void main(String[] args) ...{
        keygen = KeyGenerator.getInstance();
        for (int i = 0 ; i < 25 ; i++) ...{
            System.out.println("key(" + (i+1)
                + ")= " + keygen.getNextKey("PO_NUMBER"));
        }
    }
}


方案五:应用多例模式的设计方案


 


为了能够处理多系列键值的情况,除了可以将单例模式所封装的单一状态改为聚集状态之外,还可以采用多例模式。


 


java MetaObjectHandler 主键ID 设置null java主键生成_java

package       com.javapatterns.keygen.ver5;

      import       java.util.HashMap;

      public             class       KeyGenerator       ...      {
    private static HashMap kengens = new HashMap(10);
    private static final int POOL_SIZE = 20;
    private KeyInfo keyinfo;

    private KeyGenerator() ...{}

    private KeyGenerator(String keyName) ...{
        keyinfo = new KeyInfo(POOL_SIZE, keyName);
    }

    public static synchronized KeyGenerator
            getInstance(String keyName) ...{
        KeyGenerator keygen;
        if (kengens.containsKey(keyName)) ...{
            keygen = (KeyGenerator) kengens.get(keyName);
        } else ...{
            keygen = new KeyGenerator(keyName);
            //以下语句为初晨之阳所加,原书中没有,疑似作者笔误
            keygens.put(keyName, keygen);
        }
        return keygen;
    }

    public int getNextKey() ...{
        return keyinfo.getNextKey();
    }
}


KeyInfo类的代码与方案四类似,不再重复。


 


java MetaObjectHandler 主键ID 设置null java主键生成_java

package       com.javapatterns.keygen.ver5;

      public             class       Client       ...      {
    private static KeyGenerator keygen;

    public static void main(String[] args) ...{
        keygen = KeyGenerator.getInstance("PO_NUMBER");
        for (int i = 0 ; i < 25 ; i++) ...{
             System.out.println("key(" + (i+1)
                + ")= " + keygen.getNextKey());
        }
    }
}


在上面给出的方案中,第四个和第五个方案都是具有实用价值的设计方案。


 


如果一个单例模式是一个聚集对象的话,那么这个聚集中所保存的是对其他对象的引用。一个多例模式则不同,多例对象使用一个聚集对象登记和保存自身的实例。由于这两种设计模式的相似之处,在很多情况下它们可以互换使用。