全局唯一ID设计

作者: whthomas | 来源:发表于2016-04-07 15:14 被阅读7520次

    在分布式系统中,经常需要使用全局唯一ID查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。

    全局唯一ID在数据库中一般会被设成主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。

    这样全局唯一ID就需要保证这两个需求:

    • 全局唯一
    • 趋势有序

    全局ID产生的几种方式

    数据库自增

    当服务使用的数据库只有单库单表时,可以利用数据库的auto_increment来生成全局唯一递增ID.

    优势:

    • 简单,无需程序任何附加操作
    • 保持定长的增量
    • 在单表中能保持唯一性

    劣势:

    • 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。
    • 水平扩展困难,在分布式数据库环境下,无法保证唯一性。

    UUID

    一般的语言中会自带UUID的实现,比如Java中UUID方式UUID.randomUUID().toString(),可以通过服务程序本地产生,ID的生成不依赖数据库的实现。

    优势:

    • 本地生成ID,不需要进行远程调用。
    • 全局唯一不重复。
    • 水平扩展能力非常好。

    劣势:

    • ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。
    • 生成的ID中没有带Timestamp,无法保证趋势递增

    Twitter Snowflake

    snowflake是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12)个,也就是大约400W的ID,完全能满足业务的需求。

    根据snowflake算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中long类型的长度是64bits,所以我们设计的ID需要控制在64bits。

    比如我们设计的ID包含以下信息:

    | 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |
    

    产生唯一ID的Java代码:

    import java.security.SecureRandom;
    
    /**
     * 自定义 ID 生成器
     * ID 生成规则: ID长达 64 bits
     * 
     * | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |
     */
    public class CustomUUID {
        // 基准时间
        private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT
        // 区域标志位数
        private final static long regionIdBits = 3L;
        // 机器标识位数
        private final static long workerIdBits = 10L;
        // 序列号识位数
        private final static long sequenceBits = 10L;
    
        // 区域标志ID最大值
        private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
        // 机器ID最大值
        private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
        // 序列号ID最大值
        private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        // 机器ID偏左移10位
        private final static long workerIdShift = sequenceBits;
        // 业务ID偏左移20位
        private final static long regionIdShift = sequenceBits + workerIdBits;
        // 时间毫秒左移23位
        private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;
    
        private static long lastTimestamp = -1L;
    
        private long sequence = 0L;
        private final long workerId;
        private final long regionId;
    
        public CustomUUID(long workerId, long regionId) {
    
            // 如果超出范围就抛出异常
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
            }
            if (regionId > maxRegionId || regionId < 0) {
                throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
            }
    
            this.workerId = workerId;
            this.regionId = regionId;
        }
    
        public CustomUUID(long workerId) {
            // 如果超出范围就抛出异常
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
            }
            this.workerId = workerId;
            this.regionId = 0;
        }
    
        public long generate() {
            return this.nextId(false, 0);
        }
    
        /**
         * 实际产生代码的
         *
         * @param isPadding
         * @param busId
         * @return
         */
        private synchronized long nextId(boolean isPadding, long busId) {
    
            long timestamp = timeGen();
            long paddingnum = regionId;
    
            if (isPadding) {
                paddingnum = busId;
            }
    
            if (timestamp < lastTimestamp) {
                try {
                    throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            //如果上次生成时间和当前时间相同,在同一毫秒内
            if (lastTimestamp == timestamp) {
                //sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位
                sequence = (sequence + 1) & sequenceMask;
                //判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0
                if (sequence == 0) {
                    //自旋等待到下一毫秒
                    timestamp = tailNextMillis(lastTimestamp);
                }
            } else {
                // 如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,
                // 为了保证尾数随机性更大一些,最后一位设置一个随机数
                sequence = new SecureRandom().nextInt(10);
            }
    
            lastTimestamp = timestamp;
    
            return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
        }
    
        // 防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势.
        private long tailNextMillis(final long lastTimestamp) {
            long timestamp = this.timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = this.timeGen();
            }
            return timestamp;
        }
    
        // 获取当前的时间戳
        protected long timeGen() {
            return System.currentTimeMillis();
        }
    }
    

    使用自定义的这种方法需要注意的几点:

    • 为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免NTP时间服务器回拨服务器的时间。
    • 在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
    • 使用这个CustomUUID类,最好在一个系统中能保持单例模式运行。

    python版本的snowflake算法实现(感谢 @mailto1587 的python版本翻译):

    import sys
    import random
    import threading
    import time
    
    from concurrent import futures
    
    
    class Snowflake(object):
        region_id_bits = 2
        worker_id_bits = 10
        sequence_bits = 11
    
        MAX_REGION_ID = -1 ^ (-1 << region_id_bits)
        MAX_WORKER_ID = -1 ^ (-1 << worker_id_bits)
        SEQUENCE_MASK = -1 ^ (-1 << sequence_bits)
    
        WORKER_ID_SHIFT = sequence_bits
        REGION_ID_SHIFT = sequence_bits + worker_id_bits
        TIMESTAMP_LEFT_SHIFT = (sequence_bits + worker_id_bits + region_id_bits)
    
        def __init__(self, worker_id, region_id=0):
            self.twepoch = 1288834974657
            self.last_timestamp = -1
            self.sequence = 0
    
            assert 0 <= worker_id <= Snowflake.MAX_WORKER_ID
            assert 0 <= region_id <= Snowflake.MAX_REGION_ID
    
            self.worker_id = worker_id
            self.region_id = region_id
    
            self.lock = threading.Lock()
    
        def generate(self, bus_id=None):
            return self.next_id(
                True if bus_id is not None else False,
                bus_id if bus_id is not None else 0
            )
    
        def next_id(self, is_padding, bus_id):
            with self.lock:
                timestamp = self.get_time()
                padding_num = self.region_id
                
    
                if is_padding:
                    padding_num = bus_id
    
                if timestamp < self.last_timestamp:
                    try:
                        raise ValueError(
                            'Clock moved backwards. Refusing to'
                            'generate id for {0} milliseconds.'.format(
                                self.last_timestamp - timestamp
                            )
                        )
                    except ValueError:
                        print(sys.exc_info[2])
    
                if timestamp == self.last_timestamp:
                    self.sequence = (self.sequence + 1) & Snowflake.SEQUENCE_MASK
                    if self.sequence == 0:
                        timestamp = self.tail_next_millis(self.last_timestamp)
                else:
                    self.sequence = random.randint(0, 9)
    
                self.last_timestamp = timestamp
    
                return (
                    (timestamp - self.twepoch) << Snowflake.TIMESTAMP_LEFT_SHIFT |
                    (padding_num << Snowflake.REGION_ID_SHIFT) |
                    (self.worker_id << Snowflake.WORKER_ID_SHIFT) |
                    self.sequence
                )
    
        def tail_next_millis(self, last_timestamp):
            timestamp = self.get_time()
            while timestamp <= last_timestamp:
                timestamp = self.get_time()
            return timestamp
    
        def get_time(self):
            return int(time.time() * 1000)
    
    
    def main():
        id_set = set()
        snowflake = Snowflake(1)
    
        def gen_id():
            try:
                _id = snowflake.generate()
            except Exception as e:
                print(e)
            else:
                assert _id not in id_set
                id_set.add(_id)
    
        with futures.ThreadPoolExecutor(max_workers=16) as executor:
            futs = [executor.submit(gen_id) for _ in range(100)]
    
        print('{0} IDs in the set'.format(len(id_set)))
    
    if __name__ == '__main__':
        main()
    

    相关文章

      网友评论

      • 24c19eb79e90:分布式情况下 多个服务器时间没法保证一致啊
        whthomas:@肖邦的学徒 一般内网有统一的ntp时间服务器。

      本文标题:全局唯一ID设计

      本文链接:https://www.haomeiwen.com/subject/iqomlttx.html