分布式唯一id生成器

分布式唯一ID要求

  1. 唯一性:生成的ID全局唯一,在特定范围内冲突概率极小。
  2. 有序性:生成的ID按某种规则有序,便于数据库插入及排序递增
  3. 可用性:可保证高并发下的可用性, 确保任何时候都能正确的生成ID。
  4. 自主性:分布式环境下不依赖中心认证即可自行生成ID。
  5. 安全性:不暴露系统和业务的信息, 如:订单数,用户数等。

mysql方案

专门单库单表,用来生成id

优点:超级简单
缺点:单库单表,高并发支持不住,没有高可用,需要定期删除数据,不适合上生产。大数据低并发,用flickr的数据库唯一id生成方案。

UUID方案

优点:无压力
缺点:mysql会页分裂,查询效率低,不适合做分布式唯一id(其它场景的唯一id可以)

snowflake雪花id方案

核心思想,用long的64个bit位,最高位是0,41位放时间(最多可以用69年),10位放机器标识(最多1024台机器),12位放序号(每毫秒,每台机器,可以顺序生成4096个ID)

优点:高性能,高并发,分布式,可伸缩,可以保证局部递增
缺点:有很多生产问题需要自行处理,如时钟回拨问题,需要独立部署维护

Redis自增方案

核心思想:redis单线程,绝对有序自增;内存高性能,集群部署可以支持高并发高可用,根据集群找到机器并计算步长(如5台机器,初始值为1 2 3 4 5,步长自增为5,第一台存的值为1 6 11 16...第二台存的值为2 7 12 17...直到第5台机器是5 10 15 20)

优点:不需要额外开发,可以直接用公司提供的redis集群
缺点:客户端需要写死Redis机器数量,每次获取id都需要找到一台机器,然后根据步长去incrby,返回给系统;伸缩麻烦,抗不住不好改,需要改步长,id需要重新来搞,id方案不好搞

场景:一般不用,分库分表后,一万左右的并发,单机部署,但需要用主从同步+哨兵(异步备份,有id重复的问题)

时间戳+业务id的组合

核心思想:比如打车,时间戳+起点编号+车牌号;电商,时间戳+用户id,一般手点不会重复,还能加下单渠道、第一个商品id等等组合

优点:实现简单,没有额外成本,没有并发之类的扩容问题
缺点:有的场景会有小概率重复,有的场景没法组合成唯一id

场景:部分生产可用,能用尽量用。

flickr数据库mysql唯一id方案

原始方案

需要找一个库建一张表,作为id表

CREATE TABLE `uid_sequence` (  
  `id` bigint(20) unsigned NOT NULL auto_increment,  
  `stub` char(1) NOT NULL default '',  
  PRIMARY KEY  (`id`),  
  UNIQUE KEY `stub` (`stub`)  
) ENGINE=MyISAM;


REPLACE INTO uid_sequence (stub) VALUES ('test');  
SELECT LAST_INSERT_ID();  

REPLACE INTO 表里只有一行数据,LAST_INSERT_ID是connection级别,所以select在多个客户端之间不会有问题

支持多业务的优化点,可以把 value 的值作为一个值(业务字段),那么可以select id from table where stub=业务字段,就可以对每个业务有自己的全局自增id

mysql必须双机,主从,高可用方案,两个库的要设置不同的起始位置和步长,保证两个库的id唯一

优点:高可用,表数据量小,多业务
场景:低并发场景

flickr相对高并发变种方案

阿里有一个TDDL中间件的唯一ID生成方案,思想也是号段思想。
mysql获取的只是号段,每个号段有一定范围,如1代表1-1000,2代表1000-2000……
每台服务启动后,先获取mysql的号段,用AtomLong来自增,当超过了最大值,就重新获取号段

缺点:没有自增到最大id,会浪费,需要做号段本地持久化。再特殊高并发场景下,数据库还是扛不住,比如1000为步长,每秒有1000的并发,相当于每秒都需要取数据库获取号段。数据库难以扩容。

snowflake 雪花id方案的优化

示例代码,就是位运算,网上有很多

机器id的获取

通过zk,比如给一个顺序节点,分布式id服务启动后就获取,获取到可以持久化到本地磁盘上。在扩容后就重新获取。

时钟回拨问题解决

当发生时钟回拨时,回拨时间

  1. 1s以内,在本机内存中存储1秒内每一毫秒生成的所有的id,回到多少秒就根据原来的记录递增
  2. 1-10s,返回请求其它的状态码,让客户端去请求其它机器
  3. 10s以上,主动注册中心下线该节点,等恢复了再重新注册

服务宕机解决

异步持久化,是一个方案,但是过于麻烦,可靠性不好,最好的方案是:
通过zk重新生成一个机器码。

美团leaf方案

https://tech.meituan.com/2017/04/21/mt-leaf.html
包含flickr的优化和snowflake优化

Leaf-segment数据库方案

改动点:

  • 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

通常推荐号段(segment)长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。

这种模式有以下优缺点:

优点:

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
  • DB宕机会造成整个系统不可用。

对缺点2的双buffer优化
DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。
采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

对缺点3的Leaf高可用容灾优化
对于第三点“DB可用性”问题,我们目前采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式[5]同步数据。同时使用公司Atlas数据库中间件(已开源,改名为DBProxy)做主从切换。当然这种方案在一些情况会退化成异步模式,甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。如果你的系统要保证100%的数据强一致,可以选择使用“类Paxos算法”实现的强一致MySQL方案,如MySQL 5.7前段时间刚刚GA的MySQL Group Replication。但是运维成本和精力都会相应的增加,根据实际情况选型即可。
同时Leaf服务分IDC部署,内部的服务化框架是“MTthrift RPC”。服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施

Leaf-snowflake方案

使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。

Leaf-snowflake是按照下面几个步骤启动的:

  • 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  • 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
  • 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

弱依赖ZooKeeper
除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA。

解决时钟问题
因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

  • 若写过,则用自身系统时间与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}。
posted @ 2022-10-09 22:07  RichardHaha  阅读(266)  评论(0编辑  收藏  举报