ID主键生成策略新增ULID算法

对比UUID,ULID更加简洁,更加安全,更加快速,最关键的事可排序。
This commit is contained in:
dh-free 2024-07-23 11:56:36 +08:00
parent d5d8c86e1c
commit eabd2212c7
5 changed files with 146 additions and 1 deletions

View File

@ -19,6 +19,7 @@ import com.mybatisflex.core.exception.FlexExceptions;
import com.mybatisflex.core.exception.locale.LocalizedFormats; import com.mybatisflex.core.exception.locale.LocalizedFormats;
import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator; import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator; import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
import com.mybatisflex.core.keygen.impl.ULIDKeyGenerator;
import com.mybatisflex.core.keygen.impl.UUIDKeyGenerator; import com.mybatisflex.core.keygen.impl.UUIDKeyGenerator;
import com.mybatisflex.core.util.StringUtil; import com.mybatisflex.core.util.StringUtil;
@ -39,6 +40,8 @@ public class KeyGeneratorFactory {
register(KeyGenerators.uuid, new UUIDKeyGenerator()); register(KeyGenerators.uuid, new UUIDKeyGenerator());
register(KeyGenerators.flexId, new FlexIDKeyGenerator()); register(KeyGenerators.flexId, new FlexIDKeyGenerator());
register(KeyGenerators.snowFlakeId, new SnowFlakeIDKeyGenerator()); register(KeyGenerators.snowFlakeId, new SnowFlakeIDKeyGenerator());
register(KeyGenerators.ulid, new ULIDKeyGenerator());
} }
@ -49,7 +52,7 @@ public class KeyGeneratorFactory {
* @return 主键生成器 * @return 主键生成器
*/ */
public static IKeyGenerator getKeyGenerator(String name) { public static IKeyGenerator getKeyGenerator(String name) {
if (StringUtil.isBlank(name)){ if (StringUtil.isBlank(name)) {
throw FlexExceptions.wrap(LocalizedFormats.KEY_GENERATOR_BLANK); throw FlexExceptions.wrap(LocalizedFormats.KEY_GENERATOR_BLANK);
} }
return KEY_GENERATOR_MAP.get(name.trim()); return KEY_GENERATOR_MAP.get(name.trim());

View File

@ -38,4 +38,9 @@ public class KeyGenerators {
*/ */
public static final String snowFlakeId = "snowFlakeId"; public static final String snowFlakeId = "snowFlakeId";
/**
* ulid 主键生成器
* {@link com.mybatisflex.core.keygen.impl.ULIDKeyGenerator}
*/
public static final String ulid = "ulid";
} }

View File

@ -0,0 +1,120 @@
/*
* Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mybatisflex.core.keygen.impl;
import com.mybatisflex.core.keygen.IKeyGenerator;
import java.util.concurrent.ThreadLocalRandom;
/**
* ULID: 对比UUID的优势在于可排序性和性能
* <p>
* 特点
* 1保证 id 生成的顺序为时间顺序越往后生成的 ID 值越大
* 2可以按照生成的时间进行排序而不需要全局协调
* 3生成速度快
* <p>
* <p>参考<a href="https://github.com/ulid/spec">Sequence</a>
*/
public class ULIDKeyGenerator implements IKeyGenerator {
private static final char[] ENCODING_CHARS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X',
'Y', 'Z'
};
private static final long TIMESTAMP_OVERFLOW_MASK = 0xFFFF_0000_0000_0000L;
private static final ThreadLocal<StringBuilder> THREAD_LOCAL_BUILDER =
ThreadLocal.withInitial(() -> new StringBuilder(26));
private long lastTimestamp = 0;
private long lastRandom = 0;
@Override
public Object generate(Object entity, String keyColumn) {
return nextId();
}
/**
* 生成一个 ULID
*
* @return ULID
*/
public String nextId() {
return generateULID(System.currentTimeMillis()).toLowerCase();
}
/**
* 生成一个严格单调的 ULID
*
* @return ULID
*/
public synchronized String nextMonotonicId() {
long timestamp = System.currentTimeMillis();
if (timestamp > lastTimestamp) {
lastTimestamp = timestamp;
lastRandom = ThreadLocalRandom.current().nextLong();
} else {
lastRandom++;
if (lastRandom == 0) {
timestamp = waitNextMillis(lastTimestamp);
lastTimestamp = timestamp;
lastRandom = ThreadLocalRandom.current().nextLong();
}
}
return generateULID(lastTimestamp, lastRandom).toLowerCase();
}
private String generateULID(long timestamp) {
return generateULID(timestamp, ThreadLocalRandom.current().nextLong());
}
private String generateULID(long timestamp, long random) {
checkTimestamp(timestamp);
StringBuilder builder = THREAD_LOCAL_BUILDER.get();
builder.setLength(0);
appendCrockford(builder, timestamp, 10);
appendCrockford(builder, random, 16);
return builder.toString();
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
private static void appendCrockford(StringBuilder builder, long value, int count) {
for (int i = (count - 1) * 5; i >= 0; i -= 5) {
int index = (int) ((value >>> i) & 0x1F);
builder.append(ENCODING_CHARS[index]);
}
}
private static void checkTimestamp(long timestamp) {
if ((timestamp & TIMESTAMP_OVERFLOW_MASK) != 0) {
throw new IllegalArgumentException("ULID does not support timestamps after +10889-08-02T05:31:50.655Z!");
}
}
}

View File

@ -46,6 +46,11 @@ public class RowKey implements Serializable {
*/ */
public static final RowKey SNOW_FLAKE_ID = RowKey.of("id", KeyType.Generator, KeyGenerators.snowFlakeId, true); public static final RowKey SNOW_FLAKE_ID = RowKey.of("id", KeyType.Generator, KeyGenerators.snowFlakeId, true);
/**
* ulid
*/
public static final RowKey ULID = RowKey.of("id", KeyType.Generator, KeyGenerators.ulid, true);
public static RowKey of(String keyColumn) { public static RowKey of(String keyColumn) {
SqlUtil.keepColumnSafely(keyColumn); SqlUtil.keepColumnSafely(keyColumn);

View File

@ -22,6 +22,7 @@ import com.mybatisflex.core.keygen.KeyGenerators;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.util.Arrays;
import java.util.stream.LongStream; import java.util.stream.LongStream;
/** /**
@ -54,4 +55,15 @@ public class IdGenTest {
Assert.assertEquals(size, LongStream.of(ids).distinct().count()); Assert.assertEquals(size, LongStream.of(ids).distinct().count());
} }
@Test
public void ULID() {
int size = 100_0000;
String[] ids = new String[size];
IKeyGenerator keyGenerator = KeyGeneratorFactory.getKeyGenerator(KeyGenerators.ulid);
for (int i = 0; i < size; i++) {
ids[i] = (String) keyGenerator.generate(null, null);
}
long distinctCount = Arrays.stream(ids).distinct().count();
Assert.assertEquals(size, distinctCount);
}
} }