在当今的数字化世界中,无论是在社交媒体平台、电子商务网站,还是在企业级应用中,唯一标识符(Unique Identifier,简称ID)的生成都是至关重要的。ID生成器在许多场景中都有着广泛的应用,包括但不限于数据库的主键生成、订单号生成、用户会话管理、分布式系统中的资源标识等。有效的ID生成器不仅需要保证ID的唯一性,还需要考虑到生成速度、可排序性、可读性等因素。
ID生成器的重要性
-
唯一性:在大多数应用场景中,ID需要是全局唯一的。这是因为ID通常被用作资源的唯一标识符,如果两个资源有相同的ID,将会导致数据混淆和错误。
-
生成速度:在高并发的系统中,ID生成器需要能够快速地生成新的ID,以满足系统的需求。
-
可排序性:在某些场景中,如日志记录、订单处理等,我们可能需要根据ID的生成顺序对资源进行排序。因此,ID生成器需要能够生成可排序的ID。
-
可读性:虽然这不是所有场景都需要的,但在某些情况下,如调试和故障排查,可读性强的ID会非常有用。
应用场景
-
数据库主键生成:在数据库中,每条记录都需要一个唯一的主键。使用ID生成器,我们可以确保每条新的记录都有一个唯一的主键。
-
订单号生成:在电子商务网站中,每个订单都需要一个唯一的订单号。通过ID生成器,我们可以为每个新的订单生成一个唯一的订单号。
-
用户会话管理:在Web应用中,每个用户会话都需要一个唯一的会话ID。使用ID生成器,我们可以为每个新的用户会话生成一个唯一的会话ID。
-
分布式系统中的资源标识:在分布式系统中,每个资源(如文件、消息等)都需要一个唯一的标识符。通过ID生成器,我们可以为每个新的资源生成一个唯一的标识符。
背景
随着互联网的快速发展,应用的用户量和数据量都在爆炸式增长。在这种情况下,传统的ID生成方法(如数据库的自增主键)已经无法满足需求,因为它们无法在分布式环境中生成唯一的ID。因此,我们需要新的ID生成方法,如随机数+时间戳、雪花算法和Redis自增等,来满足在分布式环境中生成唯一ID的需求。
以下主要是为了介绍随机数+时间戳、雪花算法和Redis自增的实现跟优劣。
-
随机数+时间戳生成器
在现代应用中,生成唯一ID的需求无处不在,如数据库的主键,订单号,甚至是游戏中的角色ID等。而随机数+时间戳的ID生成方法是一种简单而易于实现的方式。它通过将当前时间的时间戳与一个随机数相结合,产生一个独一无二的ID。这种方式的优势在于它不依赖于任何外部系统,只需要本地的时间和随机数生成器,增加日期之后可读性较高,例如:Order_20240322011809221234。但同时,它也有一个主要的缺点,那就是在高并发的情况下可能无法保证产生的ID的唯一性。
public class FinCodeIdWorker implements FinIdWorker {
//默认随机长度
private static final int DEFALUT_LEGTH = 6;
private static final String yyyyMMddHHmmss = "yyyyMMddHHmmss";
private static char[] chr = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
public String nextId(){
return nextId("");
}
/**
* 指定前缀
* @param prefix
* @return
*/
public String nextId(String prefix){
StringBuilder builder = new StringBuilder();
builder.append(StrUtil.isEmpty(prefix)?"":prefix).append(DateUtil.format(new Date(), yyyyMMddHHmmss)).append(generateRandom(DEFALUT_LEGTH));
return builder.toString();
}
/**
* 指定前缀+随机数长度
* @param prefix
* @param suffixRandomLength
* @return
*/
public static String nextId(String prefix,int suffixRandomLength){
StringBuilder builder = new StringBuilder();
builder.append(StrUtil.isEmpty(prefix)?"":prefix).append(DateUtil.format(new Date(), yyyyMMddHHmmss)).append(generateRandom(suffixRandomLength));
return builder.toString();
}
/**
* 产生随机数
*
* @param suffixRandomLength 后缀随机长度
* @return 随机数
*/
public static String generateRandom(int suffixRandomLength) {
StringBuilder bld = new StringBuilder();
for (int i = 0; i < suffixRandomLength; i++) {
bld.append(chr[RandomUtil.randomInt(62)]);
}
return bld.toString();
}
2.雪花算法
雪花算法是Twitter公司开源的分布式ID生成算法,它可以在任何分布式系统中生成唯一的ID。雪花算法的强大之处在于它结合了时间戳,工作节点的标识和一个递增序列,以确保每一个生成的ID都是唯一的。这种方式的优势在于它能够在任何分布式环境下保证ID的唯一性,而且生成的ID是趋势递增的,对于需要排序的场景非常有用。但是,它也有一个潜在的问题,那就是如果系统的时间被调整,可能会造成ID的冲突。
/**
* 默认Id生成器配置
*/
@Data
public class SnowFlakeIdWorkerProperties {
/**
* 业务id
*/
private long businessId = 0L;
/**
* 机器id
*/
private long machineId = 0L;
}
/**
* 雪花算法生成器
*/
public class SnowFlakeIdWorker implements FinIdWorker {
/**
* 开始时间截 (2015-01-01)
*/
private static final long TWEPOCH = 0x14a_a113_3c00L;
/**
* 机器id所占的位数
*/
private static final long WORKER_ID_BITS = 5L;
/**
* 数据标识id所占的位数
*/
private static final long DATACENTER_ID_BITS = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
/**
* 支持的最大数据标识id,结果是31
*/
private static final long MAX_DATACENTER_ID = -1L ^ (-1L << DATACENTER_ID_BITS);
/**
* 序列在id中占的位数
*/
private static final long SEQUENCE_BITS = 12L;
/**
* 机器ID向左移12位
*/
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/**
* 数据标识id向左移17位(12+5)
*/
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/**
* 时间截向左移22位(5+5+12)
*/
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
/**
* 使用给定的配置构造
*
* @param properties 配置信息
*/
public SnowFlakeIdWorker(SnowFlakeIdWorkerProperties properties) {
if (properties.getBusinessId() > MAX_WORKER_ID || properties.getBusinessId() < 0) {
throw new IllegalArgumentException(String.format("business Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (properties.getMachineId() > MAX_DATACENTER_ID || properties.getMachineId() < 0) {
throw new IllegalArgumentException(String.format("machine Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
}
this.workerId = properties.getBusinessId();
this.datacenterId = properties.getMachineId();
}
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowFlakeId
*/
public synchronized String nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new IdWorkerException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
long seq = ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) //
| (datacenterId << DATACENTER_ID_SHIFT) //
| (workerId << WORKER_ID_SHIFT) //
| sequence;
return String.valueOf(seq);
}
public String nextId(String prefix) {
return prefix.concat(String.valueOf(this.nextId()));
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlakeIdWorkerProperties properties = new SnowFlakeIdWorkerProperties();
SnowFlakeIdWorker idWorker = new SnowFlakeIdWorker(properties);
long l = System.currentTimeMillis();
System.out.println(l);
for (int i = 0; i < 10000; i++) {
System.out.println(idWorker.nextId());
}
System.out.println(System.currentTimeMillis() -l);
System.out.println(idWorker.nextId("JDS"));
}
}
3. Redis自增ID生成器
Redis自增机制是一种利用Redis的原子性操作来生成唯一ID的方法。Redis是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息中介。利用Redis的INCR和INCRBY命令,我们可以在Redis中创建一个键,每次需要生成新的ID时,就将这个键的值增加1。这种方式的优势在于它简单,易于实现,而且因为Redis的高性能,它生成ID的速度非常快。但是,这种方式依赖于外部系统Redis,如果Redis服务出现问题,将无法生成新的ID。
/**
* redis Id生成器
*/
@Slf4j
@Component
public class RedisIdWorker implements FinIdWorker {
private static final String yyyyMMddHHmmss = "yyyyMMddHHmmss";
@Resource
private RedisTemplate<String, Object> redisTemplate;
private static final Integer MAX_COUNT = 1000;
private static final String DEFAULT_KEY = "test:idworker:default";
private static final String DEFAULT_PREFIX = "test:idworker:";
private Map<String,List<Long>> seqsMap = Maps.newHashMap();
/**
*
* @param
* @return
*/
public String nextId() {
//获取序列号
Long redisNum = generateSeq(DEFAULT_KEY);
//少于4位补0
String redisSeq = StringUtils.leftPad(redisNum.toString(), 4, "0");
// seq = 年月日+redis随机数(1s内生成1000个随机取一个)+随机数
return DateUtil.DateToString(new Date(), yyyyMMddHHmmss) + redisSeq + generateRandom(2);
}
public String nextId(String prefix) {
return prefix.concat(String.valueOf(this.nextId(DEFAULT_PREFIX.concat(prefix))));
}
private List<Long> createSeqs(String key) {
List<Long> seqs = Lists.newArrayList();
try{
Long incNum = this.redisTemplate.opsForValue().increment(key, MAX_COUNT);
//设置1s超时
this.redisTemplate.expire(key, 1, TimeUnit.SECONDS);
//生成一批序列号
for (long i = incNum - MAX_COUNT; i < incNum ; i++) {
seqs.add(i);
}
}catch (Exception e){
throw new RuntimeException(e);
}
return seqs;
}
private synchronized long generateSeq(String key){
List<Long> seqs = seqsMap.get(key);
//为空或者null
if(CollectionUtils.isEmpty(seqs)){
//生成一批序列号
seqs = createSeqs(key);
//乱序
Collections.shuffle(seqs);
seqsMap.put(key,seqs);
}
//返回第一个
return seqs.remove(0);
}
public static String generateRandom(int length) {
double pow = Math.pow(10, length);
//产生length位的随机数(不足length位前加零)
int randomNum = RandomUtil.randomInt((int) pow);
StringBuilder bld = new StringBuilder().append(randomNum);
int randLength = bld.length();
if (randLength < length) {
for (int i = 1; i <= length - randLength; i++) {
bld.append("0");
}
}
return bld.toString();
}
}