简介

从数据库性能角度考虑,我们经常需要数字型的自增主键,有时候我们并不想用像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