分布式全局唯一ID的实现

分布式全局唯一ID的实现

前言

上周末考完试,这周正好把工作整理整理,然后也把之前的一些素材,整理一番,也当自己再学习一番。
一方面正好最近看到几篇这方面的文章,另一方面也是正好工作上有所涉及,所以决定写一篇这样的文章。
先是简单介绍概念和现有解决方案,然后是我对这些方案的总结,最后是我自己项目的解决思路。

概念

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。

如在金融、电商、支付、等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求,此时一个能够生成全局唯一ID的系统是非常必要的。

特点:

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

运用场景:

分布式全局唯一ID(数据库的分库分表后需要有一个唯一ID来标识一条数据或消息;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识;MQ中消息的高可用性(确认消息是否发送成功,是否已发送等)等)
其实分布式全局ID是一个比较复杂,重要的分布式问题(什么问题涉及真正的分布式,高并发后都会比较复杂)。常见解决方案有UUID,Snowflake,Flicker,Redis,Zookeeper,Leaf等。

实现方案:

UUID(此处用的Version1:共五个版本,Version1是基于时间的)

生成一个32位16进制字符串(16字节的128位数据,通常以32位长度的字符串表示)(结合机器识别码(全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得),当前时间,一个随机数)。

优点:

  • 性能好;
  • 扩展性高;
  • 本地生成;
  • 无网络消耗;
  • 不需要考虑性能瓶颈;
  • 不需要提前商定,各自为政,但绝对不会冲突

缺点:

  • 无法保证趋势递增(由于数据库MySQL的InnoDB采用聚簇索引,有序的ID可以保证写入速度);
  • UUID过长(消耗内存,带宽等。更重要的是如果存储在数据库中,作为主键建立索引效率低)

适用场景:

不需要考虑空间占用,不需要生成有递增趋势的ID,且不在MySQL中存储。

Snowflake

Twitter开源,生成一个64bit(0和1)字符串(1bit不用,41bit表示存储时间戳,10bit表示工作机器id(5位数据标示位,5位机器标识位),12bit序列号)

结构:

  • 首位符号位:因为ID一般为正数,该值为0.
  • 41位时间戳(毫秒级):时间戳并不是当前时间戳,而是存储时间戳的差值(当前时间戳-起始时间戳(起始时间戳需要程序指定),理论可以适用(1<<41)/(1000x60x60x24x365),69年。
  • 10位数据机器位(说白了就是逻辑分片ID,具体实现和机器本身无关系):包括5位数据标识位和5位机器标识位(比如5位机房ID,5位机器ID),理论最多可以部署节点位:1<<10=1024。
  • 12位毫秒内的序列:同一节点,同一时刻(同一毫秒内)最多生成ID数1<<12=4096。

最后生成64位Long型数值(这里指,一般Long数据就是64位bit的)。

优点:

  • 趋势递增,且按照时间有序;
  • 性能高,稳定性高,不依赖数据库等第三方系统;
  • 可以按照自身业务特性灵活分配bit位(比如机器位改为15bit,序列位改为7bit)。

缺点:

  • 依赖机器时钟(虽然UUID也根据当前时间,但其非时间部分波动太大了(重新组织措辞)),时钟回拨会造成暂不可用或重复发号(分布式系统中,每台机器上的时钟不可能完全同步。在同步各个服务器的时间时,有一定几率发生时钟回拨(时间超了,往回拨))

适用场景:

要求高性能,可以不连续,数据类型为long型。

Flicker

主要思路是涉及单独的库表,利用数据库的自增ID+replace_into,来生成全局ID。

前置补充:

replace into跟insert功能类似,不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入。否则直接插入新数据。

建表:create table t_global_id(id bigint(20) unsigned not null auto_increment,stub char(1) not null default '',primary key (id),unique key stub (stub)) engine=MyISAM;
(stub:票根,对应需要生成ID的业务方编码,可以是项目名,表名,甚至是服务器IP地址。MyISAM(MYSQL5.5.8前默认数据库存储引擎,5.5.8及之后默认存储引擎为InnoDB):(此处应当有MyISAM与InnoDB引擎的区别,乃至其他引擎)基于ISAM类型。不是事务安全(没有事务隔离??),不支持外键,没有行级锁。如果执行大量的select,建议MyISAM。获取数据:# 每次业务可以使用以下SQL读写MySQL得到ID号replace into t_golbal_id(stub) values('a');select last_insert_id();

扩展:为解决单点问题,启用多台服务器,如MySQL,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增(如通过设置起始值与步长,生成奇偶数ID)

优点:

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

缺点:

  • 强依赖DB,当DB异常时,整个系统不可用,属于致命问题(配置主从复制可以尽可能地增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能导致重复发号);
  • 水平扩展困难(定义好了起始值,步长和机器台数之后,如果要添加机器就比较麻烦(为什么我想到了REDIS的哈希一致原理));
  • ID发号性能瓶颈限制在单台MySQL的读写性能。

适用场景:

数据量不大,并发量不大。

Redis

由于Redis的所有命令是单线程的,所以可以利用Redis的原子操作INCR和INCRBY,来生成全局唯一的ID。

扩展:

可以通过集群来提升吞吐量(可以通过为不同Redis节点设置不同的初始值并同意步长,从而利用Redis生成唯一且趋势递增的ID)(其实这个方法和Flicker一致,只是利用到了Redis的一些特性,如原子操作,内存数据库读写快等)(Incrby:将key中储存的数字加上指定的增量值。这是一个“INCR AND GET”的原子操作,业务方可以定义一个自己的key值,通过INCR命令来获取对应的ID)

优点:

不依赖数据库,灵活方便,且性能优于基于数据库的Flicker方案。

缺点:

  • 扩展性低,Redis集群需要设置号初始值与步长(与Flicker方案一样);
  • Redis宕机可能生成重复的ID;如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;
  • 需要编码和配置的工作量比较大。

适用场景:

Redis集群高可用,并发量高。

举例:

利用Redis来生成每天从0开始的流水号。如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,适用INCR进行累加。

zookeeper

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

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

Leaf

美团的Leaf分布式ID生成系统,在Flicker策略与Snowflake算法的基础上做了两套优化的方案:Leaf-segment数据库方案(相比Flicker方案每次都要读取数据库,该方案改用proxy server批量获取,且做了双buffer的优化)与Leaf-snowflake方案(主要针对时钟回拨问题做了特殊处理。若发生时钟回拨则拒绝发号,并进行告警)。

MongDB objectID

ObjectID可以算作和snowflake类似方法,通过”时间+机器码+pid+inc”共12个字节,通过4+3+2+3的方式,最终标识一个24长度的十六进制字符。

理论总结

其实除了上述方案外,还有ins等的方案,但总的来看,方案主要分为两种:第一有中心(如数据库,包括MYSQL,REDIS等),其中可以会利用事先的预约来实现集群(起始步长)。第二种就是无中心,通过生成足够散落的数据,来确保无冲突(如UUID等)。站在这两个方向上,来看上述方案的利弊就方便多了。

中心化方案:

优点:

  • 数据长度相对小一些;
  • 数据可以实现自增趋势等。

缺点:

  • 并发瓶颈处理;
  • 集群需要实现约定;横向扩展困难(当然有的方案看起来后两者没有那么问题,是因为,这些方案利用其技术特性,早就一定程度上解决了这些问题,如Redis的横向扩展等)。

非中心化方案:

优点:

  • 实现简单(因为不需要与其他节点存在这方面的约定,耦合);
  • 不会出现中心节点带来的性能瓶颈;
  • 扩展性较高(扩展的局限往往集中于数据的离散问题)。

缺点:

  • 数据长度较长(毕竟就是通过这一特性来实现无冲突的);
  • 无法实现数据的自增长(毕竟是随机的);
  • 依赖数据生成方案的优劣(数据生成方案的优劣会全盘接收,但可以推成出新)。

体悟:

技术是无穷无尽的,我们不仅需要看到其中体现的思想与原则,在学习新技术或方案时,需要明确其中一些特性,优缺点的来源,从而进行有效的总结归纳。

应用角度来说:(一方面想要标示符短,便于处理与存储,另一方面想要足够大,而不会产生冲突。呵呵)。最理想就是追求从0开始,每个标示符都被使用,且不重复,而且不用担心并发。呵呵。完全应该根据当前业务场景来选择,毕竟业务场景在当前是确定的。如果业务变动较大(比如发展初期,业务增长很快),那就需要考虑扩展性,便于日后进行该模块的更新与技术方案的替换实现(避免一个系统开发一年,用不到一年,那就尴尬了))。

个人经验

我曾经做过一个“工业物联网”系统,该系统系统是分为三个子系统:终端服务器(用于收集终端传感器数据);企业中控服务器(接收来自多个终端服务器的数据,进行综合查看与控制);云平台服务器(提供上云)。其中就涉及多个终端服务器的传感器数据辨识问题,这里以倾斜传感器数据为例。简述不同终端服务器的倾斜数据的如何实现全局唯一标识。

以企业中控服务器的数据库作为统一的数据标识来源

简单说,就是终端服务器发送一个数据到企业中控室,企业中控服务器就将该数据保存到数据库中,那么每个数据在企业中控服务器数据库中都有唯一的ID,并且保持了自增。

优点是实现简单,只需要做好数据收发,与数据的插入工作即可。唯一需要注意的是数据库插入时注意资源互斥,防止出现数据插入异常问题(Springframework生成的Bean默认时单例的)。

缺点是需要实时收发数据,防止数据丢失,数据积压,数据的create_time异常等问题。

以UUID等方式生成数据的全局唯一标识

简单说,就是终端服务器要发送的数据赋予UUID这样的ID,来确保全局唯一。这样终端服务器就可以和中控服务器保持同样且不冲突的ID了。数据的生成是实现在终端服务器的,而中控服务器只是作为数据的保存与调用(通过统一ID调用)。

优点是不需要数据的实时收发,避免系统在弱网络情况下出现各类异常。

缺点是数据的ID过长,并且无法保持自增。并且在某种程度上带来了数据复杂度,从而提高了系统复杂度。

落地方案

由于实际业务的需求,如弱网络,数据交互频率跨度大等情况。最终我的实现是先由终端服务器在启动之初,在企业中控服务器注册TerminalId,作为不同终端服务器的标识。不同终端服务器接收与保存数据时,都会在每条数据中插入TerminalId,便于企业中控服务器的识别。当然,具体实现当中还有一些细节。如终端服务器在注册时由于网络等情况注册失败,会先建立一个类似UUID的TerminalId来先保存监测数据。当注册成功时(系统会根据TerminalId的长度等特性来判断是否注册失败,是否需要重新注册),会重新修改所有数据的TerminalId,再允许数据上传。

优点是确保了数据在弱网络情况下的正确性,并且实现了自动注册等通用模块的实现。

缺点是最终数据插入企业中控服务器数据库时,并没有严格实现数据符合实际时间的增长(如某终端服务器由于网络等情况没法发送数据,等待一段时间后发送了这段时间堆积的数据),但保持了总体增长的趋势。

总结

IT没有银弹,我们要做的是多去了解现有的技术方案,再产生符合自己需求的技术方案。因为不同的技术方案都因为其使用场景有着各自的特点,而我们需要了解各种特点的技术来源(是什么技术造就了这一特点,或者说是什么架构造就了这一特点等),从而构建出最符合自己需求的技术方案。

没有最好,只有最适合。

附录

参考

阿里P8架构师谈:分布式系统全局唯一ID简介、特点、5种生成方式

分布式ID生成策略

分布式全局唯一ID生成策略

转载:https://www.cnblogs.com/Tiancheng-Duan/p/10962704.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/555580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql多个分类取n条_MySQL获取所有分类和每个分类的前N条记录

MySQL获取所有分类和每个分类的前N条记录。比如有文章表 test(Id,type,tiem)&#xff0c;现在要用SQL找出每种类型中时间最新的前N个数据组成的集合&#xff0c;一段不错的代码&#xff0c;留存备用。SELECT a1.* FROM test a1INNER JOIN (SELECT a.type,p.time FROM test aLEF…

Java接口学习(接口的使用、简单工厂、代理模式、接口和抽象类的区别)

前言引入 官方解释&#xff1a;Java接口是一系列方法的声明&#xff0c;是一些方法特征的集合&#xff0c;一个接口只有方法的特征没有方法的实现&#xff0c;因此这些方法可以在不同的地方被不同的类实现&#xff0c;而这些实现可以具有不同的行为&#xff08;功能&#xff0…

java 矩阵转置_图解利用Java实现数组转置

我们编写Java代码&#xff0c;如下图所示&#xff1a;package com.tina;public class demo {public static void main(String args[]) {int data[] new int[] { 1, 6, 3, 9, 5, 7, 2, 0, 4, 8 };PrintArray(data);}// 输出数组内容public static void PrintArray(int arr[]) {…

java中static、final、static final浅析

final final可以修饰类、属性、方法、局部变量、参数&#xff0c;不能修饰接口&#xff01;final修饰类&#xff1a;该类不能被继承&#xff08;解释了为什么不能修饰接口&#xff0c;不过接口里面的属性、方法等是可以用final修饰的&#xff09;&#xff1b;final修饰属性&am…

最短路径 floyd java_java实现Floyd算法求最短路径

关于无向图的最短路径问题&#xff1a;这个程序输出&#xff1a;最短路径矩阵例如:W[0][5]9 代表vo->v5的最短路径为9W:0 1 3 7 4 91 0 2 6 3 83 2 0 4 1 67 6 4 0 3 24 3 1 3 0 59 8 6 2 5 0package com.xh.Floyd;import java.util.ArrayList;public class Floyd_01 {publi…

SpringBoot 使用 log4j2

一、新建工程 选择一些基础依赖 填写工程名称和项目路径 二、工程配置 修改文件编码格式 设置Java Compiler 修改maven配置文件路径 三、pom.xml的web依赖中排除掉logging依赖&#xff0c;并且引入log4j2依赖 <dependency><groupId>org.springframework.…

springBoot 通过使用log4j2

1.排除 Spring-boot-starter 默认的日志配置 将原本的 spring-boot-starter 改为 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><groupId>…

java finalize 何时被调用_finalize()方法什么时候被调用?析构函数(final

finalize()方法也叫收尾方法。一旦垃圾回收器准备好释放对象占用的存储空间&#xff0c;首先会去调用finalize()方法①进行一些必要的清理工作(对垃圾回收器不能处理的特殊情况进行处理)(例子在下边)②也有可能使该对象重新被引用&#xff0c;我习惯叫这种作用为复活。注意&…

编程和java是什么关系_C语言和Java编程有什么区别?

C语言和Java编程有什么区别&#xff1f;Java从根本上说是c之后的一种改进语言&#xff0c;纯面向对象的一种编程语言(当然比起Ruby还是差一点)&#xff0c;有了C语言的基础固然对学习Java有帮助&#xff0c;因为在某种程度上Java和C语言是比较接近的。但是如果没有学习过C语言也…

SpringBoot默认日志logback配置解析

SpringBoot默认日志logback配置解析 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候&#xff0c;是带着下面几个问题来查资料的&#xff0c;你呢 如何引入日志&#xff1f;日志输出格式以及输出方式如何配置&#xff1f;代码中如何使用&#xff1f; 正文…

java lang报错_java.lang.UnsupportedClassVersionError:JDK版本不一致报错

08-15 14:13:29 ERROR doPost(jcm.framework.rmi.RMIServlet:155) -SchedulerService.forceRunJobFlow error.未指定错误&#xff0c;请查看详细信息at jcm.framework.rmi.ClientService.execute(ClientService.java:129)at ...(...)at jcm.flowengine.impl.JobFlowEngine.runJ…

SpringBoot 之Spring Boot Starter依赖包及作用

spring-boot-starter 这是Spring Boot的核心启动器&#xff0c;包含了自动配置、日志和YAML。 spring-boot-starter-amqp 通过spring-rabbit来支持AMQP协议&#xff08;Advanced Message Queuing Protocol. 。 spring-boot-starter-aop 支持面向方面的编程即AOP&#xff0…

java中怎么判断相等_Java中判断相等 (== 与 .equals())

1.Java中有两种判断相等的方法&#xff1a;1.1首先是运算符对于基本类型而言&#xff0c;运算符比较的是值是否相等(本质也是比较的地址&#xff0c;因为常量在常量池中的地址不可改变)int a 3;int b 3;System.out.println(ab);//结果为true对于引用类型而言&#xff0c;运算…

SpringBoot查看和修改依赖的版本

springBoot依赖管理&#xff1a; 1、引入父项目的作用是实现对所有依赖的管理。 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version> &l…

java wait 释放锁_JAVA锁之wait,notify(wait会释放锁,notify仅仅只是通知,不释放锁)...

wait是指在一个已经进入了同步锁的线程内&#xff0c;让自己暂时让出同步锁&#xff0c;以便其他正在等待此锁的线程可以得到同步锁并运行&#xff0c;只有其他线程调用了notify方法(notify并不释放锁&#xff0c;只是告诉调用过wait方法的线程可以去参与获得锁的竞争了&#x…

在IDEA中解决jar包冲突的神操作-必看

在开发过程中&#xff0c;经常会遇到导入jar包后jar包冲突的情况&#xff0c;大家也都知道&#xff0c;解决jar包冲突通常都比较麻烦&#xff0c;要找到多余的依赖&#xff0c;把低版本的依赖去掉。而大家通常能搜到IDEA解决jar包冲突的方法&#xff0c;应该是这样的&#xff1…

java 图片不能正常移动_Java,我的图像不会更新/移动

我对Java的东西是一个新手&#xff0c;但是..在网上阅读了很多内容之后&#xff0c;我一直在努力开发这款游戏并开始使用&#xff0c;我正在使用一些图片。我想通过KeyListener来更新他们的立场以展示运动的过程&#xff0c;我相信。不幸的是&#xff0c;图像仍然在同一个地方&…

MySQL保存或更新 saveOrUpdate

1. 引子 在项目开发过程中&#xff0c;有一些数据在写入时候&#xff0c;若已经存在&#xff0c;则覆盖即可。这样可以防止多次重复写入唯一键冲突报错。下面先给出两个MyBatis配置文件中使用saveOrUpdate的示例 <!-- 单条数据保存 --> <insert id"saveOrUpdat…

Java调用动态库 缺点_java调用动态库(dll)的一些问题

javac1)dos切换到java文件所在目录&#xff0c;使用javac编译出class文件javah的一些问题&#xff1a;1)切换到src目录下2)设置路径命令&#xff1a;set classpathsrc目录的完全路径3)执行 javah 类名(带包的名称)&#xff0c;将生成的文件改名为“testdll.h”4.DLL的创建 :1)创…

分布式事务六种解决方案

前言 事务想必大家并不陌生&#xff0c;至于什么是 ACID&#xff0c;也是老生常谈了。不过为了保证文章的完整性确保所有人都听得懂&#xff0c;我还是得先说说 ACID&#xff0c;然后再来介绍下什么是分布式事务和常见的分布式事务包括 2PC、3PC、TCC、本地消息表、消息事务、…