参考资料:
https://www.w3cschool.cn/architectroad/architectroad-distributed-id.html
https://www.jianshu.com/p/14712264341f

题目1:实现一个分布式集群模式下系统生成唯一流水号的算法,要求:
a.流水号不能重复;
b.不能生成一次访问一次数据库;
c.满足高并发性能要求;

方式一、数据库自增序列

这是最常见的一种方式,利用DB来生成全库唯一ID。

优点:
1)使用简单,代码方便,性能可以接受。
2)ID为数字类型,排序方便。

缺点:
1)不同数据库语法和实现不同,数据库迁移或多数据库版本支持时需要处理。
2)在单数据库、读写分离或一主多从场景下,只有一个主库可以生成,有单点故障风险。
3)在性能达不到要求时,难于扩展。
4)当多个系统需要合并或涉及数据迁移时处理复杂。
5)在分库分表时麻烦。

优化方案:
针对主库单点,如果有多个Master,可以设置每个Master库的起始数字不同,步长相同,步长可以是Master的个数。如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11,Master3生成的是 3,6,9,12。这样可以有效生成集群中的唯一ID,也可以大大降低ID生成库操作的负载。

【常见方法二:单点批量ID生成服务】

/只能生成一条/
SELECT REPLACE(UUID(),’-‘,’’) AS id;
/在数据库中找一张数据多的表执行生成多条/
SELECT (REPLACE(UUID(), ‘-‘, ‘’)) id FROM 表名 LIMIT (条数1000);

【常见方法三:uuid】

优点:
1)使用简单。
2)生成ID性能高,基本不会有性能问题。
3)全球唯一,当数据迁移、数据合并、数据库变更时,可以从容应对。

缺点:
1)数据无序,无法保证趋势递增。
2)UUID使用字符串存储,查询效率较低。
3)存储空间较大,如果是海量数据库,需考虑存储量的问题。
4)传输数据量大。
5)可读性差。

// add()和remove()方法在失败的时候会抛出异常(不推荐)
Queue<UUID> queue = new ConcurrentLinkedQueue<>();
for (int i = 0; i < 100; i++) {
    UUID uuid = UUID.randomUUID();
    queue.offer(uuid);
}
try {
     System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
} finally {
    if(queue.size() / 1000 == 0.25) {
        for (int i = 0; i < 750; i++) {
            UUID uuid = UUID.randomUUID();
            queue.offer(uuid);
        }
    }
}
【常见方法四:取当前毫秒数】

uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢?

取当前毫秒数是一种常见方案:uint64 ID = GenTimeMS();

优点:
(1)本地生成ID,不需要进行远程调用,时延低
(2)生成的ID趋势递增

(3)生成的ID是整数,建立索引后查询效率高

缺点:

(1)如果并发量超过1000,会生成重复的ID

我去,这个缺点要了命了,不能保证ID的唯一性。当然,使用微秒可以降低冲突概率,但每秒最多只能生成1000000个ID,再多的话就一定会冲突了,所以使用微秒并不从根本上解决问题。

【常见方法五:类 Snowflake算法】

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

借鉴snowflake的思想,结合各公司的业务逻辑和并发量,可以实现自己的分布式ID生成算法。

举例,假设某公司ID生成器服务的需求如下:
(1)单机高峰并发量小于1W,预计未来5年单机高峰并发量小于10W
(2)有2个机房,预计未来5年机房数量小于4个
(3)每个机房机器数小于100台
(4)目前有5个业务线有ID生成需求,预计未来业务线数量小于10个
(5)…

分析过程如下:

(1)高位取从2016年1月1日到现在的毫秒数(假设系统ID生成器服务在这个时间之后上线),假设系统至少运行10年,那至少需要10年365天24小时3600秒1000毫秒=320*10^9,差不多预留39bit给毫秒数

(2)每秒的单机高峰并发量小于10W,即平均每毫秒的单机高峰并发量小于100,差不多预留7bit给每毫秒内序列号

(3)5年内机房数小于4个,预留2bit给机房标识

(4)每个机房小于100台机器,预留7bit给每个机房内的服务器标识

(5)业务线小于10个,预留4bit给业务线标识

64bit标识

这样设计的64bit标识,可以保证:
(1)每个业务线、每个机房、每个机器生成的ID都是不同的
(2)同一个机器,每个毫秒内生成的ID都是不同的
(3)同一个机器,同一个毫秒内,以序列号区区分保证生成的ID是不同的

(4)将毫秒数放在最高位,保证生成的ID是趋势递增的

缺点:

(1)由于“没有一个全局时钟”,每台服务器分配的ID是绝对递增的,但从全局看,生成的ID只是趋势递增的(有些服务器的时间早,有些服务器的时间晚)

最后一个容易忽略的问题:

生成的ID,例如message-id/ order-id/ tiezi-id,在数据量大时往往需要分库分表,这些ID经常作为取模分库分表的依据,为了分库分表后数据均匀,ID生成往往有“取模随机性”的需求,所以我们通常把每秒内的序列号放在ID的最末位,保证生成的ID是随机的。

又如果,我们在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀。解决方法是,序列号不是每次都归0,而是归一个0到9的随机数,这个地方。

作者:Jeebiz  创建时间:2020-10-19 23:06
 更新时间:2025-10-19 02:19