原文: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牧码人」
追求技术的路上永无止境
来源:CSDN
作者:Java牧码人
链接:https://blog.csdn.net/adu003/article/details/104376233