一个简单的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合适规范性的问题;然后感觉自己能力有限,总是考虑不周到,需要继续学习;
如您在阅读时遇到任何疑问也欢迎留言,感觉这是个有趣的东西,所以贴出来,总之欢迎多交流。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)