从数据库性能角度考虑,我们经常需要数字型的自增主键,有时候我们并不想用像MySQL自带的自增,因为从1开始很数据位数不一样,对有点强迫症的人来说,不是很友好。
另外,别人也容易根据这种从1开始的自增id分析出业务数据信息。
有很多全局唯一ID的解决方案,例如snowflake等。很多时候,其实用不上,很多业务就是单机业务,完全不需要分布式。
很多时候,其实用13位时间戳完全够了,但是13位时间戳最多支持到1千的并发,感觉心里有有点不踏实。
有没有简介一点的折中方案呢?
当然,有。
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