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

  有时候我们希望生成全局唯一的序列号。可以用于生成主键或者生成全局的序列号用于生成编号或者其他。这时候我们可以用SQL语句自行管理键值。使用一个表来存储所有的键列值。如下表所示:

key  value
PO_NUMBER 105
SE_NUMBER 2555
... ...

1.  存储方式:

  预定式键值存储:在预定一个值时首先将值更新为下一个可用值,然后查出来之后提供给客户端使用。这样万一出现中断的话顶多是浪费这几个键值。每次可以预定多个键值(也就是一个键值区间)而不是一个值,也就是更新键值的时候将键值增加大于1的数目。这么做可以避免多次访问数据库。

  记录式键值存储:也就是说,键值首先被返还给客户端,然后记录到数据库中取。这样做的缺点是:一旦系统出现中断,就可能出现客户端已经使用了一个键值,而这个键值却没有来得及存储到数据库中。在系统重启之后,系统还会从这个已经被使用过的键值开始,从而导致错误。

2.单例模式与多例模式的应用

单例模式:

  可以用单例模式来实现,整个系统只有一个序列键值管理器来管理序列号。

多例模式:

  多例类往往持有一个内蕴状态(内蕴状态是存储在享元对象内部并且不会随环境的改变而改变),多例类的每一个实例都有独特的内蕴状态。一个多例类持有一个集合对象,用来登记自身的实例,而其内蕴状态往往就是登记的键值。当客户端通过多例类的静态工厂方法请求多例类的实例时,这个工厂方法都会在集合内查询是否有这样的一个实例。如果有直接返回给客户端;如果没有就创建一个这样的实例,并登记到集合中,然后返回给客户端。如下:

键名为内蕴状态;键名和自身作为map的key和value存入集合中。

package cn.qlq.singleton;

import java.util.concurrent.ConcurrentHashMap;

public class KeyGenerator {

    // 多例模式应用
    private static ConcurrentHashMap<String, KeyGenerator> keyGenerators = new ConcurrentHashMap<>();

    private KeyGenerator(String key) {
        // 用key做处理
    }

    /**
     * 静态工厂方法提供自己的实例
     * 
     * @return
     */
    public static KeyGenerator getInstance(String keyName) {
        if (keyGenerators.containsKey(keyName)) {
            return keyGenerators.get(keyName);
        }

        KeyGenerator keyGenerator = new KeyGenerator(keyName);
        keyGenerators.put(keyName, keyGenerator);
        return keyGenerator;
    }
}

3.  单例模式应用 

1.没有数据库的情况

  首先不使用数据库,用一个成员属性模拟键值。如下:

package cn.qlq;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 单例模式的唯一值生成器
 * 
 * @author QiaoLiQiang
 * @time 2019年6月12日下午10:39:42
 */
public class KeyGenerator {

    /**
     * 存放key、value
     */
    private ConcurrentHashMap<String, Integer> values = new ConcurrentHashMap<>();

    private static KeyGenerator keyGenerator = new KeyGenerator();

    private KeyGenerator() {
        // 防止反射创建实例
        if (keyGenerator != null) {
            throw new RuntimeException("not allowed!");
        }
    }

    /**
     * 静态工厂方法提供自己的实例
     * 
     * @return
     */
    public static KeyGenerator getInstance() {
        return keyGenerator;
    }

    /**
     * 获取下一个序列制
     * 
     * @param key
     *            序列的键
     * @return 自增后的值
     */
    public synchronized int getNextKey(String key) {
        // 如果存在就加1且返回
        if (values.containsKey(key)) {
            Integer nextValue = values.get(key) + 1;
            values.put(key, nextValue);
            return nextValue;
        }

        // 初始化
        Integer startValue = 1;
        values.put(key, startValue);
        return startValue;
    }

} 

测试代码:(两个线程使用序列生成器)

package cn.qlq;

import java.util.concurrent.CountDownLatch;

public class MainClass {

    public static void main(String[] args) throws InterruptedException {
        // 制造一个闭锁
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        // 模拟使用序列生成器
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 4; j++) {
                        String key = j % 2 + "";
                        System.out.println(key + "\t" + KeyGenerator.getInstance().getNextKey(key));
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 阻塞
        countDownLatch.await();
    }

}

结果:

0 1
0 2
1 1
1 2
0 3
0 4
1 3
1 4

  上面可以满足基本的使用,但是有一个问题是系统重启之后values所以的数据会重新从0开始,显然不符合要求。所以需要数据库支持。有时候可能会想到简单的存在文件中,但是存到文件中对于集群又不太适用,所以用下面的存库操作。

2.有数据库的情况

  与上面的一样,只是数据的存储是在数据库中。

数据库表结构如下:

代码如下:

package cn.qlq.singleton;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.LinkedList;

/**
 * 模拟一个简单的连接池并且执行SQL
 * 
 * @author Administrator
 *
 */
public class JDBCUtils {

    private static String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    private static String DB_URL = "jdbc:mysql://localhost:3306/test1";
    private static String USER = "sa";
    private static String PASS = "123456";
    private static LinkedList<Connection> connections = new LinkedList<>();

    public static Object executeSQL(String sql, Object... params) {
        Connection connection = null;
        try {
            connection = getConnection();
            PreparedStatement statement = connection.prepareStatement(sql);

            // 设置参数(注意JDBC的所有下标从0开始)
            if (params != null && params.length > 0) {
                for (int i = 0, length_1 = params.length; i < length_1; i++) {
                    Object param = params[i];
                    if (param instanceof String) {
                        statement.setString(i + 1, (String) param);
                    } else if (param instanceof Long || param instanceof Integer) {
                        statement.setLong(i + 1, Long.valueOf(param.toString()));
                    }
                }
            }
            
            // 查询
            if (sql.contains("select")) {
                ResultSet result = statement.executeQuery();

                // 所有的列信息(总列数、类型以及名称)
                /*ResultSetMetaData metaData = result.getMetaData();
                int columnCount = metaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = metaData.getColumnName(i);
                    String columnClassName = metaData.getColumnClassName(i);
                    System.out.println(columnName);
                    System.out.println(columnClassName);
                }*/

                // 遍历每一行的数据
                while (result.next()) {
                    return result.getInt(1);
                }

                return -1;
            }

            // 更新
            statement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
            // 记录日志
        } finally {
            if (connection != null) {
                releaseConnection(connection);
            }
        }
        return null;
    }

    private static Connection getConnection() throws ClassNotFoundException, SQLException {
        if (connections.size() == 0) {
            initConnections();
        }

        return connections.removeFirst();
    }

    private static void releaseConnection(Connection connection) {
        connections.add(connection);
    }

    private static void initConnections() {
        try {
            Class.forName(JDBC_DRIVER);
            for (int i = 0; i < 5; i++) {
                Connection conn = (Connection) DriverManager.getConnection(DB_URL, USER, PASS);
                conn.setAutoCommit(true);
                connections.add(conn);
            }
        } catch (Exception e) {
            // 记录日志
        }
    }

}
package cn.qlq.singleton;

public class KeyGenerator {

    private static String QUERY_SQL = "select idValue from ids where idType = ?";

    private static String UPDATE_SQL = "update ids set idValue = idValue + 1 where idType = ?";

    private static String INSERT_SQL = "insert into ids(idValue,idType) values(?,?)";

    private static KeyGenerator keyGenerator = new KeyGenerator();

    private KeyGenerator() {
        // 防止反射创建实例
        if (keyGenerator != null) {
            throw new RuntimeException("not allowed!");
        }
    }

    /**
     * 静态工厂方法提供自己的实例
     * 
     * @return
     */
    public static KeyGenerator getInstance() {
        return keyGenerator;
    }

    /**
     * 获取下一个序列制
     * 
     * @param key
     *            序列的键
     * @return 自增后的值
     */
    public synchronized int getNextKey(String key) {
        // 如果存在就加1且返回
        Object result = JDBCUtils.executeSQL(QUERY_SQL, key);
        if (result != null && (Integer) result > -1) {
            Integer nextValue = (Integer) result + 1;
            JDBCUtils.executeSQL(UPDATE_SQL, key);
            return nextValue;
        }

        // 初始化
        Integer startValue = 1;
        JDBCUtils.executeSQL(INSERT_SQL, startValue, key);
        return startValue;
    }

}

测试代码:

package cn.qlq.singleton;

import java.util.concurrent.CountDownLatch;

public class MainClass {

    public static void main(String[] args) throws InterruptedException {
        // 制造一个闭锁
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        // 模拟使用序列生成器
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 4; j++) {
                        String key = "测试序列号";
                        System.out.println(key + "\t" + KeyGenerator.getInstance().getNextKey(key));
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 阻塞
        countDownLatch.await();
    }

}

 

3.键值的缓存方案

  上面的操作每一次都进行数据库的访问与更新操作,太频繁了。不如每次从数据库取的时候多取出来一些值,并缓存起来。这么做可以减少数据库的频繁操作。

  与上面方案不同的是每次idValue不是自增1,变成一个区间,比如自增20。为了缓存所有与键有关的信息,特地引入一个KeyInfo类。

  KeyInfo采用多例模式。一个类型对应一个KeyInfo。

如下:从数据库取出一定的值缓存起来。

package cn.qlq.singleton;

public class KeyInfo {

    // 最大值
    private long maxKey;

    // 最小值
    private long minKey;

    // 下个值
    private long nextKey;

    // 池子大小
    private int poolSize;

    // 类型
    private String idType;

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

    /**
     * 从数据库取值并重新初始化属性
     */
    public void retrieveFromDB() {
        String query_sql = "select idValue from ids where idType = ?";
        // 首先判断对应的类型是否存在,不存在插入一个初始值0
        Integer result = (Integer) JDBCUtils.executeSQL(query_sql, idType);
        if (result.equals(-1)) {
            String insert_sql = "insert into ids(idValue,idType) values(?,?)";
            JDBCUtils.executeSQL(insert_sql, 0, idType);
        }

        // 先更新再取值
        String update_sql = "update ids set idValue = idValue + " + poolSize + " where idType = ?";
        JDBCUtils.executeSQL(update_sql, idType);
        result = (Integer) JDBCUtils.executeSQL(query_sql, idType);

        maxKey = result;
        minKey = result - poolSize + 1;
        nextKey = minKey;
    }

    public long getNextKey() {
        if (nextKey > maxKey) {
            retrieveFromDB();
        }

        return nextKey++;
    }

    public long getMaxKey() {
        return maxKey;
    }

    public void setMaxKey(long maxKey) {
        this.maxKey = maxKey;
    }

    public long getMinKey() {
        return minKey;
    }

    public void setMinKey(long minKey) {
        this.minKey = minKey;
    }

    public void setNextKey(long nextKey) {
        this.nextKey = nextKey;
    }

    public int getPoolSize() {
        return poolSize;
    }

    public void setPoolSize(int poolSize) {
        this.poolSize = poolSize;
    }

    public String getIdType() {
        return idType;
    }

    public void setIdType(String idType) {
        this.idType = idType;
    }

} 

KeyGenerator 内部维护一个集合,集合中保存的是KeyInfo对象的引用。

package cn.qlq.singleton;

import java.util.concurrent.ConcurrentHashMap;

public class KeyGenerator {

    private static KeyGenerator keyGenerator = new KeyGenerator();

    // 缓存相关
    private static final int POOL_SIZE = 20;
    private static ConcurrentHashMap<String, KeyInfo> KEYINFOS = new ConcurrentHashMap<>();

    private KeyGenerator() {
        // 防止反射创建实例
        if (keyGenerator != null) {
            throw new RuntimeException("not allowed!");
        }
    }

    /**
     * 静态工厂方法提供自己的实例
     * 
     * @return
     */
    public static KeyGenerator getInstance() {
        return keyGenerator;
    }

    /**
     * 获取下一个序列制
     * 
     * @param key
     *            序列的键
     * @return 自增后的值
     */
    public synchronized int getNextKey(String key) {
        KeyInfo keyInfo = null;
        if (KEYINFOS.containsKey(key)) {
            keyInfo = KEYINFOS.get(key);
        } else {
            keyInfo = new KeyInfo(POOL_SIZE, key);
            keyInfo.retrieveFromDB();
            KEYINFOS.put(key, keyInfo);
        }

        // 委托给KeyInfo
        return (int) keyInfo.getNextKey();
    }

}

  从源码看出,每当getNextKey()被调用时,会先根据nextKey与maxKey值大小判断是否需要更新缓存区。如果系统重启并且缓存区的号码没有被用完,这些号码不会被再次使用。

4. 多例模式的应用

  多例模式允许一个类有多个实例,这些实例各自有不同的内蕴状态。如下面的KeyGenerator就是以keyInfo作为其内蕴状态。内部的集合登记和保存自身的实例。

  客户端可以用静态工厂方法获取其所需要的KeyGenerator。这个静态工厂方法首先检查其集合里面是否有所需要的生成器,如果没有就创建一个并添加到集合中,在创建的同时创建keyInfo;如果有就直接返回。

package cn.qlq.singleton;

import java.util.concurrent.ConcurrentHashMap;

public class KeyGenerator {

    // 缓存相关
    private static final int POOL_SIZE = 20;
    private KeyInfo keyInfo;

    // 多例模式应用
    private static ConcurrentHashMap<String, KeyGenerator> keyGenerators = new ConcurrentHashMap<>();

    private KeyGenerator(String key) {
        keyInfo = new KeyInfo(POOL_SIZE, key);
        keyInfo.retrieveFromDB();
    }

    /**
     * 静态工厂方法提供自己的实例
     * 
     * @return
     */
    public static KeyGenerator getInstance(String keyName) {
        if (keyGenerators.containsKey(keyName)) {
            return keyGenerators.get(keyName);
        }

        KeyGenerator keyGenerator = new KeyGenerator(keyName);
        keyGenerators.put(keyName, keyGenerator);
        return keyGenerator;
    }

    /**
     * 获取下一个序列制
     * 
     * @param key
     *            序列的键
     * @return 自增后的值
     */
    public synchronized int getNextKey() {
        // 委托给KeyInfo
        return (int) keyInfo.getNextKey();
    }

    public KeyInfo getKeyInfo() {
        return keyInfo;
    }

    public void setKeyInfo(KeyInfo keyInfo) {
        this.keyInfo = keyInfo;
    }

    // 内部类
    private class KeyInfo {

        // 最大值
        private long maxKey;

        // 最小值
        private long minKey;

        // 下个值
        private long nextKey;

        // 池子大小
        private int poolSize;

        // 类型
        private String idType;

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

        /**
         * 从数据库取值并重新初始化属性
         */
        public void retrieveFromDB() {
            String query_sql = "select idValue from ids where idType = ?";
            // 首先判断对应的类型是否存在,不存在插入一个初始值0
            Integer result = (Integer) JDBCUtils.executeSQL(query_sql, idType);
            if (result.equals(-1)) {
                String insert_sql = "insert into ids(idValue,idType) values(?,?)";
                JDBCUtils.executeSQL(insert_sql, 0, idType);
            }

            // 先更新再取值
            String update_sql = "update ids set idValue = idValue + " + poolSize + " where idType = ?";
            JDBCUtils.executeSQL(update_sql, idType);
            result = (Integer) JDBCUtils.executeSQL(query_sql, idType);

            maxKey = result;
            minKey = result - poolSize + 1;
            nextKey = minKey;
        }

        public long getNextKey() {
            if (nextKey > maxKey) {
                retrieveFromDB();
            }

            return nextKey++;
        }

        public long getMaxKey() {
            return maxKey;
        }

        public void setMaxKey(long maxKey) {
            this.maxKey = maxKey;
        }

        public long getMinKey() {
            return minKey;
        }

        public void setMinKey(long minKey) {
            this.minKey = minKey;
        }

        public void setNextKey(long nextKey) {
            this.nextKey = nextKey;
        }

        public int getPoolSize() {
            return poolSize;
        }

        public void setPoolSize(int poolSize) {
            this.poolSize = poolSize;
        }

        public String getIdType() {
            return idType;
        }

        public void setIdType(String idType) {
            this.idType = idType;
        }
    }

}

  在这个设计里面KeyInfo类与上面设计一样,只是在这里作为内部类使用。

  下面是客户端代码,在调用工厂方法时需要传入序列建的名称作为参数,静态工厂根据键名反应对应的KeyGenerator实例;而在调用getNextKey()时无需传入参数。(其内部类KeyInfo自己维护了键名称)。

package cn.qlq.singleton;

import java.util.concurrent.CountDownLatch;

public class MainClass {

    public static void main(String[] args) throws InterruptedException {
        // 制造一个闭锁
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        // 模拟使用序列生成器
        for (int i = 0; i < 1; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 20; j++) {
                        String key = "测试序列号2";
                        System.out.println(key + "\t" + KeyGenerator.getInstance(key).getNextKey());
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }

        // 阻塞
        countDownLatch.await();
    }

}

  在上面的方案中,多例模式和单例模式的缓存使用是具有实用价值的设计方案。

猜你喜欢

转载自www.cnblogs.com/qlqwjy/p/11011679.html