有时候我们希望生成全局唯一的序列号。可以用于生成主键或者生成全局的序列号用于生成编号或者其他。这时候我们可以用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(); } }
在上面的方案中,多例模式和单例模式的缓存使用是具有实用价值的设计方案。