`

序列键生成器与单例及多例模式

    博客分类:
  • java
阅读更多
在一个关系数据库中,所有的数据都是存储在表里,而每一个表都有一个主键(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都会重新初始化,这就会造成键值的重复。为了避免这一点,就必须将每次的键值存储起来,以便一旦系统和重启时,可以将这个键值取出,并在这个值的基础上重新开始。

方案二:有数据库的情况

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()方法,向数据库查询键值。

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对象。

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个号码。

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"));
          }
      }
}

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

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

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类的代码与方案四类似,不再重复。

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());
          }
      }
}

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

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

分享到:
评论

相关推荐

    Java与模式

    实用的 java 设计原则 和 设计模式,阅读完本书,可以了解java的api背后的设计思想和理念。1000多页巨著,只有超值,没有之一。...第18章 专题:序列键生成器与单例及多例模式 第19章 建造\(Builder\)模式

    Java与模式.清晰.rar

    第18章 专题:序列键生成器与单例及多例模式 第19章 建造(Builder)模式 第20章 原始模型(Prototype)模式 第21章 专题:JavaBean的“冷藏”和“解冻” 第四部分 第22章 适配器(Adapter)模式[1]

    JAVA与模式

    com.javapatterns.singleton.mxrecord 专题:单例模式与MX记录 com.javapatterns.multilingual 专题:多例模式与多语言支持 com.javapatterns.keygen 专题:序列键生成器与单例及多例模式 ...

    源码:阎宏设计模式光盘

    com.javapatterns.keygen 专题:序列键生成器与单例及多例模式 com.javapatterns.liskov 里氏代换原则 com.javapatterns.lod 迪米特法则 com.javapatterns.mediator 调停者模式 com.javapatterns.memento 备忘录...

    java与模式chp18[归类].pdf

    《Java与模式》第18章探讨了在Java开发中如何处理序列键生成以及应用单例和多例模式。...单例模式用于确保序列键管理器的唯一性,而多例模式则适用于需要多个独立实例的情况,两者结合可以有效应对复杂系统的需求。

    设计模式之单例模式(结合工厂模式)

    将单例模式与工厂模式结合,可以创建一个单例工厂,这个工厂类负责生成单例对象。这样做有两个主要好处:一是隐藏了单例的实现细节,使得代码更加整洁,降低了耦合度;二是可以通过工厂方法扩展新的实现,如果将来...

    Java与模式(含示例代码)

    第18 章 专题:序列键生成器与单例及多 例模式...... 283 18.1 问题.............................................283 18.2 将单例模式应用到系统设计中.285 18.3 将多例模式应用到系统设计中.297 18.4 讨论..........

    java与模式

    18:序列健生成器与单例及多例模式;19:建筑模式;20:原始模型模式;21:javabean冷藏和解冻;22:适配器模式;23:缺省适配器模式;24:合成模式;25:装饰模式;26:设计模式在JAVA I/O设计原则;28:代理模式;....

    单例模式详解~~单例模式详解~~

    然而,如果涉及类加载器或跨JVM的场景,单例模式的实现就需要更复杂的策略,例如使用`序列化`和`克隆`时需要特殊处理,防止生成额外的实例。另外,如果要考虑服务集群或分布式系统,可能需要采用分布式单例,例如...

    单例模式(singleton)

    在描述中提到的随机数生成器就是一个典型的单例应用,它确保在整个应用程序运行期间,只有一个随机数生成器实例,所有客户端都通过这个实例来生成随机数。 实现单例模式的关键在于防止其他对象通过常规构造函数创建...

    单例设计模式的优缺点和设计思想

    - 当需要生成唯一序列号的环境,如ID生成器、序列号管理器等,单例模式可以确保每个生成的序列号都是唯一的。 - 在整个项目中需要一个共享访问点或共享数据时,单例模式可以提供统一的访问入口,简化数据访问的流程...

    设计模式_创建型_单例模式.md

    ## 单例模式的分类与实现 ### 饿汉式单例(Eager Initialization) 这是最简单的一种实现方式。类加载时就完成了初始化,对于多线程环境,饿汉式单例类并不安全。如果这个单例加载比较耗时,或者不需要延迟加载,...

    SQLServer数据库实体类生成器

    5. **设计模式**:分析源码中的设计模式,例如工厂模式、单例模式等,这些模式在实际开发中经常被用到。 总的来说,SQLServer数据库实体类生成器是一个实用的开发工具,不仅可以提高开发效率,还为学习数据库操作和...

    随机数产生器

    随机数生成器(Random Number Generator,RNG)是计算机程序,它们能够生成看似无规律、不可预测的数字序列。在实际应用中,这些“随机”数通常是伪随机的,因为它们基于一定的算法生成,而真正的随机性通常难以在...

    韩顺平_Java设计模式笔记.docx

    本资源摘要信息是对《韩顺平_Java设计模式笔记.docx》的总结和分析,该笔记涵盖了 Java 设计模式的基础知识、设计模式的七大原则、原型设计模式、解释器设计模式、单例设计模式等内容,并对每个设计模式的原理、实现...

    GoF设计模式Java版

    1. **创建型模式**:包括工厂方法模式、抽象工厂模式、单例模式、建造者模式和原型模式。 2. **结构型模式**:包括适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式和代理模式。 3. **行为型模式**...

    DesignModel-master_设计模式_raysits_protobuf_zip_

    常见的设计模式有单例模式、工厂模式、观察者模式、装饰器模式、策略模式等。这些模式帮助开发者在面对复杂性时,保持代码的可读性、可维护性和可扩展性。例如,单例模式确保一个类只有一个实例,而工厂模式提供了一...

    JAVA生成订单号(日期+流水号)

    通过上述步骤,我们可以在Java中实现一个基于日期和流水号的订单号生成器。对于提供的`java订单号(时间加流水号).txt`文件,可能包含了具体的代码实现或使用示例,你可以查阅该文件以获取更详细的实现细节。在实际...

Global site tag (gtag.js) - Google Analytics