JavaDriver JavaDriver
首页
  • 基础
  • 并发
  • JVM
  • 设计模式
  • 计算机网络
  • 操作系统
  • 数据结构
  • 算法
  • MYSQL
  • REDIS
  • Netty
  • Kafka
系统设计
非技术
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

YoungAnn

西二旗Java老司机一枚 致力于社会主义添砖Java
首页
  • 基础
  • 并发
  • JVM
  • 设计模式
  • 计算机网络
  • 操作系统
  • 数据结构
  • 算法
  • MYSQL
  • REDIS
  • Netty
  • Kafka
系统设计
非技术
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 系统设计
  • 电商系统中,如何实现秒杀功能?如何解决商品的超卖问题?
  • 如何解决缓存与数据库不一致的问题?
  • 什么是 CAP ?什么是最终一致性?什么是幂等操作?
  • 简述什么是两阶段提交?
  • 设计 Tiny URL 系统
  • 如何实现唯一的分布式 ID
    • 1.UUID
    • 2.UUID转Long的方式
    • 3.数据库自增ID
    • 4.Redis的incr自增
    • 5.数据库号段模式
      • 6.Snowflake
      • 6.Leaf-snowflake(美团)
      • 7.uid-generator(百度)
      • 8.Tinyid(滴滴)
      • 结语
  • 如何设计定时任务系统
  • 电商-商品系统设计
  • 系统设计
YoungAnn
2022-11-09
目录

如何实现唯一的分布式 ID

# 1.UUID

UUID.randomUUID().toString()
1

【优点】

  1. 本地生成无网络消耗,并具有唯一性。

【缺点】

  1. 长度较长且无序,也没有实际意义;
  2. 不适用于实际的业务需求。像用作订单号UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;
  3. 如果做mysql表主键的话,由于mysql的底层存储结构是B+树,节点在排序等操作时,性能差于整型。

# 2.UUID转Long的方式

UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE
1

查看UUID类的toString()方法,会发现UUID是由高64位和低64位组合而成:

public String toString() {
        return (digits(mostSigBits >> 32, 8) + "-" +
                digits(mostSigBits >> 16, 4) + "-" +
                digits(mostSigBits, 4) + "-" +
                digits(leastSigBits >> 48, 4) + "-" +
                digits(leastSigBits, 12));
    }
1
2
3
4
5
6
7

UUID的高64位与0111111111111111111111111111111111111111111111111111111111111111做按位运算,可保证生成的id始终为正数。

# 3.数据库自增ID

基于数据库的auto_increment自增ID。

CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    value char(10) NOT NULL default '',
    PRIMARY KEY (id),
) ENGINE=MyISAM;

insert into SEQUENCE_ID(value) VALUES ('values');
1
2
3
4
5
6
7
8

【优点】

  1. ID自增,数值类型存取性能好;

【缺点】 1.单点要防止宕机风险; 2.集群模式难扩展,比如步长。

# 4.Redis的incr自增

【优点】

  1. Redis的优点是单线程、免去了线程间切换的开销,存取速度快。

【缺点】

  1. Redis满足分布式CAP原则中的A(Availability可用性)、P(Partition tolerance分区容错性),不能满足C(Consistency一致性),也就是说Redis无法保证在主节点宕机时自动完成数据一致性的同步操作。那么在极端情况下,主节点挂掉后可能产生重复的id。
  2. 即使考虑用持久化的方式来规避上面的问题,仍存在以下缺陷: RDB方式:定时备份,可能存在数据丢失。假如连续自增但redis没及时持久化,而此时Redis挂掉了,重启Redis后可能会出现ID重复的情况。 AOF方式:每条写命令都备份,不会出现ID重复的情况,但文件过大,恢复数据慢。

# 5.数据库号段模式

用表记录当前最大id,和自增步长。每次机器通过乐观锁的方式修改当前最大id字段: file

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = #{version} and biz_type = XXX
1

【优点】 不会频繁的访问数据库,对数据库的压力小很多。 【图示】 file

# 6.Snowflake

【组成】 file 符号位:1bit:符号位,固定是0,表示全部ID都是正整数 时间戳:与指定日期的时间差(毫秒级),41位,够用69年 集群ID、机器ID:10位,最多支持1024台机器 自增序列:12位,每台机器每毫秒内最多产生2^12即4096个序列号 【优点】

  1. 本地生成无网络消耗。

【缺点】

  1. 依赖系统时间,如果系统时间被回调,或者改变,可能会造成id冲突或者重复,也能出现不是全局递增的情况。

# 6.Leaf-snowflake(美团)

美团对号段模式和雪花算法的实现方式均有改进。 【文档】 https://tech.meituan.com/2017/04/21/mt-leaf.html 【得名】 德国哲学家、数学家莱布尼茨的一句话: >There are no two identical leaves in the world > “世界上没有两片相同的树叶”。 【号段模式改进】 基于数据库号段模式,但取号段的时机不在号段消耗完的时候进行,而是当号段消费到某个点时就异步的把下一个号段加载到内存中,好处是不阻塞临界点的ID下发。 file

【雪花算法改进——解决时钟回退方案】

  1. 启动时: a. 弱依赖ZooKeeper,由Zookeepp的持久化顺序节点来存储wokerId。若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。 b. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。 c. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。 d. 否则认为本机系统时间发生大步长偏移,启动失败并报警。 e. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。
  2. 运行时: a. 建议可以直接关闭NTP同步;【Network Time Protocol(NTP)】 b. 做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警; file

【主要代码】 号段模式:

//LeafController.class
@RequestMapping(value = "/api/segment/get/{key}")
public String getSegmentId(@PathVariable("key") String key) {
        return get(key, segmentService.getId(key));
}

//SegmentIDGenImpl.class
@Override
public Result get(final String key) {
        // 将初始值和步长初始化
        if (!initOK) {
                return new Result(EXCEPTION_ID_IDCACHE_INIT_FALSE, Status.EXCEPTION);
        }
        // key为业务标识
        if (cache.containsKey(key)) {
                SegmentBuffer buffer = cache.get(key);
                // 如果当前还没有初始化号段
                if (!buffer.isInitOk()) {
                        synchronized (buffer) {
                                if (!buffer.isInitOk()) {
                                        try {
                                                updateSegmentFromDb(key, buffer.getCurrent());
                                                logger.info("Init buffer. Update leafkey {} {} from db", key, buffer.getCurrent());
                                                buffer.setInitOk(true);
                                        } catch (Exception e) {
                                                logger.warn("Init buffer {} exception", buffer.getCurrent(), e);
                                        }
                                }
                        }
                }
                // 从缓存中取值
                return getIdFromSegmentBuffer(cache.get(key));
        }
        return new Result(EXCEPTION_ID_KEY_NOT_EXISTS, Status.EXCEPTION);
}

private ExecutorService service = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new UpdateThreadFactory());

public Result getIdFromSegmentBuffer(final SegmentBuffer buffer) {
            while (true) {
                    buffer.rLock().lock();
                    try {
                            final Segment segment = buffer.getCurrent();
                            // 当号段消费到某个点时就异步的把下一个号段加载到内存中
                            if (!buffer.isNextReady() && (segment.getIdle() < 0.9 * segment.getStep()) && buffer.getThreadRunning().compareAndSet(false, true)) {
                                    service.execute(new Runnable() {
                                            @Override
                                            public void run() {
                                                    Segment next = buffer.getSegments()[buffer.nextPos()];
                                                    boolean updateOk = false;
                                                    try {
                                                            updateSegmentFromDb(buffer.getKey(), next);
                                                            updateOk = true;
                                                            logger.info("update segment {} from db {}", buffer.getKey(), next);
                                                    } catch (Exception e) {
                                                            logger.warn(buffer.getKey() + " updateSegmentFromDb exception", e);
                                                    } finally {
                                                            if (updateOk) {
                                                                    buffer.wLock().lock();
                                                                    buffer.setNextReady(true);
                                                                    buffer.getThreadRunning().set(false);
                                                                    buffer.wLock().unlock();
                                                            } else {
                                                                    buffer.getThreadRunning().set(false);
                                                            }
                                                    }
                                            }
                                    });
                            }
                            long value = segment.getValue().getAndIncrement();
                            if (value < segment.getMax()) {
                                    return new Result(value, Status.SUCCESS);
                            }
                    } finally {
                            buffer.rLock().unlock();
                    }
                    waitAndSleep(buffer);
                    buffer.wLock().lock();
                    try {
                            final Segment segment = buffer.getCurrent();
                            long value = segment.getValue().getAndIncrement();
                            if (value < segment.getMax()) {
                                    return new Result(value, Status.SUCCESS);
                            }
                            if (buffer.isNextReady()) {
                                    buffer.switchPos();
                                    buffer.setNextReady(false);
                            } else {
                                    logger.error("Both two segments in {} are not ready!", buffer);
                                    return new Result(EXCEPTION_ID_TWO_SEGMENTS_ARE_NULL, Status.EXCEPTION);
                            }
                    } finally {
                            buffer.wLock().unlock();
                    }
            }
    }
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

雪花算法:

//LeafController.class
@RequestMapping(value = "/api/snowflake/get/{key}")
public String getSnowflakeId(@PathVariable("key") String key) {
        return get(key, snowflakeService.getId(key));
}

//SnowflakeIDGenImpl.class
public synchronized Result get(String key) {
        long timestamp = timeGen();
        // 当前时间小于上次用的时间戳
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    // 时间偏差大小如果小于5ms,则等待两倍时间
                    wait(offset << 1);
                    // 重新获取时间戳
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        return new Result(-1, Status.EXCEPTION);
                    }
                } catch (InterruptedException e) {
                    LOGGER.error("wait interrupted");
                    return new Result(-2, Status.EXCEPTION);
                }
            } else {
                return new Result(-3, Status.EXCEPTION);
            }
        }
        // 当前时间 = 上次的时间戳
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                //seq 为0的时候表示是下一毫秒时间开始对seq做随机
                sequence = RANDOM.nextInt(100);
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //如果是新的ms开始
            sequence = RANDOM.nextInt(100);
        }
        lastTimestamp = timestamp;
        long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
        return new Result(id, Status.SUCCESS);

    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    protected long timeGen() {
        return System.currentTimeMillis();
    }
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

# 7.uid-generator(百度)

【文档】 https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md 【组成】 ● sign(1bit) ● 固定1bit符号标识,即生成的UID为正数。 ● delta seconds (28 bits) ● 当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年 ● worker id (22 bits) ● 机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。 ● sequence (13 bits) ● 每秒下的并发序列,13 bits可支持每秒8192个并发。 以上参数均可通过Spring进行自定义

# 8.Tinyid(滴滴)

Tinyid扩展了leaf-segment算法,支持了多db(master),同时提供了java-client(sdk)使id生成本地化。 【优点】

  1. 提供http和java client方式接入
  2. 支持批量获取id
  3. 支持生成1,3,5,7,9...序列的id(其实就是for循环获取)
  4. 支持多个db的配置,无单点 file

# 结语

对比了几种分布式id的方案,个人认为美团的实现方案是比较完善的,它对业界比较流行的两种方案(号段模式和雪花算法)均有改进,且百度、滴滴的方案均基于它来实现。

如果想要更简便的方式,UUID、UUID转Long、Redis自增方案是不错的选择,可以按各自的优缺点去选择方案。因为没有最优,只有最合适业务的方案。

参考: Leaf——美团点评分布式ID生成系统 (opens new window)

编辑 (opens new window)
上次更新: 2023/02/17, 17:03:51
设计 Tiny URL 系统
如何设计定时任务系统

← 设计 Tiny URL 系统 如何设计定时任务系统→

最近更新
01
电商-商品系统设计
12-17
02
关于如何写OKR
12-09
03
对事不对人 vs 对人不对事
12-09
更多文章>
Theme by Vdoing | Copyright © 2022-2023 YoungAnnn | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式