Java架构直通车——分布式唯一 ID生成方案
文章目录
- 分布式ID的几种生成方案
- UUID
- MySQL主键自增
- 数据库自增ID改进方案
- 雪花算法(SnowFlake)
- 雪花算法的优化
- Redis自增id
- Zookeeper有序节点
最近要做区块链项目,要生成很多唯一ID做业务号之类的,所以趁此机会学习学习。
分布式ID的几种生成方案
UUID
之前一直是用的UUID生成唯一ID,好处显而易见,方便快捷,坏处就是:
- 数据库里不好做索引,每次生成的ID是无序的,无法保证趋势递增。
- UUID的字符串存储,存储空间大,查询效率慢。
- ID本事无业务含义,不可读。
很明显,用来做业务号不是UUID的应用场景,使用UUID应该要保障不要求递增,无确实含义的场景,比如说做令牌Token使用。
MySQL主键自增
这个方案就是利用了MySQL的主键自增auto_increment,默认每次ID加1。
这样做的好处:
- id是有序的,能够保证自增。
- 查询效率高,具有一定的业务可读。
坏处也是显而易见:单点问题,对于单个数据库压力过大,高并发扛不住。
解决方案是按步长自增:
这样能够解决一个单点的问题,缺陷是:
- 一旦把步长定好后,就无法扩容。
- 虽然相比于单机的方式,数据库压力小了很多,但是还是有一定压力的。
数据库自增ID改进方案
- 【用户服务】在注册一个用户时,需要一个用户ID;会请求【生成ID服务(是独立的应用)】的接口。
- 【生成ID服务】会去查询数据库,找到user_tag的id,现在的max_id为0,step=1000。(可以加上行锁,防止两个事务请求到相同的结果)
- 【生成ID服务】把max_id和step返回给【用户服务】;并且把max_id更新为max_id = max_id + step,即更新为1000。
- 【用户服务】获得max_id=0,step=1000;这个用户服务可以用ID=【max_id + 1,max_id+step】区间的ID,即为【1,1000】
- 【用户服务】会把这个区间保存到jvm中。用户服务】需要用到ID的时候,在区间【1,1000】中依次获取id,可采用AtomicLong中的getAndIncrement方法。
- 如果把区间的值用完了,再去请求【生产ID服务】接口,获取到max_id为1000,即可以用【max_id + 1,max_id+step】区间的ID,即为【1001,2000】
这个方案就非常完美的解决了数据库自增的问题,而且可以自行定义max_id的起点,和step步长,非常方便扩容。
而且也解决了数据库压力的问题,因为在一段区间内,是在jvm内存中获取的,而不需要每次请求数据库。即使数据库宕机了,系统也不受影响,ID还能维持一段时间。
雪花算法(SnowFlake)
SnowFlake算法生成id的结果是一个64bit大小的整数(也就是long类型,或者说bigInt类型),它的结构如下图:
- 1bit-不用:
因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。 - 41bit-时间戳:
用来记录时间戳,毫秒级。如果只是用来记录整数的时间戳的话,那么41bit实际上可以记载:
241/(365∗24∗60∗60∗1000ms)2^{41}/(365*24*60*60*1000ms)241/(365∗24∗60∗60∗1000ms),约为69年。 - 10bit-工作机器id:
用来记录工作机器id,那么可以部署的机器数目为210=10242^{10}=1024210=1024台机器,包括5位datacenterId(机房id)和5位workerId(机器id)。 - 12bit-序列号:
用来记录同毫秒内产生的不同id,也就是一台机器上同一毫秒的并发量是212=40962^{12}=4096212=4096次。
SnowFlake可以保证:
- 所有生成的id按时间趋势递增
- 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)
这里分析一下生成ID的函数。
//下一个ID生成算法//使用了synchronized生成ID,做一个阻塞,防止同一毫秒内生成相同的12位序列号public synchronized long nextId() {long timestamp = timeGen();//获取到当前的时间戳//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常if (timestamp < lastTimestamp) {System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",lastTimestamp - timestamp));}//获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {sequence = 0;}//将上次时间戳值刷新lastTimestamp = timestamp;/*** 返回结果:* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数* (workerId << workerIdShift) 表示将工作id左移相应位数* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id*/return ((timestamp - twepoch) << timestampLeftShift) |(datacenterId << datacenterIdShift) |(workerId << workerIdShift) |sequence;}
优点:
- 此方案每秒能够产生409.6万个ID,性能快
- 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增
- 灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求
缺点:
- 依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成。
在分布式场景中,服务器时钟回拨会经常遇到(时间校准,以及其他因素,可能导致服务器时间回退),一般存在10ms之间的回拨;小伙伴们就说这点10ms,很短可以不考虑吧。但此算法就是建立在毫秒级别的生成方案,一旦回拨,就很有可能存在重复ID。
雪花算法的优化
synchronized
关键字:
此锁的目的是为了保证在多线程的情况下,只有一个线程进入方法体生成ID,保证并发情况下生成ID的唯一性,如果在竞争激烈情况下,自旋锁+ CAS原子变量的方式或许是更为合理的选择,可以达到优化部分性能的目的。
- 时钟回拨问题:
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。另外,它通过消费未来时间克服了雪花算法的并发限制。UidGenerator提前生成ID并缓存在RingBuffer中。
RingBuffer,如下图所示,它本质上是一个数组,数组中每个项被称为slot。UidGenerator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:
RingBuffer of Flag:
保存flag这个RingBuffer的每个slot的值都是0或者1,0是CAN_PUT_FLAG的标志位,1是CAN_TAKE_FLAG的标识位。也就是可以放置UID或者拿取UID的标识。RingBuffer of UID:
保存唯一ID的RingBuffer有两个指针,Tail指针和Cursor指针。
Tail指针表示最后一个生成的唯一ID。如果这个指针追上了Cursor指针,意味着RingBuffer已经满了。这时候,不允许再继续生成ID了。
Cursor指针表示最后一个已经给消费的唯一ID。如果Cursor指针追上了Tail指针,意味着RingBuffer已经空了。这时候,不允许再继续获取ID了。
初始化阶段:
- 根据boostPower的值确定RingBuffer的size。bufferSize=2132^{13}213, 扩容后bufferSize =2132^{13}213<<boostPower(位移操作)。
- 构造RingBuffer,默认paddingFactor为50。这个值的意思是当RingBuffer中剩余可用ID数量少于50%的时候,就会触发一个异步线程往RingBuffer中填充新的唯一ID。
- 初始化PUT和TAKE的拒绝策略,也就是满了或者空了之后应该怎么做。
- 初始化填满RingBuffer中所有slot(填满所有ID)。
百度UidGenerator的优势:
- 不依赖系统时间:
传统的雪花算法实现都是通过System.currentTimeMillis()来获取时间并与上一次时间进行比较,这样的实现严重依赖服务器的时间。而UidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题(这种做法也有一个小问题,即分布式ID中的时间信息可能并不是这个ID真正产生的时间点,例如:获取的某分布式ID的值为3200169789968523265,它的反解析结果为{“timestamp”:“2019-05-02 23:26:39”,“workerId”:“21”,“sequence”:“1”},但是这个ID可能并不是在"2019-05-02 23:26:39"这个时间产生的)。 - 使用缓存
Redis自增id
利用redis的incr原子性操作自增,一般算法为:年份 + 当天距当年第多少天 + 天数 + 小时 + redis自增。
优点:
- 有序递增,可读性强。
- 性能还可以。
缺点:
- 占用带宽,每次要向redis进行请求,并发强依赖了Redis。
- ID安全性的问题,如:Redis方案中,用户是可以预测下一个ID号是多少,因为算法是递增的。(当然自增的ID都存在这样的问题)
比如,竞争对手第一天中午12点下个订单,就可以看到平台的订单ID是多少,第二天中午12点再下一单,又平台订单ID到多少。这样就可以猜到平台1天能产生多少订单了。
Zookeeper有序节点
通过创建ZK的顺序模式的节点,可以生成全局唯一的ID。
Java架构直通车——分布式唯一 ID生成方案相关推荐
- 一线大厂的分布式唯一ID生成方案是什么样的?
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 一.前言 分布式系统中我们会对一些数据量大的业务进行分拆,如:用户 ...
- 一起学习下一线大厂的分布式唯一ID生成方案!
来源 | http://www.toutiao.com/i6682672464708764174 一.前言 分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表.因为数据量巨大一张表无法 ...
- mysql 分布式 生成序号_分布式唯一ID生成方案
唯一ID在业务系统中经常用到,例如数据库的唯一主键,那么唯一ID如何生成,我们这里介绍一些常见的实现方案. 字符串ID 如果采用字符串id,那么很简单,直接使用jdk自带的UUID,原始生成的是带中划 ...
- 常用的分布式唯一ID生成方案
全局唯一ID使用场景 分布式系统设计时,数据分片场景下,通常需要一个全局唯一id: 在消息系统中需要消息唯一ID标识来防止消息重复: 多系统打通需要一个全局唯一标识 (如集团各业务线面对不同用户,需要 ...
- 分布式唯一ID生成方案
在应用程序中,经常需要全局唯一的ID作为数据库主键.如何生成全局唯一ID? 首先,需要确定全局唯一ID是整型还是字符串?如果是字符串,那么现有的UUID就完全满足需求,不需要额外的工作.缺点是 ...
- Java分布式唯一ID生成方案——比UUID效率更高的生成id工具类
package com.xinyartech.erp.core.util;import java.lang.management.ManagementFactory; import java.net. ...
- Java秒杀系统实战系列~分布式唯一ID生成订单编号
摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...
- java 唯一编号_Java秒杀系统实战系列~分布式唯一ID生成订单编号
摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...
- 分布式系统唯一ID生成方案浅析
有情怀,有干货,微信搜索[荒古传说]关注这个不一样的程序员. 分布式系统唯一ID生成方案浅析 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识.业务ID需要满足的要求如下 全局唯一性:不能出 ...
最新文章
- Vue2.0使用vue-cli脚手架搭建
- 遗传算法 python包_遗传算法 (Genetic Algorithm)
- CentOS设置服务开机启动的方法
- C++11 (多线程)并发编程总结
- Accent-Insensitive, Accent Sensitive, a ã, e é 模糊查询
- Winform中实现对照片添加文字和图片水印(附代码下载)
- 某大型企业私有云建设思路解析
- 足不出户,游遍七大洲,不可错过的14部地理纪录片!
- jQuery中的几个案例:隔行变色、复选框全选和全不选
- HEVC/H265 文档获得
- 背景色渐变html代码,求html文字背景色渐变的代码
- mysql 去重命令_MySQL 命令操作
- 如何实现Miracast多个设备同时连接投屏
- javaweb零食商城系统设计与实现(ssm项目)(含论文和源码)
- 《你是三月的小桨》光剑
- rgb sw 线主板接口在哪_有颜值也有实力!利民TL-C12S幻彩RGB电脑散热风扇评测
- java qlv转mp4 代码_怎么将qlv格式转换成mp4?教你快速转换视频格式的技巧
- vue网页打印后事件失效
- Ubuntu如何通过简单的几步操作来配置系统的软件源、找不到软件和更新怎么办?(带动态图文介绍)
- echart旭日图_基于Echarts4.0实现旭日图
热门文章
- Django DRF 视图集
- 记一次面试及总结:上海春翔网络科技有限公司
- springboot 使用itextpdf 框架实现多个图片合成一个pdf文件
- Linux应用层——IO(输入输出)编程
- intel 服务器主板芯片,台积电3nm制程获Intel订单,不打造手机芯片太可惜了
- linux代码生成时间戳,linux生成连续时间戳
- 怎么做新闻营销?新闻营销如何“借势”?
- c++ 11 recursive_mutex 递归锁
- matlab2006b plp,Matlab R2006b安装教程
- 第一章:基于 SpringBoot 快速搭建QQ机器人,并监听群事件