分布式ID算法&实现
一、为什么需要分布式ID
1、跨机房部署
如果数据库是跨机房部署,分布式ID是必须的,不然后续做数据分析和统计、跨机房路由会踩大坑。
2、海量数据
如果数据量可能会超出数据库自增ID类型最大值, 分布式ID也是必然面对的。
二、分布式ID的需求有哪些
先看下功能性需求
1、全局唯一
即不管是哪个机房生成的,全局必须唯一,不能和其它机房产生的值冲突
2、单调递增
保证下一个ID一定大于上一个ID
3、具有一定的安全性
像订单号这种场景,不能让人一看最新的ID就知道你每天的订单量了
非功能性要求:
1、高可用
这个一个基础的功能,如果挂掉,会影响很多业务
2、高吞吐量
上面的功能性需求看每家公司自己的业务场景。
三、常用算法有
1、snowflake(雪花)算法
生成一个64bit的数字,数字被划分成多个段:时间戳、机器编码、序号。
优点:
- 整个ID是趋势递增的。
- 高吞吐量。
- 可以根据自身业务需求灵活调整bit划分。
缺点:
- 依赖机器时钟,如果机器时钟回拨,会导致ID重复。
- 在分布式环境下,每台机器上的时钟可能有偏差,有时候会出现不是全局递增的情况。
2、基于数据库
一般基于数据库,充分利用MySQL自增ID的机制。
3、UUID
UUID是Universally Unique Identifier的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符,UUID是16字节128位长的数字,通常以36字节的字符串表示。
优点:
- 本地生成ID,不需要进行远程调用,时延低,性能高。
缺点:
- UUID过长,16字节128位,通常以36长度的字符串表示,很多场景不适用,比如用UUID做数据库索引字段。
- 没有排序,无法保证趋势递增。
- 因为UUID是随机的,在保存数据的时候不是特别高效,查询也不方便。
这种方案一般用的比较少,除非不用存储在数据库中。
四、实现方案
上面讲了大概的理论,我们看下目前比较著名的实现方案。
4.1、美团的Leaf-Segment
实现了Leaf-segment和Leaf-snowflake方案。
4.1.1 Leaf-segment方案
以MySQL举例,利用给字段设置auto_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。
表结构如下:
Create Table Tickets64 (
id bigint(20) unsigned not null auto_increment,
stub char(1),
Primary KEY(id),
UNIQUE KEY ix_stub(stub)
)Engine = InnoDB;
如果业务比较多,可以使用多张表。
刚开始使用如下:
begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;
这个方案的缺点如下:
1)、每次都要操作1次数据库,性能不高。
2)强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。
经过改进,方案如下:
数据库设计如下
+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag | varchar(128) | NO | PRI | | |
| max_id | bigint(20) | NO | | 1 | |
| step | int(11) | NO | | NULL | |
| desc | varchar(256) | YES | | NULL | |
| update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+
重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。
部署架构
1、增加一层proxy
服务不再直连mysql,而是改为连proxy server,这样就可以做隔离了,后续存储改为其它数据库,系统升级也比较方便。
2、增加步长
每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step。
4.1.2 Leaf-snowflake方案
Leaf-segment方案可以生成趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,美团提供了 Leaf-snowflake方案。
启动步骤如下:
- 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
- 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
- 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。
时钟问题解决方案:
- 若写过,则用自身系统时间与leaf_forever/{self}节点记录时间做比较,若小于leaf_forever/{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
- 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
- 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
- 否则认为本机系统时间发生大步长偏移,启动失败并报警。
- 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。
4.2 百度的uid-generator
github地址:https://github.com/baidu/uid-generator
分配方式:
- 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个并发。
worder id由数据库来保存,个人觉得会是一个瓶颈,并且用完即弃,如果机器经常启动,感觉也会很快用完。
单机并发只有8192,看具体业务场景,当然可以再扩充,可以让worder id变短些。
4.3 微信的seqsvr
适用于面向每个用户的场景,像用户数据同步等。
实现思路是:预分配+分号段共享存储+存储和缓存分离
在容灾方面,先是用主从架构
后来再采用动态路由表的方案来解决配置不一致的问题,
细节就不在这里聊了,有兴趣的同学可以百度下:微信序列号生成器架构设计及演变
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Android倒计时神器(CountDownTimer)
- Kotlin如何安全访问lateinit变量的实现
- Android实现加载对话框
- Android RecyclerView实现悬浮吸顶、分隔线、到底提示效果
- Android监听键盘状态获取键盘高度的实现方法
- android实现小音频频繁播放
- 每日一个知识点:什么时候会触发Full GC
- 拥抱kotlin之如何习惯使用kotlin高阶函数
- Android中调用另一个Activity并返回结果(选择头像功能为例)
- Android 自定义缩短Toast显示时间的实例代码
- Android Presentation实现双屏异显
- Android中点击按钮启动另一个Activity及Activity之间传值问题
- Android中使用SeekBar拖动条实现改变图片透明度(代码实现)
- Android实现原生锁屏页面音乐控制
- android实现简单音乐播放器