简介
从数据库性能角度考虑,我们经常需要数字型的自增主键,有时候我们并不想用像MySQL自带的自增,因为从1开始很数据位数不一样,对有点强迫症的人来说,不是很友好。
另外,别人也容易根据这种从1开始的自增id分析出业务数据信息。
有很多全局唯一ID的解决方案,例如snowflake等。很多时候,其实用不上,很多业务就是单机业务,完全不需要分布式。
很多时候,其实用13位时间戳完全够了,但是13位时间戳最多支持到1千的并发,感觉心里有有点不踏实。
有没有简介一点的折中方案呢?
当然,有。
单机ID自增实现
import java.util.concurrent.atomic.AtomicLong;
public class IdGenertor {
/**
* 序列位数,建议不小于4位
* 相对id生成来说{@link System#currentTimeMillis()}是耗时操作
* 当为4时意味着每毫秒最多15个,每秒1万5千个
*/
private short sequenceBit;
/**
* 序列最大值
*/
private long maxSequence;
/**
* 序列最大值哨兵
*/
private long sentinel;
/**
* 自增序列
*/
private AtomicLong sequence;
/**
* 当前毫秒,13位数,41位时间戳
*/
private long currentMill;
public static IdGenertor build(short sequenceBit){
return new IdGenertor(sequenceBit);
}
public static IdGenertor build(int sequenceBit){
return new IdGenertor((short) sequenceBit);
}
private IdGenertor(short sequenceBit) {
if(sequenceBit > 22){
throw new RuntimeException("序列不能大于22位");
}
this.sequenceBit = sequenceBit;
maxSequence = -1L ^ (-1L << sequenceBit);
sentinel = maxSequence + 1;
currentMill = System.currentTimeMillis();
sequence = new AtomicLong(0);
}
public long getId(){
long up = sequence.compareAndExchange(sentinel, 0);
if(up == sentinel){
long current = System.currentTimeMillis();
// 避免序列重置时,时间戳还没有改变造成的重复
while (current == currentMill){
current = System.currentTimeMillis();
}
currentMill = current;
}
return currentMill << sequenceBit | sequence.getAndIncrement();
}
}
思路
思路非常简单,long 8字节,64位,13位数时间戳占41位,1位符号位,所以自增序列最多22位。
把时间戳和自增序列拼接起来就可以作为自增id了。
自增序列的位数可以设置,例如设置4位,就意味着每毫秒最多可以生成15个id,也就是每秒1万5000个,对于绝大多数场景来说都够了。
如果设置22位,每毫秒可以生成四百多万个id,这个完全没有必要,我在单机上测试,单线程情况下,当位数为22位是,每毫秒生成的id大概在9万左右,机器性能只能生成这么多,所以用不上四百多万。
再说每毫秒9万,每秒就是9000万,哪有那么大的并发量。
注意
在多线程下,性能会明显下降,和单线程比,性能大概下降了10倍,每毫秒大概只能生成9千。
可以简单做个测试:
@Test
public void multiGet() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(4);
IdGenertor idGenertor = IdGenertor.build(22);
long s = System.currentTimeMillis();
int n = 10000000;
for (int i = 0; i < 4; i++) {
service.execute(() -> {
for (int j = 0; j < n; j++) {
idGenertor.getId();
}
});
}
service.shutdown();
while (!service.awaitTermination(100, TimeUnit.MILLISECONDS)) {
}
long time = System.currentTimeMillis() - s;
System.out.println((double) n / time);
}
为看更严谨一点可以将测试用CountDownLatch改造一下。
序列位数选择
建议不小于4bit,小于4bit,可以考虑直接使用13位数时间戳。
通常来说4位基本就够绝大多数场景,并且和时间戳的关联也更紧密一些,当跨毫秒的时候,中间的差值也更小一些,自增更均匀。
下面是不同bit生成的id示例:
0bit:1670145499690
1bit:3340290999392
2bit:6680581998784
3bit:13361163997568
4bit:26722327995136
5bit:53444655990272
6bit:106889311980544
7bit:213778623961088
8bit:427557247922176
9bit:855114495844352
10bit:1710228991688704
11bit:3420457983377408
12bit:6840915966754816
13bit:13681831933509632
14bit:27363663867019264
15bit:54727327734038528
16bit:109454655468077056
17bit:218909310936154112
18bit:437818621872308224
19bit:875637243744616448
20bit:1751274487489232896
21bit:3502548974978465792
22bit:7005097949956931584