原文地址:https://duktig.cn/archives/85/
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。
概括下来,那业务系统对ID号的要求有哪些呢?
有时候也会要求含时间戳,这样就能够在开发中快速了解这个分布式id的生成时间。
上述123对应三类不同的场景,3和4需求还是互斥的,无法使用同一个方案满足。
同时除了对ID号码自身的要求,业务还对ID号生成系统的可用性要求极高,想象一下,如果ID生成系统瘫痪,整个美团点评支付、优惠券发券、骑手派单等关键动作都无法执行,这就会带来一场灾难。
ID号生成系统的可用性要求
源码参看:https://github.com/duktig666/distributed-programme
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。
每次生成的ID是无序的,无法保证趋势递增
ID本事无业务含义,不可读
不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
① MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。
All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.
② 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
UUID
类UUID uuid = UUID.randomUUID();
System.out.println(uuid); // 0e070a22-0e2f-422e-96ca-f63987dadba5
这样的UUID还是比较长的,36个字符。
String simpleUUID = IdUtil.simpleUUID();
System.out.println(simpleUUID); // 99407162a2fb48a1a3209dd9f04e5ccb
利用给字段设置
auto_increment_increment
和auto_increment_offset
来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。
begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;
这个方案就是解决mysql的单点问题,在auto_increment基础上,设置step步长。
在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。设置步长step为2,TicketServer1的初始值为1(1,3,5,7,9,11…)、TicketServer2的初始值为2(2,4,6,8,10…)。这是Flickr团队在2010年撰文介绍的一种主键生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap )。如下所示,为了实现上述方案分别设置两台机器对应的参数,TicketServer1从1开始发号,TicketServer2从2开始发号,两台机器每次发号之后都递增2。
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
假设我们要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2…N-1那么整个架构就变成了如下图所示:
利用redis的incr原子性操作自增,一般算法为: 年份 + 当天距当年第多少天 + 天数 + 小时 + redis自增
算法可以调整为 就一个 redis自增,不需要什么年份,多少天等。
关于Redis实现分布式唯一ID的方案,后续会专门写一篇文章来介绍……
Redis构建分布式唯一ID生成器
ZooKeeper分布式ID生成,原理是利用ZooKeeper的临时有序节点,生成全局唯一的ID。
多个客户端同时创建同一节点,zk保证了能有序的创建,创建成功并返回的path类似于/root/generateid0000000001酱紫的,可以看到是顺序有规律的,能较好的解决这个问题,缺点是,会依赖于zk。
关于ZooKeeper实现分布式唯一ID的方案,后续会专门写一篇文章来介绍……
ZooKeeper 构建分布式唯一ID生成器
Twitter的分布式自增ID算法snowflake,这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图所示:
(1L<<41)/(1000L*3600*24*365)=69
年的时间SnowFlake可以保证:
Twitter的分布式雪花算法SnowFlake ,经测试snowflake 每秒能够产生26万个自增可排序的ID
分布式系统中,有一些需要使用全局唯一ID的场景, 生成ID的基本要求:
想解决 【雪花算法 时钟回拨问题】 可以参看并使用美团的 Leaf 。
可使用Hutool提供的 IdUtil 快速实现雪花算法。
/**
* description:Hutool的雪花算法实现
*
* 可以使用Hutool默认提供的方法 IdUtil.getSnowflake 实现雪花算法,一般使用此即可。可根据情况传入 5位dataCenterId 和 5位workerId
*
* 如果使用 IdUtil.createSnowflake 使用雪花算法,需要自行维护单例模式(不同的Snowflake对象创建的ID可能会有重复)。
* 一个比较好的选择是交由 Spring 管理(默认单例)
*
* @author RenShiWei
* Date: 2021/7/18 17:48
**/
@Slf4j
public class HutoolSnowflake {
public static void main(String[] args) {
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
for (int i = 0; i < 1000; i++) {
long id = snowflake.nextId();
System.out.println(id);
}
}
}
雪花算法源码分析参看:雪花算法源码
Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话: >There are no two identical leaves in the world > “世界上没有两片相同的树叶”
综合对比上述几种方案,每种方案都不完全符合美团的要求。所以Leaf分别在上述 数据库自增id 和 雪花算法 方案上做了相应的优化,实现了 Leaf-segment 和 Leaf-snowflake 方案。
GitHub地址:https://github.com/Meituan-Dianping/Leaf
技术文档:https://tech.meituan.com/2017/04/21/mt-leaf.html
参看其GitHub:https://github.com/baidu/uid-generator
参看其GitHub:https://github.com/didi/tinyid