一个简单的ID生成器,请帮忙看看还有那些毛病

昨天的办法反复跑,当线程数增加后还是不行呀,继续优化,改用StampedLock实现.

/**
 * @Author PeterShen
 * @Date 2022/11/02 9:32
 * @Description 构建唯一ID:当前时间的秒数(12)+随机(2)+IP信息(6)+自增(1-7)
 * @Version 1.1
 */
public class IdBuildHelper {
    /**
     * 计数的最大值 1千万(最大七位)
     * 防止计数爆掉
     */
    static final int MAX_COUNT = 9989999;

    static StampedLock sl = new StampedLock();
    /**
     * 随机数生成
     */
    static Random random = new Random();
    static SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmmss");
    /**
     * 每秒自增计数计数器
     */
    static volatile AtomicInteger counter = new AtomicInteger(0);
    /**
     * 当前的秒数,必须存储为秒级,不能是毫秒级,防止频繁换秒操作
     */
    static volatile AtomicLong currentSecond = new AtomicLong(CurrentTimeMillisClock.getInstance().now() /1000) ;
    //服务的端口
    static int serverPort = 80;
    //服务的IP地址
    static String serviceIp = null;

    /**
     * 根据日期构建一个唯一ID
     * 由日期(12位)+两位随机+六位IP信息+自增(1-7位) 总位数:21-27位
     * @return
     */
    public static String buildId() {
        long second = currentSecond.get();;
        int i = counter.incrementAndGet();;
        long stamp = sl.tryOptimisticRead();
        if(sl.validate(stamp)){
            stamp = sl.readLock();
            try {
                //加锁获取一份生成ID因子
                second = currentSecond.get();
                i = counter.incrementAndGet();
            }finally {
                sl.unlock(stamp);
            }
        }

        //为了避免重复,日期正常只能往前推进,如果改了过去的时间,一开始拒绝重置计数,直到计数达到最大值,不得已才重置
        if (second < CurrentTimeMillisClock.getInstance().now() /1000 || i > MAX_COUNT) {
            stamp = sl.writeLock();
            try {
                long secondChange = currentSecond.get();
                int iChange = counter.get();
                Long nowChange = CurrentTimeMillisClock.getInstance().now() /1000;
                //再去获取一次最新值
                if (secondChange < nowChange || iChange > MAX_COUNT) {
                    //换秒
                    if (secondChange < nowChange) {
                       if( currentSecond.compareAndSet(secondChange,nowChange)){
                           //重置计数
                           counter.compareAndSet(iChange,0);
                       }
                    } else {
                        if (iChange > MAX_COUNT) {
                            //重置计数
                            if( counter.compareAndSet(iChange,0)){
                                //计数满,强制换秒
                                currentSecond.incrementAndGet();
                            }
                        }
                    }
                }
            }finally {
                sl.unlockWrite(stamp);
            }
        }
        //根据因子生成
        return formatter.format(second*1000) + String.format("%02d", random.nextInt(99)) + getIpStr() + i;
    }

    /**
     * 获取IP后面两段(6位)
     * 注意:这里有个假设,多个服务实例会不是在同一个网段
     *
     * @return
     */
    private static String getIpStr() {
        if (serviceIp == null) {
            try {
                InetAddress address = InetAddress.getLocalHost();
                String ip = address.getHostAddress();
                //获取IP后面两段
                String[] split = ip.split("\\.");
                if (split.length > 0) {
                    int n = Integer.valueOf(split[split.length - 2] + split[split.length - 1]);
                    //与服务端口做异或运算,隐藏真实IP
                    n = (n ^ serverPort) << 1;
                    serviceIp = String.format("%06d", n);
                } else {
                    serviceIp = "";
                }

            } catch (UnknownHostException e) {
                serviceIp = "";
            }
        }
        return serviceIp;
    }

    /**
     * 设置服务器执行端口:默认80
     * 为了防止同一个IP 用不同端口启用多个实例,建议填入
     */
    public static void setServerPort(int port) {
        IdBuildHelper.serverPort = port;
    }

}

 

 

业务系统经常需要生成各种唯一ID,想到UUID、雪花算法等;

UUID字符没有含义,掏出来给客户看,比较的很丑;

雪花算法是64位的,小业务用户感觉太长了,有点不满意;

琢磨了好些天,自己写了一个,完成初步测试没有重复;把代码贴出来请各位程序大佬指教;欢迎留意给出各种意见;

代码如下:

/**
* @Author PeterShen
* @Date 2022/11/02 9:32
* @Description 构建唯一ID:当前时间的秒数(12)+随机(2)+IP信息(6)+自增(1-7)
* @Version 1.1
*/
public class IdBuildHelper {
/**
* 计数的最大值 1千万(最大七位)
* 防止计数爆掉
*/
static final int MAX_COUNT = 9989999;
static final Object lockObject = new Object();
/**
* 随机数生成
*/
static Random random = new Random();
static SimpleDateFormat formatter = new SimpleDateFormat("yyMMddHHmmss");
/**
* 每秒自增计数计数器
*/
static volatile AtomicInteger counter = new AtomicInteger(0);
/**
* 当前的秒数,必须存储为秒级,不能是毫秒级,防止频繁换秒操作
*/
static volatile long currentSecond = CurrentTimeMillisClock.getInstance().now() /1000;
//服务的端口
static int serverPort = 80;
//服务的IP地址
static String serviceIp = null;

/**
* 根据日期构建一个唯一ID
* 由日期(12位)+两位随机+六位IP信息+自增(1-7位) 总位数:21-27位
* @return
*/
public static String buildId() {
long second;
int i;
synchronized (lockObject){
//加锁获取一份生成ID因子
second = currentSecond;
i = counter.incrementAndGet();
}

//根据当前日期判断是否需要换秒,
Long integer = CurrentTimeMillisClock.getInstance().now() /1000;
//为了避免重复,日期正常只能往前推进,如果改了过去的时间,一开始拒绝重置计数,直到计数达到最大值,不得已才重置
if (currentSecond < integer || counter.get() > MAX_COUNT) {
synchronized (lockObject){
if (currentSecond < integer || counter.get() > MAX_COUNT) {
//重置计数
counter.set(0);
//换秒
if (currentSecond < integer) {
currentSecond = integer;
} else if (counter.get() > MAX_COUNT) {
//计数满,强制换秒
currentSecond++;
}
}
}
}
//根据因子生成
return formatter.format(second*1000) + String.format("%02d", random.nextInt(99)) + getIpStr() + i;
}

/**
* 获取IP后面两段(6位)
* 注意:这里有个假设,多个服务实例会不是在同一个网段
*
* @return
*/
private static String getIpStr() {
if (serviceIp == null) {
try {
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
//获取IP后面两段
String[] split = ip.split("\\.");
if (split.length > 0) {
int n = Integer.valueOf(split[split.length - 2] + split[split.length - 1]);
//与服务端口做异或运算,隐藏真实IP
n = (n ^ serverPort) << 1;
serviceIp = String.format("%06d", n);
} else {
serviceIp = "";
}

} catch (UnknownHostException e) {
serviceIp = "";
}
}
return serviceIp;
}

/**
* 设置服务器执行端口:默认80
* 为了防止同一个IP 用不同端口启用多个实例,建议填入
*/
public static void setServerPort(int port) {
IdBuildHelper.serverPort = port;
}

}

对应的测试代码:


@Test
void multiThreadTest() throws InterruptedException {
Set<String> codeSet = new ConcurrentHashSet<>(5000000);
Thread[] threads = new Thread[50];
for (int i = 0; i < 50; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println( Thread.currentThread().getName()+"开始执行");
for (int j = 0; j < 100000; j++) {
String s = IdBuildHelper.buildId();
boolean add = codeSet.add(s);
if(!add){
System.out.println(Thread.currentThread().getName()+"发生重复"+ s+":"+j);
}
}
System.out.println("内容数:"+ codeSet.size());
System.out.println( Thread.currentThread().getName()+"执行完成");
}
});
t.setName("线程"+i);
threads[i] = t;
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
if(codeSet.size()<5000000){
System.out.println("生成异常");
}
codeSet.clear();
}

感受:

几行简单的代码真的琢磨了好久,反复改了好多次;主要反复的点是在:1.多线程重复问题;2.生成性能问题;3.生成的ID合适规范性的问题;然后感觉自己能力有限,总是考虑不周到,需要继续学习;

如您在阅读时遇到任何疑问也欢迎留言,感觉这是个有趣的东西,所以贴出来,总之欢迎多交流。

 

posted @ 2022-11-03 11:45  为爱痴狂  阅读(56)  评论(0编辑  收藏  举报