有時候咱們但願生成全局惟一的序列號。能夠用於生成主鍵或者生成全局的序列號用於生成編號或者其餘。這時候咱們能夠用SQL語句自行管理鍵值。使用一個表來存儲全部的鍵列值。以下表所示:java
key | value |
PO_NUMBER | 105 |
SE_NUMBER | 2555 |
... | ... |
預約式鍵值存儲:在預約一個值時首先將值更新爲下一個可用值,而後查出來以後提供給客戶端使用。這樣萬一出現中斷的話頂可能是浪費這幾個鍵值。每次能夠預約多個鍵值(也就是一個鍵值區間)而不是一個值,也就是更新鍵值的時候將鍵值增長大於1的數目。這麼作能夠避免屢次訪問數據庫。mysql
記錄式鍵值存儲:也就是說,鍵值首先被返還給客戶端,而後記錄到數據庫中取。這樣作的缺點是:一旦系統出現中斷,就可能出現客戶端已經使用了一個鍵值,而這個鍵值卻沒有來得及存儲到數據庫中。在系統重啓以後,系統還會從這個已經被使用過的鍵值開始,從而致使錯誤。sql
單例模式:數據庫
能夠用單例模式來實現,整個系統只有一個序列鍵值管理器來管理序列號。緩存
多例模式: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; } }
首先不使用數據庫,用一個成員屬性模擬鍵值。以下: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開始,顯然不符合要求。因此須要數據庫支持。有時候可能會想到簡單的存在文件中,可是存到文件中對於集羣又不太適用,因此用下面的存庫操做。
與上面的同樣,只是數據的存儲是在數據庫中。
數據庫表結構以下:
代碼以下:
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(); } }
上面的操做每一次都進行數據庫的訪問與更新操做,太頻繁了。不如每次從數據庫取的時候多取出來一些值,並緩存起來。這麼作能夠減小數據庫的頻繁操做。
與上面方案不一樣的是每次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值大小判斷是否須要更新緩存區。若是系統重啓而且緩存區的號碼沒有被用完,這些號碼不會被再次使用。
多例模式容許一個類有多個實例,這些實例各自有不一樣的內蘊狀態。以下面的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生成全局的編號,並且不能中斷,因此沒有采用緩存方案。我的傾向於單例模式+緩存的實現更加高效。