有些场景下,不是很想用数据库的自增ID,这可能会暴露出一些业务信息。
看到有些站点中URL中的ID路径都是字符串,够随机,看不出连续性。觉得这个很不错。
https://.../posts/Gzfr23
https://.../posts/ZXfz4l
https://.../posts/74fB4k
这些ID都是固定长度6位,由大写字母,小写字母,数字组成。应该是Base62编码后的数据。但是一直不知道人家的生成算法。(应该不会是Random吧?)
最近结合雪花算法,想了一个算法。对雪花ID进行Base62编码,取后6位。ID生成间隔时间在100毫秒内的话,就很随机了,看不出来连续。
实现
Base62Codec
Base62编解码
package io.springboot.paste.utils;
import java.util.regex.Pattern;
/**
* 62进制编码
*
*/
public class Base62Codec {
// 原始字符
private static char[] CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
// 62 进制
private static int SCALE = 62;
// 字符正则,最大6位长度
private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
/**
* 数字转换为62进制,如果小于0或者大于最大值,返回null
* @param num
* @return
*/
public static String encode(long num) {
if (num < 0) {
return null;
}
StringBuilder stringBuilder = new StringBuilder();
int remainder;
while (num > SCALE - 1) {
remainder = Long.valueOf(num % SCALE).intValue();
stringBuilder.append(CHARS[remainder]);
num = num / SCALE;
}
stringBuilder.append(CHARS[(int) num]);
return stringBuilder.reverse().toString();
}
/**
* 62进制转换为数值,如果字符串大于6个长度或者非法,返回-1
* @param str
* @return
*/
public static Long decode(String str) {
if (!PATTERN.matcher(str).matches()) {
return null;
}
long value = 0;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
value += (long) (indexOf(chars[i]) * Math.pow(SCALE, chars.length - i - 1));
}
return value;
}
private static int indexOf(char ch) {
int low = 0;
int high = CHARS.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
char midVal = CHARS[mid];
if (midVal < ch) {
low = mid + 1;
} else if (midVal > ch) {
high = mid - 1;
} else {
return mid;
}
}
return -(low + 1);
}
}
SnowflakeIdWorker
雪花ID算法
package io.springboot.paste.utils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
/**
* 来自推特的分布式ID生成算法
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = LocalDateTime.of(2021, 12, 31, 23, 59, 59).toEpochSecond(ZoneOffset.UTC);
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
// ==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", workerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", datacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen() {
return Instant.now().toEpochMilli();
}
// ==============================Test=============================================
// public static void main(String[] args) {
// System.out.println(Instant.now().toEpochMilli());
// SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
// for (int i = 0; i < 5; i++) {
// long id = idWorker.nextId();
// System.out.println(id);
// }
// }
}
MainTest
测试类,输出生成的ID
package io.springboot.paste.test;
import java.io.PrintStream;
import java.sql.SQLException;
import io.springboot.paste.utils.Base62Codec;
import io.springboot.paste.utils.SnowflakeIdWorker;
public class MainTest {
public static PrintStream out = System.out;
public static SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(0, 0);
public static void main(String[] args) throws Exception {
test();
}
public static void test () throws SQLException, InterruptedException {
for (;;) {
var id = snowflakeIdWorker.nextId();
var ret = Base62Codec.encode(id);
var last = ret.substring(ret.length() - 6);
var result = Base62Codec.decode(last);
out.printf("id=%d, encode=%s, last=%s, result=%d\n", id, ret, last, result);
Thread.sleep(100);
}
}
}
运行后控制台的输出。其中last
就是随机的6个字符,可以作为ID。
id=6870268805319229440, encode=8BVu32TkKmm, last=2TkKmm, result=2271822400
id=6870268805839323136, encode=8BVu332waum, last=32waum, result=2791916096
id=6870268806262947840, encode=8BVu33Vc53I, last=3Vc53I, result=3215540800
id=6870268806686572544, encode=8BVu33yHZBo, last=3yHZBo, result=3639165504
id=6870268807114391552, encode=8BVu34REeSO, last=4REeSO, result=4066984512
id=6870268807538016256, encode=8BVu34tu8au, last=4tu8au, result=4490609216
id=6870268807957446656, encode=8BVu35MI1bM, last=5MI1bM, result=4910039616
id=6870268808389459968, encode=8BVu35pWi00, last=5pWi00, result=5342052928
id=6870268808817278976, encode=8BVu36ITnGa, last=6ITnGa, result=5769871936
id=6870268809240903680, encode=8BVu36l9HP6, last=6l9HP6, result=6193496640
id=6870268809664528384, encode=8BVu37DolXc, last=7DolXc, result=6617121344
id=6870268810096541696, encode=8BVu37h3RwG, last=7h3RwG, result=7049134656
id=6870268810520166400, encode=8BVu389iw4m, last=89iw4m, result=7472759360
id=6870268810939596800, encode=8BVu38c6p5E, last=8c6p5E, result=7892189760
id=6870268811363221504, encode=8BVu394mJDk, last=94mJDk, result=8315814464
id=6870268811786846208, encode=8BVu39XRnMG, last=9XRnMG, result=8739439168
id=6870268812214665216, encode=8BVu3A0Oscq, last=A0Oscq, result=9167258176
id=6870268812638289920, encode=8BVu3AT4MlM, last=AT4MlM, result=9590882880
id=6870268813057720320, encode=8BVu3AvSFlo, last=AvSFlo, result=10010313280
id=6870268813481345024, encode=8BVu3BO7juK, last=BO7juK, result=10433937984
id=6870268813909164032, encode=8BVu3Br4pAu, last=Br4pAu, result=10861756992
id=6870268814332788736, encode=8BVu3CJkJJQ, last=CJkJJQ, result=11285381696
id=6870268814752219136, encode=8BVu3Cm8CJs, last=Cm8CJs, result=11704812096
id=6870268815175843840, encode=8BVu3DEngSO, last=DEngSO, result=12128436800
id=6870268815595274240, encode=8BVu3DhBZSq, last=DhBZSq, result=12547867200
id=6870268816027287552, encode=8BVu3EAQFrU, last=EAQFrU, result=12979880512
但是这种方式,ID可能会重复生成。也一直没想到合适的解决方案。