# 关于分布式唯一ID的思考-雪花算法及美团Leaf方案详解

9 篇文章 0 订阅
2 篇文章 0 订阅

## 引言

1. 引入hutool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>


1. 工具类
import cn.hutool.core.lang.Singleton;
/**
* 雪花算法工具类
*/
public class SnowFlakeUtil {
private static final long START_STMP = 1420041600000L;
private static final long SEQUENCE_BIT = 9L;
private static final long MACHINE_BIT = 2L;
private static final long DATACENTER_BIT = 2L;
private static final long MAX_SEQUENCE = 511L;
private static final long MAX_MACHINE_NUM = 3L;
private static final long MAX_DATACENTER_NUM = 3L;
private static final long MACHINE_LEFT = 9L;
private static final long DATACENTER_LEFT = 11L;
private static final long TIMESTMP_LEFT = 13L;
private long datacenterId;
private long machineId;
private long sequence = 0L;
private long lastStmp = -1L;

public SnowFlakeUtil(long datacenterId, long machineId) {
if (datacenterId <= 3L && datacenterId >= 0L) {
if (machineId <= 3L && machineId >= 0L) {
this.datacenterId = datacenterId;
this.machineId = machineId;
} else {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
} else {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
}

public synchronized long nextId() {
long currStmp = this.getNewstmp();
if (currStmp < this.lastStmp) {
throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
} else {
if (currStmp == this.lastStmp) {
this.sequence = this.sequence + 1L & 511L;
if (this.sequence == 0L) {
currStmp = this.getNextMill();
}
} else {
this.sequence = 0L;
}

this.lastStmp = currStmp;
return currStmp - 1420041600000L << 13 | this.datacenterId << 11 | this.machineId << 9 | this.sequence;
}
}

private long getNextMill() {
long mill;
for(mill = this.getNewstmp(); mill <= this.lastStmp; mill = this.getNewstmp()) {
}

return mill;
}

private long getNewstmp() {
return System.currentTimeMillis();
}

public static Long getDefaultSnowFlakeId() {
return ((SnowFlakeUtil)Singleton.get(SnowFlakeUtil.class, new Object[]{1L, 1L})).nextId();
}

public static void main(String[] args) {
for(int i = 0; i < 10; ++i) {
System.out.println(getDefaultSnowFlakeId());
System.out.println(getDefaultSnowFlakeId().toString().length());
}

}
}


## 背景

• 全局唯一性：不能出现重复的ID号，既然是唯一标识，这是最基本的要求。
• 趋势递增、单调递增：保证下一个ID一定大于上一个ID。
• 信息安全：如果ID是连续的，恶意用户的扒取工作就非常容易做了，直接按照顺序下载指定URL即可；如果是订单号就更危险了，竞对可以直接知道我们一天的单量。所以在一些应用场景下，会需要ID无规则、不规则。

## 常见方法介绍

### UUID

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字，以连字号分为五段，形式为8-4-4-4-12的36个字符，示例：550e8400-e29b-41d4-a716-446655440000，到目前为止业界一共有5种方式生成UUID，详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace。

• 优点：性能非常高：本地生成，没有网络消耗。
• 缺点
• 不易于存储：UUID太长，16字节128位，通常以36长度的字符串表示，很多场景不适用。
• 信息不安全：基于MAC地址生成UUID的算法可能会造成MAC地址泄露，这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

ID作为主键时在特定的环境会存在一些问题，比如做DB主键的场景下，UUID就非常不适用：

• MySQL官方有明确的建议主键要尽量越短越好[4]，36个字符长度的UUID不符合要求。
• 对MySQL索引不利：如果作为数据库主键，在InnoDB引擎下，UUID的无序性可能会引起数据位置频繁变动，严重影响性能。在MySQL InnoDB引擎中使用的是聚集索引，由于多数RDBMS使用B-tree的数据结构来存储索引数据，在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

### 雪花算法及其衍生

Snowflake 优缺点是：

• 优点：
• 毫秒数在高位，自增序列在低位，整个ID都是趋势递增的。
• 不依赖数据库等第三方系统，以服务的方式部署，稳定性更高，生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位，非常灵活。
• 缺点：
• 强依赖机器时钟，如果机器上时钟回拨，会导致发号重复或者服务会处于不可用状态。

### 数据库生成

#### MYSQL

1. 创建一个数据库表。
CREATE TABLE sequence_id (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
stub char(10) NOT NULL DEFAULT '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


stub 字段无意义，只是为了占位，便于我们插入或者修改数据。并且，给 stub 字段创建了唯一索引，保证其唯一性。

1. 通过 replace into 来插入数据。
BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;


• 如果发现表中已经有此行数据（根据主键或者唯一索引判断）则先删除此行数据，然后插入新的数据。
• 否则，直接插入新数据。

• 优点：
• 非常简单，利用现有数据库系统的功能实现，成本小，有DBA专业维护。ID号单调自增，存储消耗空间小。
• 缺点：
• 支持的并发量不大、存在数据库单点问题（可以使用数据库集群解决，不过增加了复杂度）、ID 没有具体业务含义、安全问题（比如根据订单 ID 的递增规律就能推算出每天的订单量 ）、每次获取 ID 都要访问一次数据库（增加了对数据库的压力，获取速度也慢）

ID没有了单调递增的特性，只能趋势递增，这个缺点对于一般业务需求不是很重要，可以容忍。

#### Redis

127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2


## 分布式ID微服务

### 美团Leaf方案实现

Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话： There are no two identical leaves in the world（“世界上没有两片相同的树叶”）
Leaf分别在MySQL和雪花上做了相应的优化，实现了Leaf-segment和Leaf-snowflake方案。

#### Leaf-segment数据库方案

Leaf-segment方案，在使用数据库的方案上，做了如下改变：

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit


• 优点：
• Leaf服务可以很方便的线性扩展，性能完全能够支撑大多数业务场景。
• ID号码是趋势递增的8byte的64位数字，满足上述数据库存储的主键要求。
• 容灾性高：Leaf服务内部有号段缓存，即使DB宕机，短时间内Leaf仍能正常对外提供服务。
• 可以自定义max_id的大小，非常方便业务从原有的ID方式上迁移过来。
• 缺点：
• ID号码不够随机，能够泄露发号数量的信息，不太安全。
• TP999数据波动大，当号段使用完之后还是会在获取新号段时在更新数据库的I/O依然会存在着等待，tg999数据会出现偶尔的尖刺。
• DB宕机会造成整个系统不可用。

#### 双buffer优化

Leaf 取号段的时机是在号段消耗完的时候进行的，也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间，并且在这期间进来的请求也会因为DB号段没有取回来，导致线程阻塞。如果请求DB的网络和DB的性能稳定，这种情况对系统的影响是不大的，但是假如取DB的时候网络发生抖动，或者DB发生慢查询就会导致整个系统的响应时间变慢。

#### Leaf高可用容灾

Leaf-snowflake方案
Leaf-segment方案可以生成趋势递增的ID，同时ID号是可计算的，不适用于订单ID生成场景，比如竞对在两天中午12点分别下单，通过订单id号相减就能大致计算出公司一天的订单量，这个是不能忍受的。面对这一问题，美团提供了 Leaf-snowflake方案。
Leaf-snowflake方案完全沿用snowflake方案的bit位设计，即是“1+41+10+12”的方式组装ID号。对于workerID的分配，当服务集群数量较小的情况下，完全可以手动配置。Leaf服务规模较大，动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的：

#### 解决时钟问题

1. 新节点通过检查综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确，具体做法是取所有运行中的Leaf-snowflake节点的服务IP：Port，然后通过RPC请求得到所有节点的系统时间，计算sum(time)/nodeSize，然后看本机时间与这个平均值是否在阈值之内来确定当前系统时间是否准确，准确正常启动服务，不准确认为本机系统时间发生大步长偏移，启动失败并报警。
2. 在ZooKeeper 中登记过的老节点，同样会比较自身系统时间和ZooKeeper 上本节点曾经的记录时间以及所有运行中的Leaf-snowflake节点的时间，不准确同样启动失败并报警。

#### 美团Leaf现状

Leaf在美团点评公司内部服务包含金融、支付交易、餐饮、外卖、酒店旅游、猫眼电影等众多业务线。目前Leaf的性能在4C8G的机器上QPS能压测到近5万/s，TP999 1ms，已经能够满足大部分的业务的需求。每天提供亿数量级的调用量。

• 1
点赞
• 6
收藏
觉得还不错? 一键收藏
• 0
评论
10-12 2407
02-08 4993
01-28 3601
06-13 2989
03-11
12-09 4228

### “相关推荐”对你有帮助么？

• 非常没帮助
• 没帮助
• 一般
• 有帮助
• 非常有帮助

1.余额是钱包充值的虚拟货币，按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载，可以购买VIP、付费专栏及课程。