本文介绍如何使用最简单的方法产生一个全局唯一的流水号,支持集群,性能可靠,并且经过实际的应用
唯一流水号的格式为当前系统时间+当前服务器编号+并发序列号,长度最短可为17位,每毫秒支持生成多个并且支持集群部署
废话不多说,直接上demo,以下demo只需要把连接数据库的工具类Dbutil换成你自己的就可以直接使用了,demo运行成功后需要注意下文中的注意事项
package com.helianxiaowu.utils;import com.newcapec.dao.SysClusterNodeDao;import org.apache.commons.lang.StringUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.net.InetAddress;import java.net.UnknownHostException;import java.util.Date;import java.util.Map;import java.util.Random;/*** @title 唯一流水号生成器* @desc* @author helianxiaowu* @date 2020/1/18 下午 11:48*/public class PrimaryGenerator {private static final Logger logger = LoggerFactory.getLogger(PrimaryGenerator.class);// 单例模式private static PrimaryGenerator primaryGenerator = new PrimaryGenerator();private PrimaryGenerator() {}/*** 对外提供单例对象* @return*/public static PrimaryGenerator getInstance() {return primaryGenerator;}/*** @title 生成一个唯一编号* @desc* @author W.jw* @date 2018/5/9 14:23*/public synchronized String make() {// 获取当前系统的时间戳Date now = new Date();long timestamp = now.getTime();/*** 判断当前时间戳和上一次产生流水号的时间是否相等* 如果不相等,可以使用当前时间戳* 如果相等,判断并发数是否达到最大值* 如果达到最大值,需要等待下一个毫秒作为新的时间戳* 如果没有达到最大值,则可以使用当前序列值*/if (this.lastTime == timestamp) {serial = serial + 1 ;if (serial == maxSerial) {timestamp = this.tilNextMillis(this.lastTime);}} else {serial = 0;}// 将当前系统时间赋值给lastTime,方便下一次做判断this.lastTime = timestamp;// 保证序列号是3位,产生的订单号长度一样。也可以不补位,订单号长度会短点String.format("%03d", serial);// 把当前系统时间格式化为yyMMddHHmmssSSSString time = DateUtil.dateToString(now, "yyMMddHHmmssSSS");// 唯一流水号 = yyMMddHHmmssSSS+当前服务器唯一编号+序列号StringBuilder sb = new StringBuilder(time).append(getMachineId()).append(serial);return sb.toString();}/*** 等待下一个毫秒* @param currentTime 当前系统时间* @return*/private long tilNextMillis(long currentTime) {long timestamp = System.currentTimeMillis();while (timestamp <= currentTime) {timestamp = System.currentTimeMillis();}return timestamp;}/*** 获取当前服务器的编号,固定长度为2位* 从数据库中获取当前服务器的ip在服务器中对应的唯一id* 如果id存在,则使用id作为唯一编号* 如果id不存在,获取当前服务器的ip并插入数据库得到id** @return*/private String getMachineId() {if (this.machineId != null) {return this.machineId;}/*** 获取当前服务器的ip,如果没有获取到ip则随机返回一个两位数*/String ip = this.getIp();if (ip == null) {// 随机返回两位数并且加上100,保证随机返回的数值和数据库中的id值不冲突return String.format("%02d", new Random().nextInt(100) + 100);}/*** 从数据库中获取当前服务器的ip对应的id值* 如果没有则插入一条记录并获取id值* 如果有则直接使用*/Map<String, Object> nodeMap = DbUtil.getByIp(ip);if (nodeMap == null || nodeMap.size() == 0) {try {DbUtil.save(ip);nodeMap = DbUtil.getByIp(ip);} catch (Exception e) {logger.error("插入集群节点信息失败:{}" , e);return String.format("%02d", new Random().nextInt(100) + 100);}}this.machineId = String.format("%02d", nodeMap.get("id"));return this.machineId;}/*** 获取当前服务器ip* @return*/private static String getIp() {InetAddress address = null;try {address = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();return null;}return address.getHostAddress();}/*** 最后一次生成订单号的时间,格式yyMMddHHmmssSSS*/private long lastTime = 0;/*** 当前服务器在集群内的编号*/private String machineId;/*** 并发序列号*/private long serial = 0L;/*** 允许并发的最大值*/private final int maxSerial = 999;}
注意事项
1.本demo需要保证单例运行,否则会产生重复的流水号。保证单例模式的方法有很多种,如:使用单例模式或把demo交由spring管理,demo中使用的是单例模式
2.序列号的最大值不可以无限大,需要考虑服务器的配置。建议使用999,每毫秒产生1000个流水号,一秒就可以产生60*1000个流水号,并发性已经很好了
3.本demo中机器编号限制为两位,最大支持99个服务器,如果你的集群中服务器数量超过99个,则需要把编号的位数扩大。当然,服务器的数量不足10个的话,也可以调小,流水号的长度也会短点
扫码关注我的公众号
每天进步一点点,不负光阴,度几扶人















