• 简洁自增ID实现方案


    简介

    从数据库性能角度考虑,我们经常需要数字型的自增主键,有时候我们并不想用像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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    思路

    思路非常简单,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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    为看更严谨一点可以将测试用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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
  • 相关阅读:
    锁升级的实现流程
    从零开始的C++之旅——string类的模拟实现
    Java Properties类
    剑指offer专项突击版第31天
    Qt 实战(4)信号与槽 | 4.3、信号连接信号
    Overloud Mark Studio 2 for Mac 模拟数字音频效果器
    Leetcode 2851. String Transformation
    面向移动支付过程中网络安全的研究与分析
    实时即未来,车联网项目之电子围栏分析【六】
    docker基础知识
  • 原文地址:https://blog.csdn.net/trayvontang/article/details/128173979