系统架构:分布式ID那点事儿

寵の児 提交于 2020-02-18 17:29:54

原文:https://zhuanlan.zhihu.com/p/107592567

 

全局唯一

不能出现重复的ID号,既然是唯一标识,这是最基本的要求。

趋势递增

为什么要趋势递增呢?
第一,由于我们的分布式ID,是用来标识数据唯一性的,所以多数时候会被定义为主键或者唯一索引。
第二,大多数互联网公司使用的数据库是MySQL,存储引擎为innoDB,对于BTree索引来讲,数据以自增顺序来写入的话,b+tree的结构不会时常被打乱重塑,存取效率是最高的,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

信息安全

由于数据是递增的,所以,恶意用户的可以根据当前ID推测出下一个,非常危险,所以,我们的分布式ID尽量做到不易被破解。如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

 

 

数据库自增方案缺点:

1.高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限

2.水平扩展困难,严重依赖数据库,扩容需要停机

 

 

Flicker方案:

[flicker算法原文] http://code.flickr.com/blog/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/

Replace into 先尝试插入数据到表中,如果发现表中已经有此行数据(根据主键或者唯一索引判断)则先删除此行数据,然后插入新的数据, 否则直接插入新数据。
一般stub为特殊的相同的值。

改进升华

MySQL配置为双主模式,也就是有两个MySQL实例,这两个都能生成ID。

 

 

数据库号段方案

利用乐观锁来进行控制,比如在数据库表中增加一个version字段,在获取号段时使用如下SQL:

update id_generator set current_max_id=#{newMaxId}, version=version+1 where stub = #{stub} and version = #{version}

这种方案不再强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启,会丢失一段ID,导致ID空洞。

 

 

UUID方案

UUID由以下几部分的组合:

(1)当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。

(2)时钟序列。

(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

缺点:

首先分布式id一般都会作为主键,但是安装mysql官方推荐主键要尽量越短越好,UUID每一个都很长,所以不是很推荐

既然分布式id是主键,然后主键是包含索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键的b+树进行很大的修改,这一点很不好

信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

https://mp.weixin.qq.com/s/kZAnYz_Jj4aBrtsk8Q9w_A

 

 

1.创建顺序节点

2.从节点名截取id

3.避免zookeeper的顺序节点暴增,可以删除创建的顺序节点

 

 

DistributedAtomicLong分布式原子锁

首先使用乐观锁,如果乐观锁失败,就使用Curator提供的InterProcessMutex锁。InterProcessMutex是Curator基于zookeeper提供的分布式锁。

 

 

instagram参考了flickr的方案,再结合twitter的经验,利用Postgre数据库的特性,实现了一个更简单可靠的ID生成服务。

我们可以通过INSERT语句的RETURNING 关键字,将ID返回给应用程序;
这里是the PL/PGSQL的完整例子(例子的schema :insta5)

https://www.jianshu.com/p/fac342e41fb6

https://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram

 

https://docs.mongodb.com/manual/reference/method/ObjectId/

按照字节顺序,依次代表:

4字节:UNIX时间戳

3字节:机器识别码

2字节:表示生成此_id的进程

3字节:由一个随机数开始的计数器生成的值

 

 

https://docs.mongodb.com/manual/reference/method/ObjectId/

前面的九个字节是保证了一秒内不同机器不同进程生成objectId不冲突,这后面的三个字节“36236b”是一个自动增加的计数器,用来确保在同一秒内产生的objectId也不会发现冲突,

允许256的3次方等于16777216条记录的唯一性。

总的来看,objectId的前4个字节时间戳,记录了文档创建的时间;接下来3个字节代表了所在主机的唯一标识符,确定了不同主机间产生不同的objectId;后2个字节的进程id,决定了在同一台机器下,不同mongodb进程产生不同的objectId;最后通过3个字节的自增计数器,确保同一秒内产生objectId的唯一性。ObjectId的这个主键生成策略,很好地解决了在分布式环境下高并发情况主键唯一性问题,值得学习借鉴。

 

 

第一个bit位是标识部分,在java中由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以固定为0。

时间戳部分占41bit,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;

41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年

工作机器id占10bit,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点。

序列号部分占12bit,支持同一毫秒内同一个节点可以生成4096个ID

 

 

缺点:

1.依赖服务器时间

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

服务器时间回调可能会生成重复ID

为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免NTP时间服务器回拨服务器的时间

2.生成的ID取模后可能不均匀

在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数

 

 

百度的uid-generator:https://github.com/baidu/uid-generator

支持自定义workerId位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。

缺点:

启动阶段依赖DB(如自定义实现, 则DB非必选依赖)

百度uid-generator扩展实现的其他算法

基于雪花儿算法百度uid-generator扩展:

https://github.com/sadness-hacker/lmt-zeus/tree/master/lmt-zeus-id-generator

 

优点:解决时间回拔,workId增长问题,通过本地缓存workId和时间戳,减少workId增长过快问题。

 

 


 

                    欢迎关注「Java牧码人

                   追求技术的路上永无止境

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!