序列鍵生成器及單例多例模式

  有時候咱們但願生成全局惟一的序列號。能夠用於生成主鍵或者生成全局的序列號用於生成編號或者其餘。這時候咱們能夠用SQL語句自行管理鍵值。使用一個表來存儲全部的鍵列值。以下表所示:java

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

1.  存儲方式:

  預約式鍵值存儲:在預約一個值時首先將值更新爲下一個可用值,而後查出來以後提供給客戶端使用。這樣萬一出現中斷的話頂可能是浪費這幾個鍵值。每次能夠預約多個鍵值(也就是一個鍵值區間)而不是一個值,也就是更新鍵值的時候將鍵值增長大於1的數目。這麼作能夠避免屢次訪問數據庫。mysql

  記錄式鍵值存儲:也就是說,鍵值首先被返還給客戶端,而後記錄到數據庫中取。這樣作的缺點是:一旦系統出現中斷,就可能出現客戶端已經使用了一個鍵值,而這個鍵值卻沒有來得及存儲到數據庫中。在系統重啓以後,系統還會從這個已經被使用過的鍵值開始,從而致使錯誤。sql

 

2.單例模式與多例模式的應用

單例模式:數據庫

  能夠用單例模式來實現,整個系統只有一個序列鍵值管理器來管理序列號。緩存

多例模式:ide

  多例類每每持有一個內蘊狀態(內蘊狀態是存儲在享元對象內部而且不會隨環境的改變而改變),多例類的每個實例都有獨特的內蘊狀態。一個多例類持有一個集合對象,用來登記自身的實例,而其內蘊狀態每每就是登記的鍵值。當客戶端經過多例類的靜態工廠方法請求多例類的實例時,這個工廠方法都會在集合內查詢是否有這樣的一個實例。若是有直接返回給客戶端;若是沒有就建立一個這樣的實例,並登記到集合中,而後返回給客戶端。以下:測試

鍵名爲內蘊狀態;鍵名和自身做爲map的key和value存入集合中。this

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.沒有數據庫的狀況

  首先不使用數據庫,用一個成員屬性模擬鍵值。以下:spa

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的全部下標從1開始)
            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();
    }

}

 

總結:

  在上面的方案中,多例模式和單例模式的緩存使用是具備實用價值的設計方案。

  在實際工做中用的是多例模式的方式,並且不帶緩存,由於系統須要根據不一樣的key生成全局的編號,並且不能中斷,因此沒有采用緩存方案。我的傾向於單例模式+緩存的實現更加高效。

相關文章
相關標籤/搜索