算法
昨天的办法反复跑,当线程数增加后还是不行呀,继续优化,改用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合适规范性的问题;然后感觉自己能力有限,总是考虑不周到,需要继续学习;
如您在阅读时遇到任何疑问也欢迎留言,感觉这是个有趣的东西,所以贴出来,总之欢迎多交流。