在我们工作中,很多场景下都需要生成唯一id,比如订单号、优惠券码等,本篇文章就给大家带来如何用java实现生成唯一id。
首先还是按惯例贴出github地址,可直接从github下载源码运行:https://github.com/whiteBX/IDGenerator
唯一ID的核心点
- 效率高。id生成器一般作为基础服务,需要有很良好的性能保证,不能让业务感知到明显的延时。
- 支持分布式部署。现在主流都是分布式环境下,发号器必须要支持分布式部署,以避免单机的磁盘IO等瓶颈。
- 部分有序。作为订单号等业务来讲,最好是有序的。
- 可反解。订单号最好能直接反解出核心信息,这样我们排查问题时就可以直接拿着订单号来反解出需要的信息使用。
唯一ID的设计要点
本例子中采用一个long类型来存储我们的id,也就是64个bit,最终根据需要转换为十进制或32进制使用,可根据自己的业务需要来选择。下面为id的构成讲解:
版本号2位 + 秒级时间30位 + 序列号20位 + 机器id5位 = 57位二进制
详细解释:
版本号2位:表示用两个bit来存储版本号,留作用于大的业务变动时使用,便于直观区分是哪个版本的业务。
秒级时间30位:采用当前时间戳减去一个系统上线的固定时间来存储,30位算下来可以使用三十多年。如果不够可以扩充。
序列号20位:每秒可以支持生成2^20-1个,即百万级个不重复的序列号,不够用的话可以扩充。
机器id5位:即可以支持2^5-1 = 31台机器部署,不够可以扩充。
由于我们存储是用long来存储的,那么这里还剩下7位可以自由扩充,比如增加序列号占用的位数或者机器id占用的位数。
具体实现
本段代码核心点其实就两点:
- 支持多机部署–通过机器id来给每台机器配置单独的id,则可以将同一段代码部署于多台机器均能保证id不重复
- 序列号生成–可以采用锁或cas来实现序列号的不重复
代码如下:
通过锁来控制序列号不重复:
public class LockSequenceGenerator extends SequenceGenerator {
private ReentrantLock lock = new ReentrantLock();
/**
* 通过锁来实现
* @param generateInfo
* @param generateProperties
*/
public void generate(IdGenerateInfo generateInfo, GenerateProperties generateProperties) {
lock.lock();
try {
long time = TimeGenerator.generateTime(generateProperties.getBeginEpoch());
if (time == timestamp) {
sequence++;
}else {
sequence = 0;
timestamp = time;
}
generateInfo.setSequence(sequence);
generateInfo.setTime(time);
}finally {
lock.unlock();
}
}
}
通过cas来实现序列号不重复:
public class CasGenerator extends SequenceGenerator {
private AtomicReference<SequenceInfo> sequenceInfo = new AtomicReference<SequenceInfo>(new SequenceInfo());
public void generate(IdGenerateInfo generateInfo, GenerateProperties generateProperties) {
while (true) {
long time = TimeGenerator.generateTime(generateProperties.getBeginEpoch());
SequenceInfo oldSequenceInfo = sequenceInfo.get();
SequenceInfo newSequenceInfo = new SequenceInfo();
if (time == oldSequenceInfo.getTimestamp()) {
newSequenceInfo.setTimestamp(time);
newSequenceInfo.setSequence(oldSequenceInfo.getSequence() + 1);
} else {
newSequenceInfo.setTimestamp(time);
}
if (sequenceInfo.compareAndSet(oldSequenceInfo, newSequenceInfo)) {
generateInfo.setTime(newSequenceInfo.getTimestamp());
generateInfo.setSequence(newSequenceInfo.getSequence());
break;
}
}
}
class SequenceInfo {
private int sequence;
private long timestamp;
public int getSequence() {
return sequence;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
}
解决掉序列号不重复问题后,剩下的其实就是获取配置的版本号和机器id来组装出我们的id即可:
// 通过spring的PropertySource来读取配置文件,使其都可自由配置。其中都赋上默认值。
@Component
@PropertySource("classpath:generate.properties")
public class GenerateProperties {
/**
* 版本号,默认占2位,即二进制的00,重大版本变更时配置,需要扩容的话要同步改动versionLength
*/
private int version = 0;
private int versionLength = 2;
/**
* 序列号占用字节,默认占20位 即每秒2^20个序列,按需配置
*/
private Integer sequenceLength = 20;
/**
* 机器ID,默认占5位,即二进制的00000,可以支持32台服务器,需要扩容的话要同步改动machineIdLength
*/
private int machineId = 0;
private int machineIdLength = 5;
/**
* 开始时间
*/
private Long beginEpoch = 1533686888L;
}
根据配置组装id:
/**
* 生成id 1 | 0 = 1,或出所有1
*/
public static long genearte(IdGenerateInfo info, GenerateProperties generateProperties) {
long id = 0;
id |= info.getMachineId();
id |= info.getSequence() << generateProperties.getMachineIdLength();
id |= info.getTime() << (generateProperties.getMachineIdLength() + generateProperties.getSequenceLength());
id |= info.getVersion() << (generateProperties.getMachineIdLength() + generateProperties.getSequenceLength() + 30);
return id;
}
反解这里就不写代码示例了,各位可以自己实现即可,即根据id的二进制码对应右移指定位数即可解出来在哪台机器,哪个时间等信息。
性能测试
运行我本机开了十个线程,测量的结果为每个线程生成一千万个id耗时都为10秒左右,单个id低于1毫秒,可以看到效率是足够使用的。
当然例子中有一些地方还没有完全完善,读者可以自行完善,比如一秒内生成的id数量超过最大上限时提示错误或者等待下个点再生成等。