分布式系统——分布式ID方案

Posted loveletters

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式系统——分布式ID方案相关的知识,希望对你有一定的参考价值。

分布式ID介绍

在分布式系统中,我们经常需要生成全局唯一的标识符,例如订单号,用户id,消息id等。这些标识符通常被称为分布式id。分布式id有以下几个常见的要求:

  • 全局唯一:不同的节点或服务不能生成重复的id。
  • 高性能:生成id的速度要快,不能成为系统的瓶颈。
  • 低延迟:生成id的过程要尽可能简单,避免多次网络通信或磁盘访问。
  • 可排序:生成的id要能按照时间顺序进行排序,方便查询和分析。
  • 易扩展:随着系统的增长,能够动态增加或减少生成id的节点或服务,不影响系统的可用性和一致性。

生成策略

数据库自增Id

自增Id是在设计表时将id字段的值设置为自增的形式,这样当插入一行数据时无需指定id会自动根据前一字段的Id值+1进行填充

优点:主键自动增长,不用手工设值、数字型,占用空间小、检索非常有利、有顺序,不会重复。

缺点:并发性能不高,受限于数据库性能、不太适合重构的系统,因为涉及旧数据迁移容易Id冲突,还有外键要考虑、新旧系统上线同时运行,数据涉及双写容易Id冲突。

场景:在互联网公司,对于高并发业务,一般的表主键都是采用数据库自增,即使采用分库分表,也是设置主键自增,原因是:一般表的主键不参与业务处理, 比较常见的设计方式是业务表加一个业务唯一健,而且唯一健不能改变,其他业务表关联这个表的唯一键参与各种逻辑处理。 例如:券表:券Id、券号----活动表:活动Id、活动code-----活动发送记录表: 记录Id、mobile、活动code、券号

UUID生成Id

UUID是通用唯一标识码的缩写,集群全局唯一不会重复,UUID是基于当前时间、计数器、机器的mac地址等数据计算生成的。

优点:本地生成没有网络IO,性能好、全局唯一不重复、使用简单不用引入中间件。

缺点:UUID占用16个字符,空间占用较多,如果是千万级别甚至亿级别特别费空间、不是递增的而且无序、随机写入索引性能下降。

场景:对表空间没太多限制、重构类系统、UUID不作为查询字段的非高并发系统、集群部署的中间件唯一Id。

Redis生成Id

Redis计数器,原子性自增,调用incr、incrBy方法。

优点:并发性能高,有顺序、生成Id格式可以自定义,例如前缀+yyyyMMdd+5位自增。

缺点:使用场景有限、因为自增,数据量容易被猜到,不安全、引入中间件、redis集群部署需要保证高可用成本高。

场景:生成Id要求自增、高性能、适合运营管理后台唯一Id生成,例如活动编号。

雪花算法生成Id

Twitter开源的分布式ID生成算法,主要是由64bit的long型生成的全局ID,引入了时间戳和ID保持自增的属性,组成结构如下:

  • 1bit(1:负数,0:正数)
  • 41bit(时间戳)
  • 5bit(表示机房Id,2^5个机房-32个机房)
  • 5bit(表示机器Id,2^5个机器-32个机器)
  • 12bit(序列号, 这个是用来记录同一个毫秒内产生的不同id,4096)

优点:不依赖外部组件、性能好、按时间递增

缺点:机房+机器设置不合理高并发下可能会重复(应用启动机房Id、机器ID作为启动参数传递保证唯一)、有时钟回拨的坑 例如:729786672057983180、729786672057983181

场景:互联网高并发系统

百度UidGenerator算法

UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。 UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于k8s环境下实例自动重启、IP漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的Uid, 并行化Uid的生产和消费, 同时对CacheLine补齐, 避免RingBuffer带来的伪共享问题. 最终单机QPS可达600万。

场景:互联网高并发系统

美团Leaf算法

全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。 高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。 高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。 接入简单,直接通过公司RPC服务或者HTTP调用即可接入。 支持号段模式和雪花算法: 号段模式依赖于数据库,但是区别于数据库主键自增的模式。假设100为一个号段100,200,300,每取一次可以获得100个Id ,性能显著提高。

场景:互联网高并发系统

实现方案

  1. 非高并发场景: 提供单独的Id生成restful服务,该服务正常情况不会经常改变,基本做到不发版,保证Id生成底层服务稳定,SLA达到4个9。

  2. 高并发场景: 提供一个封装好的Id生成SDK,业务应用集成SDK,调用sdk提供的id生成方法,这里通过以下设计保证高可用: . 业务应用启动批量从Id生成restful服务拉取指定数量的Id列表到内存的阻塞队列中(具体的数量根据应用的内存和并发量决定,比如默认为5000)。 . 业务应用获取Id直接从阻塞队列中获取,提高获取的性能。 . sdk中封装一个定时任务,比如5分钟检查一次,如果阻塞队列的Id数量小于5000* 90% = 4500,则重新通过远程服务接口拉取一批, 保证队列存储的Id足够满足业务应用的需求。

系统设计分布式唯一ID生成方案总结

分布式系统中唯一ID生成方案


在系统设计中,我们经常需要一个全局唯一的ID来标识一条数据,比如订单表,商品表的主键ID。这个ID往往能影响到数据存储、索引和查询等操作的效率。因此这个全局唯一的ID对系统的可用性和性能至关重要。


1. 唯一ID简介

在系统设计中,我们经常需要一个全局唯一的ID来标识一条数据,比如订单表,商品表的主键ID。这个ID往往能影响到数据存储、索引和查询等操作的效率。因此这个全局唯一的ID对系统的可用性和性能至关重要。

全局唯一ID的特点

  • 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求;
  • 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
  • 高可用性:同时除了对ID号码自身的要求,业务还对ID号生成系统的可用性要求极高,想象一下,如果ID生成系统瘫痪,这就会带来一场灾难。所以不能有单点故障。
  • 分片支持:可以控制ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易。
  • 长度适中

由此总结下一个ID生成系统应该做到如下几点:

  1. 平均延迟和TP999延迟都要尽可能低;
  2. 可用性5个9;
  3. 高QPS。

以上描述引用自博客

2. 全局ID常见生成方案

2.1 UUID生成

UUID (Universally Unique Identifier) 的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。如此一来,每个人都可以建立不与其它人冲突的 UUID。在这样的情况下,就不需考虑数据库建立时的名称重复问题。

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

在Java中我们可以直接使用下面的API生成UUID。

UUID uuid  =  UUID.randomUUID(); 
String s = UUID.randomUUID().toString();

优点

  • 性能非常高:本地生成,API调用方便,没有网络消耗。

缺点

  • 存储成本高:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。UUID不是趋势递增的,而现阶段主流的数据库主键索引都是选用的B+树索引,对于无序长度过长的主键插入效率比较低。

2.2 数据库生成

以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

技术图片

这种方案的优缺点如下:

优点

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

缺点

  • 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
  • ID发号性能瓶颈限制在单台MySQL的读写性能。

2.3 Redis生成

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

比较适合使用Redis来生成日切流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点

  • 不依赖于数据库,灵活方便,且性能优于数据库。
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点

  • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
  • 需要编码和配置的工作量比较大。
  • Redis单点故障,影响序列服务的可用性。
//代码整理
//待添加

2.4 利用zookeeper生成

zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。

很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想。

2.5 雪花算法生成

这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:

技术图片

1-bit的时间可以表示(1L<<41)/(1000L360024*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

这种方式的优缺点是:

优点

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

2.6 其他生成方式

百度的uid-generator:

https://github.com/baidu/uid-generator

美团Leaf:

https://github.com/zhuzhong/idleaf

以上是关于分布式系统——分布式ID方案的主要内容,如果未能解决你的问题,请参考以下文章

分布式系统ID生成方案汇总

分布式系统唯一ID生成方案汇总

分布式系统唯一ID生成方案汇总

分布式系统唯一ID生成方案汇总

分布式系统唯一ID的生成方案讨论

一文搞定分布式系统ID生成方案